Repository: TomClabault/HIPRT-Path-Tracer Branch: main Commit: d114ed0d4c1d Files: 1316 Total size: 3.2 MB Directory structure: gitextract_c_vny0t9/ ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── COPYING ├── README.md ├── README_data/ │ ├── Features/ │ │ └── features.md │ └── img/ │ └── LayeredBSDF.drawio ├── SceneCredits.txt ├── cmake/ │ ├── Clip.cmake │ ├── SetupASSIMP.cmake │ ├── SetupHIPRT.cmake │ ├── SetupOIDN.cmake │ ├── SetupOrochi.cmake │ └── SetupTracy.cmake ├── data/ │ ├── BRDFsData/ │ │ ├── GGX/ │ │ │ ├── GGX_Conductor_Correlated_128x128.hdr │ │ │ ├── GGX_Conductor_Uncorrelated_128x128.hdr │ │ │ └── Glass/ │ │ │ ├── 0GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 0GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 0GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 0GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 0inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 0inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 100GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 100GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 100inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 100inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 101GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 101GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 101inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 101inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 102GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 102GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 102inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 102inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 103GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 103GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 103inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 103inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 104GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 104GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 104inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 104inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 105GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 105GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 105inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 105inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 106GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 106GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 106inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 106inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 107GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 107GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 107inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 107inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 108GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 108GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 108inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 108inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 109GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 109GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 109inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 109inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 10GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 10GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 10GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 10GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 10inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 10inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 110GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 110GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 110inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 110inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 111GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 111GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 111inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 111inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 112GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 112GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 112inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 112inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 113GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 113GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 113inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 113inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 114GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 114GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 114inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 114inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 115GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 115GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 115inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 115inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 116GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 116GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 116inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 116inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 117GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 117GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 117inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 117inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 118GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 118GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 118inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 118inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 119GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 119GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 119inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 119inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 11GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 11GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 11GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 11GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 11inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 11inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 120GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 120GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 120inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 120inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 121GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 121GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 121inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 121inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 122GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 122GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 122inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 122inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 123GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 123GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 123inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 123inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 124GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 124GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 124inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 124inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 125GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 125GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 125inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 125inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 126GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 126GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 126inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 126inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 127GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 127GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 127inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 127inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 12GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 12GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 12GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 12GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 12inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 12inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 13GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 13GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 13GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 13GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 13inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 13inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 14GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 14GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 14GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 14GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 14inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 14inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 15GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 15GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 15GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 15GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 15inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 15inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 16GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 16GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 16GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 16GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 16inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 16inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 17GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 17GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 17GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 17GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 17inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 17inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 18GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 18GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 18GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 18GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 18inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 18inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 19GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 19GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 19GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 19GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 19inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 19inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 1GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 1GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 1GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 1GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 1inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 1inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 20GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 20GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 20GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 20GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 20inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 20inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 21GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 21GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 21GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 21GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 21inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 21inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 22GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 22GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 22GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 22GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 22inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 22inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 23GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 23GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 23GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 23GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 23inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 23inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 24GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 24GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 24GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 24GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 24inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 24inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 25GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 25GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 25GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 25GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 25inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 25inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 26GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 26GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 26GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 26GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 26inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 26inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 27GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 27GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 27GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 27GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 27inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 27inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 28GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 28GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 28GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 28GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 28inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 28inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 29GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 29GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 29GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 29GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 29inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 29inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 2GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 2GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 2GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 2GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 2inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 2inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 30GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 30GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 30GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 30GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 30inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 30inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 31GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 31GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 31GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 31GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 31inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 31inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 32GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 32GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 32GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 32GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 32inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 32inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 33GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 33GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 33GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 33GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 33inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 33inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 34GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 34GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 34GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 34GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 34inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 34inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 35GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 35GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 35GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 35GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 35inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 35inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 36GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 36GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 36GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 36GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 36inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 36inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 37GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 37GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 37GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 37GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 37inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 37inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 38GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 38GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 38GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 38GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 38inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 38inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 39GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 39GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 39GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 39GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 39inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 39inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 3GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 3GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 3GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 3GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 3inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 3inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 40GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 40GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 40GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 40GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 40inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 40inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 41GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 41GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 41GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 41GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 41inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 41inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 42GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 42GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 42GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 42GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 42inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 42inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 43GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 43GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 43GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 43GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 43inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 43inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 44GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 44GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 44GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 44GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 44inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 44inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 45GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 45GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 45GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 45GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 45inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 45inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 46GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 46GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 46GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 46GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 46inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 46inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 47GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 47GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 47GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 47GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 47inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 47inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 48GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 48GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 48GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 48GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 48inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 48inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 49GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 49GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 49GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 49GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 49inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 49inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 4GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 4GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 4GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 4GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 4inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 4inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 50GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 50GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 50GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 50GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 50inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 50inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 51GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 51GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 51GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 51GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 51inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 51inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 52GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 52GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 52GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 52GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 52inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 52inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 53GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 53GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 53GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 53GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 53inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 53inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 54GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 54GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 54GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 54GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 54inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 54inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 55GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 55GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 55GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 55GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 55inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 55inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 56GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 56GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 56GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 56GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 56inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 56inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 57GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 57GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 57GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 57GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 57inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 57inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 58GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 58GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 58GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 58GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 58inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 58inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 59GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 59GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 59GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 59GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 59inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 59inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 5GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 5GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 5GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 5GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 5inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 5inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 60GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 60GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 60GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 60GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 60inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 60inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 61GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 61GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 61GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 61GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 61inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 61inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 62GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 62GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 62GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 62GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 62inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 62inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 63GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 63GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 63GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 63GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 63inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 63inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 64GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 64GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 64GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 64GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 64inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 64inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 65GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 65GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 65GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 65GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 65inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 65inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 66GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 66GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 66GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 66GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 66inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 66inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 67GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 67GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 67GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 67GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 67inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 67inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 68GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 68GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 68GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 68GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 68inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 68inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 69GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 69GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 69GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 69GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 69inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 69inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 6GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 6GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 6GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 6GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 6inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 6inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 70GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 70GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 70GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 70GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 70inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 70inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 71GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 71GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 71GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 71GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 71inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 71inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 72GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 72GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 72GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 72GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 72inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 72inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 73GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 73GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 73GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 73GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 73inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 73inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 74GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 74GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 74GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 74GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 74inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 74inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 75GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 75GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 75GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 75GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 75inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 75inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 76GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 76GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 76GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 76GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 76inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 76inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 77GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 77GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 77GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 77GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 77inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 77inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 78GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 78GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 78GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 78GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 78inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 78inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 79GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 79GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 79GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 79GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 79inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 79inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 7GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 7GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 7GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 7GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 7inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 7inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 80GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 80GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 80GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 80GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 80inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 80inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 81GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 81GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 81GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 81GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 81inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 81inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 82GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 82GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 82GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 82GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 82inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 82inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 83GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 83GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 83GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 83GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 83inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 83inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 84GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 84GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 84GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 84GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 84inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 84inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 85GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 85GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 85GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 85GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 85inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 85inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 86GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 86GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 86GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 86GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 86inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 86inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 87GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 87GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 87GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 87GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 87inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 87inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 88GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 88GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 88GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 88GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 88inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 88inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 89GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 89GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 89GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 89GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 89inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 89inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 8GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 8GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 8GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 8GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 8inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 8inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 90GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 90GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 90GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 90GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 90inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 90inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 91GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 91GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 91GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 91GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 91inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 91inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 92GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 92GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 92GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 92GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 92inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 92inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 93GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 93GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 93GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 93GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 93inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 93inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 94GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 94GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 94GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 94GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 94inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 94inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 95GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 95GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 95GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 95GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 95inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 95inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 96GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 96GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 96inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 96inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 97GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 97GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 97inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 97inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 98GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 98GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 98inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 98inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 99GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 99GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 99inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 99inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 9GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 9GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ ├── 9GGX_Thin_Glass_Ess_Correlated_32x32x96.hdr │ │ │ ├── 9GGX_Thin_Glass_Ess_Uncorrelated_32x32x96.hdr │ │ │ ├── 9inv_GGX_Glass_Ess_Correlated_256x16x128.hdr │ │ │ ├── 9inv_GGX_Glass_Ess_Uncorrelated_256x16x128.hdr │ │ │ └── ExponentCorrection.ipynb │ │ └── GlossyDielectrics/ │ │ ├── 0Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 0Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 100Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 100Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 101Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 101Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 102Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 102Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 103Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 103Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 104Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 104Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 105Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 105Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 106Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 106Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 107Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 107Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 108Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 108Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 109Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 109Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 10Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 10Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 110Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 110Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 111Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 111Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 112Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 112Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 113Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 113Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 114Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 114Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 115Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 115Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 116Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 116Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 117Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 117Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 118Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 118Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 119Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 119Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 11Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 11Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 120Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 120Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 121Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 121Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 122Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 122Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 123Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 123Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 124Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 124Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 125Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 125Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 126Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 126Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 127Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 127Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 12Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 12Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 13Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 13Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 14Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 14Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 15Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 15Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 16Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 16Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 17Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 17Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 18Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 18Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 19Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 19Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 1Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 1Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 20Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 20Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 21Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 21Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 22Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 22Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 23Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 23Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 24Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 24Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 25Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 25Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 26Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 26Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 27Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 27Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 28Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 28Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 29Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 29Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 2Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 2Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 30Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 30Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 31Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 31Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 32Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 32Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 33Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 33Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 34Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 34Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 35Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 35Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 36Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 36Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 37Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 37Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 38Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 38Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 39Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 39Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 3Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 3Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 40Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 40Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 41Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 41Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 42Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 42Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 43Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 43Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 44Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 44Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 45Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 45Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 46Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 46Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 47Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 47Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 48Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 48Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 49Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 49Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 4Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 4Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 50Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 50Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 51Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 51Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 52Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 52Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 53Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 53Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 54Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 54Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 55Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 55Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 56Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 56Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 57Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 57Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 58Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 58Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 59Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 59Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 5Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 5Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 60Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 60Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 61Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 61Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 62Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 62Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 63Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 63Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 64Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 64Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 65Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 65Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 66Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 66Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 67Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 67Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 68Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 68Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 69Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 69Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 6Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 6Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 70Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 70Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 71Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 71Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 72Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 72Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 73Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 73Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 74Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 74Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 75Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 75Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 76Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 76Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 77Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 77Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 78Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 78Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 79Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 79Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 7Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 7Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 80Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 80Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 81Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 81Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 82Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 82Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 83Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 83Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 84Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 84Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 85Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 85Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 86Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 86Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 87Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 87Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 88Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 88Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 89Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 89Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 8Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 8Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 90Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 90Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 91Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 91Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 92Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 92Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 93Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 93Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 94Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 94Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 95Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 95Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 96Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 96Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 97Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 97Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 98Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 98Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 99Glossy_Ess_Correlated_128x64x128.hdr │ │ ├── 99Glossy_Ess_Uncorrelated_128x64x128.hdr │ │ ├── 9Glossy_Ess_Correlated_128x64x128.hdr │ │ └── 9Glossy_Ess_Uncorrelated_128x64x128.hdr │ └── GLTFs/ │ ├── cornell_pbr.gltf │ ├── multi-dispersion.gltf │ ├── nested-dielectrics-complex.gltf │ ├── nested-dielectrics.gltf │ └── the-white-room-low.gltf └── src/ ├── Compiler/ │ ├── GPUKernel.cpp │ ├── GPUKernel.h │ ├── GPUKernelCompiler.cpp │ ├── GPUKernelCompiler.h │ ├── GPUKernelCompilerOptions.cpp │ └── GPUKernelCompilerOptions.h ├── Device/ │ ├── functions/ │ │ ├── FilterFunction.h │ │ └── FilterFunctionPayload.h │ ├── includes/ │ │ ├── AdaptiveSampling.h │ │ ├── AliasTable.h │ │ ├── BSDFs/ │ │ │ ├── BSDFContext.h │ │ │ ├── BSDFIncidentLightInfo.h │ │ │ ├── CookTorrance.h │ │ │ ├── Glass.h │ │ │ ├── Lambertian.h │ │ │ ├── Microfacet.h │ │ │ ├── MicrofacetEnergyCompensation.h │ │ │ ├── MicrofacetRegularization.h │ │ │ ├── OrenNayar.h │ │ │ ├── Principled.h │ │ │ ├── PrincipledEnergyCompensation.h │ │ │ ├── SheenLTC.h │ │ │ ├── SheenLTCFittedParameters.h │ │ │ └── ThinFilm.h │ │ ├── Dispatcher.h │ │ ├── Dispersion.h │ │ ├── FixIntellisense.h │ │ ├── Fresnel.h │ │ ├── GBufferDevice.h │ │ ├── GMoN/ │ │ │ ├── GMoN.h │ │ │ ├── GMoNDevice.h │ │ │ ├── GMoNMeansRadixSort.h │ │ │ └── GMoNMeansRadixSortHistogramDeclaration.h │ │ ├── Hash.h │ │ ├── HashGrid.h │ │ ├── HashGridHash.h │ │ ├── Intersect.h │ │ ├── LightSampling/ │ │ │ ├── Envmap.h │ │ │ ├── LightUtils.h │ │ │ ├── Lights.h │ │ │ └── PDFConversion.h │ │ ├── Material.h │ │ ├── NEE++/ │ │ │ └── NEE++.h │ │ ├── NestedDielectrics.h │ │ ├── ONB.h │ │ ├── PathTracing.h │ │ ├── RIS/ │ │ │ ├── RIS.h │ │ │ └── RIS_Reservoir.h │ │ ├── RayPayload.h │ │ ├── RayVolumeState.h │ │ ├── ReSTIR/ │ │ │ ├── DI/ │ │ │ │ ├── FinalShading.h │ │ │ │ ├── PresampledLight.h │ │ │ │ ├── Reservoir.h │ │ │ │ ├── SampleFlags.h │ │ │ │ └── TargetFunction.h │ │ │ ├── GI/ │ │ │ │ ├── InitialCandidatesUtils.h │ │ │ │ ├── Reservoir.h │ │ │ │ └── TargetFunction.h │ │ │ ├── Jacobian.h │ │ │ ├── MISWeightsCommon.h │ │ │ ├── NeighborSimilarity.h │ │ │ ├── OptimalVisibilitySampling.h │ │ │ ├── ReGIR/ │ │ │ │ ├── FinalShading.h │ │ │ │ ├── GridFillSurface.h │ │ │ │ ├── HashGridCellData.h │ │ │ │ ├── HashGridSoADevice.h │ │ │ │ ├── PresampledLight.h │ │ │ │ ├── ReGIRHashGrid.h │ │ │ │ ├── Representative.h │ │ │ │ ├── Reservoir.h │ │ │ │ ├── ReservoirSoA.h │ │ │ │ ├── Settings.h │ │ │ │ ├── ShadingAdditionalInfo.h │ │ │ │ ├── ShadingSettings.h │ │ │ │ ├── TargetFunction.h │ │ │ │ └── VisibilityTest.h │ │ │ ├── SpatialMISWeight.h │ │ │ ├── SpatialNormalizationWeight.h │ │ │ ├── SpatiotemporalMISWeight.h │ │ │ ├── SpatiotemporalNormalizationWeight.h │ │ │ ├── Surface.h │ │ │ ├── TemporalMISWeight.h │ │ │ ├── TemporalNormalizationWeight.h │ │ │ ├── Utils.h │ │ │ ├── UtilsSpatial.h │ │ │ └── UtilsTemporal.h │ │ ├── RussianRoulette.h │ │ ├── Sampling.h │ │ ├── SanityCheck.h │ │ ├── Texture.h │ │ ├── TriangleStructures.h │ │ └── WarpDirectionReuse.h │ ├── kernel_parameters/ │ │ ├── NEE++/ │ │ │ └── NEEPlusPlusCachingPrepassParameters.h │ │ └── ReSTIR/ │ │ └── DI/ │ │ └── LightPresamplingParameters.h │ └── kernels/ │ ├── Baking/ │ │ ├── GGXConductorDirectionalAlbedo.h │ │ ├── GGXFresnelDirectionalAlbedo.h │ │ ├── GGXGlassDirectionalAlbedo.h │ │ ├── GGXThinGlassDirectionalAlbedo.h │ │ └── GlossyDielectricDirectionalAlbedo.h │ ├── CameraRays.h │ ├── Experimentations/ │ │ ├── RegistersTest.h │ │ ├── Test3DTexture.h │ │ ├── TestCopyKernelAlignment.h │ │ ├── TestCopyKernelRestrict.h │ │ └── TestCopyKernelSimple.h │ ├── GMoN/ │ │ └── GMoNComputeMedianOfMeans.h │ ├── Megakernel.h │ ├── NEE++/ │ │ ├── GridPrepopulate.h │ │ └── NEEPlusPlusFinalizeAccumulation.h │ ├── ReSTIR/ │ │ ├── DI/ │ │ │ ├── FusedSpatiotemporalReuse.h │ │ │ ├── InitialCandidates.h │ │ │ ├── LightsPresampling.h │ │ │ ├── SpatialReuse.h │ │ │ └── TemporalReuse.h │ │ ├── DirectionalReuseCompute.h │ │ ├── GI/ │ │ │ ├── InitialCandidates.h │ │ │ ├── Shading.h │ │ │ ├── SpatialReuse.h │ │ │ └── TemporalReuse.h │ │ └── ReGIR/ │ │ ├── GridFillTemporalReuse.h │ │ ├── GridPrepopulate.h │ │ ├── LightPresampling.h │ │ ├── PreIntegration.h │ │ ├── Rehash.h │ │ ├── SpatialReuse.h │ │ └── SupersamplingCopy.h │ ├── TraceTest.h │ └── Utils/ │ └── RayVolumeStateSize.h ├── Experimentations/ │ ├── TestCopyKernelAlignment.cpp │ ├── TestCopyKernelAlignment.h │ ├── TestCopyKernelRestrict.cpp │ ├── TestCopyKernelRestrict.h │ ├── TestCopyKernelSimple.cpp │ └── TestCopyKernelSimple.h ├── HIPRT-Orochi/ │ ├── HIPRTOrochiCtx.h │ ├── HIPRTOrochiUtils.cpp │ ├── HIPRTOrochiUtils.h │ ├── HIPRTScene.h │ ├── OrochiBuffer.h │ ├── OrochiEnvmap.cpp │ ├── OrochiEnvmap.h │ ├── OrochiTexture.cpp │ ├── OrochiTexture.h │ ├── OrochiTexture3D.cpp │ ├── OrochiTexture3D.h │ └── OrochiTextureCUDA.cpp ├── HostDeviceCommon/ │ ├── AtomicType.h │ ├── BSDFsData.h │ ├── Color.h │ ├── HIPRTCamera.h │ ├── HitInfo.h │ ├── KernelOptions/ │ │ ├── Common.h │ │ ├── DirectLightSamplingOptions.h │ │ ├── GMoNOptions.h │ │ ├── KernelOptions.h │ │ ├── NEEPlusPlusOptions.h │ │ ├── PrincipledBSDFKernelOptions.h │ │ ├── ReGIROptions.h │ │ ├── ReSTIRDIOptions.h │ │ └── ReSTIRGIOptions.h │ ├── LightSampleInformation.h │ ├── Material/ │ │ ├── MaterialCPU.h │ │ ├── MaterialConstants.h │ │ ├── MaterialPacked.h │ │ ├── MaterialPackedSoA.h │ │ ├── MaterialUnpacked.h │ │ └── MaterialUtils.h │ ├── Math.h │ ├── MicrofacetRegularizationSettings.h │ ├── Packing.h │ ├── PathRussianRoulette.h │ ├── PrecomputedEmissiveTrianglesDataSoADevice.h │ ├── RIS/ │ │ └── RISSettings.h │ ├── ReSTIR/ │ │ ├── ReSTIRCommonSettings.h │ │ ├── ReSTIRDIDefaultSettings.h │ │ ├── ReSTIRDISettings.h │ │ ├── ReSTIRGIDefaultSettings.h │ │ └── ReSTIRGISettings.h │ ├── ReSTIRSettingsHelper.h │ ├── RenderBuffers.h │ ├── RenderData.h │ ├── RenderSettings.cpp │ ├── RenderSettings.h │ ├── WorldSettings.h │ └── Xorshift.h ├── Image/ │ ├── EnvmapRGBE9995.h │ ├── Image.cpp │ └── Image.h ├── OpenGL/ │ ├── OpenGLInteropBuffer.h │ ├── OpenGLProgram.cpp │ ├── OpenGLProgram.h │ ├── OpenGLShader.cpp │ └── OpenGLShader.h ├── Renderer/ │ ├── BVH.cpp │ ├── BVH.h │ ├── BVHConstants.h │ ├── Baker/ │ │ ├── GGXConductorDirectionalAlbedoSettings.h │ │ ├── GGXFresnelDirectionalAlbedoSettings.h │ │ ├── GGXGlassDirectionalAlbedoSettings.h │ │ ├── GGXThinGlassDirectionalAlbedoSettings.h │ │ ├── GPUBaker.cpp │ │ ├── GPUBaker.h │ │ ├── GPUBakerConstants.h │ │ ├── GPUBakerKernel.cpp │ │ ├── GPUBakerKernel.h │ │ └── GlossyDielectricDirectionalAlbedoSettings.h │ ├── BoundingVolume.h │ ├── CPUDataStructures/ │ │ ├── GBufferCPUData.h │ │ ├── GMoNCPUData.h │ │ ├── MaterialPackedSoACPUData.h │ │ └── NEEPlusPlusCPUData.h │ ├── CPUGPUCommonDataStructures/ │ │ ├── DevicePackedMaterialSoACPUGPUCommonData.h │ │ ├── GMoNCPUGPUCommonData.h │ │ ├── GenericSoA.h │ │ ├── PrecomputedEmissiveTrianglesDataSoAHost.h │ │ ├── ReGIRGridBufferSoAHost.h │ │ ├── ReGIRHashCellDataSoAHost.h │ │ ├── ReGIRHashGridSoAHost.h │ │ └── ReGIRPresampledLightsSoAHost.h │ ├── CPURenderer.cpp │ ├── CPURenderer.h │ ├── GPUDataStructures/ │ │ ├── DenoiserBuffersGPUData.cpp │ │ ├── DenoiserBuffersGPUData.h │ │ ├── GBufferGPUData.h │ │ ├── GMoNGPUData.h │ │ ├── MaterialPackedSoAGPUData.h │ │ └── StatusBuffersGPUData.h │ ├── GPURenderer.cpp │ ├── GPURenderer.h │ ├── GPURendererThread.cpp │ ├── GPURendererThread.h │ ├── HardwareAccelerationSupport.h │ ├── OpenImageDenoiser.cpp │ ├── OpenImageDenoiser.h │ ├── RenderPasses/ │ │ ├── FillGBufferRenderPass.cpp │ │ ├── FillGBufferRenderPass.h │ │ ├── GMoNRenderPass.cpp │ │ ├── GMoNRenderPass.h │ │ ├── MegaKernelRenderPass.cpp │ │ ├── MegaKernelRenderPass.h │ │ ├── NEEPlusPlusHashGridStorage.cpp │ │ ├── NEEPlusPlusHashGridStorage.h │ │ ├── NEEPlusPlusRenderPass.cpp │ │ ├── NEEPlusPlusRenderPass.h │ │ ├── ReGIRHashGridStorage.cpp │ │ ├── ReGIRHashGridStorage.h │ │ ├── ReGIRRenderPass.cpp │ │ ├── ReGIRRenderPass.h │ │ ├── ReSTIRDIRenderPass.cpp │ │ ├── ReSTIRDIRenderPass.h │ │ ├── ReSTIRGIRenderPass.cpp │ │ ├── ReSTIRGIRenderPass.h │ │ ├── ReSTIRRenderPassCommon.cpp │ │ ├── ReSTIRRenderPassCommon.h │ │ ├── RenderGraph.cpp │ │ ├── RenderGraph.h │ │ ├── RenderPass.cpp │ │ └── RenderPass.h │ ├── RendererAnimationState.h │ ├── RendererEnvmap.cpp │ ├── RendererEnvmap.h │ ├── Sphere.h │ ├── StatusBuffersValues.h │ ├── Triangle.cpp │ └── Triangle.h ├── Scene/ │ ├── BoundingBox.h │ ├── Camera.cpp │ ├── Camera.h │ ├── CameraAnimation.cpp │ ├── CameraAnimation.h │ ├── CameraRotationType.h │ ├── SceneParser.cpp │ └── SceneParser.h ├── Shaders/ │ ├── albedo_display.frag │ ├── blend_2_display.frag │ ├── boolmap_int.frag │ ├── default_display.frag │ ├── fullscreen_quad.vert │ ├── heatmap_int.frag │ ├── normal_display.frag │ └── white_furnace_threshold.frag ├── Threads/ │ ├── ThreadFunctions.cpp │ ├── ThreadFunctions.h │ ├── ThreadManager.cpp │ ├── ThreadManager.h │ └── ThreadState.h ├── UI/ │ ├── ApplicationSettings.h │ ├── ApplicationState.h │ ├── DisplayView/ │ │ ├── DisplaySettings.h │ │ ├── DisplayTextureType.h │ │ ├── DisplayView.cpp │ │ ├── DisplayView.h │ │ ├── DisplayViewEnum.h │ │ ├── DisplayViewSystem.cpp │ │ └── DisplayViewSystem.h │ ├── ImGui/ │ │ ├── ImGuiAnimationWindow.cpp │ │ ├── ImGuiAnimationWindow.h │ │ ├── ImGuiLogWindow.cpp │ │ ├── ImGuiLogWindow.h │ │ ├── ImGuiLogger.cpp │ │ ├── ImGuiLogger.h │ │ ├── ImGuiLoggerLine.h │ │ ├── ImGuiLoggerSeverity.h │ │ ├── ImGuiObjectsWindow.cpp │ │ ├── ImGuiObjectsWindow.h │ │ ├── ImGuiRenderWindow.cpp │ │ ├── ImGuiRenderWindow.h │ │ ├── ImGuiRenderer.cpp │ │ ├── ImGuiRenderer.h │ │ ├── ImGuiRendererPerformancePreset.h │ │ ├── ImGuiSettingsWindow.cpp │ │ ├── ImGuiSettingsWindow.h │ │ ├── ImGuiToolsWindow.cpp │ │ └── ImGuiToolsWindow.h │ ├── Interaction/ │ │ ├── LinuxRenderWindowMouseInteractor.cpp │ │ ├── LinuxRenderWindowMouseInteractor.h │ │ ├── RenderWindowKeyboardInteractor.cpp │ │ ├── RenderWindowKeyboardInteractor.h │ │ ├── RenderWindowMouseInteractor.cpp │ │ ├── RenderWindowMouseInteractor.h │ │ ├── WindowsRenderWindowMouseInteractor.cpp │ │ └── WindowsRenderWindowMouseInteractor.h │ ├── PerformanceMetricsComputer.cpp │ ├── PerformanceMetricsComputer.h │ ├── RenderWindow.cpp │ ├── RenderWindow.h │ ├── Screenshoter.cpp │ └── Screenshoter.h ├── Utils/ │ ├── CommandlineArguments.cpp │ ├── CommandlineArguments.h │ ├── Utils.cpp │ └── Utils.h ├── llvm-compile-kernel.h └── main.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ CMakeLists.txt.user RT_output*.png .vscode .vs build* data/OBJs/*.blend data/OBJs/*.obj data/OBJs/*.mtl data/Skyspheres/*.hdr data/OBJs/Projects/ data/Skyspheres/evening_road_01_puresky_8k.hdr data/Skyspheres/AllSkyFree_Sky_EpicGloriousPink_EquirectDebug.jpg data/Skyspheres/AllSkyFree_Sky_EpicGloriousPink_Equirect.jpg oidn/bin/ oidn/doc/ oidn/lib/ thirdparties/ contrib/ hiprt/ ================================================ FILE: .gitmodules ================================================ [submodule "Orochi-Fork"] path = thirdparties/Orochi-Fork url = https://github.com/TomClabault/Orochi.git [submodule "HIPRT-Fork"] path = thirdparties/HIPRT-Fork url = https://github.com/TomClabault/HIPRT.git [submodule "ASSIMP-Fork"] path = thirdparties/ASSIMP-Fork url = https://github.com/TomClabault/assimp.git [submodule "thirdparties/imgui"] path = thirdparties/imgui url = https://github.com/ocornut/imgui.git [submodule "thirdparties/tracy"] path = thirdparties/tracy url = https://github.com/wolfpld/tracy.git [submodule "thirdparties/libtinyfiledialogs"] path = thirdparties/libtinyfiledialogs url = https://github.com/TomClabault/libtinyfiledialogs.git [submodule "thirdparties/clip"] path = thirdparties/clip url = https://github.com/dacap/clip.git ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.24) project(HIPRTPathTracer LANGUAGES CXX) # To be able to use the ExternalProject_Add() command include(ExternalProject) include(FetchContent) # To see the progress of FetchContent Set(FETCHCONTENT_QUIET FALSE) # Policy for what timestamp to use when downloading stuff with FetchContent / ExternelProject / ... # NEW sets the timestamps to the extraction time cmake_policy(SET CMP0135 NEW) # If the build type wasn't given on the commandline, we're defaulting to release if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug") message(STATUS "Build type not specified: Using Debug by default") endif() # Sets up ASSIMP library CMake variable to prepare the building step include (cmake/SetupASSIMP.cmake) # Open Image Denoise binaries include(cmake/SetupOIDN.cmake) # Preparing HIPRT include(cmake/SetupHIPRT.cmake) # Preparing Orochi include(cmake/SetupOrochi.cmake) # Include Tracy for OpenGL profiling include(cmake/SetupTracy.cmake) # Clip for copying images to clipboard include(cmake/Clip.cmake) set(GLFW_LIB_DIR "thirdparties/opengl/lib/GLFW") set(GLEW_LIB_DIR "thirdparties/opengl/lib/GLEW") set(GLEW_BIN_DIR "thirdparties/opengl/bin/GLEW") set(TRACY_PUBLIC_DIR "thirdparties/tracy/public") # Using CMake here to define C++ macros that will be used to find the directory of the kernels, etc... # in the C++ code. This basically avoids hardcoding the path to the kernels in C++ and instead # use the more flexible approach of defining it in the CMake add_compile_definitions(DEVICE_KERNELS_DIRECTORY="../src/Device/kernels") add_compile_definitions(DEVICE_INCLUDES_DIRECTORY="../src/") # This gives access to Device/ and HostDeviceCommon/ add_compile_definitions(OROCHI_INCLUDES_DIRECTORY="${OROCHI_SOURCES_DIR}/..") # This gives access to in the kernels add_compile_definitions(GLSL_SHADERS_DIRECTORY="../src/Shaders") add_compile_definitions(DATA_DIRECTORY="${CMAKE_SOURCE_DIR}/data") add_compile_definitions(BRDFS_DATA_DIRECTORY="${CMAKE_SOURCE_DIR}/data/BRDFsData") #add_compile_definitions(TRACY_ENABLE="1") link_directories(${CMAKE_SOURCE_DIR}/${GLFW_LIB_DIR}) link_directories(${CMAKE_SOURCE_DIR}/${GLEW_LIB_DIR}) file(GLOB_RECURSE SOURCE_FILES src/*.cpp src/*.h) file(GLOB_RECURSE OPENGL_HEADERS thirdparties/opengl/include/*.h) file(GLOB_RECURSE STBI_HEADERS thirdparties/stbi/*.h) # Selecting only what we need from the whole ImGui submodule file(GLOB_RECURSE IMGUI_FILES thirdparties/imgui/imgui.h thirdparties/imgui/imgui.cpp thirdparties/imgui/imgui_demo.cpp thirdparties/imgui/imgui_draw.cpp thirdparties/imgui/imgui_tables.cpp thirdparties/imgui/imgui_widgets.cpp thirdparties/imgui/backends/imgui_impl_glfw.cpp thirdparties/imgui/backends/imgui_impl_opengl3.cpp thirdparties/imgui/misc/cpp/imgui_stdlib.cpp) file(GLOB_RECURSE DEVICE_SOURCES src/Device/*.h) file(GLOB_RECURSE GLSL_SHADERS src/Shaders/*.frag src/Shaders/*.vert) file(GLOB_RECURSE HIPRT_HEADERS ${HIPRT_HEADERS_DIR}/*.h) file(GLOB_RECURSE OROCHI_SOURCES_AND_HEADERS ${OROCHI_SOURCES_DIR}/*.h ${OROCHI_SOURCES_DIR}/*.cpp) file(GLOB_RECURSE CUEW_SOURCES_AND_HEADERS ${CUEW_SOURCES_DIR}/*.h ${CUEW_SOURCES_DIR}/*.cpp) file(GLOB_RECURSE HIPEW_SOURCES_AND_HEADERS ${HIPEW_SOURCES_DIR}/*.h ${HIPEW_SOURCES_DIR}/*.cpp) file(GLOB_RECURSE TINYFILEDIALOGS_SOURCE_AND_HEADERS thirdparties/libtinyfiledialogs/tinyfiledialogs.h thirdparties/libtinyfiledialogs/tinyfiledialogs.cpp) file(GLOB_RECURSE NVIDIA_FLIP_HEADERS thirdparties/nvidia-FLIP/*.h) add_executable(HIPRTPathTracer ${SOURCE_FILES} ${OPENGL_HEADERS} ${STBI_HEADERS} ${IMGUI_FILES} ${ASSIMP_HEADERS} ${DEVICE_SOURCES} ${GLSL_SHADERS} ${HIPRT_HEADERS} ${OROCHI_SOURCES_AND_HEADERS} ${CUEW_SOURCES_AND_HEADERS} ${HIPEW_SOURCES_AND_HEADERS} ${TINYFILEDIALOGS_SOURCE_AND_HEADERS} ${NVIDIA_FLIP_HEADERS} ) set_property(TARGET HIPRTPathTracer PROPERTY CXX_STANDARD 20) find_package(OpenMP REQUIRED) find_package(OpenGL REQUIRED) find_package(OpenImageDenoise REQUIRED HINTS ${oidnbinaries_SOURCE_DIR}) # HINTS to indicate a folder to search for the library in if (WIN32) # "version" is a library from the Windows SDK target_link_libraries(HIPRTPathTracer PRIVATE OpenMP::OpenMP_CXX assimp OpenImageDenoise ${OPENGL_LIBRARY} glfw3 glew32 hiprt02004 TracyClient clip version) elseif(UNIX) find_package(GLEW REQUIRED) target_link_libraries(HIPRTPathTracer PRIVATE OpenMP::OpenMP_CXX assimp OpenImageDenoise ${OPENGL_LIBRARY} glfw GLEW::GLEW hiprt02004 clip TracyClient) endif() target_include_directories(HIPRTPathTracer PRIVATE "src/") target_include_directories(HIPRTPathTracer PRIVATE "thirdparties/opengl/include") target_include_directories(HIPRTPathTracer PRIVATE "thirdparties/stbi/") target_include_directories(HIPRTPathTracer PRIVATE "thirdparties/libtinyfiledialogs/") target_include_directories(HIPRTPathTracer PRIVATE "thirdparties/glm/") target_include_directories(HIPRTPathTracer PRIVATE "thirdparties/imgui/") target_include_directories(HIPRTPathTracer PRIVATE "thirdparties/imgui/backends") target_include_directories(HIPRTPathTracer PRIVATE "thirdparties/tinyexr/") target_include_directories(HIPRTPathTracer PRIVATE "thirdparties/nvidia-FLIP/") target_include_directories(HIPRTPathTracer PRIVATE "thirdparties/clip/") target_include_directories(HIPRTPathTracer PRIVATE ${HIPRT_HEADERS_DIR}/..) target_include_directories(HIPRTPathTracer PRIVATE ${OROCHI_SOURCES_DIR}/..) target_include_directories(HIPRTPathTracer PRIVATE "${EXTERNAL_ASSIMP_INSTALL_LOCATION}/include/") target_include_directories(HIPRTPathTracer PRIVATE ".") target_include_directories(HIPRTPathTracer PRIVATE ${TRACY_PUBLIC_DIR}) # Auto setup of Orochi for NVIDIA by including their cmake file include(${HIPRT_SUBMODULE_DIR}/contrib/Orochi/Orochi/enable_cuew.cmake) if (WIN32) message(STATUS "Copying OpenImageDenoise binaries...") file(GLOB OIDN_BINARIES ${oidnbinaries_SOURCE_DIR}/bin/*.dll) file(COPY ${OIDN_BINARIES} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/) # For copying HIPRT's DLL get_target_property(HIPRT_DLL_NAME hiprt02004 OUTPUT_NAME) #if (${CMAKE_BUILD_TYPE} STREQUAL "Debug" AND MSVC_IDE) # Adding the 'd' suffix that MSVC adds to debug libraries file names set(HIPRT_DLL_NAME_DEBUG ${HIPRT_DLL_NAME}d.dll) #endif() # Appending .dll extension set(HIPRT_DLL_NAME ${HIPRT_DLL_NAME}.dll) add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/${HIPRT_DLL_NAME} COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_BINARY_DIR}/${HIPRT_DLL_NAME} DEPENDS hiprt02004) add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/${HIPRT_DLL_NAME_DEBUG} COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_BINARY_DIR}/${HIPRT_DLL_NAME_DEBUG} DEPENDS hiprt02004) # Create target which consume the command via DEPENDS. add_custom_target(hiprtCopyDLL ALL DEPENDS ${CMAKE_BINARY_DIR}/${HIPRT_DLL_NAME} ${CMAKE_BINARY_DIR}/${HIPRT_DLL_NAME_DEBUG}) add_dependencies(HIPRTPathTracer hiprtCopyDLL) message(STATUS "Copying Glew binaries...") file(COPY ${CMAKE_SOURCE_DIR}/${GLEW_BIN_DIR}/glew32.dll DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) endif() if(MSVC_IDE) # Enabling parallel compilation on MSVC which isn't enabled by default if(MSVC) add_definitions(/MP) if (${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo") # In RelWithDebInfo, we're disabling all optimizations # for easier debugging: # - RelWithDebInfo is faster than debug so we want to use # that when debugging line by line # # - But the optimizations that are enabled by default in # RelWithDebInfo still mess up with MSVC debugger and the # debugger jumps everywhere, variables are optimized away etc... # Even if we're supposed to include debug infos in the compilation... # So we're just disabling optimizations then # No optimizations add_definitions(/Od) # No inlining of functions add_definitions(/Ob0) endif() endif() # Macro to preserve nice beautiful source files hierarchy in Visual Studio macro(GroupSources curdir) file(GLOB children RELATIVE ${PROJECT_SOURCE_DIR}/${curdir} ${PROJECT_SOURCE_DIR}/${curdir}/*) foreach(child ${children}) if(IS_DIRECTORY ${PROJECT_SOURCE_DIR}/${curdir}/${child}) GroupSources(${curdir}/${child}) else() string(REPLACE "/" "\\" groupname ${curdir}) string(REPLACE "src" "Sources" groupname ${groupname}) source_group(${groupname} FILES ${PROJECT_SOURCE_DIR}/${curdir}/${child}) endif() endforeach() endmacro() # Run macro GroupSources(src) # Creating a Visual Studio folder for the targets we don't care about so we have # a way to have our IDE look clean set_property(GLOBAL PROPERTY USE_FOLDERS ON) set_target_properties( assimp uninstall zlibstatic UpdateAssimpLibsDebugSymbolsAndDLLs # ASSIMP Targets hiprt02004 hiprtCopyDLL # HIPRT Targets TracyClient # Tracy hello_world copy clip_user_format_tests clip_text_tests clip_image_tests clip int_format paste put_image show_image # Clip PROPERTIES FOLDER ExternalTargets) endif() ================================================ FILE: COPYING ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # HIPRT-Path-Tracer ![HIPRT path tracer cover](README_data/img/Bistro.jpg) Physically based unidirectional (backwards) Monte Carlo path tracer written with the [HIPRT](https://gpuopen.com/hiprt/) and [Orochi](https://gpuopen.com/orochi/) libraries. HIPRT is AMD's equivalent to [OptiX](https://developer.nvidia.com/rtx/ray-tracing/optix). It allows the use of the ray tracing accelerators of RDNA2+ AMD GPUs and can run on NVIDIA devices as well (although it wouldn't take advatange of RT cores) as it is not AMD specific. The Orochi library allows the loading of HIP and CUDA libraries at runtime meaning that the application doesn't have to be recompiled to be used on a GPU from a different vendor (unlike HIP alone which, despite being compatible with NVIDIA and AMD hardware, would require a recompilation). # System requirements - AMD RDNA1 GPU or newer (RX 5000 or newer) **or** NVIDIA Maxwell GPU or newer (GTX 700 & GTX 900 Series or newer) - Visual Studio 2022 (only version tested but older versions might work as well) on Windows - CMake - CUDA for NVIDIA compilation # Features: ### Layered Principled BSDF: - Coat Microfacet GGX Layer + Anisotropy, Anisotropy Rotation, Medium Absorption & Thickness - SGGX Volumetric Sheen Lobe LTC Fit [\[Zeltner, Burley, Chiang, 2022\]](https://tizianzeltner.com/projects/Zeltner2022Practical/) - Specular Microfacet GGX Layer - Diffuse BRDF lobe. Support for: - Lambertian - Oren-nayar - Metallic Microfacet GGX Layer + Anisotropy & Anisotropy Rotation + Double Roughness [\[Kulla & Conty, 2017\]](https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf) - Specular transmission BTDF + Beer Lambert Volumetric Absorption [\[Burley, 2015\]](https://blog.selfshadow.com/publications/s2015-shading-course/#course_content) - Diffuse lambertian BTDF - Spectral dispersion using Cauchy's equation - Multiple-scattering energy compensation for conductors (double metal layer), dielectrics (transmission layer) and glossy-diffuse (specular + diffuse layer) materials [\[Turquin, 2019\]](https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf) - Thin-film interference over dielectrics and conductors [\[Belcour, Barla, 2017\]](https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html) - Thin-walled model ![LayeredBSDF](README_data/img/LayeredBSDF.png) ![LayeredBSDF](README_data/img/metallic-energy.png) ![LayeredBSDF](README_data/img/glass-energy.png) ![LayeredBSDF](README_data/img/specular-diffuse-energy.png) ### Sampling - Base light sampling techniques: - Uniform light sampling for direct lighting estimation + MIS - Power-proportional light sampling - ReGIR [\[Boksansky et al., 2021\]](https://cwyman.org/papers/rtg2-manyLightReGIR.pdf) augmented with: - Representative cell surface-data + integration with NEE++ for resampling according to the product **BRDF \* L_i \* G \* V** - Spatial reuse - Hash grid - Per-cell RIS integral normalization factor pre-integration for multiple importance sampling support - Next-event estimation strategies (built on-top of base techniques): - MIS with BSDF sampling - Resampled Importance Sampling (RIS) [\[Talbot et al., 2005\]](https://www.researchgate.net/publication/220852928_Importance_Resampling_for_Global_Illumination)+ Weighted Reservoir Sampling (WRS) for many light sampling + [\[M. T. Chao, 1982\]](https://www.jstor.org/stable/2336002) - ReSTIR DI - Next Event Estimation++ [\[Guo et al., 2020\]](https://graphics.tudelft.nl/Publications-new/2020/GEE20/GEE20-NEE++.pdf) + Custom envmap support - HDR Environment map + Multiple Importance Sampling using - CDF-inversion & binary search - Alias Table (Vose's O(N) construction [\[Vose, 1991\]](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=f65bcde1fcf82e05388b31de80cba10bf65acc07)) - BSDF sampling: - GGX NDF Sampling: - Visible Normal Distribution Function (VNDF) [\[Heitz, 2018\]](https://jcgt.org/published/0007/04/01/) - Spherical caps VNDF Sampling [\[Dupuy, Benyoub, 2023\]](https://arxiv.org/abs/2306.05044) - Path sampling: - BSDF Sampling: - One sample MIS for lobe sampling [\[Hery et al., 2017\]](https://graphics.pixar.com/library/PxrMaterialsCourse2017/paper.pdf) - ReSTIR GI [\[Ouyang et al., 2021\]](https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing) - Experimental warp-wide direction reuse for improved indirect rays coherency [\[Liu et al., 2023\]](https://arxiv.org/abs/2310.07182) - ReSTIR Samplers: - ReSTIR DI [\[Bitterli et al., 2020\]](https://research.nvidia.com/labs/rtr/publication/bitterli2020spatiotemporal/) - Supports envmap sampling - Fused Spatiotemporal Reuse [\[Wyman, Panteleev, 2021\]](https://research.nvidia.com/publication/2021-07_rearchitecting-spatiotemporal-resampling-production) - Light Presampling [\[Wyman, Panteleev, 2021\]](https://research.nvidia.com/publication/2021-07_rearchitecting-spatiotemporal-resampling-production) - ReSTIR GI [\[Ouyang et al., 2021\]](https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing) - Many bias correction weighting schemes: - 1/M - 1/Z - MIS-like, - Generalized balance heuristic - Pairwise MIS [\[Bitterli, 2022\]](https://digitalcommons.dartmouth.edu/dissertations/77/) & defensive formulation [\[Lin et al., 2022\]](https://research.nvidia.com/publication/2022-07_generalized-resampled-importance-sampling-foundations-restir)) - Pairwise symmetric & asymmetric ratio MIS weights [\[Pan et al., 2024\]](https://diglib.eg.org/items/df9d727e-13a1-4d48-9275-57da7fb87f7f) - Adaptive-directional spatial reuse for improved offline rendering efficiency - Optimal visibility sampling [\[Pan et al., 2024\]](https://diglib.eg.org/items/df9d727e-13a1-4d48-9275-57da7fb87f7f) ### Other rendering features - Microfacet Model Regularization for Robust Light Transport [\[Jendersie et al., 2019\]](https://jojendersie.de/wp-content/uploads/2013/06/2019_Jendersie_brdfregularization.pdf) - G-MoN - Adaptive median of means for unbiased firefly removal [\[Buisine et al., 2021\]](https://hal.science/hal-03201630v2) - Texture support for all the parameters of the BSDF - Texture alpha transparency support - Stochastic material opacity support - Normal mapping - Nested dielectrics support - Handling with priorities as proposed in [\[Simple Nested Dielectrics in Ray Traced Images, Schmidt, 2002\]](https://www.researchgate.net/publication/247523037_Simple_Nested_Dielectrics_in_Ray_Traced_Images) - A Low-Distortion Map Between Triangle and Square [\[Heitz, 2019\]](https://hal.science/hal-02073696v2/document) - Per-pixel variance based adaptive sampling - Intel [Open Image Denoise](https://github.com/RenderKit/oidn) + Normals & Albedo AOV support ### UI - Interactive ImGui interface - Asynchronous interface to guarantee smooth UI interactions even with heavy path tracing kernels - Interactive first-person camera - Different frame-buffer visualization (visualize the adaptive sampling heatmap, converged pixels, the denoiser normals / albedo, ...) ### Other features - Use of the [\[ASSIMP\]](https://github.com/assimp/assimp) library to support [many](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md) scene file formats. - Multithreaded scene parsing/texture loading/shader compiling/BVH building/envmap processing/... for faster application startup times - Background-asynchronous path tracing kernels pre-compilation - Shader cache to avoid recompiling kernels unnecessarily # Building ## Prerequisites ### Windows #### - AMD GPUs 1) Install the [HIP SDK](https://www.amd.com/en/developer/resources/rocm-hub/hip-sdk.html) 2) Follow the "[**Compiling**](#compiling)" steps. #### - NVIDIA GPUs To build the project on NVIDIA hardware, you will need to install the NVIDIA CUDA SDK v12.2 (minimum). It can be downloaded and installed from [here](https://developer.nvidia.com/cuda-downloads). Your `CUDA_PATH` environment variable then needs to be defined. This should automatically be the case after installing the CUDA Toolkit but just in case, you can define it yourself such that `CUDA_PATH/include/cuda.h` is a valid file path. ### Linux #### - AMD GPUs 1) Install OpenGL, GLFW and glew dependencies: ```sh sudo apt install freeglut3-dev sudo apt install libglfw3-dev sudo apt install libglew-dev ``` 2) Install AMD HIP (if you already have ROCm installed, you should have a `/opt/rocm` folder on your system and you can skip this step): Download `amdgpu-install` package: https://rocm.docs.amd.com/projects/install-on-linux/en/latest/install/amdgpu-install.html Install the package: ```sh sudo apt install ./amdgpu-install_xxxx.deb ``` Install HIP: ```sh sudo amdgpu-install --usecase=hip ``` 3) Normally, you would have to run the path tracer as `sudo` to be able to acces GPGPU compute capabilities. However, you can save yourself the trouble by adding the user to the `render` group and **rebooting your system** : ```sh sudo usermod -a -G render $LOGNAME ``` #### - NVIDIA GPUs 1) Install OpenGL, GLFW and glew dependencies: ```sh sudo apt install freeglut3-dev sudo apt install libglfw3-dev sudo apt install libglew-dev sudo apt install libomp-dev ``` 2) Install the NVIDIA CUDA SDK (called "CUDA Toolkit"). It can be downloaded and installed from [here](https://developer.nvidia.com/cuda-downloads). ## Compiling With the pre-requisites fulfilled, you now just have to run the CMake: ``` sh git clone https://github.com/TomClabault/HIPRT-Path-Tracer.git --recursive cd HIPRT-Path-Tracer mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Debug .. ``` On Windows, a Visual Studio solution will be generated in the `build` folder that you can open and compile the project with (select `HIPRTPathTracer` as startup project). On Linux, the `HIPRTPathTracer` executable will be generated in the `build` folder. ## Usage `./HIPRT-Path-Tracer` The following arguments are available: - `` an argument of the commandline without prefix will be considered as the scene file. File formats [supported](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). - `--sky=` for the equirectangular skysphere used during rendering (HDR or not) - `--samples=N` for the number of samples to trace* - `--bounces=N` for the maximum number of bounces in the scene* - `--w=N` / `--width=N` for the width of the rendering* - `--h=N` / `--height=N` for the height of the rendering* \* CPU only commandline arguments. These parameters are controlled through the UI when running on the GPU. # Gallery ![DispersionDiamonds](README_data/img/DispersionDiamonds.jpg)![Bistro](README_data/img/Bistro.jpg) ![P1 street](README_data/img/P1_environment.jpg) ![Contemporary bedroom](README_data/img/contemporary-bedroom.jpg)![Blender 4.1 splash](README_data/img/blender-4.1-splash.jpg) ![Dragon glass](README_data/img/dragon-glass.jpg) ![Beeple Zero Day Measure Seven](README_data/img/bzd-measure-seven.jpg) ![Lux Core Orbs](README_data/img/LuxCoreBalls.jpg) ![Mitsuba Knob Sheen Dust](README_data/img/MitsubaSheenDustOrbs.jpg) ![Dragon indirect lighting](README_data/img/DragonBTDF.jpg)![Dragon indirect lighting](README_data/img/pbrt-dragon-indirect-v2.jpg) ![MIS vs. RIS vs. ReSTIR DI Comparison](README_data/img/RIS.ReSTIR.Comparison.jpg) ![ImGui Interface](README_data/img/ImGuiDemo.jpg) Sources of the scenes can be found [here](./SceneCredits.txt). # Live YouTube Demos ### Material Editor Demo [![Material Editor Demo](./README_data/img/Material_editor_thumbnail.jpg)](https://www.youtube.com/watch?v=LOVBwOoLVVQ "Material Editor Demo") ### OIDN AOVs Quality Comparison [![OIDN AOVs Comparison](./README_data/img/OIDN_AOVs_thumbnail.jpg)](https://www.youtube.com/watch?v=GnCi7K2w9go "OIDN AOVs Comparison") ### ReSTIR DI vs. RIS vs. MIS Showcase [![ReSTIR DI Showcase](./README_data/img/ReSTIR_DI_Showcase_thumbnail.jpg)](https://www.youtube.com/watch?v=R6nkhSDoJ4U "ReSTIR DI vs. RIS vs. MIS Showcase") ### Thin-film iridescence render [![OIDN AOVs Comparison](./README_data/img/thin-film-iri-thumbnail.jpg)](https://www.youtube.com/watch?v=rGwkacGbd3g "Thin-film iridescence render") # License GNU General Public License v3.0 or later See [COPYING](https://github.com/TomClabault/HIPRT-Path-Tracer/blob/main/COPYING) to see the full text. ================================================ FILE: README_data/Features/features.md ================================================ ### TODO - Disney BSDF (Diffuse, fake subsurface, metallic, roughness, anisotropy + anisotropy rotation, clearcoat, sheen, glass, volumetric Beer-Lambert absorption, ...) \[Burley, 2015\] - For experimentation purposes, the BRDF diffuse lobe can be switched for either: - The original "Disney diffuse" presented in [\[Burley, 2012\]](https://disneyanimation.com/publications/physically-based-shading-at-disney/) - A lambertian distribution - The Oren Nayar microfacet diffuse model. ### TODO - Texture support for all the parameters of the BSDF ### TODO - BSDF Direct lighting multiple importance sampling ### TODO - HDR Environment map + importance sampling using - CDF-inversion binary search ### TODO - Emissive geometry light sampling ### TODO - Nested dielectrics support - Automatic handling as presented in \[Ray Tracing Gems, 2019\] - Handling with priorities as proposed in \[Simple Nested Dielectrics in Ray Traced Images, Schmidt, 2002\] ### Per-pixel adaptive sampling Adaptive sampling is a technique that allows focusing the samples on pixels that need more of them. This is useful because not all parts of a scene are equally complex to render. Consider this modified cornell box for example: ![Cornell box PBR reflective caustic reference](./img/cornell_pbr_reference.jpg) Half of the rays of this scene don't even intersect any geometry and directly end up in the environment where the color of the environment map is computed. The variance of the radiance of these rays is very low since a given camera ray direction basically always results in the same radiance (almost) being returned. However, the same cannot be said for the reflective caustic (the emissive light panel reflecting off the mirror small box) at the top right of the Cornell box. A camera ray that hits this region of the ceiling then has a fairly low chance of bouncing in direction of the small box to then bounce directly in the direction of the light. This makes the variance of these rays very high which really slows down the convergence of this part of the scene. As a result, we would like to shoot more rays at these pixels than at other parts of the scene. Adaptive sampling allows us to do just that. The idea is to estimate the error of each pixel of the image, compare this estimated error with a user-defined threshold $T$ and only continue to sample the pixel if the pixel's error is still larger than the threshold. A very simple error metric is that of the variance of the luminance $\sigma^2$ of the pixel. In practice, we want to estimate the variance of a pixel across the $N$ samples $x_k$ it has received so far. The variance of $N$ samples is usually computed as: $$\sigma^2 = \frac{1}{N}\sum_{k=1}^N (x_k - \mu) ^2$$ However, this approach would imply keeping the average of each pixel's samples (which is the framebuffer itself so that's fine) as well as the values of all samples (that's not fine). Every time we want to estimate the error of a single pixel, we would then have to loop over all the previous samples to compute their difference with the average and get our variance $\sigma^2$. Keeping track of all the samples is infeasible in terms of memory consumption (that would be 2GB of RAM/VRAM for a mere 256 samples' floating-point luminance at 1080p) and looping over all the samples seen so far is computationally way too demanding. The practical solution is to evaluate the running-variance of the $N$ pixel samples $x_k$: $$\sigma^2 = \frac{1}{N - 1} \left(\sum_{k=1}^N x_k^2 - \left( \sum_{k=1}^N x_k \right)^2\right)$$ *Note that due to the nature of floating point numbers, this formula can have some precision issues. [This](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm) Wikipedia article presents good alternatives.* With the variance, we can compute a 95% confidence interval $I$: $$I = 1.96 \frac{\sigma}{\sqrt{N}}$$ This 95% confidence interval gives us a range around our samples mean $\mu$ and we can be 95% sure that, for the current number of samples $N$ and and their variance $\sigma$ that we used to compute this interval, the converged mean (true mean) of an infinite amount of samples is in that interval. ![Confidence interval visualization](./img/confidenceInterval.png) *Visualization of the confidence interval **I** (green arrows) around **µ**.* Judging by how $I$ is computed, it is easy to see that as the number of samples $N$ increases or the variance $\sigma^2$ decreases (and thus $\sigma$ decreases too), $I$ decreases. That should make sense since as we increase the number of samples, our mean $\mu$ should get closer and closer to the "true" mean value of the pixel (which is the value of the fully converged pixel when an infinite amount of samples are averaged together). If $I$ gets smaller, this means for our $\mu$ that it also gets closer to the "true" mean and that is the sign that our pixel has converged a little more. ![Confidence interval smaller visualization](./img/confidenceInterval2.png) *As the number of samples increases (or as the computed variance decreases), **I** gets smaller, meaning that the true mean is closer to our current mean which in turn means that our pixel has converged a little more.* Knowing that we can interpret $I$ as a measure of the convergence of our pixel, the question now becomes: **When do we assume that our pixel has sufficiently converged and stop sampling?** We use that user-given threshold $T$ we talked about earlier! Specifically, we can assume that if: $$I \leq T\mu$$ Then that pixel has converged enough for that threshold $T$. As a practical example, consider $T=0$. We then have: ``` math \displaylines{I \leq T\mu \\ I \leq 0} ``` If $I =0$, then the interval completely collapses on $\mu$. Said otherwise, $\mu$ **is** the true mean and our pixel has completely converged. Thus, for $T=0$, we will only stop sampling the pixel when it has fully converged. In practice, having $I=0$ is infeasible. After some experimentations a $T$ threshold of $0.1$ seems to target a visually very reasonable amount of noise. Any $T$ lower than that represents quite the overhead in terms of rendering times but can still provide some improvements on the perceived level of noise: ![cornellThreshold](./img/cornellThreshold.jpg) *Comparison of the noise level obtained after all pixels have converged and stopped sampling with a varying **T** threshold* Now if you look at the render with $T=0.1$, you'll notice that the caustic on the ceiling is awkwardly noisier than the rest of the image. There are some "holes" in the caustic (easy to see when you compare it to the $T=0.05$ render). This is an issue of the per-pixel approach used here: because that caustic has so much variance, it is actually possible that we sample a pixel on the ceiling 50 times (arbitrary number) without ever finding a path to the light. The sampled pixel will then remain gray-ish (diffuse color of the ceiling) instead of being bright because of the caustic. Our evaluation of the error of this pixel will then assume that it has converged since it has gone through 50 samples without that much of a change in radiance, meaning that it has a low variance, meaning that we can stop sampling it. But we shouldn't! If we had sampled it maybe 50 more times, we would have probably found a path that leads to the light, spiking the variance of the pixel which in turn would be sampled until the variance has attenuated enough so that our confidence interval $I$ is small again and gets below our threshold. One solution is simply to increase the minimum number of samples that must be traced through a pixel before evaluating its error. This way, the pixels of the image all get a chance to show their true variance and can't escape the adaptive sampling strategy! ![minimumSampleNumber](./img/minimumSampleNumber.jpg) *Impact of the minimum amount of samples to trace before starting evaluating adaptive sampling for the same **T** threshold.* This is however a poor solution since this forces all pixels of the image to be sampled at least 100 times, even the ones that would only need 50 samples. This is a waste of computational resources. A better way of estimating the error of the scene is presented in the "Hierarchical Adaptive Sampling" section. Nonetheless, this naive way of estimating the error of a pixel can provide very appreciable speedups in rendering time: ![Adaptive Sampling Speedup](./img/testedScenes.jpg) The application also offers the possibility to visualize where the rays are being concentrated on the image thanks to a heatmap (based on the number of rays per pixel): ![Adaptive sampling heatmap](./img/heatmap.jpg) ### TODO - Hierarchical adaptive sampling ### Normal mapping Normal mapping (or bump mapping) is a technique that aims at visually improving perceived geometric details without actually having the geometry for it. This is done through the use of normal maps which are textures that look like this:

*An example normal map.* Each pixel of this texture represents a perturbation of the geometric normal of the surface. Because the lighting of a surface strongly depends on its orientation (its normal), if the normal of the surface is altered, then the lighting will be too. The three channels RGB of a pixel of the texture respectively represent the X, Y and Z coordinates of the perturbed normal. However, you cannot just read from the texture using texture coordinates and assume that the RGB values of the pixel you get is going to be 1:1 the new normal of your surface: - The pixel are in $[0, 1]$ (or $[0, 255]$ if your prefer) but a normal is in $[-1, 1]$ - The normals of the texture are in their own coordinate space called tangent space. They are not in the same space as your model. They will have to be transformed. Bringing the pixel from $[0, 1]$ to the tangent space normal in $[-1, 1]$ is fairly straightforward: $$N_{TS} = Pixel * 2 - 1$$ The more interesting question is how to bring the normal from tangent space to the coordinate space of our model (and then the world) so that we can actually use our normal for the lighting calculations. To do that, we're going to need a transformation matrix, also called an ONB (Orthonormal Basis) in this case. This matrix will let us bring the tangent space normal to model space (a change of basis).

*TBN vectors used for the ONB matrix calculation. Illustration from* [LearnOpenGL](https://learnopengl.com/Advanced-Lighting/Normal-Mapping). But how do we find that matrix? The matrix is going to be built from three vectors: $T$, $B$ and $N$. $T$ and $B$ are called the tangent and bitangent vectors (depicted in the figure above). They represent the $X$ and $Y$ coordinates of our tangent space. $N$ is the geometric normal of our surface (or smooth normal if you're using interpolated vertex normals), it is the $Z$ coordinate of our tangent space. *Sidenote: you may have noticed that normal maps are blue-ish in general. This is due to the normals being mostly oriented towards the **Z** axis (which is the blue channel of the pixel) of the tangent space which is the normal of our surface. Since a normal map represents perturbations of the surface normal, it is expected that the normal map is going to be mostly the normal of our surface itself.* The goal is then to find these $T$ and $B$ vectors. We know that these two vectors are aligned with the $U$ and $V$ directions of the texture respectively. If $p_0$, $p_1$ and $p_2$ are the three vertices in counter-clockwise order of the triangle that we intersected and that they have $UV_1=(u_1, v_1)$, $UV_2=(u_2, v_2)$ and $UV_3=(u_3, v_3)$ for texture coordinates respectively, we can define two of the edges $e_1$ and $e_2$ of our triangle simply as: ```math \displaylines{e_1 = p_2-p_1 \\ e_2 = p_3-p_2} ``` *Sidenote again: Note that the **T** and **B** we're computing **need** to be aligned with the **U** and **V** directions of the texture. A generic algorithm ([Duff, 2017](https://graphics.pixar.com/library/OrthonormalB/paper.pdf) for example) for finding arbitrary tangent and bitangent vectors to a normal cannot be used here. The process of building the ONB for normal mapping here isn't the same as when building an ONB of the "shading space" for BSDF evaluation.* Similarly, we can define the differences $\Delta UV_1$ and $\Delta UV_2$ in textures coordinates of these vertices: ```math \displaylines{\Delta UV_1 = (\Delta U_1, \Delta V_1)=UV_2-UV_1 \\ \Delta UV_2 = (\Delta U_2, \Delta V_2) = UV_3-UV_2} ```

***e1** and **e2** can be expressed in terms of **ΔU\*T** and **ΔV\*B**. Illustration from* [LearnOpenGL](https://learnopengl.com/Advanced-Lighting/Normal-Mapping). These $\Delta UV_1$ and $\Delta UV_2$ can be understood as the edges $e_1$ and $e_2$ but expressed in the coordinate space of the texture, the $UV$ coordinate space. Because we know that $UV$ coordinates are aligned with the $T$ and $B$ vectors that we're looking for (remember the "TBN vectors used for the ONB matrix calculation" illustration), we can therefore express $e_1$ and $e_2$ in terms of $\Delta UV_1$, $\Delta UV_2$, $T$ and $B$: ```math \displaylines{e_1 = T*\Delta U_1 + B*\Delta V_1 \\ e_2 = T*\Delta U_2 + B*\Delta V_2} ``` In matrix form, this can be written as: ```math \begin{bmatrix} \uparrow & \uparrow \\ e_1 & e_2 \\ \downarrow & \downarrow \end{bmatrix} = \begin{bmatrix} \uparrow & \uparrow \\ T & B \\ \downarrow & \downarrow \end{bmatrix} \begin{bmatrix} \Delta U_1 & \Delta U_2 \\ \Delta V_1 & \Delta V_2 \end{bmatrix} ``` We can then solve for $T$ and $B$ by multiplying by the inverse of the $\left[ \Delta U_1 \Delta U_2, \Delta V_1 \Delta V_2 \right]$ matrix: ```math \begin{bmatrix} \uparrow & \uparrow \\ T & B \\ \downarrow & \downarrow \end{bmatrix} = \begin{bmatrix} \uparrow & \uparrow \\ e_1 & e_2 \\ \downarrow & \downarrow \end{bmatrix} \frac{1}{\Delta U_1\Delta V_2 -\Delta V_1\Delta U_2} \begin{bmatrix} \Delta V_2 & -\Delta U_2 \\ -\Delta V_1 & \Delta U_1 \end{bmatrix} ``` This stems from the fact that the inverse of a 2x2 matrix is given by: ```math \begin{bmatrix} a & b \\ c & d \end{bmatrix}^{-1} = \frac{1}{ad-bc} \begin{bmatrix} d & -b \\ -c & a \end{bmatrix} ``` *Sidenote yet again: when the determinant $ad-bc$ of the matrix is equal to 0, we're getting a division by 0 in the fraction and we cannot compute the inverse of the matrix. This is why matrices that have a determinant equal to 0 cannot be inversed. Such a matrix is said to be singular.* Our $T$ and $B$ vectors now computed, the TBN matrix that will allow us to pass from the normal of our normal map (tangent space) to the model-space is given by: ```math Mat_{TBN} = \begin{bmatrix} \uparrow & \uparrow & \uparrow \\ T & B & N \\ \downarrow & \downarrow & \downarrow \end{bmatrix} ``` The final normal that we can use for our shading is thus: $$N_{shading}=Mat_{TBN}*N_{TS}$$ TODO visual impact ### Interactive ImGui Interface & FPS Camera When rendering on the GPU, an ImGui interface is available to help playing with the parameters of the path tracer. The goal of the interface really is to allow experimentations in terms of performance and visual impact. ![ImGui interface](./img/imguiInterface.jpg) The GUI also offers a first-person camera to move around the scene: - Right click to pan - Left click for rotating the view - Mouse wheel for zooming in/out ### Visualization Again with the goal of experimenting and better understand what is happening under the hood, the "Display view" option in the ImGui interface under "Render settings" allows to change what the viewport is displaying. For example, The AOVs (Arbitrary Output Values, which are additional data fed to the denoiser to help it denoiser better) of the denoiser such as the normals and albedo color of the scene can be visualized (this can also serve for debugging and making sure everything is in order) ![Denoiser normal visualization](./img/denoiserAlbedoNormal.jpg) More visualization options are available (adaptive sampling heatmap as used in the [adaptive sampling section](#per-pixel-adaptive-sampling) is one of them), have a look at them in the app! ### ASSIMP [ASSIMP](https://github.com/assimp/assimp) is a library that provides a uniform interface for parsing [many](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md) different file formats. Although not all extensions of some important file formats are not supported (ASSIMP doesn't seem to be recognizing the PBR extension of OBJ ("aniso" keyword issue) files and doesn't support all GLTF 2.0 extensions for example), ASSIMP vastly improves the range of scene files supported by the application. ### TODO - Optimized application startup time with: - Multithreaded texture loading - Asynchronous path tracing kernel compilation ### TODO - Intel Open Image Denoise + Normals & Albedo AOV support ## TODO Filter functions ================================================ FILE: README_data/img/LayeredBSDF.drawio ================================================ ================================================ FILE: SceneCredits.txt ================================================ Bistro: https://developer.nvidia.com/orca/amazon-lumberyard-bistro Beeple Zero Day: https://developer.nvidia.com/orca/beeple-zero-day Blender 4.1 Splash: https://www.blender.org/download/demo-files/ Contemporary bedroom: https://www.cgtrader.com/free-3d-models/interior/bedroom/bedroom-interior-contemporer Glass dragon: https://benedikt-bitterli.me/resources/ Suzanne Caustics (old render): Just Suzanne from Blender with some very small light inside of it McLaren P1 + Environment: https://sketchfab.com/3d-models/mclaren-p1-6d6072b79ae444058a8f7c7ffd548fb4#download + https://www.cgtrader.com/items/4740776/download-page PBRT Dragon Indirect Lighting: https://www.pbrt.org/scenes-v3 Rolex: https://www.cgtrader.com/items/4228612/download-page LuxCoreBalls: https://luxcorerender.org/example-scenes/ Mitsuba-knob: https://casual-effects.com/data/ Porsche 718 Cayman GT4: https://www.cgtrader.com/free-3d-models/car/sport-car/porsche-718-cayman-gt4-5734530d-8afb-4852-9421-b71bde9adce3 ================================================ FILE: cmake/Clip.cmake ================================================ add_subdirectory(thirdparties/clip) # Enabling CLIP's image support add_compile_definitions(CLIP_ENABLE_IMAGE=1) ================================================ FILE: cmake/SetupASSIMP.cmake ================================================ # We're going to disable shared libs for assimp but we need to save # the current value of BUILD_SHARED_LIBS before overriding it with # OFF (for assimp only) set(BUILD_SHARED_LIBS_BACKUP ${BUILD_SHARED_LIBS}) set(CMAKE_BUILD_TYPE_BACKUP ${CMAKE_BUILD_TYPE}) set(BUILD_SHARED_LIBS OFF) set(ASSIMP_NO_EXPORT ON) set(ASSIMP_BUILD_TESTS OFF) set(ASSIMP_INSTALL_PDB OFF) set(ASSIMP_BUILD_ZLIB ON) set(ASSIMP_BUILD_ASSIMP_VIEW OFF) set(ASSIMP_SUBMODULE_DIR ${CMAKE_SOURCE_DIR}/thirdparties/ASSIMP-Fork) if(NOT EXISTS ${ASSIMP_SUBMODULE_DIR}/code) # Making sure that the HIPRT submodule was cloned message(FATAL_ERROR "The ASSIMP submodule couldn't be found. Did you forget to clone the submodules? Run 'git submodule update --init --recursive'.") endif() add_subdirectory(${ASSIMP_SUBMODULE_DIR}) # Restoring varaibles set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_BACKUP}) set(CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE_BACKUP}) ================================================ FILE: cmake/SetupHIPRT.cmake ================================================ set(HIPRT_SUBMODULE_DIR ${CMAKE_SOURCE_DIR}/thirdparties/HIPRT-Fork) if(NOT EXISTS ${HIPRT_SUBMODULE_DIR}/hiprt) # Making sure that the HIPRT submodule was cloned message(FATAL_ERROR "The HIPRT submodule couldn't be found. Did you forget to clone the submodules? Run 'git submodule update --init --recursive'.") endif() set(NO_ENCRYPT ON) set(NO_UNITTEST ON) #option(HIPRT_PREFER_HIP_5 "Prefer HIP 5" OFF) set(CMAKE_EXE_LINKER_FLAGS_DEBUGGPU "") add_subdirectory(${HIPRT_SUBMODULE_DIR}) # Now that we built HIPRT, we can set the variables that will be used in the rest of the CMake # to find the headers, the libraries, ... set(HIPRT_BIN_DIR ${HIPRT_SUBMODULE_DIR}/dist/bin/${CMAKE_BUILD_TYPE}) set(HIPRT_HEADERS_DIR ${HIPRT_SUBMODULE_DIR}/hiprt) # The GPU compiler will need this additional include folder to properly compile some kernels add_compile_definitions(KERNEL_COMPILER_ADDITIONAL_INCLUDE="${HIPRT_SUBMODULE_DIR}") # Replacing backslashes in the Windows paths that lead to wrong escape character # note that the four backslashes \\\\ are required because we need a regular expression that # compiles to '\'. # \\ is converted by CMake to a single '\' # so \\\\ is converted by CMake to '\\' which is the regular expression for the single '\' character STRING(REGEX REPLACE "\\\\" "/" HIPRT_HEADERS_DIR ${HIPRT_HEADERS_DIR}) link_directories(${HIPRT_BIN_DIR}) ================================================ FILE: cmake/SetupOIDN.cmake ================================================ if (WIN32) set(OIDN_URL https://github.com/RenderKit/oidn/releases/download/v2.3.0/oidn-2.3.0.x64.windows.zip) elseif(UNIX) set(OIDN_URL https://github.com/RenderKit/oidn/releases/download/v2.3.0/oidn-2.3.0.x86_64.linux.tar.gz) endif() FetchContent_Declare( oidnbinaries URL ${OIDN_URL} ) FetchContent_MakeAvailable( oidnbinaries ) ================================================ FILE: cmake/SetupOrochi.cmake ================================================ set(OROCHI_SUBMODULE_DIR ${CMAKE_SOURCE_DIR}/thirdparties/Orochi-Fork) if(NOT EXISTS ${OROCHI_SUBMODULE_DIR}/ParallelPrimitives) # Making sure that the Orochi submodule was cloned message(FATAL_ERROR "The Orochi submodule couldn't be found. Did you forget to clone the submodules? Run 'git submodule update --init --recursive'.") endif() set(OROCHI_BIN_DIR ${OROCHI_SUBMODULE_DIR}) set(OROCHI_SOURCES_DIR ${OROCHI_SUBMODULE_DIR}/Orochi) set(CUEW_SOURCES_DIR ${OROCHI_SUBMODULE_DIR}/contrib/cuew) set(HIPEW_SOURCES_DIR ${OROCHI_SUBMODULE_DIR}/contrib/hipew) STRING(REGEX REPLACE "\\\\" "/" OROCHI_SOURCES_DIR ${OROCHI_SOURCES_DIR}) STRING(REGEX REPLACE "\\\\" "/" CUEW_SOURCES_DIR ${CUEW_SOURCES_DIR}) STRING(REGEX REPLACE "\\\\" "/" HIPEW_SOURCES_DIR ${HIPEW_SOURCES_DIR}) # TODO remove when issue #7 (https://github.com/GPUOpen-LibrariesAndSDKs/HIPRT/issues/7) is fixed if (true) file(COPY ${OROCHI_SUBMODULE_DIR}/ParallelPrimitives DESTINATION ${CMAKE_SOURCE_DIR}/contrib/Orochi) file(COPY ${HIPRT_SUBMODULE_DIR}/hiprt/impl DESTINATION ${CMAKE_SOURCE_DIR}/hiprt) file(GLOB HIPRT_FILES_TO_COPY ${HIPRT_SUBMODULE_DIR}/hiprt/*.h ${HIPRT_SUBMODULE_DIR}/hiprt/*.in) file(COPY ${HIPRT_FILES_TO_COPY} DESTINATION ${CMAKE_SOURCE_DIR}/hiprt/) endif() ================================================ FILE: cmake/SetupTracy.cmake ================================================ add_subdirectory(thirdparties/tracy) set(DISABLE_TRACY_PROFILING ON) if (DISABLE_TRACY_PROFILING) get_target_property(TRACY_INTERFACE TracyClient INTERFACE_COMPILE_DEFINITIONS) list(REMOVE_ITEM TRACY_INTERFACE "TRACY_ENABLE") set_target_properties(TracyClient PROPERTIES INTERFACE_COMPILE_DEFINITIONS "${TRACY_INTERFACE}") endif() ================================================ FILE: data/BRDFsData/GGX/Glass/ExponentCorrection.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 110, "id": "6992e79b-eec0-4bd5-8e69-13e2a002e919", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "relative_eta * -13.439691641502959+roughness * -14.648819997830152+relative_eta*relative_eta * 6.8789072129524165+relative_eta * roughness * 2.277848368999256+roughness*roughness * 30.73190238514913+relative_eta*relative_eta*relative_eta * -1.1621500927731714+relative_eta*relative_eta * roughness * 0.37498830694981306+relative_eta * roughness*roughness * 0.8224134608255266+roughness*roughness*roughness * -21.325291375291403+\n", "\n", "Fitting Errors:\n", "Mean Squared Error (MSE): 1.513004991070829\n", "Root Mean Squared Error (RMSE): 1.2300426785566543\n", "Mean Absolute Error (MAE): 0.6879266691669605\n", "\n", "\n", "float lower_relative_eta_bound;\n", "float lower_correction;\n", "if (relative_eta > 1.01f && relative_eta <= 1.02f)\n", "{\n", "\tlower_relative_eta_bound = 1.01f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.4f, 2.45f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.45f, 2.4665f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(2.4665f, 2.52f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(2.52f, 2.55f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = 2.55f;\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(2.55f, 2.585f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(2.585f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta > 1.02f && relative_eta <= 1.03f)\n", "{\n", "\tlower_relative_eta_bound = 1.02f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.4f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.475f, 2.51f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(2.51f, 2.54f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(2.54f, 2.565f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = hippt::lerp(2.565f, 2.57f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(2.57f, 2.59f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(2.59f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta > 1.03f && relative_eta <= 1.1f)\n", "{\n", "\tlower_relative_eta_bound = 1.03f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.4f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.475f, 2.51f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(2.51f, 2.544f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(2.544f, 2.565f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = hippt::lerp(2.565f, 2.58f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(2.58f, 2.6f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(2.6f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta > 1.1f && relative_eta <= 1.2f)\n", "{\n", "\tlower_relative_eta_bound = 1.1f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.475f, 2.54f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(2.54f, 2.575f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(2.575f, 2.61f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = hippt::lerp(2.61f, 2.63f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(2.63f, 2.6f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(2.6f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta > 1.2f && relative_eta <= 1.4f)\n", "{\n", "\tlower_relative_eta_bound = 1.2f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 1.8f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(1.8f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.475f, 2.55f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(2.55f, 2.65f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(2.65f, 2.675f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = hippt::lerp(2.675f, 2.7f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(2.7f, 2.675f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(2.675f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta > 1.4f && relative_eta <= 1.5f)\n", "{\n", "\tlower_relative_eta_bound = 1.4f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 1.8f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(1.8f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.475f, 2.7f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(2.7f, 2.875f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(2.875f, 2.925f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = hippt::lerp(2.925f, 2.95f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(2.95f, 2.8f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(2.8f, 2.55f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta > 1.5f && relative_eta <= 2.0f)\n", "{\n", "\tlower_relative_eta_bound = 1.5f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 1.6f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(1.6f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.475f, 2.7f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(2.7f, 2.95f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(2.95f, 3.1f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = 3.1f;\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(3.1f, 3.05f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(3.05f, 2.57f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta > 2.0f && relative_eta <= 2.4f)\n", "{\n", "\tlower_relative_eta_bound = 2.0f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(1.5f, 2.2f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(2.2f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.475f, 2.75f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(2.75f, 3.5f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(3.5f, 4.85f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = hippt::lerp(4.85f, 6.0f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(6.0f, 7.0f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(7.0f, 2.57f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta > 2.4f && relative_eta <= 3.0f)\n", "{\n", "\tlower_relative_eta_bound = 2.4f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(1.5f, 2.0f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(2.0f, 2.44f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.44f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.475f, 3.0f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(3.0f, 3.8f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(3.8f, 7.0f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = hippt::lerp(7.0f, 10.0f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(10.0f, 12.0f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(12.0f, 3.9f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta > 3.0f)\n", "{\n", "\tlower_relative_eta_bound = 3.0f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\tlower_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\tlower_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\tlower_correction = hippt::lerp(1.5f, 1.7f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\tlower_correction = hippt::lerp(1.7f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\tlower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\tlower_correction = hippt::lerp(2.475f, 2.9f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\tlower_correction = hippt::lerp(2.9f, 3.8f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\tlower_correction = hippt::lerp(3.8f, 7.5f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\tlower_correction = hippt::lerp(7.5f, 12.0f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\tlower_correction = hippt::lerp(12.0f, 13.75f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\tlower_correction = hippt::lerp(13.75f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "float higher_relative_eta_bound;\n", "float higher_correction;\n", "if (relative_eta <= 1.01f)\n", "{\n", "\thigher_relative_eta_bound = 1.01f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.4f, 2.45f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.45f, 2.4665f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(2.4665f, 2.52f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(2.52f, 2.55f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = 2.55f;\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(2.55f, 2.585f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(2.585f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta <= 1.02f)\n", "{\n", "\thigher_relative_eta_bound = 1.02f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.4f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.475f, 2.51f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(2.51f, 2.54f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(2.54f, 2.565f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = hippt::lerp(2.565f, 2.57f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(2.57f, 2.59f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(2.59f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta <= 1.03f)\n", "{\n", "\thigher_relative_eta_bound = 1.03f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.4f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.475f, 2.51f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(2.51f, 2.544f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(2.544f, 2.565f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = hippt::lerp(2.565f, 2.58f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(2.58f, 2.6f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(2.6f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta <= 1.1f)\n", "{\n", "\thigher_relative_eta_bound = 1.1f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.475f, 2.54f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(2.54f, 2.575f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(2.575f, 2.61f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = hippt::lerp(2.61f, 2.63f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(2.63f, 2.6f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(2.6f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta <= 1.2f)\n", "{\n", "\thigher_relative_eta_bound = 1.2f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 1.8f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(1.8f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.475f, 2.55f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(2.55f, 2.65f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(2.65f, 2.675f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = hippt::lerp(2.675f, 2.7f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(2.7f, 2.675f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(2.675f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta <= 1.4f)\n", "{\n", "\thigher_relative_eta_bound = 1.4f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 1.8f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(1.8f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.475f, 2.7f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(2.7f, 2.875f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(2.875f, 2.925f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = hippt::lerp(2.925f, 2.95f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(2.95f, 2.8f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(2.8f, 2.55f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta <= 1.5f)\n", "{\n", "\thigher_relative_eta_bound = 1.5f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 1.6f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(1.6f, 2.3f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.475f, 2.7f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(2.7f, 2.95f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(2.95f, 3.1f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = 3.1f;\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(3.1f, 3.05f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(3.05f, 2.57f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta <= 2.0f)\n", "{\n", "\thigher_relative_eta_bound = 2.0f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(1.5f, 2.2f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(2.2f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.475f, 2.75f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(2.75f, 3.5f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(3.5f, 4.85f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = hippt::lerp(4.85f, 6.0f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(6.0f, 7.0f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(7.0f, 2.57f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta <= 2.4f)\n", "{\n", "\thigher_relative_eta_bound = 2.4f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(1.5f, 2.0f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(2.0f, 2.44f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.44f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.475f, 3.0f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(3.0f, 3.8f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(3.8f, 7.0f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = hippt::lerp(7.0f, 10.0f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(10.0f, 12.0f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(12.0f, 3.9f, (roughness - 0.9f) / 0.1f);\n", "}\n", "else if (relative_eta <= 3.0f)\n", "{\n", "\thigher_relative_eta_bound = 3.0f;\n", "\n", "\tif (roughness <= 0.0f)\n", "\t\thigher_correction = 2.5f;\n", "\telse if (roughness <= 0.1f)\n", "\t\thigher_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f);\n", "\telse if (roughness <= 0.2f)\n", "\t\thigher_correction = hippt::lerp(1.5f, 1.7f, (roughness - 0.1f) / 0.1f);\n", "\telse if (roughness <= 0.3f)\n", "\t\thigher_correction = hippt::lerp(1.7f, 2.38f, (roughness - 0.2f) / 0.1f);\n", "\telse if (roughness <= 0.4f)\n", "\t\thigher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f);\n", "\telse if (roughness <= 0.5f)\n", "\t\thigher_correction = hippt::lerp(2.475f, 2.9f, (roughness - 0.4f) / 0.1f);\n", "\telse if (roughness <= 0.6f)\n", "\t\thigher_correction = hippt::lerp(2.9f, 3.8f, (roughness - 0.5f) / 0.1f);\n", "\telse if (roughness <= 0.7f)\n", "\t\thigher_correction = hippt::lerp(3.8f, 7.5f, (roughness - 0.6f) / 0.1f);\n", "\telse if (roughness <= 0.8f)\n", "\t\thigher_correction = hippt::lerp(7.5f, 12.0f, (roughness - 0.7f) / 0.1f);\n", "\telse if (roughness <= 0.9f)\n", "\t\thigher_correction = hippt::lerp(12.0f, 13.75f, (roughness - 0.8f) / 0.1f);\n", "\telse if (roughness <= 1.0f)\n", "\t\thigher_correction = hippt::lerp(13.75f, 2.5f, (roughness - 0.9f) / 0.1f);\n", "}\n", "\n", "return hippt::lerp(lower_correction, higher_correction, (relative_eta - lower_relative_eta_bound) / (higher_relative_eta_bound - lower_relative_eta_bound));\n" ] } ], "source": [ "import numpy as np\n", "import re\n", "from sklearn.preprocessing import PolynomialFeatures\n", "from sklearn.linear_model import LinearRegression\n", "\n", "DEGREE = 3\n", "\n", "# Notebook for fitting a polynomial for the correction exponent for glass dielectrics \n", "# given a certain IOR and roughness\n", "#\n", "# The 'values' array has been eyeballed manually to try and minimize the visual energy loss/gain\n", "# The idea of the polynomial is to replace the massive if(), else if(), else if() that would have been\n", "# needed otherwise\n", "#\n", "# TURNS OUT THAT THE FITTING ERROR IS TOO LARGE AND THE POLYNOMIAL\n", "# FITTED BY THIS SCRIPT IS THUS NEVER USED IN THE RENDERER\n", "# \n", "# Instead, we just translate the double entry table IO-Roughness into a massive and disgusting\n", "# if(), else if(). This is exactly what we wanted to avoid but in the end, it's efficient and fits well.\n", "# The only single downside is that it looks disgusting but who cares?\n", "\n", "ior_values = [1.01, 1.02, 1.03, 1.1, 1.2, 1.4, 1.5, 2.0, 2.4, 3.0]\n", "roughness_values = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]\n", "\n", "# Table of output values (for each combination of IOR and roughness)\n", "# For example, these could represent reflectance values:\n", "values = np.array([\n", " [2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5], # Roughness 0.0\n", " [2.5, 2.5, 2.5, 2.5, 1.8, 1.8, 1.6, 1.5, 1.5, 1.5], # Roughness 0.1\n", " [2.3, 2.3, 2.3, 2.3, 2.3, 2.3, 2.3, 2.2, 2, 1.7], # Roughness 0.2\n", " [2.4, 2.4, 2.4, 2.38, 2.38, 2.38, 2.38, 2.38, 2.44, 2.38], # Roughness 0.3\n", " [2.45, 2.475, 2.475, 2.475, 2.475, 2.475, 2.475, 2.475, 2.475, 2.475], # Roughness 0.4\n", " [2.4665, 2.51, 2.51, 2.54, 2.55, 2.7, 2.7, 2.75, 3, 2.9], # Roughness 0.5\n", " [2.52, 2.54, 2.544, 2.575, 2.65, 2.875, 2.95, 3.5, 3.8, 3.8], # Roughness 0.6\n", " [2.55, 2.565, 2.565, 2.61, 2.675, 2.925, 3.1, 4.85, 7, 7.5], # Roughness 0.7\n", " [2.55, 2.57, 2.58, 2.63, 2.7, 2.95, 3.1, 6, 10, 12], # Roughness 0.8\n", " [2.585, 2.59, 2.6, 2.6, 2.675, 2.8, 3.05, 7, 12, 13.75], # Roughness 0.9\n", " [2.5, 2.5, 2.5, 2.5, 2.5, 2.55, 2.57, 2.57, 3.9, 2.5], # Roughness 1.0\n", "])\n", "\n", "# Create combinations of IOR and roughness\n", "ior, roughness = np.meshgrid(ior_values, roughness_values) # Create grid\n", "ior = ior.ravel() # Flatten to 1D array\n", "roughness = roughness.ravel() # Flatten to 1D array\n", "outputs = values.ravel() # Flatten table of outputs\n", "\n", "# Prepare design matrix for polynomial features\n", "X = np.column_stack((ior, roughness))\n", "poly = PolynomialFeatures(degree=DEGREE, include_bias=False) # Bias=False avoids adding constant\n", "X_poly = poly.fit_transform(X)\n", "\n", "# Fit the polynomial model\n", "model = LinearRegression()\n", "model.fit(X_poly, outputs)\n", "\n", "# Extract coefficients\n", "intercept = model.intercept_ # Constant term\n", "coefficients = model.coef_ # Remaining terms\n", "\n", "# Display results\n", "# print(\"Intercept (constant term):\", intercept)\n", "# print(\"Coefficients (for polynomial terms):\", coefficients)\n", "\n", "# Polynomial interpretation\n", "terms = poly.get_feature_names_out([\"relative_eta\", \"roughness\"])\n", "for term, coef in zip(terms, coefficients):\n", " # Replace '^2' with ' * IOR' or ' * Roughness' for terms like IOR^2\n", " term = re.sub(r\"(relative_eta|roughness)\\^([0-9]+)\", lambda m: \"*\".join([m.group(1)] * int(m.group(2))), term)\n", "\n", " # If the term has more than one factor, join them with ' * '\n", " formatted_term = \" * \".join(term.split(\" \"))\n", " \n", " print(f\"{formatted_term} * {coef}\", end='+')\n", "\n", "from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score\n", "\n", "# Predict the values using the fitted model\n", "predicted_outputs = model.predict(X_poly)\n", "\n", "# Calculate error metrics\n", "mse = mean_squared_error(outputs, predicted_outputs)\n", "rmse = np.sqrt(mse)\n", "mae = mean_absolute_error(outputs, predicted_outputs)\n", "\n", "# Display the errors\n", "print(\"\\n\\nFitting Errors:\")\n", "print(f\"Mean Squared Error (MSE): {mse}\")\n", "print(f\"Root Mean Squared Error (RMSE): {rmse}\")\n", "print(f\"Mean Absolute Error (MAE): {mae}\")\n", "\n", "#######\n", "# Printing the massive if(), else if() block so that it is ready to be copy pasted in the shader\n", "#######\n", "\n", "print()\n", "print()\n", "\n", "print(\"float lower_relative_eta_bound;\")\n", "print(\"float lower_correction;\")\n", "for i in range(0, len(ior_values)):\n", " relative_eta = ior_values[i]\n", " next_relative_eta = 1\n", " if (i != len(ior_values) - 1):\n", " next_relative_eta = ior_values[i + 1]\n", "\n", " if (i == 0):\n", " print(\"if (\", end='');\n", " else:\n", " print(\"else if (\", end='');\n", " if (i != len(ior_values) - 1):\n", " print(\"relative_eta > \" + str(relative_eta) + \"f && relative_eta <= \" + str(next_relative_eta) + \"f)\\n{\")\n", " else:\n", " print(\"relative_eta > \" + str(relative_eta) + \"f)\\n{\")\n", " \n", " print(\"\\tlower_relative_eta_bound = \" + str(relative_eta) + \"f;\\n\")\n", " for j in range(0, len(roughness_values)):\n", " roughness = roughness_values[j]\n", " str_roughness = \"\"\n", " str_roughness_minus_1 = \"\"\n", " if (j == 0):\n", " str_roughness = \"0.0f\"\n", " elif (j == len(roughness_values) - 1):\n", " str_roughness = \"1.0f\"\n", " str_roughness_minus_1 = str(round(roughness - 1 / (len(roughness_values) - 1), 1)) + \"f\"\n", " else:\n", " str_roughness = str(roughness) + \"f\"\n", " str_roughness_minus_1 = str(round(roughness - 1 / (len(roughness_values) - 1), 1)) + \"f\"\n", "\n", " print(\"\\t\", end='')\n", " if (j == 0):\n", " print(\"if (\", end='')\n", " else:\n", " print(\"else if (\", end='')\n", "\n", " print(\"roughness <= \" + str_roughness + \")\")\n", "\n", " if (j == 0):\n", " print(\"\\t\\tlower_correction = \" + str(values[j][i]) + \"f;\")\n", " else:\n", " lower_lerp = values[j - 1][i]\n", " higher_lerp = values[j][i]\n", " if (lower_lerp == higher_lerp):\n", " print(\"\\t\\tlower_correction = \" + str(lower_lerp) + \"f;\")\n", " else:\n", " print(\"\\t\\tlower_correction = hippt::lerp(\" + str(lower_lerp) + \"f, \" + str(higher_lerp) + \"f, (roughness - \" + str_roughness_minus_1 + \") / 0.1f);\")\n", "\n", " print(\"}\")\n", " \n", "print(\"float higher_relative_eta_bound;\")\n", "print(\"float higher_correction;\")\n", "for i in range(0, len(ior_values)):\n", " relative_eta = ior_values[i]\n", "\n", " if (i == 0):\n", " print(\"if (\", end='');\n", " else:\n", " print(\"else if (\", end='');\n", " print(\"relative_eta <= \" + str(relative_eta) + \"f)\\n{\")\n", "\n", " print(\"\\thigher_relative_eta_bound = \" + str(relative_eta) + \"f;\\n\")\n", " for j in range(0, len(roughness_values)):\n", " roughness = roughness_values[j]\n", " str_roughness = \"\"\n", " str_roughness_minus_1 = \"\"\n", " if (j == 0):\n", " str_roughness = \"0.0f\"\n", " elif (j == len(roughness_values) - 1):\n", " str_roughness = \"1.0f\"\n", " str_roughness_minus_1 = str(round(roughness - 1 / (len(roughness_values) - 1), 1)) + \"f\"\n", " else:\n", " str_roughness = str(roughness) + \"f\"\n", " str_roughness_minus_1 = str(round(roughness - 1 / (len(roughness_values) - 1), 1)) + \"f\"\n", "\n", " print(\"\\t\", end='')\n", " if (j == 0):\n", " print(\"if (\", end='')\n", " else:\n", " print(\"else if (\", end='')\n", "\n", " print(\"roughness <= \" + str_roughness + \")\")\n", "\n", " if (j == 0):\n", " print(\"\\t\\thigher_correction = \" + str(values[j][i]) + \"f;\")\n", " else:\n", " lower_lerp = values[j - 1][i]\n", " higher_lerp = values[j][i]\n", " if (lower_lerp == higher_lerp):\n", " print(\"\\t\\thigher_correction = \" + str(lower_lerp) + \"f;\")\n", " else:\n", " print(\"\\t\\thigher_correction = hippt::lerp(\" + str(lower_lerp) + \"f, \" + str(higher_lerp) + \"f, (roughness - \" + str_roughness_minus_1 + \") / 0.1f);\")\n", "\n", " print(\"}\")\n", "\n", "print(\"\\nreturn hippt::lerp(lower_correction, higher_correction, (relative_eta - lower_relative_eta_bound) / (higher_relative_eta_bound - lower_relative_eta_bound));\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.5" } }, "nbformat": 4, "nbformat_minor": 5 } ================================================ FILE: data/GLTFs/cornell_pbr.gltf ================================================ { "asset":{ "generator":"Khronos glTF Blender I/O v4.2.70", "version":"2.0" }, "extensionsUsed":[ "KHR_materials_clearcoat", "KHR_materials_transmission", "KHR_materials_emissive_strength", "KHR_materials_specular", "KHR_materials_ior" ], "scene":0, "scenes":[ { "name":"Scene", "nodes":[ 0, 1, 2 ] } ], "nodes":[ { "mesh":0, "name":"cornell.001", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ] }, { "camera":0, "name":"Camera.001", "translation":[ -0.10000000149011612, 1, 6.499999523162842 ] }, { "mesh":1, "name":"Sphere", "scale":[ 0.20000000298023224, 0.20000000298023224, 0.20000000298023224 ], "translation":[ 0.3562004268169403, 0.800000011920929, 0.47223222255706787 ] } ], "cameras":[ { "name":"Camera.001", "perspective":{ "aspectRatio":1.7777777777777777, "yfov":0.4038851857185364, "zfar":100, "znear":0.10000000149011612 }, "type":"perspective" } ], "materials":[ { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_specular":{ "specularFactor":0 } }, "name":"shortBox.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.09947466850280762, 0.09947466850280762, 0.09947466850280762, 1 ], "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"leftWall.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 1, 0, 0, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 1, 1 ], "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_emissive_strength":{ "emissiveStrength":100 } }, "name":"light.001", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0.9009850025177002 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"tallBox.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 1, 1, 0, 1 ], "metallicFactor":0, "roughnessFactor":0.6837720274925232 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"floor.001", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0.8418859839439392 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"backWall.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 0, 1, 1 ], "metallicFactor":0, "roughnessFactor":0.9009850025177002 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"ceiling.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 1, 1, 1 ], "metallicFactor":0, "roughnessFactor":0.9009850025177002 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"rightWall.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 1, 0, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_transmission":{ "transmissionFactor":1 }, "KHR_materials_ior":{ "ior":1.4500000476837158 } }, "name":"Material.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.8000000715255737, 0, 0.004255097359418869, 1 ], "metallicFactor":0, "roughnessFactor":0 } } ], "meshes":[ { "name":"cornell.001", "primitives":[ { "attributes":{ "POSITION":0, "NORMAL":1 }, "indices":2, "material":0 }, { "attributes":{ "POSITION":3, "NORMAL":4 }, "indices":5, "material":1 }, { "attributes":{ "POSITION":6, "NORMAL":7 }, "indices":5, "material":2 }, { "attributes":{ "POSITION":8, "NORMAL":9 }, "indices":2, "material":3 }, { "attributes":{ "POSITION":10, "NORMAL":11 }, "indices":5, "material":4 }, { "attributes":{ "POSITION":12, "NORMAL":13 }, "indices":5, "material":5 }, { "attributes":{ "POSITION":14, "NORMAL":15 }, "indices":5, "material":6 }, { "attributes":{ "POSITION":16, "NORMAL":17 }, "indices":5, "material":7 } ] }, { "name":"Sphere", "primitives":[ { "attributes":{ "POSITION":18, "NORMAL":19, "TEXCOORD_0":20 }, "indices":21, "material":8 } ] } ], "accessors":[ { "bufferView":0, "componentType":5126, "count":20, "max":[ 0.699999988079071, 0.75, 0 ], "min":[ -0.05000000074505806, 0, -0.6000000238418579 ], "type":"VEC3" }, { "bufferView":1, "componentType":5126, "count":20, "type":"VEC3" }, { "bufferView":2, "componentType":5123, "count":30, "type":"SCALAR" }, { "bufferView":3, "componentType":5126, "count":4, "max":[ -0.9900000095367432, 0.9900000095367432, 0 ], "min":[ -1.0199999809265137, -1.0399999618530273, -1.9900000095367432 ], "type":"VEC3" }, { "bufferView":4, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":5, "componentType":5123, "count":6, "type":"SCALAR" }, { "bufferView":6, "componentType":5126, "count":4, "max":[ 0.23000000417232513, 0.1599999964237213, -1.9800000190734863 ], "min":[ -0.23999999463558197, -0.2199999988079071, -1.9800000190734863 ], "type":"VEC3" }, { "bufferView":7, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":8, "componentType":5126, "count":20, "max":[ 0.03999999910593033, 0.09000000357627869, 0 ], "min":[ -0.7099999785423279, -0.6700000166893005, -1.2000000476837158 ], "type":"VEC3" }, { "bufferView":9, "componentType":5126, "count":20, "type":"VEC3" }, { "bufferView":10, "componentType":5126, "count":4, "max":[ 1, 0.9900000095367432, 0 ], "min":[ -1.0099999904632568, -1.0399999618530273, 0 ], "type":"VEC3" }, { "bufferView":11, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":12, "componentType":5126, "count":4, "max":[ 1, -1.0399999618530273, 0 ], "min":[ -1.0199999809265137, -1.0399999618530273, -1.9900000095367432 ], "type":"VEC3" }, { "bufferView":13, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":14, "componentType":5126, "count":4, "max":[ 1, 0.9900000095367432, -1.9900000095367432 ], "min":[ -1.0199999809265137, -1.0399999618530273, -1.9900000095367432 ], "type":"VEC3" }, { "bufferView":15, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":16, "componentType":5126, "count":4, "max":[ 1, 0.9900000095367432, 0 ], "min":[ 1, -1.0399999618530273, -1.9900000095367432 ], "type":"VEC3" }, { "bufferView":17, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":18, "componentType":5126, "count":4223, "max":[ 0.9999998807907104, 1, 0.9999997615814209 ], "min":[ -0.9999997019767761, -1, -1 ], "type":"VEC3" }, { "bufferView":19, "componentType":5126, "count":4223, "type":"VEC3" }, { "bufferView":20, "componentType":5126, "count":4223, "type":"VEC2" }, { "bufferView":21, "componentType":5123, "count":24192, "type":"SCALAR" } ], "bufferViews":[ { "buffer":0, "byteLength":240, "byteOffset":0, "target":34962 }, { "buffer":0, "byteLength":240, "byteOffset":240, "target":34962 }, { "buffer":0, "byteLength":60, "byteOffset":480, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":540, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":588, "target":34962 }, { "buffer":0, "byteLength":12, "byteOffset":636, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":648, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":696, "target":34962 }, { "buffer":0, "byteLength":240, "byteOffset":744, "target":34962 }, { "buffer":0, "byteLength":240, "byteOffset":984, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1224, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1272, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1320, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1368, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1416, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1464, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1512, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1560, "target":34962 }, { "buffer":0, "byteLength":50676, "byteOffset":1608, "target":34962 }, { "buffer":0, "byteLength":50676, "byteOffset":52284, "target":34962 }, { "buffer":0, "byteLength":33784, "byteOffset":102960, "target":34962 }, { "buffer":0, "byteLength":48384, "byteOffset":136744, "target":34963 } ], "buffers":[ { "byteLength":185128, "uri":"cornell_pbr.bin" } ] } ================================================ FILE: data/GLTFs/multi-dispersion.gltf ================================================ { "asset":{ "generator":"Khronos glTF Blender I/O v4.2.70", "version":"2.0" }, "extensionsUsed":[ "KHR_materials_transmission" ], "scene":0, "scenes":[ { "name":"Scene", "nodes":[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 ] } ], "nodes":[ { "mesh":0, "name":"Big", "rotation":[ -0.18059857189655304, 0.04401765391230583, 0.2745361924171448, 0.9434386491775513 ], "translation":[ -0.12121788412332535, 0.08730625361204147, -0.17660263180732727 ] }, { "camera":0, "name":"Camera", "rotation":[ -0.022813020274043083, 0.9744870066642761, 0.11683055013418198, 0.19027690589427948 ], "translation":[ 0.2137400358915329, 0.26666662096977234, -0.9217798113822937 ] }, { "mesh":1, "name":"Plane", "scale":[ 68.26968383789062, 68.26968383789062, 68.26968383789062 ], "translation":[ -0.03350231796503067, 0, 0 ] }, { "mesh":2, "name":"GeoSphere003.001", "rotation":[ -0.2717084586620331, 0.8061394691467285, -0.16540518403053284, 0.4989537298679352 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.20849435031414032, 0.011372121050953865, -0.33038368821144104 ] }, { "mesh":3, "name":"GeoSphere003.002", "rotation":[ -0.505194365978241, 0.028006816282868385, 0.8624654412269592, 0.012149344198405743 ], "scale":[ 0.13006502389907837, 0.13006500899791718, 0.13006502389907837 ], "translation":[ -0.5041190385818481, 0.007328478619456291, -0.3771926760673523 ] }, { "mesh":4, "name":"GeoSphere003.003", "rotation":[ 0.18523885309696198, -0.8525427579879761, 0.2537952959537506, 0.41766682267189026 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.17694644629955292, 0.011370167136192322, -0.3251064717769623 ] }, { "mesh":5, "name":"GeoSphere003.004", "rotation":[ -0.26327353715896606, -0.9462590217590332, -0.1835026741027832, 0.04009705409407616 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.11534974724054337, 0.011368044652044773, -0.30308225750923157 ] }, { "mesh":6, "name":"GeoSphere003.005", "rotation":[ -0.2972252368927002, -0.18090642988681793, -0.11166474223136902, 0.9308388829231262 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006500899791718 ], "translation":[ -0.02503671869635582, 0.011372373439371586, -0.08266749233007431 ] }, { "mesh":7, "name":"GeoSphere003.006", "rotation":[ 0.24270711839199066, 0.18106096982955933, 0.20432241261005402, 0.9308934211730957 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.05786854028701782, 0.011371721513569355, 0.03448312357068062 ] }, { "mesh":8, "name":"GeoSphere003.007", "rotation":[ 0.30991220474243164, -0.9454272389411926, -0.04572071135044098, 0.08961784839630127 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006503880023956 ], "translation":[ -0.10885285586118698, 0.011380909010767937, -0.022299697622656822 ] }, { "mesh":9, "name":"GeoSphere003.008", "rotation":[ -0.15861886739730835, 0.3616071045398712, -0.2718893587589264, 0.8775856494903564 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ 0.11128944158554077, 0.011369084939360619, -0.06842060387134552 ] }, { "mesh":10, "name":"GeoSphere003.009", "rotation":[ -0.2916436493396759, -0.19744791090488434, -0.13127796351909637, 0.9266738295555115 ], "scale":[ 0.13006500899791718, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.022038046270608902, 0.01136020291596651, -0.10843902826309204 ] }, { "mesh":11, "name":"GeoSphere003.010", "rotation":[ -0.1330464482307434, 0.239720419049263, -0.28655847907066345, 0.9179962277412415 ], "scale":[ 0.13006500899791718, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.39776915311813354, 0.011412850581109524, 0.36004236340522766 ] }, { "mesh":12, "name":"GeoSphere003.011", "rotation":[ 0.25378233194351196, -0.717930257320404, 0.1858185976743698, 0.6210009455680847 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.23853854835033417, 0.01137317530810833, -0.36935099959373474 ] }, { "mesh":13, "name":"GeoSphere003.012", "rotation":[ -0.8004741072654724, -0.02516813389956951, 0.598798930644989, 0.006902455817908049 ], "scale":[ 0.13006502389907837, 0.13006500899791718, 0.13006500899791718 ], "translation":[ -0.3570106029510498, 0.007246554829180241, -0.3618093430995941 ] }, { "mesh":14, "name":"GeoSphere003.013", "rotation":[ -0.9252974390983582, 0.03435178101062775, -0.37766754627227783, 0.0034516039304435253 ], "scale":[ 0.13006500899791718, 0.13006502389907837, 0.13006502389907837 ], "translation":[ 0.07533064484596252, 0.00737784942612052, -0.3752342760562897 ] }, { "mesh":15, "name":"GeoSphere003.014", "rotation":[ 0.018853910267353058, 0.024486443027853966, 0.9993785619735718, 0.016955794766545296 ], "scale":[ 0.13006503880023956, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.1543421447277069, 0.007303744088858366, 0.10264983028173447 ] }, { "mesh":16, "name":"GeoSphere003.015", "rotation":[ 0.05616613104939461, -0.941322386264801, -0.3144487142562866, 0.10899325460195541 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.10102517902851105, 0.011370140127837658, 0.052674245089292526 ] }, { "mesh":17, "name":"GeoSphere003.016", "rotation":[ 0.4871572256088257, -0.018992548808455467, -0.8731009364128113, 0.003448193660005927 ], "scale":[ 0.13006502389907837, 0.13006500899791718, 0.13006500899791718 ], "translation":[ -0.296036958694458, 0.007107077166438103, -0.2748781442642212 ] }, { "mesh":18, "name":"GeoSphere003.017", "rotation":[ 0.5666192173957825, 0.014482385478913784, -0.8233940005302429, 0.027481136843562126 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.38634154200553894, 0.007334591820836067, -0.3463435471057892 ] }, { "mesh":19, "name":"GeoSphere003.018", "rotation":[ 0.2868089973926544, -0.02325175516307354, -0.12863944470882416, 0.9490268230438232 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.3769674003124237, 0.011365294456481934, -0.31926217675209045 ] }, { "mesh":20, "name":"GeoSphere003.019", "rotation":[ -0.2635954022407532, -0.9462764859199524, 0.1861024647951126, 0.021076705306768417 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.17578920722007751, 0.011364184319972992, 0.1279369443655014 ] }, { "mesh":21, "name":"GeoSphere003.020", "rotation":[ 0.981778621673584, 0.028752772137522697, -0.18784041702747345, 0.00028548462432809174 ], "scale":[ 0.13006502389907837, 0.13006500899791718, 0.13006502389907837 ], "translation":[ -0.052354007959365845, 0.0072728716768324375, 0.06500548124313354 ] }, { "mesh":22, "name":"GeoSphere003.021", "rotation":[ -0.6077518463134766, 0.007505889516323805, -0.7933579683303833, 0.03412551432847977 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.020264793187379837, 0.007347577717155218, 0.05279305577278137 ] }, { "mesh":23, "name":"GeoSphere003.022", "rotation":[ 0.29746347665786743, 0.4036971926689148, 0.11747676879167557, 0.8571717143058777 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.011225644499063492, 0.011366810649633408, -0.052403487265110016 ] }, { "mesh":24, "name":"GeoSphere003.023", "rotation":[ -0.15131594240665436, 0.3455721139907837, -0.27116018533706665, 0.8855257034301758 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ 0.020422939211130142, 0.011381527408957481, -0.0693168044090271 ] }, { "mesh":25, "name":"GeoSphere003.024", "rotation":[ -0.5399572253227234, -0.005164918024092913, -0.8411224484443665, 0.03053930588066578 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.07693775743246078, 0.007273177616298199, -0.45747384428977966 ] }, { "mesh":26, "name":"GeoSphere003.025", "rotation":[ -0.2752854824066162, 0.8250166177749634, -0.14071622490882874, 0.4730375111103058 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006500899791718 ], "translation":[ -0.09479845315217972, 0.011392923071980476, -0.34356972575187683 ] }, { "mesh":27, "name":"GeoSphere003.026", "rotation":[ 0.21575218439102173, 0.4657882750034332, -0.2441786229610443, 0.8227207064628601 ], "scale":[ 0.13006500899791718, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.07972949743270874, 0.011360841803252697, -0.29636630415916443 ] }, { "mesh":28, "name":"GeoSphere003.027", "rotation":[ 0.6783722639083862, 0.015337063930928707, 0.7341387867927551, 0.02482239529490471 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.16386955976486206, 0.007287341635674238, -0.35593411326408386 ] }, { "mesh":29, "name":"GeoSphere003.028", "rotation":[ 0.6260389089584351, 0.027038700878620148, 0.7792672514915466, 0.009315108880400658 ], "scale":[ 0.13006500899791718, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.05065222829580307, 0.007316164206713438, -0.39160624146461487 ] }, { "mesh":30, "name":"GeoSphere003.029", "rotation":[ -0.29577139019966125, 0.9138772487640381, -0.1264592409133911, 0.24770089983940125 ], "scale":[ 0.13006500899791718, 0.13006502389907837, 0.13006502389907837 ], "translation":[ 0.026655618101358414, 0.011398173868656158, 0.6290746927261353 ] }, { "mesh":31, "name":"GeoSphere003.030", "rotation":[ 0.2367924451828003, -0.6930391192436218, -0.2132471799850464, 0.6466465592384338 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.2075895071029663, 0.011371053755283356, 0.12243460863828659 ] }, { "mesh":32, "name":"GeoSphere003.031", "rotation":[ 0.2906414866447449, -0.5638951063156128, 0.06424777954816818, 0.7703388929367065 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.46048444509506226, 0.011488606221973896, -0.1842009425163269 ] }, { "mesh":33, "name":"GeoSphere003.032", "rotation":[ -0.11308038979768753, 0.023495102301239967, 0.9932093024253845, 0.014003305695950985 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.4645261764526367, 0.00723264180123806, -0.2942407727241516 ] }, { "mesh":34, "name":"GeoSphere003.033", "rotation":[ -0.8586738705635071, 0.020679840818047523, 0.511664628982544, 0.02123766951262951 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.1340523511171341, 0.00732304947450757, 0.06637275964021683 ] }, { "mesh":35, "name":"GeoSphere003.034", "rotation":[ 0.3220692574977875, 0.8323050737380981, -0.03735608980059624, 0.44960448145866394 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.14520026743412018, 0.011362835764884949, 0.13079720735549927 ] }, { "mesh":36, "name":"GeoSphere003.035", "rotation":[ -0.2184649407863617, 0.8654321432113647, 0.23974138498306274, 0.3818695545196533 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.12440294772386551, 0.011363371275365353, 0.10869459807872772 ] }, { "mesh":37, "name":"GeoSphere003.036", "rotation":[ -0.14333312213420868, -0.7966905832290649, 0.28383228182792664, 0.513983428478241 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ 0.028535349294543266, 0.011363580822944641, -0.5388242602348328 ] }, { "mesh":38, "name":"GeoSphere003.037", "rotation":[ 0.5051876902580261, -0.03213047236204147, -0.8624112010002136, 0.00020426412811502814 ], "scale":[ 0.13006502389907837, 0.13006500899791718, 0.13006502389907837 ], "translation":[ -0.008328701369464397, 0.007349267136305571, -0.4517853856086731 ] }, { "mesh":39, "name":"GeoSphere003.038", "rotation":[ 0.18252430856227875, -0.9161030054092407, 0.2706860303878784, 0.2327428162097931 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.02293732576072216, 0.011366519145667553, -0.5010008811950684 ] }, { "mesh":40, "name":"GeoSphere003.039", "rotation":[ -0.3251582086086273, 0.26995760202407837, 0.009040526114404202, 0.9062634110450745 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.029143130406737328, 0.011376534588634968, -0.47301021218299866 ] }, { "mesh":41, "name":"GeoSphere003.040", "rotation":[ -0.3161882162094116, -0.7897778749465942, 0.024548182263970375, 0.5250460505485535 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ 0.14146271347999573, 0.011362064629793167, -0.3374761641025543 ] }, { "mesh":42, "name":"GeoSphere003.041", "rotation":[ -0.8519217371940613, -0.0034365083556622267, -0.5236560702323914, 0.0013694290537387133 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ 0.16632358729839325, 0.006841893773525953, -0.30004122853279114 ] }, { "mesh":43, "name":"GeoSphere003.042", "rotation":[ -0.09581859409809113, -0.025791911408305168, -0.9950175881385803, 0.009676595218479633 ], "scale":[ 0.13006502389907837, 0.13006503880023956, 0.13006502389907837 ], "translation":[ 0.10315526276826859, 0.007282526697963476, -0.18153215944766998 ] }, { "mesh":44, "name":"GeoSphere003.043", "rotation":[ -0.6865485906600952, 0.015963802114129066, -0.7263807058334351, 0.02770187519490719 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ 0.01211047824472189, 0.007334437221288681, -0.09995658695697784 ] }, { "mesh":45, "name":"GeoSphere003.044", "rotation":[ -0.07489863783121109, -0.8347724676132202, 0.3080703318119049, 0.45015305280685425 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.48611730337142944, 0.011453031562268734, -0.054811492562294006 ] }, { "mesh":46, "name":"GeoSphere003.045", "rotation":[ 0.2053934782743454, 0.08531651645898819, 0.243620827794075, 0.9440251588821411 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006500899791718 ], "translation":[ -0.24661734700202942, 0.011371743865311146, -0.3051324784755707 ] }, { "mesh":47, "name":"GeoSphere003.046", "rotation":[ -0.9538770318031311, 0.02990647964179516, -0.29867857694625854, 0.0039438814856112 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.1312432438135147, 0.007307452615350485, -0.36265984177589417 ] }, { "mesh":48, "name":"GeoSphere003.047", "rotation":[ 0.22314214706420898, -0.719915509223938, -0.22631610929965973, 0.6170172691345215 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ -0.09208523482084274, 0.011365761049091816, -0.2695140540599823 ] }, { "mesh":49, "name":"GeoSphere003.048", "rotation":[ -0.3145400285720825, -0.7661687135696411, 0.042526405304670334, 0.5587858557701111 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006502389907837 ], "translation":[ 0.034097280353307724, 0.011358587071299553, -0.45633184909820557 ] }, { "mesh":50, "name":"GeoSphere003.049", "rotation":[ 0.8170110583305359, -0.03178128972649574, 0.5755193829536438, 0.01613527163863182 ], "scale":[ 0.13006502389907837, 0.13006502389907837, 0.13006500899791718 ], "translation":[ 0.11040344834327698, 0.007420437876135111, -0.3653630018234253 ] } ], "cameras":[ { "name":"Camera.001", "perspective":{ "aspectRatio":1.7777777777777777, "yfov":0.39959652046304894, "zfar":1000, "znear":0.10000000149011612 }, "type":"perspective" } ], "materials":[ { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.003", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "name":"Material.002", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.10000000149011612, 0.10000000149011612, 0.10000000149011612, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.005", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.007", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.008", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.009", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.010", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.011", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.012", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.013", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.014", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.015", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.016", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.017", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.018", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.019", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.020", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.021", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.022", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.023", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.024", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.025", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.026", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.027", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.028", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.029", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.030", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.031", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.032", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.033", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.034", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.035", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.036", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.037", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.038", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.039", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.040", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.041", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.042", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.043", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.044", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.045", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.046", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.047", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.048", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.049", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.050", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.051", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.052", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.053", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material.054", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } } ], "meshes":[ { "name":"Mesh.003", "primitives":[ { "attributes":{ "POSITION":0, "NORMAL":1, "TEXCOORD_0":2 }, "indices":3, "material":0 } ] }, { "name":"Plane", "primitives":[ { "attributes":{ "POSITION":4, "NORMAL":5, "TEXCOORD_0":6 }, "indices":7, "material":1 } ] }, { "name":"Mesh.002", "primitives":[ { "attributes":{ "POSITION":8, "NORMAL":9, "TEXCOORD_0":10 }, "indices":3, "material":2 } ] }, { "name":"Mesh.005", "primitives":[ { "attributes":{ "POSITION":11, "NORMAL":12, "TEXCOORD_0":13 }, "indices":3, "material":3 } ] }, { "name":"Mesh.006", "primitives":[ { "attributes":{ "POSITION":14, "NORMAL":15, "TEXCOORD_0":16 }, "indices":3, "material":4 } ] }, { "name":"Mesh.007", "primitives":[ { "attributes":{ "POSITION":17, "NORMAL":18, "TEXCOORD_0":19 }, "indices":3, "material":5 } ] }, { "name":"Mesh.008", "primitives":[ { "attributes":{ "POSITION":20, "NORMAL":21, "TEXCOORD_0":22 }, "indices":3, "material":6 } ] }, { "name":"Mesh.009", "primitives":[ { "attributes":{ "POSITION":23, "NORMAL":24, "TEXCOORD_0":25 }, "indices":3, "material":7 } ] }, { "name":"Mesh.010", "primitives":[ { "attributes":{ "POSITION":26, "NORMAL":27, "TEXCOORD_0":28 }, "indices":3, "material":8 } ] }, { "name":"Mesh.011", "primitives":[ { "attributes":{ "POSITION":29, "NORMAL":30, "TEXCOORD_0":31 }, "indices":3, "material":9 } ] }, { "name":"Mesh.012", "primitives":[ { "attributes":{ "POSITION":32, "NORMAL":33, "TEXCOORD_0":34 }, "indices":3, "material":10 } ] }, { "name":"Mesh.013", "primitives":[ { "attributes":{ "POSITION":35, "NORMAL":36, "TEXCOORD_0":37 }, "indices":3, "material":11 } ] }, { "name":"Mesh.014", "primitives":[ { "attributes":{ "POSITION":38, "NORMAL":39, "TEXCOORD_0":40 }, "indices":3, "material":12 } ] }, { "name":"Mesh.015", "primitives":[ { "attributes":{ "POSITION":41, "NORMAL":42, "TEXCOORD_0":43 }, "indices":3, "material":13 } ] }, { "name":"Mesh.016", "primitives":[ { "attributes":{ "POSITION":44, "NORMAL":45, "TEXCOORD_0":46 }, "indices":3, "material":14 } ] }, { "name":"Mesh.017", "primitives":[ { "attributes":{ "POSITION":47, "NORMAL":48, "TEXCOORD_0":49 }, "indices":3, "material":15 } ] }, { "name":"Mesh.018", "primitives":[ { "attributes":{ "POSITION":50, "NORMAL":51, "TEXCOORD_0":52 }, "indices":3, "material":16 } ] }, { "name":"Mesh.019", "primitives":[ { "attributes":{ "POSITION":53, "NORMAL":54, "TEXCOORD_0":55 }, "indices":3, "material":17 } ] }, { "name":"Mesh.020", "primitives":[ { "attributes":{ "POSITION":56, "NORMAL":57, "TEXCOORD_0":58 }, "indices":3, "material":18 } ] }, { "name":"Mesh.021", "primitives":[ { "attributes":{ "POSITION":59, "NORMAL":60, "TEXCOORD_0":61 }, "indices":3, "material":19 } ] }, { "name":"Mesh.022", "primitives":[ { "attributes":{ "POSITION":62, "NORMAL":63, "TEXCOORD_0":64 }, "indices":3, "material":20 } ] }, { "name":"Mesh.023", "primitives":[ { "attributes":{ "POSITION":65, "NORMAL":66, "TEXCOORD_0":67 }, "indices":3, "material":21 } ] }, { "name":"Mesh.024", "primitives":[ { "attributes":{ "POSITION":68, "NORMAL":69, "TEXCOORD_0":70 }, "indices":3, "material":22 } ] }, { "name":"Mesh.025", "primitives":[ { "attributes":{ "POSITION":71, "NORMAL":72, "TEXCOORD_0":73 }, "indices":3, "material":23 } ] }, { "name":"Mesh.026", "primitives":[ { "attributes":{ "POSITION":74, "NORMAL":75, "TEXCOORD_0":76 }, "indices":3, "material":24 } ] }, { "name":"Mesh.027", "primitives":[ { "attributes":{ "POSITION":77, "NORMAL":78, "TEXCOORD_0":79 }, "indices":3, "material":25 } ] }, { "name":"Mesh.028", "primitives":[ { "attributes":{ "POSITION":80, "NORMAL":81, "TEXCOORD_0":82 }, "indices":3, "material":26 } ] }, { "name":"Mesh.029", "primitives":[ { "attributes":{ "POSITION":83, "NORMAL":84, "TEXCOORD_0":85 }, "indices":3, "material":27 } ] }, { "name":"Mesh.030", "primitives":[ { "attributes":{ "POSITION":86, "NORMAL":87, "TEXCOORD_0":88 }, "indices":3, "material":28 } ] }, { "name":"Mesh.031", "primitives":[ { "attributes":{ "POSITION":89, "NORMAL":90, "TEXCOORD_0":91 }, "indices":3, "material":29 } ] }, { "name":"Mesh.032", "primitives":[ { "attributes":{ "POSITION":92, "NORMAL":93, "TEXCOORD_0":94 }, "indices":3, "material":30 } ] }, { "name":"Mesh.033", "primitives":[ { "attributes":{ "POSITION":95, "NORMAL":96, "TEXCOORD_0":97 }, "indices":3, "material":31 } ] }, { "name":"Mesh.034", "primitives":[ { "attributes":{ "POSITION":98, "NORMAL":99, "TEXCOORD_0":100 }, "indices":3, "material":32 } ] }, { "name":"Mesh.035", "primitives":[ { "attributes":{ "POSITION":101, "NORMAL":102, "TEXCOORD_0":103 }, "indices":3, "material":33 } ] }, { "name":"Mesh.036", "primitives":[ { "attributes":{ "POSITION":104, "NORMAL":105, "TEXCOORD_0":106 }, "indices":3, "material":34 } ] }, { "name":"Mesh.037", "primitives":[ { "attributes":{ "POSITION":107, "NORMAL":108, "TEXCOORD_0":109 }, "indices":3, "material":35 } ] }, { "name":"Mesh.038", "primitives":[ { "attributes":{ "POSITION":110, "NORMAL":111, "TEXCOORD_0":112 }, "indices":3, "material":36 } ] }, { "name":"Mesh.039", "primitives":[ { "attributes":{ "POSITION":113, "NORMAL":114, "TEXCOORD_0":115 }, "indices":3, "material":37 } ] }, { "name":"Mesh.040", "primitives":[ { "attributes":{ "POSITION":116, "NORMAL":117, "TEXCOORD_0":118 }, "indices":3, "material":38 } ] }, { "name":"Mesh.041", "primitives":[ { "attributes":{ "POSITION":119, "NORMAL":120, "TEXCOORD_0":121 }, "indices":3, "material":39 } ] }, { "name":"Mesh.042", "primitives":[ { "attributes":{ "POSITION":122, "NORMAL":123, "TEXCOORD_0":124 }, "indices":3, "material":40 } ] }, { "name":"Mesh.043", "primitives":[ { "attributes":{ "POSITION":125, "NORMAL":126, "TEXCOORD_0":127 }, "indices":3, "material":41 } ] }, { "name":"Mesh.044", "primitives":[ { "attributes":{ "POSITION":128, "NORMAL":129, "TEXCOORD_0":130 }, "indices":3, "material":42 } ] }, { "name":"Mesh.045", "primitives":[ { "attributes":{ "POSITION":131, "NORMAL":132, "TEXCOORD_0":133 }, "indices":3, "material":43 } ] }, { "name":"Mesh.046", "primitives":[ { "attributes":{ "POSITION":134, "NORMAL":135, "TEXCOORD_0":136 }, "indices":3, "material":44 } ] }, { "name":"Mesh.047", "primitives":[ { "attributes":{ "POSITION":137, "NORMAL":138, "TEXCOORD_0":139 }, "indices":3, "material":45 } ] }, { "name":"Mesh.048", "primitives":[ { "attributes":{ "POSITION":140, "NORMAL":141, "TEXCOORD_0":142 }, "indices":3, "material":46 } ] }, { "name":"Mesh.049", "primitives":[ { "attributes":{ "POSITION":143, "NORMAL":144, "TEXCOORD_0":145 }, "indices":3, "material":47 } ] }, { "name":"Mesh.050", "primitives":[ { "attributes":{ "POSITION":146, "NORMAL":147, "TEXCOORD_0":148 }, "indices":3, "material":48 } ] }, { "name":"Mesh.051", "primitives":[ { "attributes":{ "POSITION":149, "NORMAL":150, "TEXCOORD_0":151 }, "indices":3, "material":49 } ] }, { "name":"Mesh.052", "primitives":[ { "attributes":{ "POSITION":152, "NORMAL":153, "TEXCOORD_0":154 }, "indices":3, "material":50 } ] } ], "accessors":[ { "bufferView":0, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":1, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":2, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":3, "componentType":5123, "count":540, "type":"SCALAR" }, { "bufferView":4, "componentType":5126, "count":4, "max":[ 1, 0, 1 ], "min":[ -1, 0, -1 ], "type":"VEC3" }, { "bufferView":5, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":6, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":7, "componentType":5123, "count":6, "type":"SCALAR" }, { "bufferView":8, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":9, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":10, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":11, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":12, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":13, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":14, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":15, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":16, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":17, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":18, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":19, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":20, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":21, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":22, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":23, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":24, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":25, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":26, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":27, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":28, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":29, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":30, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":31, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":32, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":33, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":34, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":35, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":36, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":37, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":38, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":39, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":40, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":41, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":42, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":43, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":44, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":45, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":46, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":47, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":48, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":49, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":50, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":51, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":52, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":53, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":54, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":55, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":56, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":57, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":58, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":59, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":60, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":61, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":62, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":63, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":64, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":65, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":66, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":67, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":68, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":69, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":70, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":71, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":72, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":73, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":74, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":75, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":76, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":77, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":78, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":79, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":80, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":81, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":82, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":83, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":84, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":85, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":86, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":87, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":88, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":89, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":90, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":91, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":92, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":93, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":94, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":95, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":96, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":97, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":98, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":99, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":100, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":101, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":102, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":103, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":104, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":105, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":106, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":107, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":108, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":109, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":110, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":111, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":112, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":113, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":114, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":115, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":116, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":117, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":118, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":119, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":120, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":121, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":122, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":123, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":124, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":125, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":126, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":127, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":128, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":129, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":130, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":131, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":132, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":133, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":134, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":135, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":136, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":137, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":138, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":139, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":140, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":141, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":142, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":143, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":144, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":145, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":146, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":147, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":148, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":149, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":150, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":151, "componentType":5126, "count":540, "type":"VEC2" }, { "bufferView":152, "componentType":5126, "count":540, "max":[ 0.13739751279354095, 0.05211269110441208, 0.13802951574325562 ], "min":[ -0.13643281161785126, -0.10680017620325089, -0.13802950084209442 ], "type":"VEC3" }, { "bufferView":153, "componentType":5126, "count":540, "type":"VEC3" }, { "bufferView":154, "componentType":5126, "count":540, "type":"VEC2" } ], "bufferViews":[ { "buffer":0, "byteLength":6480, "byteOffset":0, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":6480, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":12960, "target":34962 }, { "buffer":0, "byteLength":1080, "byteOffset":17280, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":18360, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":18408, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":18456, "target":34962 }, { "buffer":0, "byteLength":12, "byteOffset":18488, "target":34963 }, { "buffer":0, "byteLength":6480, "byteOffset":18500, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":24980, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":31460, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":35780, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":42260, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":48740, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":53060, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":59540, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":66020, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":70340, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":76820, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":83300, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":87620, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":94100, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":100580, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":104900, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":111380, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":117860, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":122180, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":128660, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":135140, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":139460, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":145940, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":152420, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":156740, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":163220, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":169700, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":174020, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":180500, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":186980, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":191300, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":197780, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":204260, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":208580, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":215060, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":221540, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":225860, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":232340, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":238820, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":243140, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":249620, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":256100, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":260420, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":266900, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":273380, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":277700, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":284180, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":290660, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":294980, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":301460, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":307940, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":312260, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":318740, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":325220, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":329540, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":336020, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":342500, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":346820, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":353300, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":359780, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":364100, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":370580, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":377060, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":381380, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":387860, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":394340, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":398660, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":405140, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":411620, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":415940, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":422420, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":428900, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":433220, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":439700, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":446180, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":450500, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":456980, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":463460, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":467780, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":474260, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":480740, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":485060, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":491540, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":498020, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":502340, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":508820, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":515300, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":519620, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":526100, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":532580, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":536900, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":543380, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":549860, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":554180, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":560660, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":567140, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":571460, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":577940, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":584420, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":588740, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":595220, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":601700, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":606020, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":612500, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":618980, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":623300, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":629780, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":636260, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":640580, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":647060, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":653540, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":657860, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":664340, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":670820, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":675140, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":681620, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":688100, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":692420, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":698900, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":705380, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":709700, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":716180, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":722660, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":726980, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":733460, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":739940, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":744260, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":750740, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":757220, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":761540, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":768020, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":774500, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":778820, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":785300, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":791780, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":796100, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":802580, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":809060, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":813380, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":819860, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":826340, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":830660, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":837140, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":843620, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":847940, "target":34962 }, { "buffer":0, "byteLength":6480, "byteOffset":854420, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":860900, "target":34962 } ], "buffers":[ { "byteLength":865220, "uri":"multi-dispersion.bin" } ] } ================================================ FILE: data/GLTFs/nested-dielectrics-complex.gltf ================================================ { "asset":{ "generator":"Khronos glTF Blender I/O v4.1.62", "version":"2.0" }, "extensionsUsed":[ "KHR_materials_transmission", "KHR_materials_ior", "KHR_materials_specular", "KHR_materials_emissive_strength" ], "scene":0, "scenes":[ { "name":"Scene", "nodes":[ 0, 1, 2, 3, 4, 5, 6 ] } ], "nodes":[ { "mesh":0, "name":"Sphere" }, { "mesh":1, "name":"Sphere.001", "translation":[ 0.014221577905118465, 0, 0 ] }, { "mesh":2, "name":"Plane", "rotation":[ 0, 0, -0.3826834559440613, 0.9238795638084412 ], "translation":[ 2.1114394664764404, 1.827852487564087, 0 ] }, { "camera":0, "name":"Camera", "rotation":[ -0.3826834261417389, 0, 0, 0.9238795638084412 ], "translation":[ 0, 5.029460906982422, 5.029465198516846 ] }, { "mesh":3, "name":"Plane.001", "scale":[ 510.8236389160156, 1, 510.8236389160156 ], "translation":[ 0, -0.7477800250053406, 0 ] }, { "mesh":4, "name":"Cube", "scale":[ 0.20000000298023224, 0.20000000298023224, 0.20000000298023224 ], "translation":[ -0.9598821401596069, 0.033187270164489746, 0 ] }, { "mesh":5, "name":"Torus.001", "rotation":[ -0.9547613859176636, 0, 0, 0.29737329483032227 ] } ], "cameras":[ { "name":"Camera.002", "perspective":{ "aspectRatio":1.7777777777777777, "yfov":0.39959651231765747, "zfar":1000, "znear":0.10000000149011612 }, "type":"perspective" } ], "materials":[ { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 }, "KHR_materials_ior":{ "ior":1.399999976158142 } }, "name":"Material.004", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 }, "KHR_materials_specular":{ "specularColorFactor":[ 2.0, 2.0, 2.0 ] }, "KHR_materials_ior":{ "ior":1 } }, "name":"Material.005", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 0.04319041967391968, 0.8040210008621216, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "emissiveFactor":[ 1, 1, 1 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":25 } }, "name":"Material.006", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material", "pbrMetallicRoughness":{ "baseColorFactor":[ 1, 0, 0, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 }, "KHR_materials_ior":{ "ior":2 } }, "name":"Material.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 0.38120442628860474, 0.8007099628448486, 1 ], "metallicFactor":0, "roughnessFactor":0.20000000298023224 } } ], "meshes":[ { "name":"Sphere.002", "primitives":[ { "attributes":{ "POSITION":0, "NORMAL":1, "TEXCOORD_0":2 }, "indices":3, "material":0 } ] }, { "name":"Sphere.003", "primitives":[ { "attributes":{ "POSITION":4, "NORMAL":5, "TEXCOORD_0":6 }, "indices":7, "material":1 } ] }, { "name":"Plane.002", "primitives":[ { "attributes":{ "POSITION":8, "NORMAL":9, "TEXCOORD_0":10 }, "indices":11, "material":2 } ] }, { "name":"Plane.003", "primitives":[ { "attributes":{ "POSITION":12, "NORMAL":13, "TEXCOORD_0":14 }, "indices":11 } ] }, { "name":"Cube", "primitives":[ { "attributes":{ "POSITION":15, "NORMAL":16, "TEXCOORD_0":17 }, "indices":18, "material":3 } ] }, { "name":"Torus.001", "primitives":[ { "attributes":{ "POSITION":19, "NORMAL":20, "TEXCOORD_0":21 }, "indices":22, "material":4 } ] } ], "accessors":[ { "bufferView":0, "componentType":5126, "count":559, "max":[ 0.9999997019767761, 1, 0.9999993443489075 ], "min":[ -0.9999990463256836, -1, -1 ], "type":"VEC3" }, { "bufferView":1, "componentType":5126, "count":559, "type":"VEC3" }, { "bufferView":2, "componentType":5126, "count":559, "type":"VEC2" }, { "bufferView":3, "componentType":5123, "count":2880, "type":"SCALAR" }, { "bufferView":4, "componentType":5126, "count":559, "max":[ 0.34999987483024597, 0.3499999940395355, 0.3499997556209564 ], "min":[ -0.34999966621398926, -0.3499999940395355, -0.3499999940395355 ], "type":"VEC3" }, { "bufferView":5, "componentType":5126, "count":559, "type":"VEC3" }, { "bufferView":6, "componentType":5126, "count":559, "type":"VEC2" }, { "bufferView":7, "componentType":5123, "count":2880, "type":"SCALAR" }, { "bufferView":8, "componentType":5126, "count":4, "max":[ 1, 0, 1 ], "min":[ -1, 0, -1 ], "type":"VEC3" }, { "bufferView":9, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":10, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":11, "componentType":5123, "count":6, "type":"SCALAR" }, { "bufferView":12, "componentType":5126, "count":4, "max":[ 1, 0, 1 ], "min":[ -1, 0, -1 ], "type":"VEC3" }, { "bufferView":13, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":14, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":15, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":16, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":17, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":18, "componentType":5123, "count":36, "type":"SCALAR" }, { "bufferView":19, "componentType":5126, "count":637, "max":[ 0.6000000238418579, 0.10000000149011612, 0.6000000238418579 ], "min":[ -0.6000000238418579, -0.10000000149011612, -0.6000000238418579 ], "type":"VEC3" }, { "bufferView":20, "componentType":5126, "count":637, "type":"VEC3" }, { "bufferView":21, "componentType":5126, "count":637, "type":"VEC2" }, { "bufferView":22, "componentType":5123, "count":3456, "type":"SCALAR" } ], "bufferViews":[ { "buffer":0, "byteLength":6708, "byteOffset":0, "target":34962 }, { "buffer":0, "byteLength":6708, "byteOffset":6708, "target":34962 }, { "buffer":0, "byteLength":4472, "byteOffset":13416, "target":34962 }, { "buffer":0, "byteLength":5760, "byteOffset":17888, "target":34963 }, { "buffer":0, "byteLength":6708, "byteOffset":23648, "target":34962 }, { "buffer":0, "byteLength":6708, "byteOffset":30356, "target":34962 }, { "buffer":0, "byteLength":4472, "byteOffset":37064, "target":34962 }, { "buffer":0, "byteLength":5760, "byteOffset":41536, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":47296, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":47344, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":47392, "target":34962 }, { "buffer":0, "byteLength":12, "byteOffset":47424, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":47436, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":47484, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":47532, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":47564, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":47852, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":48140, "target":34962 }, { "buffer":0, "byteLength":72, "byteOffset":48332, "target":34963 }, { "buffer":0, "byteLength":7644, "byteOffset":48404, "target":34962 }, { "buffer":0, "byteLength":7644, "byteOffset":56048, "target":34962 }, { "buffer":0, "byteLength":5096, "byteOffset":63692, "target":34962 }, { "buffer":0, "byteLength":6912, "byteOffset":68788, "target":34963 } ], "buffers":[ { "byteLength":75700, "uri":"nested-dielectrics-complex.bin" } ] } ================================================ FILE: data/GLTFs/nested-dielectrics.gltf ================================================ { "asset":{ "generator":"Khronos glTF Blender I/O v4.4.55", "version":"2.0" }, "extensionsUsed":[ "KHR_materials_clearcoat", "KHR_materials_transmission", "KHR_materials_emissive_strength", "KHR_materials_specular", "KHR_materials_ior" ], "scene":0, "scenes":[ { "name":"Scene", "nodes":[ 0, 1, 2, 3, 4, 5, 6 ] } ], "nodes":[ { "mesh":0, "name":"Sphere" }, { "mesh":1, "name":"Sphere.001", "translation":[ 0.014221577905118465, 0, 0 ] }, { "mesh":2, "name":"Plane", "rotation":[ 0, 0, -0.3826834559440613, 0.9238795638084412 ], "translation":[ 2.1114394664764404, 1.827852487564087, 0 ] }, { "camera":0, "name":"Camera", "rotation":[ -0.3826834261417389, 0, 0, 0.9238795638084412 ], "translation":[ 0, 5.029460906982422, 5.029465198516846 ] }, { "mesh":3, "name":"Plane.001", "scale":[ 6.2513837814331055, 0.01223785150796175, 6.2513837814331055 ], "translation":[ 0, -0.7477800250053406, 0 ] }, { "mesh":4, "name":"Cube", "scale":[ 0.20000000298023224, 0.20000000298023224, 0.20000000298023224 ], "translation":[ -0.9598821401596069, 0.033187270164489746, 0 ] }, { "mesh":5, "name":"Torus.001", "rotation":[ -0.9547613859176636, 0, 0, 0.29737329483032227 ] } ], "cameras":[ { "name":"Camera.002", "perspective":{ "aspectRatio":1.7777777777777777, "yfov":0.39959651231765747, "zfar":1000, "znear":0.10000000149011612 }, "type":"perspective" } ], "materials":[ { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_transmission":{ "transmissionFactor":1 }, "KHR_materials_ior":{ "ior":1.399999976158142 } }, "name":"Material.004", "pbrMetallicRoughness":{ "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_transmission":{ "transmissionFactor":1 }, "KHR_materials_specular":{ "specularColorFactor":[ 2.0, 2.0, 2.0 ] }, "KHR_materials_ior":{ "ior":1 } }, "name":"Material.005", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 0.04319041967391968, 0.8040210008621216, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "emissiveFactor":[ 1, 1, 1 ], "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_emissive_strength":{ "emissiveStrength":25 } }, "name":"Material.006", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"Material", "pbrMetallicRoughness":{ "baseColorFactor":[ 1, 0, 0, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 }, "KHR_materials_ior":{ "ior":2 } }, "name":"Material.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 0.38120442628860474, 0.8007099628448486, 1 ], "metallicFactor":0, "roughnessFactor":0.20000000298023224 } } ], "meshes":[ { "name":"Sphere.002", "primitives":[ { "attributes":{ "POSITION":0, "NORMAL":1, "TEXCOORD_0":2 }, "indices":3, "material":0 } ] }, { "name":"Sphere.003", "primitives":[ { "attributes":{ "POSITION":4, "NORMAL":5, "TEXCOORD_0":6 }, "indices":7, "material":1 } ] }, { "name":"Plane.002", "primitives":[ { "attributes":{ "POSITION":8, "NORMAL":9, "TEXCOORD_0":10 }, "indices":11, "material":2 } ] }, { "name":"Plane.003", "primitives":[ { "attributes":{ "POSITION":12, "NORMAL":13, "TEXCOORD_0":14 }, "indices":11 } ] }, { "name":"Cube", "primitives":[ { "attributes":{ "POSITION":15, "NORMAL":16, "TEXCOORD_0":17 }, "indices":18, "material":3 } ] }, { "name":"Torus.001", "primitives":[ { "attributes":{ "POSITION":19, "NORMAL":20, "TEXCOORD_0":21 }, "indices":22, "material":4 } ] } ], "accessors":[ { "bufferView":0, "componentType":5126, "count":559, "max":[ 0.9999997019767761, 1, 0.9999993443489075 ], "min":[ -0.9999990463256836, -1, -1 ], "type":"VEC3" }, { "bufferView":1, "componentType":5126, "count":559, "type":"VEC3" }, { "bufferView":2, "componentType":5126, "count":559, "type":"VEC2" }, { "bufferView":3, "componentType":5123, "count":2880, "type":"SCALAR" }, { "bufferView":4, "componentType":5126, "count":559, "max":[ 0.34999987483024597, 0.3499999940395355, 0.3499997556209564 ], "min":[ -0.34999966621398926, -0.3499999940395355, -0.3499999940395355 ], "type":"VEC3" }, { "bufferView":5, "componentType":5126, "count":559, "type":"VEC3" }, { "bufferView":6, "componentType":5126, "count":559, "type":"VEC2" }, { "bufferView":7, "componentType":5123, "count":2880, "type":"SCALAR" }, { "bufferView":8, "componentType":5126, "count":4, "max":[ 1, 0, 1 ], "min":[ -1, 0, -1 ], "type":"VEC3" }, { "bufferView":9, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":10, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":11, "componentType":5123, "count":6, "type":"SCALAR" }, { "bufferView":12, "componentType":5126, "count":4, "max":[ 1, 0, 1 ], "min":[ -1, 0, -1 ], "type":"VEC3" }, { "bufferView":13, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":14, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":15, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":16, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":17, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":18, "componentType":5123, "count":36, "type":"SCALAR" }, { "bufferView":19, "componentType":5126, "count":637, "max":[ 0.6000000238418579, 0.10000000149011612, 0.6000000238418579 ], "min":[ -0.6000000238418579, -0.10000000149011612, -0.6000000238418579 ], "type":"VEC3" }, { "bufferView":20, "componentType":5126, "count":637, "type":"VEC3" }, { "bufferView":21, "componentType":5126, "count":637, "type":"VEC2" }, { "bufferView":22, "componentType":5123, "count":3456, "type":"SCALAR" } ], "bufferViews":[ { "buffer":0, "byteLength":6708, "byteOffset":0, "target":34962 }, { "buffer":0, "byteLength":6708, "byteOffset":6708, "target":34962 }, { "buffer":0, "byteLength":4472, "byteOffset":13416, "target":34962 }, { "buffer":0, "byteLength":5760, "byteOffset":17888, "target":34963 }, { "buffer":0, "byteLength":6708, "byteOffset":23648, "target":34962 }, { "buffer":0, "byteLength":6708, "byteOffset":30356, "target":34962 }, { "buffer":0, "byteLength":4472, "byteOffset":37064, "target":34962 }, { "buffer":0, "byteLength":5760, "byteOffset":41536, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":47296, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":47344, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":47392, "target":34962 }, { "buffer":0, "byteLength":12, "byteOffset":47424, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":47436, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":47484, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":47532, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":47564, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":47852, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":48140, "target":34962 }, { "buffer":0, "byteLength":72, "byteOffset":48332, "target":34963 }, { "buffer":0, "byteLength":7644, "byteOffset":48404, "target":34962 }, { "buffer":0, "byteLength":7644, "byteOffset":56048, "target":34962 }, { "buffer":0, "byteLength":5096, "byteOffset":63692, "target":34962 }, { "buffer":0, "byteLength":6912, "byteOffset":68788, "target":34963 } ], "buffers":[ { "byteLength":75700, "uri":"nested-dielectrics.bin" } ] } ================================================ FILE: data/GLTFs/the-white-room-low.gltf ================================================ { "asset":{ "generator":"Khronos glTF Blender I/O v4.2.70", "version":"2.0" }, "extensionsUsed":[ "KHR_materials_clearcoat", "KHR_materials_transmission", "KHR_materials_emissive_strength", "KHR_materials_ior" ], "scene":0, "scenes":[ { "name":"Scene", "nodes":[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190 ] } ], "nodes":[ { "mesh":0, "name":"Picture-rail", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ], "translation":[ -1.911670207977295, 2.6229679584503174, 3.221792697906494 ] }, { "mesh":1, "name":"Ceiling", "translation":[ -0.48702001571655273, 3.168609857559204, 3.2564644813537598 ] }, { "mesh":2, "name":"Plane.009", "translation":[ -0.48702001571655273, 0.02875208854675293, 3.2564644813537598 ] }, { "mesh":3, "name":"Skirting-Board", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ], "translation":[ -2.4447784423828125, 0.03202188014984131, 3.7508115768432617 ] }, { "mesh":4, "name":"Cornis", "rotation":[ 0.5000000596046448, -0.4999999403953552, 0.5, 0.5 ], "translation":[ -2.3613674640655518, 3.0883710384368896, 2.1537551879882812 ] }, { "mesh":5, "name":"Walls", "translation":[ 0.21100452542304993, 0.02875208854675293, 3.0448410511016846 ] }, { "mesh":6, "name":"Window-panel-Right", "rotation":[ -0.1740332841873169, 0.6853556632995605, 0.6853557229042053, 0.1740332990884781 ], "translation":[ 1.4325926303863525, 2.377373456954956, 0.7042121887207031 ] }, { "mesh":7, "name":"Window-panel-Left", "rotation":[ -0.17408347129821777, -0.6853429079055786, -0.6853430271148682, 0.1740833818912506 ], "translation":[ -1.0055828094482422, 2.377373456954956, 0.7044205665588379 ] }, { "mesh":8, "name":"Window-panel-Middle", "rotation":[ 0, 0.7071066498756409, 0.70710688829422, 7.586152861449591e-08 ], "translation":[ 0.21441829204559326, 2.377373456954956, 0.42468491196632385 ] }, { "mesh":9, "name":"Right-Window-Mesh-Emiiter", "rotation":[ 0.6857413053512573, -0.1725078821182251, 0.17250794172286987, 0.685741126537323 ], "scale":[ 0.3643953502178192, 1.0219993591308594, 0.8908603191375732 ], "translation":[ 1.469131588935852, 1.9036474227905273, 0.6438109278678894 ] }, { "mesh":9, "name":"Left-Window-Mesh-Emiiter", "rotation":[ 0.6861234307289124, 0.17098161578178406, -0.17098166048526764, 0.6861233711242676 ], "scale":[ 0.3643953204154968, 1.0219993591308594, 0.8908604383468628 ], "translation":[ -1.038603663444519, 1.9036474227905273, 0.646122395992279 ] }, { "mesh":9, "name":"Middle-Window-Mesh-Emiiter", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ], "scale":[ 0.4622892439365387, 1, 0.871683657169342 ], "translation":[ 0.21810980141162872, 1.9036474227905273, 0.36019620299339294 ] }, { "mesh":10, "name":"Rear-Window-Mesh-Emiiter", "rotation":[ 0, -0.7071066498756409, 0.70710688829422, 5.338507236274381e-08 ], "scale":[ 1.5630946159362793, 1, 1 ], "translation":[ -0.1428062915802002, 2.013288974761963, 8.212237358093262 ] }, { "mesh":11, "name":"Standing-Lamp-Base", "scale":[ 0.17058558762073517, 0.17058558762073517, 0.17058558762073517 ], "translation":[ 2.6291208267211914, 0.028948593884706497, 1.6885590553283691 ] }, { "mesh":12, "name":"T-lightholder-3", "rotation":[ 0, 0.7071068286895752, 0, 0.7071067094802856 ], "scale":[ 0.039623700082302094, 0.039623696357011795, 0.039623700082302094 ], "translation":[ -1.780541181564331, 1.3029799461364746, 2.9569900035858154 ] }, { "mesh":13, "name":"T-lightholder-2", "rotation":[ 0, 0.7071068286895752, 0, 0.7071067094802856 ], "scale":[ 0.0486246719956398, 0.048624664545059204, 0.0486246719956398 ], "translation":[ -1.780541181564331, 1.3028395175933838, 3.057652235031128 ] }, { "mesh":14, "name":"T-lightholder-1", "rotation":[ 0, 0.7071068286895752, 0, 0.7071067094802856 ], "scale":[ 0.0486246719956398, 0.048624664545059204, 0.0486246719956398 ], "translation":[ -1.780541181564331, 1.3027700185775757, 3.2538845539093018 ] }, { "mesh":15, "name":"3D-Artists-Magazine", "rotation":[ 0, 0.16750279068946838, 0, 0.9858716130256653 ], "scale":[ 0.11785215884447098, 0.11785216629505157, 0.1425195038318634 ], "translation":[ 0.5425272583961487, 0.46624672412872314, 2.958265542984009 ] }, { "mesh":16, "name":"Standing-Lamp-Shade-Inner", "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 2.6291208267211914, 1.4259439706802368, 1.6885590553283691 ] }, { "mesh":17, "name":"Fruit-Bowl", "scale":[ 0.0804700255393982, 0.0804700255393982, 0.0804700255393982 ], "translation":[ 0.25288286805152893, 0.4664691686630249, 2.9443252086639404 ] }, { "mesh":18, "name":"LOVE-Stand", "rotation":[ 0, 0.70710688829422, 0, 0.7071067094802856 ], "scale":[ 0.0073470757342875, 0.007347074802964926, 0.0073470757342875 ], "translation":[ -2.2202749252319336, 0.6724759936332703, 4.0033159255981445 ] }, { "mesh":19, "name":"LOVE-Base", "rotation":[ 0, 0.7071068286895752, 0, 0.7071067094802856 ], "scale":[ 0.14449827373027802, 0.09015031903982162, 0.04091236740350723 ], "translation":[ -2.2202749252319336, 0.6763082146644592, 4.0033159255981445 ] }, { "mesh":20, "name":"Plane.078", "rotation":[ 0.5, 0.5, -0.5, 0.5 ], "scale":[ 0.09015031903982162, 0.09015031903982162, 0.09015031903982162 ], "translation":[ -2.2156827449798584, 0.7574570775032043, 3.9275951385498047 ] }, { "mesh":21, "name":"Plane.077", "rotation":[ 0.5, 0.5, -0.5, 0.5 ], "scale":[ 0.09015031903982162, 0.09015031903982162, 0.09015031903982162 ], "translation":[ -2.2156827449798584, 0.7539929747581482, 3.971747398376465 ] }, { "mesh":22, "name":"LOVE-Letter-Back-O", "rotation":[ 0.5, 0.5, -0.5, 0.5 ], "scale":[ 0.09015031903982162, 0.09015031903982162, 0.09015031903982162 ], "translation":[ -2.2156825065612793, 0.7626820206642151, 4.010139465332031 ] }, { "mesh":23, "name":"LOVE-Letter-Back-L", "rotation":[ 0.5, 0.5, -0.5, 0.5 ], "scale":[ 0.09015031903982162, 0.09015031903982162, 0.09015031903982162 ], "translation":[ -2.2156825065612793, 0.7714517712593079, 4.0644025802612305 ] }, { "mesh":24, "name":"LOVE-Letter-Back-E", "rotation":[ 0.4947656989097595, 0.4947656989097595, -0.5051801204681396, 0.5051800012588501 ], "scale":[ 0.09015031903982162, 0.09015031903982162, 0.09015031903982162 ], "translation":[ -2.207442045211792, 0.76943439245224, 3.9362401962280273 ] }, { "mesh":25, "name":"LOVE-Letter-Back-V", "rotation":[ 0.5, 0.5, -0.5, 0.5 ], "scale":[ 0.09015031903982162, 0.09015031903982162, 0.09015031903982162 ], "translation":[ -2.209141492843628, 0.8162298798561096, 4.0639543533325195 ] }, { "mesh":26, "name":"LOVE-Letter-O", "rotation":[ 0.5, 0.5, -0.5, 0.5 ], "scale":[ 0.09015031903982162, 0.09015031903982162, 0.09015031903982162 ], "translation":[ -2.206592321395874, 0.7743820548057556, 4.017569541931152 ] }, { "mesh":27, "name":"LOVE-Letter-L", "rotation":[ 0.5, 0.5, -0.5, 0.5 ], "scale":[ 0.09015031903982162, 0.09015031903982162, 0.09015031903982162 ], "translation":[ -2.2090871334075928, 0.810184895992279, 4.076678276062012 ] }, { "mesh":28, "name":"Photo-frame-pic-BR", "rotation":[ 0.5026351809501648, 0.497350811958313, -0.50263512134552, 0.4973509907722473 ], "translation":[ -2.4332211017608643, 1.5313353538513184, 3.834543228149414 ] }, { "mesh":29, "name":"Photo-frame-BR", "rotation":[ 0.5026351809501648, 0.497350811958313, -0.50263512134552, 0.4973509907722473 ], "translation":[ -2.434985876083374, 1.530707836151123, 3.834543228149414 ] }, { "mesh":30, "name":"Photo-frame-BL", "rotation":[ 0.5026351809501648, 0.497350811958313, -0.50263512134552, 0.4973509907722473 ], "translation":[ -2.434985876083374, 1.530707836151123, 4.0659613609313965 ] }, { "mesh":31, "name":"Photo-frame-pic-BL", "rotation":[ 0.5026351809501648, 0.497350811958313, -0.50263512134552, 0.4973509907722473 ], "translation":[ -2.4332211017608643, 1.5313353538513184, 4.0659613609313965 ] }, { "mesh":32, "name":"Photo-frame-pic-TL", "rotation":[ 0.5042079091072083, 0.4957563281059265, -0.504207968711853, 0.49575650691986084 ], "translation":[ -2.4322803020477295, 1.8389313220977783, 4.0659613609313965 ] }, { "mesh":33, "name":"Photo-frame-TL", "rotation":[ 0.5042079091072083, 0.4957563281059265, -0.504207968711853, 0.49575650691986084 ], "translation":[ -2.434049129486084, 1.8383150100708008, 4.0659613609313965 ] }, { "mesh":34, "name":"Cushion.001", "rotation":[ -0.5113464593887329, 0.6619700193405151, -0.22544872760772705, 0.49949315190315247 ], "translation":[ 0.8854628801345825, 0.637373685836792, 4.625072002410889 ] }, { "mesh":35, "name":"Ceiling-light-fitting", "translation":[ 0.3832467794418335, 3.173314332962036, 2.8927838802337646 ] }, { "mesh":36, "name":"Ceiling-light-bulb-fitting", "translation":[ 0.3832467794418335, 2.7197864055633545, 2.8927838802337646 ] }, { "mesh":37, "name":"Ceiling-light-cord", "translation":[ 0.3832467794418335, 2.7294976711273193, 2.8927838802337646 ] }, { "mesh":38, "name":"Ceiling-light-shade-wire", "scale":[ 1.1797983646392822, 1.0473384857177734, 1.1797983646392822 ], "translation":[ 0.3832467794418335, 2.7020483016967773, 2.8927838802337646 ] }, { "mesh":39, "name":"Ceiling-light-Shade", "scale":[ 1.1797983646392822, 1.0473384857177734, 1.1797983646392822 ], "translation":[ 0.3832467794418335, 2.410550594329834, 2.8927838802337646 ] }, { "mesh":40, "name":"Photo-frame-TR", "rotation":[ 0.5042079091072083, 0.4957563281059265, -0.504207968711853, 0.49575650691986084 ], "translation":[ -2.434049129486084, 1.8383150100708008, 3.834543228149414 ] }, { "mesh":41, "name":"Book1", "rotation":[ 0.054181575775146484, 0, 0, 0.9985311031341553 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.012490554712712765 ], "translation":[ -2.2731266021728516, 0.9334782361984253, 4.177110195159912 ] }, { "mesh":42, "name":"Book5", "rotation":[ 0.06546599417924881, 0, 0, 0.9978548884391785 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.012490554712712765 ], "translation":[ -2.2731266021728516, 0.9333921074867249, 4.08355712890625 ] }, { "mesh":43, "name":"Book2", "rotation":[ 0.059965748339891434, 0, 0, 0.9982004761695862 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.012490552850067616 ], "translation":[ -2.279275417327881, 0.9342971444129944, 4.148632049560547 ] }, { "mesh":44, "name":"Book3", "rotation":[ 0.06338035315275192, 0, 0, 0.9979895353317261 ], "scale":[ 0.08613072335720062, 0.08613074570894241, 0.01249055378139019 ], "translation":[ -2.2731266021728516, 0.9331662654876709, 4.129520893096924 ] }, { "mesh":45, "name":"Book6", "rotation":[ 0.10614249855279922, 0, 0, 0.9943510293960571 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.012490554712712765 ], "translation":[ -2.3038711547851562, 0.9338763356208801, 4.05584192276001 ] }, { "mesh":46, "name":"Book16", "rotation":[ -0.030540894716978073, 0, 0, 0.9995335340499878 ], "scale":[ 0.08613072335720062, 0.08613071590662003, 0.01249055564403534 ], "translation":[ -2.2731266021728516, 0.663068413734436, 3.724823474884033 ] }, { "mesh":47, "name":"Book17", "rotation":[ -0.02232883684337139, 0, 0, 0.9997506737709045 ], "scale":[ 0.08613072335720062, 0.08613070845603943, 0.012490556575357914 ], "translation":[ -2.2731266021728516, 0.6628230810165405, 3.6945221424102783 ] }, { "mesh":48, "name":"Book15", "rotation":[ -0.04502567648887634, 0, 0, 0.9989858269691467 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.012490552850067616 ], "translation":[ -2.2731122970581055, 0.6633697152137756, 3.747779369354248 ] }, { "mesh":49, "name":"Book18", "rotation":[ -0.008273575454950333, 0, 0, 0.9999657869338989 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.012490554712712765 ], "translation":[ -2.2731266021728516, 0.6619119048118591, 3.6714670658111572 ] }, { "mesh":50, "name":"Book4", "rotation":[ 0.06292934715747833, 0, 0, 0.9980179667472839 ], "scale":[ 0.08613072335720062, 0.08613073080778122, 0.01249055378139019 ], "translation":[ -2.2639031410217285, 0.9350541830062866, 4.10331392288208 ] }, { "mesh":51, "name":"Book8", "scale":[ 0.08613072335720062, 0.08613072335720062, 0.01249055378139019 ], "translation":[ -2.2657485008239746, 0.9318746328353882, 3.8247056007385254 ] }, { "mesh":52, "name":"Book9", "scale":[ 0.08613072335720062, 0.08613072335720062, 0.01249055378139019 ], "translation":[ -2.2731266021728516, 0.9318746328353882, 3.800419569015503 ] }, { "mesh":53, "name":"Book10", "rotation":[ -0.05672706663608551, 0, 0, 0.998389720916748 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.01249055378139019 ], "translation":[ -2.285423517227173, 0.9329015016555786, 3.7837345600128174 ] }, { "mesh":54, "name":"Book11", "rotation":[ -0.04585462808609009, 0, 0, 0.9989481568336487 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.012490554712712765 ], "translation":[ -2.2817344665527344, 0.932712197303772, 3.764090061187744 ] }, { "mesh":55, "name":"Book7", "rotation":[ -0.01554703526198864, 0, 0, 0.9998792409896851 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.01249055378139019 ], "translation":[ -2.292801856994629, 0.9318746328353882, 3.8652091026306152 ] }, { "mesh":56, "name":"Book12", "rotation":[ -0.026683012023568153, 0, 0, 0.9996439814567566 ], "scale":[ 0.08613072335720062, 0.08613073080778122, 0.012490551918745041 ], "translation":[ -2.2878830432891846, 0.9317463636398315, 3.730381488800049 ] }, { "mesh":57, "name":"Book13", "rotation":[ -0.020824573934078217, 0, 0, 0.9997831583023071 ], "scale":[ 0.08613072335720062, 0.08613074570894241, 0.01249055564403534 ], "translation":[ -2.27681565284729, 0.9321562647819519, 3.702394962310791 ] }, { "mesh":58, "name":"Book14", "rotation":[ -0.017526455223560333, 0, 0, 0.9998463988304138 ], "scale":[ 0.08613072335720062, 0.08613072335720062, 0.012490554712712765 ], "translation":[ -2.2731266021728516, 0.9320932626724243, 3.6755876541137695 ] }, { "mesh":59, "name":"Chest-of-Drawers-Base", "rotation":[ 0.7071068286895752, 0, -0.7071068286895752, 5.338509012631221e-08 ], "scale":[ 0.8320465683937073, 0.832046627998352, 0.8320465683937073 ], "translation":[ -2.154634952545166, 0.14899420738220215, 1.7385165691375732 ] }, { "mesh":60, "name":"Chest-of-Drawers-Sides", "rotation":[ 0, 0, -0.7071065902709961, 0.7071070075035095 ], "scale":[ 0.832046389579773, 0.832046389579773, 0.8320464491844177 ], "translation":[ -2.242722511291504, 0.5351095199584961, 1.7384898662567139 ] }, { "mesh":61, "name":"Chest-of-Drawers-Top", "rotation":[ 0.4999999701976776, 0.4999999701976776, -0.5, 0.5 ], "scale":[ 0.8320465683937073, 0.8320465683937073, 0.8320465683937073 ], "translation":[ -2.242722511291504, 1.184232473373413, 1.7385165691375732 ] }, { "mesh":62, "name":"Chest-of-Drawers-Handles", "rotation":[ 0, 0.7071068286895752, 0, 0.7071067094802856 ], "scale":[ 0.8320465683937073, 0.8320464491844177, 0.8320465683937073 ], "translation":[ -2.347294569015503, 1.0520886182785034, 1.7364802360534668 ] }, { "mesh":63, "name":"Chest-of-Drawers-Drawers-Top", "rotation":[ 0, 0.7071068286895752, 0, 0.7071067094802856 ], "scale":[ 0.8320465683937073, 0.8320464491844177, 0.8320465683937073 ], "translation":[ -2.3677444458007812, 0.7001770734786987, 1.7385165691375732 ] }, { "mesh":64, "name":"Chest-of-Drawers-Drawers", "rotation":[ 0, 0.7071068286895752, 0, 0.7071067094802856 ], "scale":[ 0.8320465683937073, 0.8320464491844177, 0.8320465683937073 ], "translation":[ -2.3677444458007812, 0.7815507650375366, 1.7385165691375732 ] }, { "mesh":65, "name":"LCD-TV", "rotation":[ 0.5000002384185791, 0.49999988079071045, -0.5000001192092896, 0.49999988079071045 ], "translation":[ -1.8813024759292603, 1.8465850353240967, 2.878190755844116 ] }, { "mesh":66, "name":"Plane.042", "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 2.6291208267211914, 1.2779653072357178, 1.6885590553283691 ] }, { "mesh":67, "name":"Cushion.003", "rotation":[ 0.4908314347267151, -0.8651542067527771, 0.09994808584451675, 0.024558179080486298 ], "translation":[ 2.329519033432007, 0.6274178624153137, 2.938812494277954 ] }, { "mesh":68, "name":"Chair-base.001", "rotation":[ 0, 0.7071069478988647, 0, 0.7071066498756409 ], "scale":[ 0.832046389579773, 0.8320464491844177, 0.832046389579773 ], "translation":[ 0.42162710428237915, 0.26330262422561646, 1.7494981288909912 ] }, { "mesh":69, "name":"Chair-back.001", "rotation":[ 0.445726215839386, 0.548933744430542, 0.44572603702545166, 0.5489336848258972 ], "scale":[ 0.8320465683937073, 0.8320465683937073, 0.8320464491844177 ], "translation":[ 0.4248037040233612, 0.34699004888534546, 1.4029626846313477 ] }, { "mesh":70, "name":"Chair-arms.001", "rotation":[ 0, 0.7071069478988647, 0, 0.7071066498756409 ], "scale":[ 0.832046389579773, 0.8320464491844177, 0.832046389579773 ], "translation":[ 0.4243548512458801, 0.05198943614959717, 1.7494981288909912 ] }, { "mesh":71, "name":"Cushion.002", "rotation":[ 0.4925888776779175, 0.009835230186581612, 0.05611172690987587, 0.8683956265449524 ], "translation":[ 0.5686255097389221, 0.6308478116989136, 1.5467344522476196 ] }, { "mesh":72, "name":"Standing-Lamp-Shade-Outer", "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 2.6291208267211914, 1.4259439706802368, 1.6885590553283691 ] }, { "mesh":73, "name":"Plane.040", "rotation":[ -0.7071068286895752, 0, 0.7071068286895752, 1.9949604279645428e-07 ], "scale":[ 0.8320465683937073, 0.832046627998352, 0.8320465683937073 ], "translation":[ -2.1752309799194336, 0.06389200687408447, 4.070070743560791 ] }, { "mesh":74, "name":"Plane.039", "rotation":[ 0.5000001192092896, 0.5, -0.5, 0.5 ], "scale":[ 0.8320465683937073, 0.8320465683937073, 0.8320465683937073 ], "translation":[ -2.282989740371704, 0.9208285808563232, 4.070070743560791 ] }, { "mesh":75, "name":"Book-Shelf", "rotation":[ 0, 0.7071068286895752, 0, 0.7071067094802856 ], "scale":[ 0.8320465683937073, 0.8320464491844177, 0.8320465683937073 ], "translation":[ -2.282989740371704, 0.8074179291725159, 4.070070743560791 ] }, { "mesh":76, "name":"Coffee-Table-Handle1", "rotation":[ 0, -1, 0, 7.549791547489804e-08 ], "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 0.5338399410247803, 0.06817221641540527, 2.934676170349121 ] }, { "mesh":77, "name":"Coffee-Table-Drawer", "rotation":[ 0, -1, 0, 7.549791547489804e-08 ], "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 0.5331442356109619, 0.055516839027404785, 2.9537901878356934 ] }, { "mesh":78, "name":"Coffee-Table-Suround", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ], "scale":[ 0.24178721010684967, 0.24178719520568848, 0.24178719520568848 ], "translation":[ 0.5331442356109619, 0.3593718409538269, 2.957340955734253 ] }, { "mesh":79, "name":"Plug-Socket3", "rotation":[ 0.4760635793209076, -0.5228416919708252, 0.4760635495185852, 0.5228418707847595 ], "scale":[ 0.832046389579773, 0.8320465087890625, 0.8320465087890625 ], "translation":[ 3.0061659812927246, 0.34290236234664917, 1.6741163730621338 ] }, { "mesh":80, "name":"Big-Pic-frame-Right", "rotation":[ 0.5075001120567322, -0.49238571524620056, 0.507500171661377, 0.492385596036911 ], "translation":[ 2.9721333980560303, 1.7000353336334229, 2.652094841003418 ] }, { "mesh":81, "name":"Big-Pic-frame-pic-Right", "rotation":[ 0.5075001120567322, -0.49238571524620056, 0.507500171661377, 0.492385596036911 ], "translation":[ 2.9676499366760254, 1.6997840404510498, 2.652094841003418 ] }, { "mesh":79, "name":"Plug-Socket", "rotation":[ 0.4760635495185852, 0.52284175157547, -0.4760635793209076, 0.5228416919708252 ], "scale":[ 0.832046389579773, 0.8320465087890625, 0.8320465087890625 ], "translation":[ -2.4398510456085205, 0.34290236234664917, 3.8262343406677246 ] }, { "mesh":82, "name":"Big-Picture-frame-glass-Left", "rotation":[ 0.5053536295890808, 0.4945884346961975, -0.5053535103797913, 0.49458855390548706 ], "translation":[ -2.415652275085449, 1.806846022605896, 1.741856575012207 ] }, { "mesh":83, "name":"Big-Picture-frame-pic-Left", "rotation":[ 0.5053536295890808, 0.4945884346961975, -0.5053535103797913, 0.49458855390548706 ], "translation":[ -2.4186534881591797, 1.8066214323043823, 1.741856575012207 ] }, { "mesh":84, "name":"Big-Picture-frame-Left", "rotation":[ 0.5053536295890808, 0.4945884346961975, -0.5053535103797913, 0.49458855390548706 ], "translation":[ -2.422860622406006, 1.8050754070281982, 1.741856575012207 ] }, { "mesh":85, "name":"Blind-cord-knob-Right", "rotation":[ 0, -0.2473352551460266, 0, 0.9689300656318665 ], "translation":[ 1.4115173816680908, 2.380880117416382, 0.7399067878723145 ] }, { "mesh":86, "name":"Blind-cord-Right", "rotation":[ 0, -0.2473352551460266, 0, 0.9689300656318665 ], "translation":[ 1.4115173816680908, 2.398815870285034, 0.7399067878723145 ] }, { "mesh":87, "name":"Blind-cord-Fixing-Right", "rotation":[ 0.6851370334625244, -0.1748923659324646, 0.17489241063594818, 0.6851369142532349 ], "translation":[ 1.410313367843628, 2.4108941555023193, 0.7421114444732666 ] }, { "mesh":88, "name":"Blind-Roll-Ends-Fixings-Right", "rotation":[ 0, -0.2473352551460266, 0, 0.9689300656318665 ], "translation":[ 1.4864286184310913, 2.890126943588257, 0.6014595031738281 ] }, { "mesh":89, "name":"Blind-Roll-Ends-Right", "rotation":[ 0.3607974946498871, -0.608132541179657, 0.608132541179657, 0.3607974946498871 ], "translation":[ 1.3991007804870605, 2.7530863285064697, 0.7626429796218872 ] }, { "mesh":90, "name":"Blind-drop-bar-Right", "rotation":[ 0.3607974946498871, -0.608132541179657, 0.608132541179657, 0.3607974946498871 ], "translation":[ 1.4121025800704956, 2.6807801723480225, 0.7388352155685425 ] }, { "mesh":91, "name":"Blind-drop-Right", "rotation":[ 0.3607974946498871, -0.608132541179657, 0.608132541179657, 0.3607974946498871 ], "translation":[ 1.4125889539718628, 2.7497336864471436, 0.7379449605941772 ] }, { "mesh":92, "name":"Blind-roll-Right", "rotation":[ 0.3607974946498871, -0.608132541179657, 0.608132541179657, 0.3607974946498871 ], "translation":[ 1.3991007804870605, 2.7530863285064697, 0.7626429796218872 ] }, { "mesh":93, "name":"Blind-roll-Left", "rotation":[ 0.6077407598495483, -0.3614570200443268, 0.36145704984664917, 0.6077407598495483 ], "translation":[ -0.9705768823623657, 2.7530863285064697, 0.7683078050613403 ] }, { "mesh":94, "name":"Blind-drop-Left", "rotation":[ 0.6077407598495483, -0.3614570200443268, 0.36145704984664917, 0.6077407598495483 ], "translation":[ -0.9840114116668701, 2.7497336864471436, 0.7435802221298218 ] }, { "mesh":95, "name":"Blind-drop-bar-Left", "rotation":[ 0.6077407598495483, -0.3614570200443268, 0.36145704984664917, 0.6077407598495483 ], "translation":[ -0.9835271835327148, 2.65942645072937, 0.744471549987793 ] }, { "mesh":96, "name":"Blind-Roll-Ends-Left", "rotation":[ 0.6077407598495483, -0.3614570200443268, 0.36145704984664917, 0.6077407598495483 ], "translation":[ -0.9705768823623657, 2.7530863285064697, 0.7683078050613403 ] }, { "mesh":97, "name":"Blind-Roll-Ends-Fixings-Left", "rotation":[ 0, 0.24628375470638275, 0, 0.9691977500915527 ], "translation":[ -1.0586304664611816, 2.890126943588257, 0.6075193881988525 ] }, { "mesh":98, "name":"Blind-cord-Fixing-Left", "rotation":[ 0.685326337814331, 0.17414893209934235, -0.17414894700050354, 0.685326337814331 ], "translation":[ -0.9817452430725098, 2.389540433883667, 0.7477517127990723 ] }, { "mesh":99, "name":"Blind-cord-Left", "rotation":[ 0, 0.24628375470638275, 0, 0.9691977500915527 ], "translation":[ -0.9829443693161011, 2.377462148666382, 0.74554443359375 ] }, { "mesh":100, "name":"Blind-cord-knob-Left", "rotation":[ 0, 0.24628375470638275, 0, 0.9691977500915527 ], "translation":[ -0.9829443693161011, 2.3595263957977295, 0.74554443359375 ] }, { "mesh":101, "name":"Blind-cord-knob-Middle", "translation":[ 0.2154962122440338, 2.4515154361724854, 0.46897247433662415 ] }, { "mesh":102, "name":"Blind-cord-Middle", "translation":[ 0.2154962122440338, 2.4694507122039795, 0.46897247433662415 ] }, { "mesh":103, "name":"Blind-cord-Fixing-Middle", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ], "translation":[ 0.2154962122440338, 2.4807980060577393, 0.4714844524860382 ] }, { "mesh":104, "name":"Blind-Roll-Ends-Fixings-Middle", "translation":[ 0.21488434076309204, 2.890126943588257, 0.3115592300891876 ] }, { "mesh":105, "name":"Blind-Roll-Ends-Middle", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ 0.2154962122440338, 2.7530863285064697, 0.4948785603046417 ] }, { "mesh":106, "name":"Blind-drop-bar-Middle", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ 0.2154962122440338, 2.7497336864471436, 0.4677514135837555 ] }, { "mesh":107, "name":"Blind-drop-Middle", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ 0.2154962122440338, 2.7497336864471436, 0.46673718094825745 ] }, { "mesh":108, "name":"Blind-roll-Middle", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ 0.2154962122440338, 2.7530863285064697, 0.4948785603046417 ] }, { "mesh":79, "name":"Plug-Socket2", "rotation":[ 0.6732555627822876, 0, 0, 0.739409863948822 ], "scale":[ 0.8320464491844177, 0.8320465087890625, 0.8320465087890625 ], "translation":[ -1.6679747104644775, 0.28773653507232666, 1.3029168844223022 ] }, { "mesh":109, "name":"Chair-arms", "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 2.1286563873291016, 0.05198943614959717, 2.830965518951416 ] }, { "mesh":110, "name":"Chair-back", "rotation":[ 0, 0, 0.6303520202636719, 0.7763094305992126 ], "scale":[ 0.8320465683937073, 0.8320465683937073, 0.8320464491844177 ], "translation":[ 2.475191831588745, 0.34699004888534546, 2.8267672061920166 ] }, { "mesh":111, "name":"Chair-base", "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 2.1286563873291016, 0.26330262422561646, 2.828237771987915 ] }, { "mesh":112, "name":"Sofa-arms", "rotation":[ 0, -0.7071066498756409, 0, 0.7071069478988647 ], "scale":[ 0.832046389579773, 0.8320464491844177, 0.832046389579773 ], "translation":[ 0.3138567805290222, 0.053914427757263184, 4.4244704246521 ] }, { "mesh":113, "name":"Sofa-back", "rotation":[ -0.44572609663009644, -0.548933744430542, 0.4457261562347412, 0.5489336848258972 ], "scale":[ 0.8320465683937073, 0.8320465683937073, 0.8320464491844177 ], "translation":[ 0.31503480672836304, 0.3489152193069458, 4.771006107330322 ] }, { "mesh":114, "name":"Sofa-base", "rotation":[ 0, -0.7071066498756409, 0, 0.7071069478988647 ], "scale":[ 0.832046389579773, 0.8320464491844177, 0.832046389579773 ], "translation":[ -0.06952560693025589, 0.265227735042572, 4.4244704246521 ] }, { "mesh":115, "name":"Cushion", "rotation":[ 0.1643114686012268, 0.848813533782959, -0.4677543044090271, 0.18363890051841736 ], "translation":[ -0.2681952118873596, 0.6439410448074341, 4.570359706878662 ] }, { "mesh":116, "name":"Rug", "translation":[ 0.5366979837417603, 0.02943408489227295, 2.9517576694488525 ] }, { "mesh":117, "name":"Coffee-Table-drawer2", "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 0.5331442356109619, 0.055649399757385254, 2.9589078426361084 ] }, { "mesh":118, "name":"Coffee-Table-Handle2", "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 0.5331442356109619, 0.06817221641540527, 2.9793577194213867 ] }, { "mesh":119, "name":"Coffee-Table-Legs", "rotation":[ 0, -0.7071066498756409, 0, 0.7071069478988647 ], "scale":[ 0.832046389579773, 0.8320464491844177, 0.832046389579773 ], "translation":[ 0.5331442356109619, 0.05169498920440674, 2.956437110900879 ] }, { "mesh":120, "name":"Coffee-Table-uprights", "scale":[ 0.8320464491844177, 0.8320464491844177, 0.8320464491844177 ], "translation":[ 0.5331442356109619, 0.055435530841350555, 2.956437110900879 ] }, { "mesh":121, "name":"Rad-Floor-Cover-Left", "translation":[ 2.9334805011749268, 0.03835034370422363, 3.1527419090270996 ] }, { "mesh":122, "name":"Rad-Floor-Cover-Right", "translation":[ 2.933389663696289, 0.03835034370422363, 2.0671274662017822 ] }, { "mesh":123, "name":"Rad-Knob", "translation":[ 2.9334805011749268, 0.25281208753585815, 3.1524460315704346 ] }, { "mesh":124, "name":"Rad-Nut-End-Left", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ], "translation":[ 2.9334259033203125, 0.6420339941978455, 3.103543281555176 ] }, { "mesh":125, "name":"Rad-Nut-End-Right", "rotation":[ 0, 0.7071066498756409, -0.70710688829422, 8.432163411953297e-09 ], "translation":[ 2.9334259033203125, 0.6420339941978455, 2.1122641563415527 ] }, { "mesh":126, "name":"Rad-panel-End-Left", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ], "translation":[ 2.9334259033203125, 0.40119630098342896, 3.0704586505889893 ] }, { "mesh":127, "name":"Rad-panel-End-Right", "rotation":[ 0, 0.7071066498756409, -0.70710688829422, 8.432163411953297e-09 ], "translation":[ 2.9334259033203125, 0.40119630098342896, 2.1455821990966797 ] }, { "mesh":128, "name":"Rad-panel-Mid", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ], "translation":[ 2.9334259033203125, 0.40119630098342896, 3.004451036453247 ] }, { "mesh":129, "name":"Rad-Pipe-ball-Left", "rotation":[ 0, 1, 0, 1.1924880638503055e-08 ], "translation":[ 2.9334805011749268, 0.16174757480621338, 3.1524460315704346 ] }, { "mesh":130, "name":"Rad-Pipework-Left", "translation":[ 2.934143543243408, 0.24655157327651978, 3.1524460315704346 ] }, { "mesh":131, "name":"Rad-Pipework-Right", "translation":[ 2.933389663696289, 0.24655157327651978, 2.0923078060150146 ] }, { "mesh":132, "name":"Tea-cup", "rotation":[ 0, -0.327330619096756, 0, 0.9449098706245422 ], "translation":[ 0.8274208903312683, 0.4649931490421295, 3.107694625854492 ] }, { "mesh":133, "name":"Tea-cup-saucer", "rotation":[ 0, -0.2557510733604431, 0, 0.9667426943778992 ], "translation":[ 0.8274208903312683, 0.4618278443813324, 3.107694625854492 ] }, { "mesh":134, "name":"Tea-spoon2", "rotation":[ -0.006642984226346016, -0.2804909348487854, 0.022725779563188553, 0.9595646262168884 ], "translation":[ 0.8394526243209839, 0.46989956498146057, 3.162907838821411 ] }, { "mesh":135, "name":"Teapot-base", "rotation":[ 0, -0.06640083342790604, 0, 0.9977930188179016 ], "translation":[ 0.9331851601600647, 0.46185222268104553, 2.9529385566711426 ] }, { "mesh":136, "name":"Teapot-lid", "rotation":[ 0, -0.06640083342790604, 0, 0.9977930188179016 ], "translation":[ 0.9331851601600647, 0.46100959181785583, 2.9529385566711426 ] }, { "mesh":137, "name":"Radio-back", "rotation":[ 0, 0, 1, 4.17172027445626e-15 ], "scale":[ -1.3006994724273682, -1.3006997108459473, -23.850276947021484 ], "translation":[ -0.05415674299001694, -0.5765234231948853, -29.3918399810791 ] }, { "mesh":138, "name":"Radio-BUSH-logo", "rotation":[ 0, 0, 1, 2.980232594040899e-08 ], "translation":[ -0.9381040930747986, 0.19397583603858948, -25.337797164916992 ] }, { "mesh":139, "name":"Radio-dial-back", "rotation":[ 0, 0, 1, 4.972420697413327e-08 ], "scale":[ -0.7491905689239502, -0.7491905689239502, -13.737532615661621 ], "translation":[ 0.46280914545059204, -0.547615647315979, -26.8626651763916 ] }, { "mesh":140, "name":"Radio-dial-edge", "rotation":[ 0, 0, 1, 2.980232594040899e-08 ], "translation":[ 0.46280914545059204, -0.547615647315979, -26.8626651763916 ] }, { "mesh":141, "name":"Radio-dial-glass", "rotation":[ 0, 0, -1, 7.4505766001209395e-09 ], "translation":[ 0.4636607766151428, 0.20150700211524963, -26.809812545776367 ] }, { "mesh":142, "name":"Radio-feet", "rotation":[ 0, -0.7071065902709961, 0.7071070671081543, 2.472583560120256e-08 ], "scale":[ -0.039950791746377945, -0.7325574159622192, -0.2552826702594757 ], "translation":[ 0.9590277075767517, -0.21494066715240479, -52.56930160522461 ] }, { "mesh":143, "name":"Radio-front", "rotation":[ 0, 0, -1, 7.4505766001209395e-09 ], "translation":[ -0.05423874780535698, 0.1839851438999176, -26.346830368041992 ] }, { "mesh":144, "name":"Radio-handle-chrome", "scale":[ -0.28543949127197266, -0.28543949127197266, -5.233961582183838 ], "translation":[ -0.04926714673638344, -0.2073417454957962, -5.458431243896484 ] }, { "mesh":145, "name":"Radio-inner", "rotation":[ 0, 0, -1, 1.1656565490625326e-08 ], "scale":[ -1.2783485651016235, -1.278348684310913, -23.4404354095459 ], "translation":[ -0.05270620435476303, 0.11164035648107529, -29.391843795776367 ] }, { "mesh":146, "name":"Radio-Knob", "rotation":[ 0, 0, 1, 2.980232594040899e-08 ], "translation":[ 0.46366173028945923, 0.18398705124855042, -26.809812545776367 ] }, { "mesh":147, "name":"Radio-shell", "rotation":[ 0, 0, 1, 4.17172027445626e-15 ], "scale":[ -1.3006994724273682, -1.3006997108459473, -23.850276947021484 ], "translation":[ -0.05415578931570053, 0.13842526078224182, -29.391843795776367 ] }, { "mesh":148, "name":"Radio-trim-Back", "rotation":[ 0, 0, 1, 4.17172027445626e-15 ], "scale":[ -1.3165284395217896, -1.3075554370880127, -23.975990295410156 ], "translation":[ -0.054157696664333344, -0.9042078256607056, -29.3918399810791 ] }, { "mesh":149, "name":"Radio-trim-front", "rotation":[ 0, 0, 1, 2.980232594040899e-08 ], "translation":[ -0.05415865033864975, -0.2120414674282074, -29.391841888427734 ] }, { "children":[ 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153 ], "mesh":150, "name":"Radio-handle", "rotation":[ 0.28421643376350403, -0.6474727392196655, 0.6474728584289551, 0.28421640396118164 ], "scale":[ -0.07415362447500229, -0.07415362447500229, -0.004044043831527233 ], "translation":[ -2.2290596961975098, 1.4213799238204956, 1.5119402408599854 ] }, { "mesh":151, "name":"Photo-frame-pic-BR.001", "rotation":[ 0.5026351809501648, 0.497350811958313, -0.50263512134552, 0.4973509907722473 ], "translation":[ -2.4332211017608643, 1.8389573097229004, 3.834543228149414 ] }, { "camera":0, "name":"Camera", "rotation":[ -0.10218629986047745, 0.2902959883213043, 0.031194215640425682, 0.9509537816047668 ], "scale":[ 0.8709295988082886, 0.8709295392036438, 0.8709296584129333 ], "translation":[ 2.679414749145508, 1.5535809993743896, 6.165511608123779 ] }, { "mesh":152, "name":"Circle.003", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ -1.8469732999801636, 0.14883899688720703, 2.8784449100494385 ] }, { "mesh":153, "name":"Fireplace-fire-main-Tray", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ -1.845271110534668, 0.11596381664276123, 2.8784449100494385 ] }, { "mesh":154, "name":"Fireplace-fire-main-base", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ -1.829506278038025, 0.1039959192276001, 2.8784449100494385 ] }, { "mesh":155, "name":"Fireplace-fire-Grill-tops", "translation":[ -1.9010403156280518, 0.31762272119522095, 2.8784449100494385 ] }, { "mesh":156, "name":"Plane.019", "translation":[ -1.9010403156280518, 0.3052085041999817, 2.8784449100494385 ] }, { "mesh":157, "name":"Fireplace-fire-grate", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ -1.845271110534668, 0.1813194751739502, 2.8784449100494385 ] }, { "mesh":158, "name":"Fireplace-fire-front-Grill", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ -1.845271110534668, 0.31016409397125244, 2.8784449100494385 ] }, { "mesh":159, "name":"Fireplace-fire-back-plane", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ -1.885275959968567, 0.6804149746894836, 2.8784449100494385 ] }, { "mesh":160, "name":"Fireplace-fire-main-Arch", "rotation":[ 0.5, -0.5, 0.5, 0.5 ], "translation":[ -1.885275959968567, 0.6804149746894836, 2.8784449100494385 ] }, { "mesh":161, "name":"Fireplace-Suround-Scrolls", "rotation":[ 0.70710688829422, 0, 0, 0.7071066498756409 ], "translation":[ -1.7559783458709717, 1.2352337837219238, 2.876844644546509 ] }, { "mesh":162, "name":"Plane.015", "translation":[ -1.825409173965454, -1.0686419010162354, 2.8784449100494385 ] }, { "mesh":163, "name":"Fireplace-Stone-Half", "translation":[ -1.825409173965454, -1.0485877990722656, 2.8784449100494385 ] }, { "mesh":164, "name":"Fireplace-Suround-Top", "translation":[ -1.825409173965454, 0.18601608276367188, 2.8784449100494385 ] }, { "mesh":165, "name":"Fireplace-Suround-Base", "translation":[ -1.825409173965454, 0.09243941307067871, 2.8784449100494385 ] }, { "mesh":166, "name":"Sphere", "scale":[ 0.02500000037252903, 0.05000000074505806, 0.02500000037252903 ], "translation":[ 2.630155324935913, 1.4528168439865112, 1.686143398284912 ] }, { "mesh":167, "name":"Icosphere.005", "rotation":[ 0.3624155521392822, 0.5969444513320923, 0.07239257544279099, 0.7120895981788635 ], "scale":[ 0.42566344141960144, 0.42566347122192383, 0.4256633520126343 ], "translation":[ 0.19917480647563934, 0.507774293422699, 2.924175500869751 ] }, { "mesh":168, "name":"Icosphere.004", "rotation":[ -0.08696160465478897, 0.2491137534379959, 0.30457642674446106, 0.915212094783783 ], "scale":[ 0.25405651330947876, 0.25405651330947876, 0.25405651330947876 ], "translation":[ 0.2456478327512741, 0.4927559494972229, 2.889678478240967 ] }, { "mesh":169, "name":"Sphere.001", "rotation":[ -0.0615028440952301, -0.03091888129711151, 0.1267399936914444, 0.9895446300506592 ], "scale":[ 0.02835756726562977, 0.02835756726562977, 0.028357569128274918 ], "translation":[ 0.27565765380859375, 0.498931348323822, 2.9445929527282715 ] }, { "mesh":170, "name":"Icosphere.001", "rotation":[ -0.28945568203926086, -0.43979963660240173, 0.15163302421569824, 0.8365401029586792 ], "scale":[ 0.10750720649957657, 0.10750721395015717, 0.10750720649957657 ], "translation":[ 0.2463006228208542, 0.48019275069236755, 2.993616819381714 ] }, { "mesh":171, "name":"Icosphere.002", "rotation":[ 0.3760858476161957, 0.4652629494667053, 0.616178572177887, 0.5122635364532471 ], "scale":[ 0.10750720649957657, 0.10750720649957657, 0.10750720649957657 ], "translation":[ 0.24362239241600037, 0.4800707697868347, 2.943013906478882 ] }, { "mesh":172, "name":"Sphere.002", "rotation":[ -0.02287924475967884, -0.014835351146757603, 0.16830526292324066, 0.9853578209877014 ], "scale":[ 0.018231168389320374, 0.018231168389320374, 0.018231168389320374 ], "translation":[ 0.3063569962978363, 0.4899011254310608, 2.9778501987457275 ] }, { "mesh":173, "name":"Torus", "scale":[ 0.07076700031757355, 0.07076724618673325, 0.07076724618673325 ], "translation":[ -1.7830919027328491, 1.3236432075500488, 3.2523293495178223 ] }, { "mesh":174, "name":"Torus.002", "scale":[ 0.051515132188797, 0.051515132188797, 0.051515132188797 ], "translation":[ -1.7831077575683594, 1.3130135536193848, 3.0580222606658936 ] }, { "mesh":175, "name":"Cube", "rotation":[ -0.001130877761170268, 0.033382151275873184, -0.0008058790699578822, 0.9994418025016785 ], "scale":[ 0.02203996293246746, 0.02203996293246746, 0.02203996293246746 ], "translation":[ -1.9765292406082153, 0.13079993426799774, 2.9674112796783447 ] }, { "mesh":176, "name":"Cube.001", "scale":[ 0.02203996293246746, 0.02203996293246746, 0.04837724566459656 ], "translation":[ -1.9504104852676392, 0.23689241707324982, 2.847280502319336 ] }, { "mesh":177, "name":"Cube.002", "rotation":[ 0.0001098120555980131, -0.0047805458307266235, 0.0024924587924033403, 0.9999855756759644 ], "scale":[ 0.010889016091823578, 0.010889015160501003, 0.010889015160501003 ], "translation":[ -2.0331785678863525, 0.11950372904539108, 2.813229560852051 ] }, { "mesh":178, "name":"Cube.003", "rotation":[ -0.0008358443737961352, 0.07081835716962814, -0.005114189814776182, 0.997475802898407 ], "scale":[ 0.010889015160501003, 0.010889015160501003, 0.010889015160501003 ], "translation":[ -1.9475034475326538, 0.11968352645635605, 2.836214780807495 ] }, { "mesh":179, "name":"Cube.004", "rotation":[ -0.0003359581169206649, 0.002002191497012973, -0.008173426613211632, 0.9999645352363586 ], "scale":[ 0.010889015160501003, 0.010889015160501003, 0.010889016091823578 ], "translation":[ -1.9457011222839355, 0.11974502354860306, 2.8892157077789307 ] }, { "mesh":180, "name":"Cube.005", "rotation":[ -0.03455517068505287, 0.0004973384784534574, -0.05653561279177666, 0.9978023767471313 ], "scale":[ 0.011178364977240562, 0.01027369312942028, 0.04837724566459656 ], "translation":[ -1.9360562562942505, 0.14450013637542725, 2.851992130279541 ] }, { "mesh":181, "name":"Cube.006", "rotation":[ -0.07428764551877975, 0.032259441912174225, -0.0026640030555427074, 0.9967113733291626 ], "scale":[ 0.011178364045917988, 0.010273694060742855, 0.04837724566459656 ], "translation":[ -1.9860270023345947, 0.1248173639178276, 2.8209848403930664 ] }, { "mesh":182, "name":"Cube.007", "scale":[ 0.007836089469492435, 0.008200375363230705, 0.1282462328672409 ], "translation":[ -1.9504104852676392, 0.23689241707324982, 2.847280502319336 ] }, { "mesh":183, "name":"Cube.008", "rotation":[ -0.0011886717984452844, 0.12032090127468109, -0.004237604793161154, 0.9927253127098083 ], "scale":[ 0.010889015160501003, 0.010889015160501003, 0.010889015160501003 ], "translation":[ -1.9777460098266602, 0.11967272311449051, 2.7575795650482178 ] }, { "mesh":184, "name":"Cube.009", "rotation":[ 0.0018595510628074408, 0.0021360747050493956, -0.001154154073446989, 0.9999954104423523 ], "scale":[ 0.010889015160501003, 0.010889015160501003, 0.010889016091823578 ], "translation":[ -1.8985399007797241, 0.11956838518381119, 2.9718236923217773 ] }, { "mesh":185, "name":"Sphere.003", "scale":[ 0.02500000037252903, 0.05000000074505806, 0.02500000037252903 ], "translation":[ 0.3987860381603241, 2.3758857250213623, 2.8945016860961914 ] } ], "cameras":[ { "name":"Camera.001", "perspective":{ "aspectRatio":1.7777777777777777, "yfov":0.9799147248268127, "zfar":20, "znear":0.10000000149011612 }, "type":"perspective" } ], "materials":[ { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Painted-white-wood", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Walls", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Floor", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":0 }, "metallicFactor":0, "roughnessFactor":0.17299999296665192 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.8468742966651917, 0.571125328540802 ], "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_emissive_strength":{ "emissiveStrength":10 } }, "name":"Emitter-mid-window", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 0.7196826934814453, 0.8815838098526001, 1 ], "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_emissive_strength":{ "emissiveStrength":5 } }, "name":"Emitter-Rear", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 0, 0, 1 ] } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"lamp-upright-base", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0.18900343775749207, "roughnessFactor":0.09793815016746521 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatFactor":0.10309278219938278 } }, "name":"candle-holders", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.054479971528053284, 0.027320902794599533, 0.01938236877322197, 1 ], "metallicFactor":0, "roughnessFactor":0.46460479497909546 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"magazine", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":1 }, "metallicFactor":0, "roughnessFactor":0.17697593569755554 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_transmission":{ "transmissionFactor":0.6271477937698364 }, "KHR_materials_ior":{ "ior":1 } }, "name":"lampshade-inner", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"FruitBowl", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Love-letters-back", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.08021938055753708, 0.04666513204574585, 0.0331047959625721, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "emissiveFactor":[ 1, 1, 1 ], "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_emissive_strength":{ "emissiveStrength":10 } }, "name":"Love-letters", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"picframe-pica", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":2 }, "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"picframe-black", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.04543089121580124, 0.04543089121580124, 0.04543089121580124, 1 ], "metallicFactor":0.40893471240997314, "roughnessFactor":0.1494845449924469 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"picframe-picb", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":3 }, "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"picframe-pic", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":4 }, "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Cushion1", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":5 }, "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"light-fitting", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"ceiling-shade-wire", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.24619978666305542, 0.24620160460472107, 0.2462015002965927, 1 ], "metallicFactor":0, "roughnessFactor":0.9037800431251526 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Ceiling-lampshade", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":6 }, "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"book-shader", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":7 }, "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"C-Table-box", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0.20000000298023224, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"C-Table-handle-metal", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "roughnessFactor":0.30288368463516235 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Wide-tv-bevel", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.051249973475933075, 0.051249973475933075, 0.051249973475933075, 1 ], "roughnessFactor":0.05670103430747986 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Wide-tv-screen", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.033104557543992996, 0.03310480713844299, 0.0331047922372818, 1 ], "metallicFactor":0, "roughnessFactor":0.10000000149011612 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Cushion3", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":8 }, "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Chair-sofa-leather", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.5088780522346497, 0.46778422594070435, 0.42869067192077637, 1 ], "metallicFactor":0, "roughnessFactor":0.3865979313850403 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Cushion", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":9 }, "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Lampshader-outer", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":10 }, "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Main-wallsocket", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"picframe", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"picframe-pic2", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":11 }, "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_transmission":{ "transmissionFactor":1 }, "KHR_materials_ior":{ "ior":1 } }, "name":"Glass-Fake-picframe", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"picframe-pic2.001", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":12 }, "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Blind-string-knob", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.266353964805603, 0.21586070954799652, 0.17464756965637207, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Blind-String", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Blind-ends", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Blind-wood-strip", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Blind-material", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.800000011920929 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Carpet", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":13 }, "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"C-Table-box-legs", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.8999999761581421 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Chrome-Dull", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "roughnessFactor":0.25 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Rad-Knob-Outer", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.020288454368710518, 0, 0, 1 ], "metallicFactor":0.5, "roughnessFactor":0.737113356590271 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Rad-Knob-Centre", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.15895980596542358, 0.1589609980583191, 0.15896092355251312, 1 ], "metallicFactor":0.41237112879753113, "roughnessFactor":0.47250857949256897 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Rad-Panels-Enamel", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"White-pot.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Chrome-steel-dull", "pbrMetallicRoughness":{ "roughnessFactor":0.20000000298023224 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"TeapotGlass", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 0.4638318419456482, 0.8004080057144165, 1 ], "metallicFactor":0, "roughnessFactor":0.30000001192092896 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Radio-plastic", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.7990978956222534, 0.7529430389404297, 0.5457249879837036, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"bush-logo", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "roughnessFactor":0.03162277862429619 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Radio-dialback", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Radio-dial-red", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.20863580703735352, 0.015996286645531654, 0.010960102081298828, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Radio-metal-ring", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.7990977764129639, 0.7835385203361511, 0.7230554819107056, 1 ], "roughnessFactor":0.43986254930496216 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_transmission":{ "transmissionFactor":1 }, "KHR_materials_ior":{ "ior":1 } }, "name":"Radio-glass", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Handle-chrome", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "roughnessFactor":0.03162277862429619 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Radio-inside", "pbrMetallicRoughness":{ "baseColorFactor":[ 0, 0, 0, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Radio-knob", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.44519850611686707, 0.3762626051902771, 0.16202951967716217, 1 ], "roughnessFactor":0.03162277862429619 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Radio-suround", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.3004956841468811, 0.11759431660175323, 0.05351651459932327, 1 ], "metallicFactor":0.9106529355049133, "roughnessFactor":0 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Radio-edges", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "roughnessFactor":0.029999999329447746 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Radio-handle", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.48514682054519653, 0.46778425574302673, 0.3662528097629547, 1 ], "metallicFactor":0, "roughnessFactor":0.19415807723999023 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"picframe-picc.001", "pbrMetallicRoughness":{ "baseColorTexture":{ "index":14 }, "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Steel", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "roughnessFactor":0.22360679507255554 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Black-Raught-Iron", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.10000000149011612 } }, { "doubleSided":true, "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 } }, "name":"Marble-harf", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.07680906355381012, 0.07680906355381012, 0.07680906355381012, 1 ], "metallicFactor":0.8556700944900513, "roughnessFactor":0.20790377259254456 } }, { "doubleSided":true, "emissiveFactor":[ 1, 1, 1 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":500 } }, "name":"Material.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 0.25, 1, 0.338214635848999 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":3 } }, "name":"ShapesLight", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.9559678435325623, 0.6307579278945923, 0.3049875497817993, 1 ], "roughnessFactor":0.20000000298023224 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.6780573129653931, 0.25 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":5 } }, "name":"ShapesLight.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.9559678435325623, 0.6307579278945923, 0.3049875497817993, 1 ], "roughnessFactor":0.20000000298023224 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"RedSphere", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.8003232479095459, 0.009998607449233532, 0.03754507377743721, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "emissiveFactor":[ 0.8772789239883423, 0.25, 1 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":7 } }, "name":"ShapesLight.003", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.9559678435325623, 0.6307579278945923, 0.3049875497817993, 1 ], "roughnessFactor":0.20000000298023224 } }, { "doubleSided":true, "emissiveFactor":[ 0.25, 0.3961591124534607, 1 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":7 } }, "name":"ShapesLight.002", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.9559678435325623, 0.6307579278945923, 0.3049875497817993, 1 ], "roughnessFactor":0.20000000298023224 } }, { "doubleSided":true, "extensions":{ "KHR_materials_transmission":{ "transmissionFactor":1 } }, "name":"GreenSphere", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.006491726264357567, 0.8006302714347839, 0, 1 ], "metallicFactor":0, "roughnessFactor":0 } }, { "doubleSided":true, "emissiveFactor":[ 1, 1, 1 ], "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_emissive_strength":{ "emissiveStrength":5 } }, "name":"LargerDonut", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "emissiveFactor":[ 1, 1, 1 ], "extensions":{ "KHR_materials_clearcoat":{ "clearcoatRoughnessFactor":0 }, "KHR_materials_emissive_strength":{ "emissiveStrength":10 } }, "name":"SmallerDonut", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.7658302783966064, 0.2915126085281372 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":2 } }, "name":"Poorman's Wood Fire 3", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.5459583401679993, 0.1512901932001114 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":30 } }, "name":"Poorman's Wood Fire 1", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.18275855481624603, 0.039912912994623184 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":2 } }, "name":"Poorman's Wood Fire 2.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.18275855481624603, 0.039912912994623184 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":2 } }, "name":"Poorman's Wood Fire 2.002", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.18275855481624603, 0.039912912994623184 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":2 } }, "name":"Poorman's Wood Fire 2.003", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.5459583401679993, 0.1512901932001114 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":30 } }, "name":"Poorman's Wood Fire 1.002", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.5459583401679993, 0.1512901932001114 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":30 } }, "name":"Poorman's Wood Fire 1.003", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.7658302783966064, 0.2915126085281372 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":30 } }, "name":"Poorman's Wood Fire 3.001", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.18275855481624603, 0.039912912994623184 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":2 } }, "name":"Poorman's Wood Fire 2.004", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.18275855481624603, 0.039912912994623184 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":2 } }, "name":"Poorman's Wood Fire 2.005", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } }, { "doubleSided":true, "emissiveFactor":[ 1, 0.75804549008447, 0.4183522278860195 ], "extensions":{ "KHR_materials_emissive_strength":{ "emissiveStrength":2920.3181862831116 } }, "name":"Material.002", "pbrMetallicRoughness":{ "baseColorFactor":[ 0.800000011920929, 0.800000011920929, 0.800000011920929, 1 ], "metallicFactor":0, "roughnessFactor":0.5 } } ], "meshes":[ { "name":"Plane.011", "primitives":[ { "attributes":{ "POSITION":0, "NORMAL":1 }, "indices":2, "material":0 } ] }, { "name":"Plane.010", "primitives":[ { "attributes":{ "POSITION":3, "NORMAL":4, "TEXCOORD_0":5 }, "indices":6, "material":1 } ] }, { "name":"Plane.006", "primitives":[ { "attributes":{ "POSITION":7, "NORMAL":8, "TEXCOORD_0":9 }, "indices":10, "material":2 } ] }, { "name":"Plane.001", "primitives":[ { "attributes":{ "POSITION":11, "NORMAL":12 }, "indices":13, "material":0 } ] }, { "name":"Plane.009", "primitives":[ { "attributes":{ "POSITION":14, "NORMAL":15 }, "indices":16, "material":0 } ] }, { "name":"Plane.007", "primitives":[ { "attributes":{ "POSITION":17, "NORMAL":18 }, "indices":19, "material":1 } ] }, { "name":"Plane.004", "primitives":[ { "attributes":{ "POSITION":20, "NORMAL":21 }, "indices":22, "material":0 } ] }, { "name":"Plane.003", "primitives":[ { "attributes":{ "POSITION":23, "NORMAL":24 }, "indices":25, "material":0 } ] }, { "name":"Plane.053", "primitives":[ { "attributes":{ "POSITION":26, "NORMAL":27 }, "indices":28, "material":0 } ] }, { "name":"Plane.062", "primitives":[ { "attributes":{ "POSITION":29, "NORMAL":30 }, "indices":31, "material":3 } ] }, { "name":"Plane.061", "primitives":[ { "attributes":{ "POSITION":32, "NORMAL":33 }, "indices":31, "material":4 } ] }, { "name":"Mesh", "primitives":[ { "attributes":{ "POSITION":34, "NORMAL":35 }, "indices":36, "material":5 } ] }, { "name":"Mesh.001", "primitives":[ { "attributes":{ "POSITION":37, "NORMAL":38 }, "indices":39, "material":6 } ] }, { "name":"Mesh.002", "primitives":[ { "attributes":{ "POSITION":40, "NORMAL":41 }, "indices":42, "material":6 } ] }, { "name":"Mesh.003", "primitives":[ { "attributes":{ "POSITION":43, "NORMAL":44 }, "indices":45, "material":6 } ] }, { "name":"Plane", "primitives":[ { "attributes":{ "POSITION":46, "NORMAL":47, "TEXCOORD_0":48 }, "indices":49, "material":7 } ] }, { "name":"Mesh.004", "primitives":[ { "attributes":{ "POSITION":50, "NORMAL":51, "TEXCOORD_0":52 }, "indices":53, "material":8 } ] }, { "name":"Mesh.009", "primitives":[ { "attributes":{ "POSITION":54, "NORMAL":55 }, "indices":56, "material":9 } ] }, { "name":"Circle.043", "primitives":[ { "attributes":{ "POSITION":57, "NORMAL":58 }, "indices":59, "material":10 } ] }, { "name":"Plane.134", "primitives":[ { "attributes":{ "POSITION":60, "NORMAL":61 }, "indices":62, "material":10 } ] }, { "name":"Plane.133", "primitives":[ { "attributes":{ "POSITION":63, "NORMAL":64 }, "indices":62, "material":10 } ] }, { "name":"Plane.132", "primitives":[ { "attributes":{ "POSITION":65, "NORMAL":66 }, "indices":67, "material":10 } ] }, { "name":"Plane.131", "primitives":[ { "attributes":{ "POSITION":68, "NORMAL":69 }, "indices":62, "material":10 } ] }, { "name":"Plane.130", "primitives":[ { "attributes":{ "POSITION":70, "NORMAL":71 }, "indices":72, "material":10 } ] }, { "name":"Plane.129", "primitives":[ { "attributes":{ "POSITION":73, "NORMAL":74 }, "indices":75, "material":11 } ] }, { "name":"Plane.128", "primitives":[ { "attributes":{ "POSITION":76, "NORMAL":77 }, "indices":78, "material":11 } ] }, { "name":"Plane.126", "primitives":[ { "attributes":{ "POSITION":79, "NORMAL":80 }, "indices":81, "material":11 } ] }, { "name":"Plane.125", "primitives":[ { "attributes":{ "POSITION":82, "NORMAL":83 }, "indices":84, "material":11 } ] }, { "name":"Plane.123", "primitives":[ { "attributes":{ "POSITION":85, "NORMAL":86, "TEXCOORD_0":87 }, "indices":88, "material":12 } ] }, { "name":"Plane.122", "primitives":[ { "attributes":{ "POSITION":89, "NORMAL":90 }, "indices":91, "material":13 } ] }, { "name":"Plane.121", "primitives":[ { "attributes":{ "POSITION":92, "NORMAL":93 }, "indices":91, "material":13 } ] }, { "name":"Plane.120", "primitives":[ { "attributes":{ "POSITION":94, "NORMAL":95, "TEXCOORD_0":96 }, "indices":88, "material":14 } ] }, { "name":"Plane.114", "primitives":[ { "attributes":{ "POSITION":97, "NORMAL":98, "TEXCOORD_0":99 }, "indices":88, "material":15 } ] }, { "name":"Plane.113", "primitives":[ { "attributes":{ "POSITION":100, "NORMAL":101 }, "indices":91, "material":13 } ] }, { "name":"Mesh.010", "primitives":[ { "attributes":{ "POSITION":102, "NORMAL":103, "TEXCOORD_0":104 }, "indices":105, "material":16 } ] }, { "name":"Mesh.011", "primitives":[ { "attributes":{ "POSITION":106, "NORMAL":107 }, "indices":108, "material":17 } ] }, { "name":"Circle.039", "primitives":[ { "attributes":{ "POSITION":109, "NORMAL":110 }, "indices":111, "material":17 } ] }, { "name":"Circle.038", "primitives":[ { "attributes":{ "POSITION":112, "NORMAL":113 }, "indices":114, "material":17 } ] }, { "name":"Circle.034", "primitives":[ { "attributes":{ "POSITION":115, "NORMAL":116 }, "indices":117, "material":18 } ] }, { "name":"Mesh.012", "primitives":[ { "attributes":{ "POSITION":118, "NORMAL":119, "TEXCOORD_0":120 }, "indices":121, "material":19 } ] }, { "name":"Plane.110", "primitives":[ { "attributes":{ "POSITION":122, "NORMAL":123 }, "indices":91, "material":13 } ] }, { "name":"Plane.106", "primitives":[ { "attributes":{ "POSITION":124, "NORMAL":125, "TEXCOORD_0":126 }, "indices":127, "material":20 } ] }, { "name":"Plane.105", "primitives":[ { "attributes":{ "POSITION":128, "NORMAL":129, "TEXCOORD_0":130 }, "indices":127, "material":20 } ] }, { "name":"Plane.104", "primitives":[ { "attributes":{ "POSITION":131, "NORMAL":132, "TEXCOORD_0":133 }, "indices":127, "material":20 } ] }, { "name":"Plane.103", "primitives":[ { "attributes":{ "POSITION":134, "NORMAL":135, "TEXCOORD_0":136 }, "indices":127, "material":20 } ] }, { "name":"Plane.102", "primitives":[ { "attributes":{ "POSITION":137, "NORMAL":138, "TEXCOORD_0":139 }, "indices":127, "material":20 } ] }, { "name":"Plane.101", "primitives":[ { "attributes":{ "POSITION":140, "NORMAL":141, "TEXCOORD_0":142 }, "indices":127, "material":20 } ] }, { "name":"Plane.100", "primitives":[ { "attributes":{ "POSITION":143, "NORMAL":144, "TEXCOORD_0":145 }, "indices":127, "material":20 } ] }, { "name":"Plane.099", "primitives":[ { "attributes":{ "POSITION":146, "NORMAL":147, "TEXCOORD_0":148 }, "indices":127, "material":20 } ] }, { "name":"Plane.098", "primitives":[ { "attributes":{ "POSITION":149, "NORMAL":150, "TEXCOORD_0":151 }, "indices":127, "material":20 } ] }, { "name":"Plane.097", "primitives":[ { "attributes":{ "POSITION":152, "NORMAL":153, "TEXCOORD_0":154 }, "indices":127, "material":20 } ] }, { "name":"Plane.096", "primitives":[ { "attributes":{ "POSITION":155, "NORMAL":156, "TEXCOORD_0":157 }, "indices":127, "material":20 } ] }, { "name":"Plane.095", "primitives":[ { "attributes":{ "POSITION":158, "NORMAL":159, "TEXCOORD_0":160 }, "indices":127, "material":20 } ] }, { "name":"Plane.094", "primitives":[ { "attributes":{ "POSITION":161, "NORMAL":162, "TEXCOORD_0":163 }, "indices":127, "material":20 } ] }, { "name":"Plane.093", "primitives":[ { "attributes":{ "POSITION":164, "NORMAL":165, "TEXCOORD_0":166 }, "indices":127, "material":20 } ] }, { "name":"Plane.092", "primitives":[ { "attributes":{ "POSITION":167, "NORMAL":168, "TEXCOORD_0":169 }, "indices":127, "material":20 } ] }, { "name":"Plane.091", "primitives":[ { "attributes":{ "POSITION":170, "NORMAL":171, "TEXCOORD_0":172 }, "indices":127, "material":20 } ] }, { "name":"Plane.090", "primitives":[ { "attributes":{ "POSITION":173, "NORMAL":174, "TEXCOORD_0":175 }, "indices":127, "material":20 } ] }, { "name":"Plane.089", "primitives":[ { "attributes":{ "POSITION":176, "NORMAL":177, "TEXCOORD_0":178 }, "indices":127, "material":20 } ] }, { "name":"Plane.088", "primitives":[ { "attributes":{ "POSITION":179, "NORMAL":180, "TEXCOORD_0":181 }, "indices":182, "material":21 } ] }, { "name":"Plane.086", "primitives":[ { "attributes":{ "POSITION":183, "NORMAL":184, "TEXCOORD_0":185 }, "indices":186, "material":21 } ] }, { "name":"Plane.085", "primitives":[ { "attributes":{ "POSITION":187, "NORMAL":188, "TEXCOORD_0":189 }, "indices":182, "material":21 } ] }, { "name":"Plane.083", "primitives":[ { "attributes":{ "POSITION":190, "NORMAL":191 }, "indices":192, "material":22 } ] }, { "name":"Plane.082", "primitives":[ { "attributes":{ "POSITION":193, "NORMAL":194, "TEXCOORD_0":195 }, "indices":196, "material":21 } ] }, { "name":"Plane.080", "primitives":[ { "attributes":{ "POSITION":197, "NORMAL":198, "TEXCOORD_0":199 }, "indices":200, "material":21 } ] }, { "name":"Plane.079", "primitives":[ { "attributes":{ "POSITION":201, "NORMAL":202 }, "indices":203, "material":23 }, { "attributes":{ "POSITION":204, "NORMAL":205 }, "indices":31, "material":24 } ] }, { "name":"Plane.078", "primitives":[ { "attributes":{ "POSITION":206, "NORMAL":207 }, "indices":208 } ] }, { "name":"Mesh.013", "primitives":[ { "attributes":{ "POSITION":209, "NORMAL":210, "TEXCOORD_0":211 }, "indices":212, "material":25 } ] }, { "name":"Mesh.014", "primitives":[ { "attributes":{ "POSITION":213, "NORMAL":214 }, "indices":215, "material":26 } ] }, { "name":"Plane.023", "primitives":[ { "attributes":{ "POSITION":216, "NORMAL":217 }, "indices":218, "material":26 } ] }, { "name":"Plane.008", "primitives":[ { "attributes":{ "POSITION":219, "NORMAL":220 }, "indices":221, "material":26 } ] }, { "name":"Mesh.015", "primitives":[ { "attributes":{ "POSITION":222, "NORMAL":223, "TEXCOORD_0":224 }, "indices":225, "material":27 } ] }, { "name":"Mesh.016", "primitives":[ { "attributes":{ "POSITION":226, "NORMAL":227, "TEXCOORD_0":228 }, "indices":229, "material":28 } ] }, { "name":"Plane.077", "primitives":[ { "attributes":{ "POSITION":230, "NORMAL":231 }, "indices":62 } ] }, { "name":"Plane.076", "primitives":[ { "attributes":{ "POSITION":232, "NORMAL":233 }, "indices":234 } ] }, { "name":"Plane.074", "primitives":[ { "attributes":{ "POSITION":235, "NORMAL":236 }, "indices":237, "material":0 } ] }, { "name":"Plane.072", "primitives":[ { "attributes":{ "POSITION":238, "NORMAL":239 }, "indices":240, "material":22 } ] }, { "name":"Plane.071", "primitives":[ { "attributes":{ "POSITION":241, "NORMAL":242, "TEXCOORD_0":243 }, "indices":244, "material":21 } ] }, { "name":"Mesh.017", "primitives":[ { "attributes":{ "POSITION":245, "NORMAL":246 }, "indices":247, "material":21 } ] }, { "name":"Plane.022", "primitives":[ { "attributes":{ "POSITION":248, "NORMAL":249 }, "indices":250, "material":29 } ] }, { "name":"Plane.069", "primitives":[ { "attributes":{ "POSITION":251, "NORMAL":252 }, "indices":253, "material":30 } ] }, { "name":"Plane.068", "primitives":[ { "attributes":{ "POSITION":254, "NORMAL":255, "TEXCOORD_0":256 }, "indices":88, "material":31 } ] }, { "name":"Plane.048", "primitives":[ { "attributes":{ "POSITION":257, "NORMAL":258 }, "indices":88, "material":32 } ] }, { "name":"Plane.047", "primitives":[ { "attributes":{ "POSITION":259, "NORMAL":260, "TEXCOORD_0":261 }, "indices":88, "material":33 } ] }, { "name":"Plane.042", "primitives":[ { "attributes":{ "POSITION":262, "NORMAL":263 }, "indices":264, "material":30 } ] }, { "name":"Mesh.018", "primitives":[ { "attributes":{ "POSITION":265, "NORMAL":266 }, "indices":267, "material":34 } ] }, { "name":"Circle.028", "primitives":[ { "attributes":{ "POSITION":268, "NORMAL":269 }, "indices":270, "material":35 } ] }, { "name":"Plane.046", "primitives":[ { "attributes":{ "POSITION":271, "NORMAL":272 }, "indices":273, "material":36 } ] }, { "name":"Plane.045", "primitives":[ { "attributes":{ "POSITION":274, "NORMAL":275 }, "indices":276, "material":36 } ] }, { "name":"Mesh.019", "primitives":[ { "attributes":{ "POSITION":277, "NORMAL":278 }, "indices":279, "material":36 } ] }, { "name":"Mesh.020", "primitives":[ { "attributes":{ "POSITION":280, "NORMAL":281 }, "indices":282, "material":37 } ] }, { "name":"Mesh.021", "primitives":[ { "attributes":{ "POSITION":283, "NORMAL":284 }, "indices":285, "material":38 } ] }, { "name":"Mesh.022", "primitives":[ { "attributes":{ "POSITION":286, "NORMAL":287 }, "indices":288, "material":38 } ] }, { "name":"Mesh.023", "primitives":[ { "attributes":{ "POSITION":289, "NORMAL":290 }, "indices":288, "material":38 } ] }, { "name":"Mesh.024", "primitives":[ { "attributes":{ "POSITION":291, "NORMAL":292 }, "indices":293, "material":38 } ] }, { "name":"Mesh.025", "primitives":[ { "attributes":{ "POSITION":294, "NORMAL":295 }, "indices":282, "material":37 } ] }, { "name":"Mesh.026", "primitives":[ { "attributes":{ "POSITION":296, "NORMAL":297 }, "indices":279, "material":36 } ] }, { "name":"Plane.044", "primitives":[ { "attributes":{ "POSITION":298, "NORMAL":299 }, "indices":276, "material":36 } ] }, { "name":"Plane.043", "primitives":[ { "attributes":{ "POSITION":300, "NORMAL":301 }, "indices":273, "material":36 } ] }, { "name":"Circle.019", "primitives":[ { "attributes":{ "POSITION":302, "NORMAL":303 }, "indices":270, "material":35 } ] }, { "name":"Mesh.027", "primitives":[ { "attributes":{ "POSITION":304, "NORMAL":305 }, "indices":267, "material":34 } ] }, { "name":"Mesh.028", "primitives":[ { "attributes":{ "POSITION":306, "NORMAL":307 }, "indices":267, "material":34 } ] }, { "name":"Circle.010", "primitives":[ { "attributes":{ "POSITION":308, "NORMAL":309 }, "indices":270, "material":35 } ] }, { "name":"Plane.040", "primitives":[ { "attributes":{ "POSITION":310, "NORMAL":311 }, "indices":273, "material":36 } ] }, { "name":"Plane.039", "primitives":[ { "attributes":{ "POSITION":312, "NORMAL":313 }, "indices":276, "material":36 } ] }, { "name":"Mesh.029", "primitives":[ { "attributes":{ "POSITION":314, "NORMAL":315 }, "indices":316, "material":36 } ] }, { "name":"Mesh.030", "primitives":[ { "attributes":{ "POSITION":317, "NORMAL":318 }, "indices":319, "material":37 } ] }, { "name":"Mesh.031", "primitives":[ { "attributes":{ "POSITION":320, "NORMAL":321 }, "indices":322, "material":38 } ] }, { "name":"Mesh.032", "primitives":[ { "attributes":{ "POSITION":323, "NORMAL":324 }, "indices":325, "material":38 } ] }, { "name":"Plane.002", "primitives":[ { "attributes":{ "POSITION":326, "NORMAL":327 }, "indices":328, "material":26 } ] }, { "name":"Plane.005", "primitives":[ { "attributes":{ "POSITION":329, "NORMAL":330 }, "indices":331, "material":26 } ] }, { "name":"Mesh.033", "primitives":[ { "attributes":{ "POSITION":332, "NORMAL":333 }, "indices":334, "material":26 } ] }, { "name":"Plane.026", "primitives":[ { "attributes":{ "POSITION":335, "NORMAL":336 }, "indices":337, "material":26 } ] }, { "name":"Plane.027", "primitives":[ { "attributes":{ "POSITION":338, "NORMAL":339 }, "indices":340, "material":26 } ] }, { "name":"Mesh.034", "primitives":[ { "attributes":{ "POSITION":341, "NORMAL":342 }, "indices":343, "material":26 } ] }, { "name":"Mesh.035", "primitives":[ { "attributes":{ "POSITION":344, "NORMAL":345, "TEXCOORD_0":346 }, "indices":347, "material":16 } ] }, { "name":"Mesh.036", "primitives":[ { "attributes":{ "POSITION":348, "NORMAL":349, "TEXCOORD_0":350 }, "indices":351, "material":39 } ] }, { "name":"Plane.033", "primitives":[ { "attributes":{ "POSITION":352, "NORMAL":353, "TEXCOORD_0":354 }, "indices":182, "material":21 } ] }, { "name":"Plane.034", "primitives":[ { "attributes":{ "POSITION":355, "NORMAL":356 }, "indices":357, "material":22 } ] }, { "name":"Mesh.037", "primitives":[ { "attributes":{ "POSITION":358, "NORMAL":359, "TEXCOORD_0":360 }, "indices":361, "material":40 } ] }, { "name":"Plane.036", "primitives":[ { "attributes":{ "POSITION":362, "NORMAL":363, "TEXCOORD_0":364 }, "indices":365, "material":21 } ] }, { "name":"Mesh.038", "primitives":[ { "attributes":{ "POSITION":366, "NORMAL":367 }, "indices":368, "material":41 } ] }, { "name":"Mesh.039", "primitives":[ { "attributes":{ "POSITION":369, "NORMAL":370 }, "indices":368, "material":41 } ] }, { "name":"Mesh.040", "primitives":[ { "attributes":{ "POSITION":371, "NORMAL":372 }, "indices":373, "material":42 }, { "attributes":{ "POSITION":374, "NORMAL":375 }, "indices":376, "material":43 } ] }, { "name":"Mesh.041", "primitives":[ { "attributes":{ "POSITION":377, "NORMAL":378 }, "indices":379, "material":44 } ] }, { "name":"Mesh.042", "primitives":[ { "attributes":{ "POSITION":380, "NORMAL":381 }, "indices":382 } ] }, { "name":"Mesh.043", "primitives":[ { "attributes":{ "POSITION":383, "NORMAL":384 }, "indices":385, "material":44 } ] }, { "name":"Mesh.044", "primitives":[ { "attributes":{ "POSITION":386, "NORMAL":387 }, "indices":388 } ] }, { "name":"Plane.055", "primitives":[ { "attributes":{ "POSITION":389, "NORMAL":390 }, "indices":391, "material":44 } ] }, { "name":"Mesh.045", "primitives":[ { "attributes":{ "POSITION":392, "NORMAL":393 }, "indices":394, "material":44 } ] }, { "name":"Mesh.046", "primitives":[ { "attributes":{ "POSITION":395, "NORMAL":396 }, "indices":397, "material":44 } ] }, { "name":"Mesh.047", "primitives":[ { "attributes":{ "POSITION":398, "NORMAL":399 }, "indices":400, "material":44 } ] }, { "name":"Mesh.048", "primitives":[ { "attributes":{ "POSITION":401, "NORMAL":402 }, "indices":403, "material":45 } ] }, { "name":"Mesh.049", "primitives":[ { "attributes":{ "POSITION":404, "NORMAL":405 }, "indices":406, "material":45 } ] }, { "name":"Mesh.050", "primitives":[ { "attributes":{ "POSITION":407, "NORMAL":408 }, "indices":409, "material":46 } ] }, { "name":"Mesh.051", "primitives":[ { "attributes":{ "POSITION":410, "NORMAL":411 }, "indices":412, "material":47 } ] }, { "name":"Mesh.052", "primitives":[ { "attributes":{ "POSITION":413, "NORMAL":414 }, "indices":415, "material":47 } ] }, { "name":"Mesh.054", "primitives":[ { "attributes":{ "POSITION":416, "NORMAL":417 }, "indices":418, "material":48 } ] }, { "name":"Curve.001", "primitives":[ { "attributes":{ "POSITION":419, "NORMAL":420, "TEXCOORD_0":421 }, "indices":422, "material":49 } ] }, { "name":"Mesh.055", "primitives":[ { "attributes":{ "POSITION":423, "NORMAL":424, "TEXCOORD_0":425 }, "indices":426, "material":50 }, { "attributes":{ "POSITION":427, "NORMAL":428, "TEXCOORD_0":429 }, "indices":430, "material":51 } ] }, { "name":"Mesh.056", "primitives":[ { "attributes":{ "POSITION":431, "NORMAL":432 }, "indices":433, "material":52 } ] }, { "name":"Mesh.057", "primitives":[ { "attributes":{ "POSITION":434, "NORMAL":435 }, "indices":436, "material":53 } ] }, { "name":"Mesh.058", "primitives":[ { "attributes":{ "POSITION":437, "NORMAL":438 }, "indices":439, "material":48 } ] }, { "name":"Mesh.059", "primitives":[ { "attributes":{ "POSITION":440, "NORMAL":441 }, "indices":442, "material":48 } ] }, { "name":"Mesh.060", "primitives":[ { "attributes":{ "POSITION":443, "NORMAL":444 }, "indices":445, "material":54 } ] }, { "name":"Plane.058", "primitives":[ { "attributes":{ "POSITION":446, "NORMAL":447 }, "indices":448, "material":55 } ] }, { "name":"Mesh.062", "primitives":[ { "attributes":{ "POSITION":449, "NORMAL":450 }, "indices":451, "material":56 } ] }, { "name":"Mesh.063", "primitives":[ { "attributes":{ "POSITION":452, "NORMAL":453 }, "indices":454, "material":57 } ] }, { "name":"Mesh.064", "primitives":[ { "attributes":{ "POSITION":455, "NORMAL":456 }, "indices":457, "material":58 } ] }, { "name":"Mesh.065", "primitives":[ { "attributes":{ "POSITION":458, "NORMAL":459 }, "indices":460, "material":58 } ] }, { "name":"Mesh.053", "primitives":[ { "attributes":{ "POSITION":461, "NORMAL":462 }, "indices":463, "material":59 } ] }, { "name":"Plane.063", "primitives":[ { "attributes":{ "POSITION":464, "NORMAL":465, "TEXCOORD_0":466 }, "indices":88, "material":60 } ] }, { "name":"Mesh.066", "primitives":[ { "attributes":{ "POSITION":467, "NORMAL":468 }, "indices":469, "material":61 } ] }, { "name":"Mesh.067", "primitives":[ { "attributes":{ "POSITION":470, "NORMAL":471 }, "indices":472, "material":62 } ] }, { "name":"Mesh.068", "primitives":[ { "attributes":{ "POSITION":473, "NORMAL":474 }, "indices":475, "material":62 } ] }, { "name":"Mesh.069", "primitives":[ { "attributes":{ "POSITION":476, "NORMAL":477 }, "indices":478, "material":61 } ] }, { "name":"Mesh.070", "primitives":[ { "attributes":{ "POSITION":479, "NORMAL":480 }, "indices":481, "material":62 } ] }, { "name":"Mesh.071", "primitives":[ { "attributes":{ "POSITION":482, "NORMAL":483 }, "indices":484, "material":62 } ] }, { "name":"Plane.017", "primitives":[ { "attributes":{ "POSITION":485, "NORMAL":486 }, "indices":487, "material":62 } ] }, { "name":"Mesh.072", "primitives":[ { "attributes":{ "POSITION":488, "NORMAL":489 }, "indices":490, "material":62 } ] }, { "name":"Mesh.073", "primitives":[ { "attributes":{ "POSITION":491, "NORMAL":492 }, "indices":493, "material":62 } ] }, { "name":"Plane.016", "primitives":[ { "attributes":{ "POSITION":494, "NORMAL":495 }, "indices":496, "material":0 } ] }, { "name":"Plane.015", "primitives":[ { "attributes":{ "POSITION":497, "NORMAL":498 }, "indices":499, "material":0 } ] }, { "name":"Plane.014", "primitives":[ { "attributes":{ "POSITION":500, "NORMAL":501 }, "indices":502, "material":63 } ] }, { "name":"Plane.013", "primitives":[ { "attributes":{ "POSITION":503, "NORMAL":504 }, "indices":505, "material":0 } ] }, { "name":"Plane.018", "primitives":[ { "attributes":{ "POSITION":506, "NORMAL":507 }, "indices":508, "material":0 } ] }, { "name":"Sphere", "primitives":[ { "attributes":{ "POSITION":509, "NORMAL":510, "TEXCOORD_0":511 }, "indices":512, "material":64 } ] }, { "name":"Icosphere.009", "primitives":[ { "attributes":{ "POSITION":513, "NORMAL":514, "TEXCOORD_0":515 }, "indices":516, "material":65 } ] }, { "name":"Icosphere.008", "primitives":[ { "attributes":{ "POSITION":517, "NORMAL":518, "TEXCOORD_0":519 }, "indices":520, "material":66 } ] }, { "name":"Sphere.001", "primitives":[ { "attributes":{ "POSITION":521, "NORMAL":522, "TEXCOORD_0":523 }, "indices":524, "material":67 } ] }, { "name":"Icosphere.001", "primitives":[ { "attributes":{ "POSITION":525, "NORMAL":526, "TEXCOORD_0":527 }, "indices":520, "material":68 } ] }, { "name":"Icosphere.002", "primitives":[ { "attributes":{ "POSITION":528, "NORMAL":529, "TEXCOORD_0":530 }, "indices":520, "material":69 } ] }, { "name":"Sphere.002", "primitives":[ { "attributes":{ "POSITION":531, "NORMAL":532, "TEXCOORD_0":533 }, "indices":524, "material":70 } ] }, { "name":"Torus", "primitives":[ { "attributes":{ "POSITION":534, "NORMAL":535, "TEXCOORD_0":536 }, "indices":537, "material":71 } ] }, { "name":"Torus.002", "primitives":[ { "attributes":{ "POSITION":538, "NORMAL":539, "TEXCOORD_0":540 }, "indices":541, "material":72 } ] }, { "name":"Cube", "primitives":[ { "attributes":{ "POSITION":542, "NORMAL":543, "TEXCOORD_0":544 }, "indices":545, "material":73 } ] }, { "name":"Cube.001", "primitives":[ { "attributes":{ "POSITION":546, "NORMAL":547, "TEXCOORD_0":548 }, "indices":545, "material":74 } ] }, { "name":"Cube.002", "primitives":[ { "attributes":{ "POSITION":549, "NORMAL":550, "TEXCOORD_0":551 }, "indices":545, "material":75 } ] }, { "name":"Cube.003", "primitives":[ { "attributes":{ "POSITION":552, "NORMAL":553, "TEXCOORD_0":554 }, "indices":545, "material":76 } ] }, { "name":"Cube.004", "primitives":[ { "attributes":{ "POSITION":555, "NORMAL":556, "TEXCOORD_0":557 }, "indices":545, "material":77 } ] }, { "name":"Cube.005", "primitives":[ { "attributes":{ "POSITION":558, "NORMAL":559, "TEXCOORD_0":560 }, "indices":545, "material":78 } ] }, { "name":"Cube.006", "primitives":[ { "attributes":{ "POSITION":561, "NORMAL":562, "TEXCOORD_0":563 }, "indices":545, "material":79 } ] }, { "name":"Cube.007", "primitives":[ { "attributes":{ "POSITION":564, "NORMAL":565, "TEXCOORD_0":566 }, "indices":545, "material":80 } ] }, { "name":"Cube.008", "primitives":[ { "attributes":{ "POSITION":567, "NORMAL":568, "TEXCOORD_0":569 }, "indices":545, "material":81 } ] }, { "name":"Cube.009", "primitives":[ { "attributes":{ "POSITION":570, "NORMAL":571, "TEXCOORD_0":572 }, "indices":545, "material":82 } ] }, { "name":"Sphere.003", "primitives":[ { "attributes":{ "POSITION":573, "NORMAL":574, "TEXCOORD_0":575 }, "indices":512, "material":83 } ] } ], "textures":[ { "sampler":0, "source":0 }, { "sampler":0, "source":1 }, { "sampler":0, "source":2 }, { "sampler":0, "source":3 }, { "sampler":0, "source":4 }, { "sampler":0, "source":5 }, { "sampler":0, "source":6 }, { "sampler":0, "source":7 }, { "sampler":0, "source":8 }, { "sampler":0, "source":9 }, { "sampler":0, "source":10 }, { "sampler":0, "source":11 }, { "sampler":0, "source":12 }, { "sampler":0, "source":13 }, { "sampler":0, "source":14 } ], "images":[ { "mimeType":"image/jpeg", "name":"WoodFloor051_2K-JPG_Color_reduced", "uri":"the-white-room/WoodFloor051_2K-JPG_Color_reduced.jpg" }, { "mimeType":"image/jpeg", "name":"magazine", "uri":"the-white-room/magazine.jpg" }, { "mimeType":"image/jpeg", "name":"photo3", "uri":"the-white-room/photo3.jpg" }, { "mimeType":"image/jpeg", "name":"photo4", "uri":"the-white-room/photo4.jpg" }, { "mimeType":"image/jpeg", "name":"photo1", "uri":"the-white-room/photo1.jpg" }, { "mimeType":"image/jpeg", "name":"cushion-stripe-purple", "uri":"the-white-room/cushion-stripe-purple.jpg" }, { "mimeType":"image/jpeg", "name":"Shade-paper", "uri":"the-white-room/Shade-paper.jpg" }, { "mimeType":"image/jpeg", "name":"book-spines", "uri":"the-white-room/book-spines.jpg" }, { "mimeType":"image/jpeg", "name":"cushion-purple-yellow-stripe", "uri":"the-white-room/cushion-purple-yellow-stripe.jpg" }, { "mimeType":"image/jpeg", "name":"cushion-green-circles", "uri":"the-white-room/cushion-green-circles.jpg" }, { "mimeType":"image/jpeg", "name":"shade-stripes", "uri":"the-white-room/shade-stripes.jpg" }, { "mimeType":"image/jpeg", "name":"pic5wide", "uri":"the-white-room/pic5wide.jpg" }, { "mimeType":"image/jpeg", "name":"pic3", "uri":"the-white-room/pic3.jpg" }, { "mimeType":"image/jpeg", "name":"carpet-text3b", "uri":"the-white-room/carpet-text3b.jpg" }, { "mimeType":"image/jpeg", "name":"photo2", "uri":"the-white-room/photo2.jpg" } ], "accessors":[ { "bufferView":0, "componentType":5126, "count":648, "max":[ 4.922062397003174, 4.82349157333374, 0.010916324332356453 ], "min":[ -0.5326654314994812, -2.162641763687134, -0.04265495017170906 ], "type":"VEC3" }, { "bufferView":1, "componentType":5126, "count":648, "type":"VEC3" }, { "bufferView":2, "componentType":5123, "count":936, "type":"SCALAR" }, { "bufferView":3, "componentType":5126, "count":48, "max":[ 3.5748255252838135, 0, 4.858819484710693 ], "min":[ -2.0094664096832275, 0, -2.792449951171875 ], "type":"VEC3" }, { "bufferView":4, "componentType":5126, "count":48, "type":"VEC3" }, { "bufferView":5, "componentType":5126, "count":48, "type":"VEC2" }, { "bufferView":6, "componentType":5123, "count":198, "type":"SCALAR" }, { "bufferView":7, "componentType":5126, "count":48, "max":[ 3.5019261837005615, 0, 4.7957072257995605 ], "min":[ -1.968488335609436, 0, -2.7355048656463623 ], "type":"VEC3" }, { "bufferView":8, "componentType":5126, "count":48, "type":"VEC3" }, { "bufferView":9, "componentType":5126, "count":48, "type":"VEC2" }, { "bufferView":10, "componentType":5123, "count":198, "type":"SCALAR" }, { "bufferView":11, "componentType":5126, "count":950, "max":[ 5.455154895782471, 3.2512876987457275, -0.00025314855156466365 ], "min":[ 0.00048618417349644005, -2.7012579441070557, -0.16332300007343292 ], "type":"VEC3" }, { "bufferView":12, "componentType":5126, "count":950, "type":"VEC3" }, { "bufferView":13, "componentType":5123, "count":1716, "type":"SCALAR" }, { "bufferView":14, "componentType":5126, "count":2384, "max":[ 5.891605377197266, 0.08312232792377472, 0.08869484812021255 ], "min":[ -1.6215317249298096, -5.371967315673828, -0.08274871110916138 ], "type":"VEC3" }, { "bufferView":15, "componentType":5126, "count":2384, "type":"VEC3" }, { "bufferView":16, "componentType":5123, "count":3780, "type":"SCALAR" }, { "bufferView":17, "componentType":5126, "count":1864, "max":[ 2.9372189044952393, 3.1419870853424072, 5.128809928894043 ], "min":[ -2.7930970191955566, -1.24563757708529e-06, -2.6410415172576904 ], "type":"VEC3" }, { "bufferView":18, "componentType":5126, "count":1864, "type":"VEC3" }, { "bufferView":19, "componentType":5123, "count":3774, "type":"SCALAR" }, { "bufferView":20, "componentType":5126, "count":12640, "max":[ 0.5476992130279541, 0.17167966067790985, 0.6571645736694336 ], "min":[ -0.5477049350738525, -0.054475005716085434, -2.3420510292053223 ], "type":"VEC3" }, { "bufferView":21, "componentType":5126, "count":12640, "type":"VEC3" }, { "bufferView":22, "componentType":5123, "count":18660, "type":"SCALAR" }, { "bufferView":23, "componentType":5126, "count":12645, "max":[ 0.5476990938186646, 0.17167964577674866, 0.6571645736694336 ], "min":[ -0.5477047562599182, -0.05447499826550484, -2.3420510292053223 ], "type":"VEC3" }, { "bufferView":24, "componentType":5126, "count":12645, "type":"VEC3" }, { "bufferView":25, "componentType":5123, "count":18660, "type":"SCALAR" }, { "bufferView":26, "componentType":5126, "count":12662, "max":[ 0.696713387966156, 0.17167964577674866, 0.6571645736694336 ], "min":[ -0.6954436302185059, -0.054475005716085434, -2.3420510292053223 ], "type":"VEC3" }, { "bufferView":27, "componentType":5126, "count":12662, "type":"VEC3" }, { "bufferView":28, "componentType":5123, "count":18660, "type":"SCALAR" }, { "bufferView":29, "componentType":5126, "count":4, "max":[ 0.9999999403953552, 0, 0.8618231415748596 ], "min":[ -1.0000001192092896, -2.2511860819918184e-08, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":30, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":31, "componentType":5123, "count":6, "type":"SCALAR" }, { "bufferView":32, "componentType":5126, "count":4, "max":[ 0.9999999403953552, 0, 0.8618231415748596 ], "min":[ -1.0000001192092896, -2.2511860819918184e-08, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":33, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":34, "componentType":5126, "count":374, "max":[ 1.1131902933120728, 7.659307479858398, 1.1132620573043823 ], "min":[ -1.1132620573043823, -0.0019058843608945608, -1.1132620573043823 ], "type":"VEC3" }, { "bufferView":35, "componentType":5126, "count":374, "type":"VEC3" }, { "bufferView":36, "componentType":5123, "count":2148, "type":"SCALAR" }, { "bufferView":37, "componentType":5126, "count":554, "max":[ 0.6911384463310242, 0.9967806935310364, 0.6907230615615845 ], "min":[ -0.6903073191642761, 0.0020614692475646734, -0.6907227635383606 ], "type":"VEC3" }, { "bufferView":38, "componentType":5126, "count":554, "type":"VEC3" }, { "bufferView":39, "componentType":5123, "count":3312, "type":"SCALAR" }, { "bufferView":40, "componentType":5126, "count":602, "max":[ 0.6978312730789185, 1.2345601320266724, 0.6981743574142456 ], "min":[ -0.6985169649124146, 0.0007714119856245816, -0.6981739401817322 ], "type":"VEC3" }, { "bufferView":41, "componentType":5126, "count":602, "type":"VEC3" }, { "bufferView":42, "componentType":5123, "count":3600, "type":"SCALAR" }, { "bufferView":43, "componentType":5126, "count":650, "max":[ 0.9311015009880066, 1.7556588649749756, 0.9311015009880066 ], "min":[ -0.9311015009880066, 0.0005759156192652881, -0.9311015009880066 ], "type":"VEC3" }, { "bufferView":44, "componentType":5126, "count":650, "type":"VEC3" }, { "bufferView":45, "componentType":5123, "count":3888, "type":"SCALAR" }, { "bufferView":46, "componentType":5126, "count":104, "max":[ 0.9999999403953552, 0.0006058623548597097, 0.9999999403953552 ], "min":[ -1.003533124923706, -0.03656959533691406, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":47, "componentType":5126, "count":104, "type":"VEC3" }, { "bufferView":48, "componentType":5126, "count":104, "type":"VEC2" }, { "bufferView":49, "componentType":5123, "count":228, "type":"SCALAR" }, { "bufferView":50, "componentType":5126, "count":459, "max":[ 0.2668321430683136, 0.20129023492336273, 0.26803481578826904 ], "min":[ -0.2691414952278137, -0.20129023492336273, -0.2679380476474762 ], "type":"VEC3" }, { "bufferView":51, "componentType":5126, "count":459, "type":"VEC3" }, { "bufferView":52, "componentType":5126, "count":459, "type":"VEC2" }, { "bufferView":53, "componentType":5123, "count":2304, "type":"SCALAR" }, { "bufferView":54, "componentType":5126, "count":2114, "max":[ 1.6868435144424438, 0.5824713110923767, 1.6868435144424438 ], "min":[ -1.6868435144424438, -0.048781149089336395, -1.6868433952331543 ], "type":"VEC3" }, { "bufferView":55, "componentType":5126, "count":2114, "type":"VEC3" }, { "bufferView":56, "componentType":5123, "count":12672, "type":"SCALAR" }, { "bufferView":57, "componentType":5126, "count":18, "max":[ 1, 8.977396965026855, 1 ], "min":[ -1, 0, -1 ], "type":"VEC3" }, { "bufferView":58, "componentType":5126, "count":18, "type":"VEC3" }, { "bufferView":59, "componentType":5123, "count":48, "type":"SCALAR" }, { "bufferView":60, "componentType":5126, "count":96, "max":[ 0.3340943455696106, -0.04397451877593994, 0.5399194955825806 ], "min":[ -0.33409440517425537, -0.154801607131958, -0.5399197340011597 ], "type":"VEC3" }, { "bufferView":61, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":62, "componentType":5123, "count":132, "type":"SCALAR" }, { "bufferView":63, "componentType":5126, "count":96, "max":[ 0.15047866106033325, 0.02035975456237793, 0.2590247690677643 ], "min":[ -0.33409440517425537, -0.15337657928466797, -0.5399197340011597 ], "type":"VEC3" }, { "bufferView":64, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":65, "componentType":5126, "count":96, "max":[ 0.22951847314834595, 0, 0.2590247690677643 ], "min":[ -0.33409440517425537, -0.15473389625549316, -0.5399197340011597 ], "type":"VEC3" }, { "bufferView":66, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":67, "componentType":5123, "count":132, "type":"SCALAR" }, { "bufferView":68, "componentType":5126, "count":96, "max":[ 0.15777474641799927, 0.030539510771632195, 0.27240052819252014 ], "min":[ -0.33409440517425537, -0.15337657928466797, -0.5399197340011597 ], "type":"VEC3" }, { "bufferView":69, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":70, "componentType":5126, "count":96, "max":[ 0.3340943455696106, 0, 0.5399194955825806 ], "min":[ -0.33409440517425537, -0.154801607131958, -0.5399197340011597 ], "type":"VEC3" }, { "bufferView":71, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":72, "componentType":5123, "count":132, "type":"SCALAR" }, { "bufferView":73, "componentType":5126, "count":790, "max":[ 0.1862427443265915, 0, 0.36614668369293213 ], "min":[ -0.19704250991344452, -0.07045507431030273, -0.36636513471603394 ], "type":"VEC3" }, { "bufferView":74, "componentType":5126, "count":790, "type":"VEC3" }, { "bufferView":75, "componentType":5123, "count":2208, "type":"SCALAR" }, { "bufferView":76, "componentType":5126, "count":852, "max":[ 1.2040683031082153, 0, 0.9120947122573853 ], "min":[ 0.7216556072235107, -0.07064461708068848, 0.2003590315580368 ], "type":"VEC3" }, { "bufferView":77, "componentType":5126, "count":852, "type":"VEC3" }, { "bufferView":78, "componentType":5123, "count":2052, "type":"SCALAR" }, { "bufferView":79, "componentType":5126, "count":704, "max":[ 0.21129059791564941, 0, 0.36592167615890503 ], "min":[ -0.21129059791564941, -0.07028055191040039, -0.36592167615890503 ], "type":"VEC3" }, { "bufferView":80, "componentType":5126, "count":704, "type":"VEC3" }, { "bufferView":81, "componentType":5123, "count":2112, "type":"SCALAR" }, { "bufferView":82, "componentType":5126, "count":916, "max":[ 0.43959543108940125, 0, 0.9227252006530762 ], "min":[ -0.18019215762615204, -0.0712590217590332, -0.07826060056686401 ], "type":"VEC3" }, { "bufferView":83, "componentType":5126, "count":916, "type":"VEC3" }, { "bufferView":84, "componentType":5123, "count":2532, "type":"SCALAR" }, { "bufferView":85, "componentType":5126, "count":4, "max":[ 0.08039847761392593, -0.004371070768684149, 0.10489880293607712 ], "min":[ -0.08039848506450653, -0.0043710870668292046, -0.13081341981887817 ], "type":"VEC3" }, { "bufferView":86, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":87, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":88, "componentType":5123, "count":6, "type":"SCALAR" }, { "bufferView":89, "componentType":5126, "count":256, "max":[ 0.0932014212012291, -5.687807060894556e-06, 0.11835099011659622 ], "min":[ -0.09320143610239029, -0.004655706696212292, -0.14590656757354736 ], "type":"VEC3" }, { "bufferView":90, "componentType":5126, "count":256, "type":"VEC3" }, { "bufferView":91, "componentType":5123, "count":384, "type":"SCALAR" }, { "bufferView":92, "componentType":5126, "count":256, "max":[ 0.0932014212012291, -5.687807060894556e-06, 0.11835099011659622 ], "min":[ -0.09320143610239029, -0.004655706696212292, -0.14590656757354736 ], "type":"VEC3" }, { "bufferView":93, "componentType":5126, "count":256, "type":"VEC3" }, { "bufferView":94, "componentType":5126, "count":4, "max":[ 0.08039847761392593, -0.004371070768684149, 0.10489880293607712 ], "min":[ -0.08039848506450653, -0.0043710870668292046, -0.13081341981887817 ], "type":"VEC3" }, { "bufferView":95, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":96, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":97, "componentType":5126, "count":4, "max":[ 0.08039847761392593, -0.004371070768684149, 0.10489880293607712 ], "min":[ -0.08039848506450653, -0.0043710870668292046, -0.13081341981887817 ], "type":"VEC3" }, { "bufferView":98, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":99, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":100, "componentType":5126, "count":256, "max":[ 0.0932014212012291, -5.687807060894556e-06, 0.11835099011659622 ], "min":[ -0.09320143610239029, -0.004655706696212292, -0.14590656757354736 ], "type":"VEC3" }, { "bufferView":101, "componentType":5126, "count":256, "type":"VEC3" }, { "bufferView":102, "componentType":5126, "count":639, "max":[ 0.21286319196224213, 0.055435582995414734, 0.21552662551403046 ], "min":[ -0.21678972244262695, -0.060694679617881775, -0.20884688198566437 ], "type":"VEC3" }, { "bufferView":103, "componentType":5126, "count":639, "type":"VEC3" }, { "bufferView":104, "componentType":5126, "count":639, "type":"VEC2" }, { "bufferView":105, "componentType":5123, "count":2517, "type":"SCALAR" }, { "bufferView":106, "componentType":5126, "count":64, "max":[ 0.07242931425571442, -0.0021060851868242025, 0.07242931425571442 ], "min":[ -0.07242932170629501, -0.046004775911569595, -0.07242931425571442 ], "type":"VEC3" }, { "bufferView":107, "componentType":5126, "count":64, "type":"VEC3" }, { "bufferView":108, "componentType":5123, "count":306, "type":"SCALAR" }, { "bufferView":109, "componentType":5126, "count":72, "max":[ 0.027787016704678535, 0.016154149547219276, 0.027787016704678535 ], "min":[ -0.027787016704678535, -0.07282548397779465, -0.027787016704678535 ], "type":"VEC3" }, { "bufferView":110, "componentType":5126, "count":72, "type":"VEC3" }, { "bufferView":111, "componentType":5123, "count":384, "type":"SCALAR" }, { "bufferView":112, "componentType":5126, "count":12, "max":[ 0.0025792610831558704, 0.4033385217189789, 0.0029782739002257586 ], "min":[ -0.0025792610831558704, 0, -0.0029782739002257586 ], "type":"VEC3" }, { "bufferView":113, "componentType":5126, "count":12, "type":"VEC3" }, { "bufferView":114, "componentType":5123, "count":36, "type":"SCALAR" }, { "bufferView":115, "componentType":5126, "count":192, "max":[ 0.08703261613845825, 0.003850689623504877, 0.015090188942849636 ], "min":[ -0.08703261613845825, -0.5693657398223877, -0.002090851776301861 ], "type":"VEC3" }, { "bufferView":116, "componentType":5126, "count":192, "type":"VEC3" }, { "bufferView":117, "componentType":5123, "count":1152, "type":"SCALAR" }, { "bufferView":118, "componentType":5126, "count":1876, "max":[ 0.2814808189868927, 0.2795661389827728, 0.28042706847190857 ], "min":[ -0.28073644638061523, -0.2794150114059448, -0.27962353825569153 ], "type":"VEC3" }, { "bufferView":119, "componentType":5126, "count":1876, "type":"VEC3" }, { "bufferView":120, "componentType":5126, "count":1876, "type":"VEC2" }, { "bufferView":121, "componentType":5123, "count":8064, "type":"SCALAR" }, { "bufferView":122, "componentType":5126, "count":256, "max":[ 0.0932014212012291, -5.687807060894556e-06, 0.11835099011659622 ], "min":[ -0.09320143610239029, -0.004655706696212292, -0.14590656757354736 ], "type":"VEC3" }, { "bufferView":123, "componentType":5126, "count":256, "type":"VEC3" }, { "bufferView":124, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.8540287017822266, 0.480212926864624 ], "min":[ -0.5835559368133545, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":125, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":126, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":127, "componentType":5123, "count":84, "type":"SCALAR" }, { "bufferView":128, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.0786960124969482, 0.40594857931137085 ], "min":[ -1.5949207544326782, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":129, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":130, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":131, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.4986674785614014, 0.9999999403953552 ], "min":[ -0.5835559368133545, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":132, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":133, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":134, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.4986674785614014, 0.3316842317581177 ], "min":[ -0.5835559368133545, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":135, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":136, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":137, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 1.2279832363128662, 0.480212926864624 ], "min":[ 0.16604459285736084, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":138, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":139, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":140, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.8109545707702637, 0.257451593875885 ], "min":[ -1.0000001192092896, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":141, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":142, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":143, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.1000568866729736, 1.1368076801300049 ], "min":[ -1.0000001192092896, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":144, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":145, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":146, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.7930243015289307, 0.27076077461242676 ], "min":[ -1.0000001192092896, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":147, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":148, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":149, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.2139461040496826, 0.4390467405319214 ], "min":[ -0.5002657175064087, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":150, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":151, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":152, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.4986674785614014, 0.9999999403953552 ], "min":[ -1.0000001192092896, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":153, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":154, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":155, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 1.9551841020584106, 1.8975250720977783 ], "min":[ -0.21470481157302856, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":156, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":157, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":158, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 1.880414366722107, 0.8317139744758606 ], "min":[ -0.22660309076309204, 0, -1.1682863235473633 ], "type":"VEC3" }, { "bufferView":159, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":160, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":161, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.7427144050598145, 0.10247480869293213 ], "min":[ -1.0000001192092896, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":162, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":163, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":164, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.7427144050598145, 0.10247480869293213 ], "min":[ -1.0000001192092896, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":165, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":166, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":167, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 1.449264407157898, 0.38295137882232666 ], "min":[ -0.0005335807800292969, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":168, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":169, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":170, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.4986674785614014, 0.9999999403953552 ], "min":[ -0.6430703401565552, -4.227981499832091e-18, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":171, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":172, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":173, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.4986674785614014, 0.9999999403953552 ], "min":[ -1.0000001192092896, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":174, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":175, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":176, "componentType":5126, "count":44, "max":[ 1.0095903873443604, 2.4986674785614014, 0.9999999403953552 ], "min":[ -1.0000001192092896, 0, -1.0000003576278687 ], "type":"VEC3" }, { "bufferView":177, "componentType":5126, "count":44, "type":"VEC3" }, { "bufferView":178, "componentType":5126, "count":44, "type":"VEC2" }, { "bufferView":179, "componentType":5126, "count":96, "max":[ 0.4436837136745453, 0.14453157782554626, 0.00977183785289526 ], "min":[ -0.4436837136745453, 0.08475638926029205, -0.013087860308587551 ], "type":"VEC3" }, { "bufferView":180, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":181, "componentType":5126, "count":96, "type":"VEC2" }, { "bufferView":182, "componentType":5123, "count":132, "type":"SCALAR" }, { "bufferView":183, "componentType":5126, "count":192, "max":[ 0.6080718636512756, 0.12074020504951477, 0.46730321645736694 ], "min":[ -0.770543098449707, -0.20597746968269348, -0.46730321645736694 ], "type":"VEC3" }, { "bufferView":184, "componentType":5126, "count":192, "type":"VEC3" }, { "bufferView":185, "componentType":5126, "count":192, "type":"VEC2" }, { "bufferView":186, "componentType":5123, "count":264, "type":"SCALAR" }, { "bufferView":187, "componentType":5126, "count":96, "max":[ 0.476053386926651, 0.16016773879528046, 0.0097717996686697 ], "min":[ -0.476053386926651, -0.21107666194438934, -0.025682564824819565 ], "type":"VEC3" }, { "bufferView":188, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":189, "componentType":5126, "count":96, "type":"VEC2" }, { "bufferView":190, "componentType":5126, "count":3179, "max":[ 0.27941370010375977, 0.01292315125465393, 0.29084140062332153 ], "min":[ -0.27941370010375977, -1.0405468940734863, 0.2718624770641327 ], "type":"VEC3" }, { "bufferView":191, "componentType":5126, "count":3179, "type":"VEC3" }, { "bufferView":192, "componentType":5123, "count":5160, "type":"SCALAR" }, { "bufferView":193, "componentType":5126, "count":192, "max":[ 0.46760934591293335, 0.5681414008140564, 0.2960485517978668 ], "min":[ -0.46760934591293335, 0.30794796347618103, 0.273823618888855 ], "type":"VEC3" }, { "bufferView":194, "componentType":5126, "count":192, "type":"VEC3" }, { "bufferView":195, "componentType":5126, "count":192, "type":"VEC2" }, { "bufferView":196, "componentType":5123, "count":264, "type":"SCALAR" }, { "bufferView":197, "componentType":5126, "count":384, "max":[ 0.467467725276947, 0.2073485553264618, 0.2960485517978668 ], "min":[ -0.467467725276947, -0.836802065372467, 0.273823618888855 ], "type":"VEC3" }, { "bufferView":198, "componentType":5126, "count":384, "type":"VEC3" }, { "bufferView":199, "componentType":5126, "count":384, "type":"VEC2" }, { "bufferView":200, "componentType":5123, "count":528, "type":"SCALAR" }, { "bufferView":201, "componentType":5126, "count":64, "max":[ 0.5612022876739502, 3.965745054301806e-05, 0.3776973783969879 ], "min":[ -0.5612024664878845, -0.015438186004757881, -0.35583171248435974 ], "type":"VEC3" }, { "bufferView":202, "componentType":5126, "count":64, "type":"VEC3" }, { "bufferView":203, "componentType":5123, "count":126, "type":"SCALAR" }, { "bufferView":204, "componentType":5126, "count":4, "max":[ 0.5526052713394165, 0, 0.3473944365978241 ], "min":[ -0.5526053309440613, 0, -0.34739458560943604 ], "type":"VEC3" }, { "bufferView":205, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":206, "componentType":5126, "count":24, "max":[ 0.27151554822921753, 0.0034180879592895508, 0.0038742555771023035 ], "min":[ -0.2715156078338623, 0, -0.003874257206916809 ], "type":"VEC3" }, { "bufferView":207, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":208, "componentType":5123, "count":36, "type":"SCALAR" }, { "bufferView":209, "componentType":5126, "count":643, "max":[ 0.21412484347820282, 0.0617770254611969, 0.20867447555065155 ], "min":[ -0.21678970754146576, -0.060674868524074554, -0.20884692668914795 ], "type":"VEC3" }, { "bufferView":210, "componentType":5126, "count":643, "type":"VEC3" }, { "bufferView":211, "componentType":5126, "count":643, "type":"VEC2" }, { "bufferView":212, "componentType":5123, "count":2517, "type":"SCALAR" }, { "bufferView":213, "componentType":5126, "count":1100, "max":[ 0.5314524173736572, 0.28472211956977844, 0.427214652299881 ], "min":[ -0.5333153009414673, -0.2524959444999695, -0.49720922112464905 ], "type":"VEC3" }, { "bufferView":214, "componentType":5126, "count":1100, "type":"VEC3" }, { "bufferView":215, "componentType":5123, "count":6528, "type":"SCALAR" }, { "bufferView":216, "componentType":5126, "count":4148, "max":[ 0.6636195778846741, 0.1477474719285965, 0.42036381363868713 ], "min":[ 0.003775038057938218, -0.07614853233098984, -0.42036381363868713 ], "type":"VEC3" }, { "bufferView":217, "componentType":5126, "count":4148, "type":"VEC3" }, { "bufferView":218, "componentType":5123, "count":24384, "type":"SCALAR" }, { "bufferView":219, "componentType":5126, "count":3734, "max":[ 0.5434067845344543, 0.7616413831710815, 0.6681085228919983 ], "min":[ -0.5434136986732483, 3.0291071198007558e-06, -0.6681085228919983 ], "type":"VEC3" }, { "bufferView":220, "componentType":5126, "count":3734, "type":"VEC3" }, { "bufferView":221, "componentType":5123, "count":22272, "type":"SCALAR" }, { "bufferView":222, "componentType":5126, "count":651, "max":[ 0.21286319196224213, 0.05543559044599533, 0.21552662551403046 ], "min":[ -0.21678972244262695, -0.06069469451904297, -0.20884694159030914 ], "type":"VEC3" }, { "bufferView":223, "componentType":5126, "count":651, "type":"VEC3" }, { "bufferView":224, "componentType":5126, "count":651, "type":"VEC2" }, { "bufferView":225, "componentType":5123, "count":2517, "type":"SCALAR" }, { "bufferView":226, "componentType":5126, "count":1291, "max":[ 0.2711111903190613, 0.20877385139465332, 0.27094924449920654 ], "min":[ -0.27347132563591003, -0.20877386629581451, -0.27096250653266907 ], "type":"VEC3" }, { "bufferView":227, "componentType":5126, "count":1291, "type":"VEC3" }, { "bufferView":228, "componentType":5126, "count":1291, "type":"VEC2" }, { "bufferView":229, "componentType":5123, "count":6756, "type":"SCALAR" }, { "bufferView":230, "componentType":5126, "count":96, "max":[ 0.4989388585090637, 0.04189485311508179, 0.012411701492965221 ], "min":[ -0.16537515819072723, -0.04615183174610138, -0.012411782518029213 ], "type":"VEC3" }, { "bufferView":231, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":232, "componentType":5126, "count":384, "max":[ 0.4989388883113861, 0.15458370745182037, 0.983651340007782 ], "min":[ -0.16847984492778778, -0.1685476452112198, -0.012411730363965034 ], "type":"VEC3" }, { "bufferView":233, "componentType":5126, "count":384, "type":"VEC3" }, { "bufferView":234, "componentType":5123, "count":528, "type":"SCALAR" }, { "bufferView":235, "componentType":5126, "count":208, "max":[ 0.5214969515800476, 0.5009514689445496, 0.1687571406364441 ], "min":[ -0.19113034009933472, -0.935589075088501, -0.1687571257352829 ], "type":"VEC3" }, { "bufferView":236, "componentType":5126, "count":208, "type":"VEC3" }, { "bufferView":237, "componentType":5123, "count":324, "type":"SCALAR" }, { "bufferView":238, "componentType":5126, "count":306, "max":[ 0.045450564473867416, 0.3660617172718048, 0.29084140062332153 ], "min":[ -0.045450564473867416, 0.351283460855484, 0.2718624770641327 ], "type":"VEC3" }, { "bufferView":239, "componentType":5126, "count":306, "type":"VEC3" }, { "bufferView":240, "componentType":5123, "count":516, "type":"SCALAR" }, { "bufferView":241, "componentType":5126, "count":96, "max":[ 0.2637956142425537, 0.4708976447582245, 0.2960485517978668 ], "min":[ -0.2637956142425537, 0.2577918767929077, 0.273823618888855 ], "type":"VEC3" }, { "bufferView":242, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":243, "componentType":5126, "count":96, "type":"VEC2" }, { "bufferView":244, "componentType":5123, "count":132, "type":"SCALAR" }, { "bufferView":245, "componentType":5126, "count":2781, "max":[ 2.504610300064087, 1.0724855661392212, 0.42410728335380554 ], "min":[ -2.504610300064087, -1.0724855661392212, -0.42410728335380554 ], "type":"VEC3" }, { "bufferView":246, "componentType":5126, "count":2781, "type":"VEC3" }, { "bufferView":247, "componentType":5123, "count":13440, "type":"SCALAR" }, { "bufferView":248, "componentType":5126, "count":1248, "max":[ 0.08179128170013428, 0.0073179202154278755, 0.04479164630174637 ], "min":[ -0.08179129660129547, -0.0073179202154278755, -0.04479164630174637 ], "type":"VEC3" }, { "bufferView":249, "componentType":5126, "count":1248, "type":"VEC3" }, { "bufferView":250, "componentType":5123, "count":3996, "type":"SCALAR" }, { "bufferView":251, "componentType":5126, "count":228, "max":[ 0.8769688606262207, -9.049092031254702e-11, 0.47029218077659607 ], "min":[ -0.37035518884658813, -0.018523098900914192, -0.5797899961471558 ], "type":"VEC3" }, { "bufferView":252, "componentType":5126, "count":228, "type":"VEC3" }, { "bufferView":253, "componentType":5123, "count":384, "type":"SCALAR" }, { "bufferView":254, "componentType":5126, "count":4, "max":[ 0.8473078608512878, -0.018522948026657104, 0.4431114196777344 ], "min":[ -0.34069839119911194, -0.018523098900914192, -0.5525795221328735 ], "type":"VEC3" }, { "bufferView":255, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":256, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":257, "componentType":5126, "count":4, "max":[ 0.191134512424469, -0.010391523130238056, 0.25108322501182556 ], "min":[ -0.1911345273256302, -0.01039156224578619, -0.31312862038612366 ], "type":"VEC3" }, { "bufferView":258, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":259, "componentType":5126, "count":4, "max":[ 0.19238071143627167, -0.010459273122251034, 0.25100603699684143 ], "min":[ -0.19238072633743286, -0.010459313169121742, -0.31301558017730713 ], "type":"VEC3" }, { "bufferView":260, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":261, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":262, "componentType":5126, "count":256, "max":[ 0.22301608324050903, -1.1375549547665287e-05, 0.28319498896598816 ], "min":[ -0.22301611304283142, -0.011142595671117306, -0.3491310477256775 ], "type":"VEC3" }, { "bufferView":263, "componentType":5126, "count":256, "type":"VEC3" }, { "bufferView":264, "componentType":5123, "count":384, "type":"SCALAR" }, { "bufferView":265, "componentType":5126, "count":252, "max":[ 0.009505322203040123, -0.300081729888916, 0.009544174186885357 ], "min":[ -0.009505323134362698, -0.3362524211406708, -0.009544174186885357 ], "type":"VEC3" }, { "bufferView":266, "componentType":5126, "count":252, "type":"VEC3" }, { "bufferView":267, "componentType":5123, "count":1440, "type":"SCALAR" }, { "bufferView":268, "componentType":5126, "count":12, "max":[ 0.0007438515312969685, -0.00010429711983306333, 0.0008589255739934742 ], "min":[ -0.0007438514148816466, -0.31804805994033813, -0.0008589255739934742 ], "type":"VEC3" }, { "bufferView":269, "componentType":5126, "count":12, "type":"VEC3" }, { "bufferView":270, "componentType":5123, "count":36, "type":"SCALAR" }, { "bufferView":271, "componentType":5126, "count":48, "max":[ 0.018276110291481018, 3.0816596496840987e-10, 0.011351683177053928 ], "min":[ -0.018276114016771317, -0.004464164841920137, -0.011873414739966393 ], "type":"VEC3" }, { "bufferView":272, "componentType":5126, "count":48, "type":"VEC3" }, { "bufferView":273, "componentType":5123, "count":84, "type":"SCALAR" }, { "bufferView":274, "componentType":5126, "count":88, "max":[ 0.30904194712638855, -0.09976454079151154, 0.1980161815881729 ], "min":[ -0.30904194712638855, -0.15826284885406494, 0.16872520744800568 ], "type":"VEC3" }, { "bufferView":275, "componentType":5126, "count":88, "type":"VEC3" }, { "bufferView":276, "componentType":5123, "count":168, "type":"SCALAR" }, { "bufferView":277, "componentType":5126, "count":682, "max":[ 0.023798061534762383, 0.29990243911743164, 0.02172921784222126 ], "min":[ -0.02436229959130287, -0.29990243911743164, -0.024080166593194008 ], "type":"VEC3" }, { "bufferView":278, "componentType":5126, "count":682, "type":"VEC3" }, { "bufferView":279, "componentType":5123, "count":1152, "type":"SCALAR" }, { "bufferView":280, "componentType":5126, "count":1116, "max":[ 0.002438030205667019, 0.2740495800971985, 0.2786678671836853 ], "min":[ -0.0008455177885480225, -0.2740495800971985, 0.23602034151554108 ], "type":"VEC3" }, { "bufferView":281, "componentType":5126, "count":1116, "type":"VEC3" }, { "bufferView":282, "componentType":5123, "count":1800, "type":"SCALAR" }, { "bufferView":283, "componentType":5126, "count":2866, "max":[ 0.004097497556358576, 0.2889848053455353, 0.34853753447532654 ], "min":[ -0.0004354871343821287, -0.2889847755432129, -0.0017901991959661245 ], "type":"VEC3" }, { "bufferView":284, "componentType":5126, "count":2866, "type":"VEC3" }, { "bufferView":285, "componentType":5123, "count":4416, "type":"SCALAR" }, { "bufferView":286, "componentType":5126, "count":194, "max":[ 0.028982071205973625, 0.28915202617645264, 0.03211777284741402 ], "min":[ -0.02898208610713482, -0.28915202617645264, -0.03211776167154312 ], "type":"VEC3" }, { "bufferView":287, "componentType":5126, "count":194, "type":"VEC3" }, { "bufferView":288, "componentType":5123, "count":768, "type":"SCALAR" }, { "bufferView":289, "componentType":5126, "count":194, "max":[ 0.028982071205973625, 0.28915202617645264, 0.03211777284741402 ], "min":[ -0.02898208610713482, -0.28915202617645264, -0.03211776167154312 ], "type":"VEC3" }, { "bufferView":290, "componentType":5126, "count":194, "type":"VEC3" }, { "bufferView":291, "componentType":5126, "count":2908, "max":[ 0.004097497556358576, 0.2889848053455353, 0.3711926341056824 ], "min":[ -0.0004354871343821287, -0.2889847755432129, -0.0017901991959661245 ], "type":"VEC3" }, { "bufferView":292, "componentType":5126, "count":2908, "type":"VEC3" }, { "bufferView":293, "componentType":5123, "count":4416, "type":"SCALAR" }, { "bufferView":294, "componentType":5126, "count":1116, "max":[ 0.002438030205667019, 0.2740495800971985, 0.2786678671836853 ], "min":[ -0.0008455177885480225, -0.2740495800971985, 0.23602034151554108 ], "type":"VEC3" }, { "bufferView":295, "componentType":5126, "count":1116, "type":"VEC3" }, { "bufferView":296, "componentType":5126, "count":682, "max":[ 0.023798061534762383, 0.29990243911743164, 0.02172921784222126 ], "min":[ -0.02436229959130287, -0.29990243911743164, -0.024080166593194008 ], "type":"VEC3" }, { "bufferView":297, "componentType":5126, "count":682, "type":"VEC3" }, { "bufferView":298, "componentType":5126, "count":88, "max":[ 0.30904194712638855, -0.09976454079151154, 0.1980161815881729 ], "min":[ -0.30904194712638855, -0.15826284885406494, 0.16872520744800568 ], "type":"VEC3" }, { "bufferView":299, "componentType":5126, "count":88, "type":"VEC3" }, { "bufferView":300, "componentType":5126, "count":48, "max":[ 0.018276110291481018, 3.0816596496840987e-10, 0.011351683177053928 ], "min":[ -0.018276114016771317, -0.004464164841920137, -0.011873414739966393 ], "type":"VEC3" }, { "bufferView":301, "componentType":5126, "count":48, "type":"VEC3" }, { "bufferView":302, "componentType":5126, "count":12, "max":[ 0.0007438513566739857, -0.00010429711983306333, 0.0008589253993704915 ], "min":[ -0.0007438512402586639, -0.31804805994033813, -0.0008589253993704915 ], "type":"VEC3" }, { "bufferView":303, "componentType":5126, "count":12, "type":"VEC3" }, { "bufferView":304, "componentType":5126, "count":252, "max":[ 0.009505320340394974, -0.300081729888916, 0.009544173255562782 ], "min":[ -0.009505320340394974, -0.3362524211406708, -0.009544172324240208 ], "type":"VEC3" }, { "bufferView":305, "componentType":5126, "count":252, "type":"VEC3" }, { "bufferView":306, "componentType":5126, "count":252, "max":[ 0.009505320340394974, -0.300081729888916, 0.009544173255562782 ], "min":[ -0.009505322203040123, -0.3362524211406708, -0.009544172324240208 ], "type":"VEC3" }, { "bufferView":307, "componentType":5126, "count":252, "type":"VEC3" }, { "bufferView":308, "componentType":5126, "count":12, "max":[ 0.0007438514148816466, -0.00010429711983306333, 0.0008589254575781524 ], "min":[ -0.0007438512984663248, -0.31804805994033813, -0.0008589254575781524 ], "type":"VEC3" }, { "bufferView":309, "componentType":5126, "count":12, "type":"VEC3" }, { "bufferView":310, "componentType":5126, "count":48, "max":[ 0.01827610842883587, 3.0816593721283425e-10, 0.011351683177053928 ], "min":[ -0.018276112154126167, -0.00446416437625885, -0.011873414739966393 ], "type":"VEC3" }, { "bufferView":311, "componentType":5126, "count":48, "type":"VEC3" }, { "bufferView":312, "componentType":5126, "count":88, "max":[ 0.4316304624080658, -0.0997641459107399, 0.1980161815881729 ], "min":[ -0.4316304624080658, -0.1582624465227127, 0.16872520744800568 ], "type":"VEC3" }, { "bufferView":313, "componentType":5126, "count":88, "type":"VEC3" }, { "bufferView":314, "componentType":5126, "count":692, "max":[ 0.02408015914261341, 0.42319056391716003, 0.021729225292801857 ], "min":[ -0.024080200120806694, -0.42319056391716003, -0.02408016100525856 ], "type":"VEC3" }, { "bufferView":315, "componentType":5126, "count":692, "type":"VEC3" }, { "bufferView":316, "componentType":5123, "count":1152, "type":"SCALAR" }, { "bufferView":317, "componentType":5126, "count":1092, "max":[ 0.0023479796946048737, 0.4123048782348633, 0.2786678075790405 ], "min":[ -0.0008455414208583534, -0.4123048782348633, 0.23601903021335602 ], "type":"VEC3" }, { "bufferView":318, "componentType":5126, "count":1092, "type":"VEC3" }, { "bufferView":319, "componentType":5123, "count":1800, "type":"SCALAR" }, { "bufferView":320, "componentType":5126, "count":2721, "max":[ 0.003890065010637045, 0.4122636616230011, 0.2789841592311859 ], "min":[ -0.00043549638940021396, -0.4122636914253235, 0.0001247574109584093 ], "type":"VEC3" }, { "bufferView":321, "componentType":5126, "count":2721, "type":"VEC3" }, { "bufferView":322, "componentType":5123, "count":4416, "type":"SCALAR" }, { "bufferView":323, "componentType":5126, "count":288, "max":[ 0.028982071205973625, 0.41272681951522827, 0.02898208424448967 ], "min":[ -0.02898208238184452, -0.41272681951522827, -0.028982071205973625 ], "type":"VEC3" }, { "bufferView":324, "componentType":5126, "count":288, "type":"VEC3" }, { "bufferView":325, "componentType":5123, "count":1536, "type":"SCALAR" }, { "bufferView":326, "componentType":5126, "count":1506, "max":[ 0.5433828830718994, 0.7616105079650879, 0.668077826499939 ], "min":[ -0.5433710217475891, -2.7697407858795486e-05, -0.668077826499939 ], "type":"VEC3" }, { "bufferView":327, "componentType":5126, "count":1506, "type":"VEC3" }, { "bufferView":328, "componentType":5123, "count":8904, "type":"SCALAR" }, { "bufferView":329, "componentType":5126, "count":2052, "max":[ 0.6636162996292114, 0.1477474421262741, 0.4203604757785797 ], "min":[ 0.0037609413266181946, -0.07614852488040924, -0.4203604757785797 ], "type":"VEC3" }, { "bufferView":330, "componentType":5126, "count":2052, "type":"VEC3" }, { "bufferView":331, "componentType":5123, "count":12192, "type":"SCALAR" }, { "bufferView":332, "componentType":5126, "count":2211, "max":[ 0.531452476978302, 0.28472208976745605, 0.427214652299881 ], "min":[ -0.5333153009414673, -0.25249597430229187, -0.49720922112464905 ], "type":"VEC3" }, { "bufferView":333, "componentType":5126, "count":2211, "type":"VEC3" }, { "bufferView":334, "componentType":5123, "count":13056, "type":"SCALAR" }, { "bufferView":335, "componentType":5126, "count":1502, "max":[ 0.5433831214904785, 0.7671076059341431, 1.1618307828903198 ], "min":[ -0.5433714985847473, 2.4727849449845962e-05, -1.16182279586792 ], "type":"VEC3" }, { "bufferView":336, "componentType":5126, "count":1502, "type":"VEC3" }, { "bufferView":337, "componentType":5123, "count":8904, "type":"SCALAR" }, { "bufferView":338, "componentType":5126, "count":4158, "max":[ 0.6720637083053589, 0.1477474868297577, 0.9239981770515442 ], "min":[ 0.0037750855553895235, -0.07614850252866745, -0.9239981770515442 ], "type":"VEC3" }, { "bufferView":339, "componentType":5126, "count":4158, "type":"VEC3" }, { "bufferView":340, "componentType":5123, "count":24384, "type":"SCALAR" }, { "bufferView":341, "componentType":5126, "count":1320, "max":[ 0.5314525961875916, 0.2846877872943878, 0.46221214532852173 ], "min":[ -0.5332691669464111, -0.2524959444999695, -1.3866393566131592 ], "type":"VEC3" }, { "bufferView":342, "componentType":5126, "count":1320, "type":"VEC3" }, { "bufferView":343, "componentType":5123, "count":7830, "type":"SCALAR" }, { "bufferView":344, "componentType":5126, "count":640, "max":[ 0.21412476897239685, 0.055548928678035736, 0.21552662551403046 ], "min":[ -0.21678969264030457, -0.06069469079375267, -0.20884692668914795 ], "type":"VEC3" }, { "bufferView":345, "componentType":5126, "count":640, "type":"VEC3" }, { "bufferView":346, "componentType":5126, "count":640, "type":"VEC2" }, { "bufferView":347, "componentType":5123, "count":2517, "type":"SCALAR" }, { "bufferView":348, "componentType":5126, "count":470, "max":[ 0.8406214714050293, 0.014209745451807976, 0.5578946471214294 ], "min":[ -0.8413571119308472, 2.396662239334546e-05, -0.554294228553772 ], "type":"VEC3" }, { "bufferView":349, "componentType":5126, "count":470, "type":"VEC3" }, { "bufferView":350, "componentType":5126, "count":470, "type":"VEC2" }, { "bufferView":351, "componentType":5123, "count":2064, "type":"SCALAR" }, { "bufferView":352, "componentType":5126, "count":96, "max":[ 0.2637956142425537, 0.4708976447582245, 0.2960485517978668 ], "min":[ -0.2637956142425537, 0.2577918767929077, 0.273823618888855 ], "type":"VEC3" }, { "bufferView":353, "componentType":5126, "count":96, "type":"VEC3" }, { "bufferView":354, "componentType":5126, "count":96, "type":"VEC2" }, { "bufferView":355, "componentType":5126, "count":306, "max":[ 0.045450564473867416, 0.3660617172718048, 0.29084140062332153 ], "min":[ -0.045450564473867416, 0.351283460855484, 0.2718624770641327 ], "type":"VEC3" }, { "bufferView":356, "componentType":5126, "count":306, "type":"VEC3" }, { "bufferView":357, "componentType":5123, "count":516, "type":"SCALAR" }, { "bufferView":358, "componentType":5126, "count":2652, "max":[ 0.2668611407279968, 0.24918457865715027, 0.6201762557029724 ], "min":[ -0.2668611407279968, -0.010967996902763844, -0.6201762557029724 ], "type":"VEC3" }, { "bufferView":359, "componentType":5126, "count":2652, "type":"VEC3" }, { "bufferView":360, "componentType":5126, "count":2652, "type":"VEC2" }, { "bufferView":361, "componentType":5123, "count":8139, "type":"SCALAR" }, { "bufferView":362, "componentType":5126, "count":48, "max":[ 0.2878775894641876, 0.47464823722839355, 0.29901182651519775 ], "min":[ -0.2878775894641876, 0.25646138191223145, -0.2990120053291321 ], "type":"VEC3" }, { "bufferView":363, "componentType":5126, "count":48, "type":"VEC3" }, { "bufferView":364, "componentType":5126, "count":48, "type":"VEC2" }, { "bufferView":365, "componentType":5123, "count":72, "type":"SCALAR" }, { "bufferView":366, "componentType":5126, "count":672, "max":[ 0.024215681478381157, 2.5066010493901558e-05, 0.024479782208800316 ], "min":[ -0.024215679615736008, -0.0065596685744822025, -0.02395157888531685 ], "type":"VEC3" }, { "bufferView":367, "componentType":5126, "count":672, "type":"VEC3" }, { "bufferView":368, "componentType":5123, "count":3840, "type":"SCALAR" }, { "bufferView":369, "componentType":5126, "count":672, "max":[ 0.024215681478381157, 2.5066010493901558e-05, 0.024479782208800316 ], "min":[ -0.024215679615736008, -0.0065596685744822025, -0.02395157888531685 ], "type":"VEC3" }, { "bufferView":370, "componentType":5126, "count":672, "type":"VEC3" }, { "bufferView":371, "componentType":5126, "count":1696, "max":[ 0.028645366430282593, -8.73478566063568e-05, 0.028645364567637444 ], "min":[ -0.028645364567637444, -0.013904731720685959, -0.028645362704992294 ], "type":"VEC3" }, { "bufferView":372, "componentType":5126, "count":1696, "type":"VEC3" }, { "bufferView":373, "componentType":5123, "count":9984, "type":"SCALAR" }, { "bufferView":374, "componentType":5126, "count":243, "max":[ 0.013928772881627083, -0.004239760804921389, 0.013928774744272232 ], "min":[ -0.013928775675594807, -0.009752316400408745, -0.013928773812949657 ], "type":"VEC3" }, { "bufferView":375, "componentType":5126, "count":243, "type":"VEC3" }, { "bufferView":376, "componentType":5123, "count":1152, "type":"SCALAR" }, { "bufferView":377, "componentType":5126, "count":3664, "max":[ 0.02991061471402645, 0.015455699525773525, 0.5116071105003357 ], "min":[ -0.0299106165766716, -3.0331082598422654e-05, -0.02991061471402645 ], "type":"VEC3" }, { "bufferView":378, "componentType":5126, "count":3664, "type":"VEC3" }, { "bufferView":379, "componentType":5123, "count":19776, "type":"SCALAR" }, { "bufferView":380, "componentType":5126, "count":4584, "max":[ 0.02991061471402645, 0.018386347219347954, 0.5116071105003357 ], "min":[ -0.0299106165766716, -3.0331082598422654e-05, -0.02991061471402645 ], "type":"VEC3" }, { "bufferView":381, "componentType":5126, "count":4584, "type":"VEC3" }, { "bufferView":382, "componentType":5123, "count":25152, "type":"SCALAR" }, { "bufferView":383, "componentType":5126, "count":2075, "max":[ 0.05743894353508949, 0.03309363126754761, 0.3695560693740845 ], "min":[ -0.057435255497694016, -0.03309362754225731, -0.30479103326797485 ], "type":"VEC3" }, { "bufferView":384, "componentType":5126, "count":2075, "type":"VEC3" }, { "bufferView":385, "componentType":5123, "count":11784, "type":"SCALAR" }, { "bufferView":386, "componentType":5126, "count":3063, "max":[ 0.05743894353508949, 0.03304240480065346, 0.3695560693740845 ], "min":[ -0.057435255497694016, -0.033042408525943756, -0.30486488342285156 ], "type":"VEC3" }, { "bufferView":387, "componentType":5126, "count":3063, "type":"VEC3" }, { "bufferView":388, "componentType":5123, "count":17682, "type":"SCALAR" }, { "bufferView":389, "componentType":5126, "count":5205, "max":[ 0.057455360889434814, 0.03329318016767502, 0.30475133657455444 ], "min":[ -0.05747444927692413, -0.825806736946106, -0.30475133657455444 ], "type":"VEC3" }, { "bufferView":390, "componentType":5126, "count":5205, "type":"VEC3" }, { "bufferView":391, "componentType":5123, "count":27954, "type":"SCALAR" }, { "bufferView":392, "componentType":5126, "count":387, "max":[ 0.02317667007446289, 0.017622478306293488, 0.036703143268823624 ], "min":[ -0.023189472034573555, -0.02382655441761017, -0.023113075643777847 ], "type":"VEC3" }, { "bufferView":393, "componentType":5126, "count":387, "type":"VEC3" }, { "bufferView":394, "componentType":5123, "count":2160, "type":"SCALAR" }, { "bufferView":395, "componentType":5126, "count":5368, "max":[ 0.019682668149471283, 0.0077690789476037025, 0.019682668149471283 ], "min":[ -0.019682668149471283, -0.21444717049598694, -0.019682668149471283 ], "type":"VEC3" }, { "bufferView":396, "componentType":5126, "count":5368, "type":"VEC3" }, { "bufferView":397, "componentType":5123, "count":30528, "type":"SCALAR" }, { "bufferView":398, "componentType":5126, "count":761, "max":[ 0.015527750365436077, -0.06980611383914948, 0.0072495341300964355 ], "min":[ -0.015527751296758652, -0.21446961164474487, -0.04016909748315811 ], "type":"VEC3" }, { "bufferView":399, "componentType":5126, "count":761, "type":"VEC3" }, { "bufferView":400, "componentType":5123, "count":4374, "type":"SCALAR" }, { "bufferView":401, "componentType":5126, "count":2323, "max":[ 0.06420924514532089, 0.05869099125266075, 0.044763900339603424 ], "min":[ -0.044763900339603424, 3.658107380033471e-05, -0.04476391151547432 ], "type":"VEC3" }, { "bufferView":402, "componentType":5126, "count":2323, "type":"VEC3" }, { "bufferView":403, "componentType":5123, "count":13650, "type":"SCALAR" }, { "bufferView":404, "componentType":5126, "count":2370, "max":[ 0.0691460371017456, 0.011226482689380646, 0.0691460445523262 ], "min":[ -0.0691460520029068, 0.00014830658619757742, -0.0691460371017456 ], "type":"VEC3" }, { "bufferView":405, "componentType":5126, "count":2370, "type":"VEC3" }, { "bufferView":406, "componentType":5123, "count":14208, "type":"SCALAR" }, { "bufferView":407, "componentType":5126, "count":984, "max":[ 0.045120880007743835, 0.01545932050794363, 0.011687418445944786 ], "min":[ -0.08727649599313736, -1.3494890481524635e-05, -0.011687335558235645 ], "type":"VEC3" }, { "bufferView":408, "componentType":5126, "count":984, "type":"VEC3" }, { "bufferView":409, "componentType":5123, "count":5892, "type":"SCALAR" }, { "bufferView":410, "componentType":5126, "count":2919, "max":[ 0.11718066036701202, 0.13938021659851074, 0.07898714393377304 ], "min":[ -0.14315040409564972, 6.240474613150582e-05, -0.07898714393377304 ], "type":"VEC3" }, { "bufferView":411, "componentType":5126, "count":2919, "type":"VEC3" }, { "bufferView":412, "componentType":5123, "count":17451, "type":"SCALAR" }, { "bufferView":413, "componentType":5126, "count":4833, "max":[ 0.04082781821489334, 0.16709978878498077, 0.04082781821489334 ], "min":[ -0.04082781821489334, 0.12753257155418396, -0.04082782566547394 ], "type":"VEC3" }, { "bufferView":414, "componentType":5126, "count":4833, "type":"VEC3" }, { "bufferView":415, "componentType":5123, "count":28800, "type":"SCALAR" }, { "bufferView":416, "componentType":5126, "count":1951, "max":[ 1.2633671760559082, 7.536155699483515e-09, 0.9429534077644348 ], "min":[ -1.2633671760559082, -0.03758739307522774, -0.9311918020248413 ], "type":"VEC3" }, { "bufferView":417, "componentType":5126, "count":1951, "type":"VEC3" }, { "bufferView":418, "componentType":5123, "count":10560, "type":"SCALAR" }, { "bufferView":419, "componentType":5126, "count":2892, "max":[ 0.5521746277809143, 0.015419553965330124, 0.6326165199279785 ], "min":[ -0.49043479561805725, -0.015419621020555496, -0.6613004803657532 ], "type":"VEC3" }, { "bufferView":420, "componentType":5126, "count":2892, "type":"VEC3" }, { "bufferView":421, "componentType":5126, "count":2892, "type":"VEC2" }, { "bufferView":422, "componentType":5123, "count":12906, "type":"SCALAR" }, { "bufferView":423, "componentType":5126, "count":458, "max":[ 0.9241786599159241, 0.958294689655304, 0.9241788387298584 ], "min":[ -0.9241786599159241, 0.898910641670227, -0.9241783022880554 ], "type":"VEC3" }, { "bufferView":424, "componentType":5126, "count":458, "type":"VEC3" }, { "bufferView":425, "componentType":5126, "count":458, "type":"VEC2" }, { "bufferView":426, "componentType":5123, "count":2304, "type":"SCALAR" }, { "bufferView":427, "componentType":5126, "count":192, "max":[ 0.5798277258872986, 0.8989107608795166, 0.579828679561615 ], "min":[ -0.5798280239105225, 0.8822780251502991, -0.5798268914222717 ], "type":"VEC3" }, { "bufferView":428, "componentType":5126, "count":192, "type":"VEC3" }, { "bufferView":429, "componentType":5126, "count":192, "type":"VEC2" }, { "bufferView":430, "componentType":5123, "count":768, "type":"SCALAR" }, { "bufferView":431, "componentType":5126, "count":448, "max":[ 0.7299965620040894, -0.7179452776908875, 13.385577201843262 ], "min":[ -0.7299966216087341, -0.7817333340644836, -13.38558292388916 ], "type":"VEC3" }, { "bufferView":432, "componentType":5126, "count":448, "type":"VEC3" }, { "bufferView":433, "componentType":5123, "count":2304, "type":"SCALAR" }, { "bufferView":434, "componentType":5126, "count":513, "max":[ 0.7297933101654053, 0.040191370993852615, 13.381857872009277 ], "min":[ -0.7297933101654053, -0.054171591997146606, -13.381854057312012 ], "type":"VEC3" }, { "bufferView":435, "componentType":5126, "count":513, "type":"VEC3" }, { "bufferView":436, "componentType":5123, "count":2880, "type":"SCALAR" }, { "bufferView":437, "componentType":5126, "count":772, "max":[ 1.0000001192092896, 1.0000001192092896, 1.2264912128448486 ], "min":[ -52.15318298339844, -1.0000001192092896, -1.3088507652282715 ], "type":"VEC3" }, { "bufferView":438, "componentType":5126, "count":772, "type":"VEC3" }, { "bufferView":439, "componentType":5123, "count":4608, "type":"SCALAR" }, { "bufferView":440, "componentType":5126, "count":1896, "max":[ 1.6478947401046753, 0.03785727918148041, 19.271907806396484 ], "min":[ -1.6527422666549683, -2.6613372028805315e-05, -25.50284194946289 ], "type":"VEC3" }, { "bufferView":441, "componentType":5126, "count":1896, "type":"VEC3" }, { "bufferView":442, "componentType":5123, "count":9729, "type":"SCALAR" }, { "bufferView":443, "componentType":5126, "count":424, "max":[ 5.885627746582031, 0.504646897315979, 2.0028798580169678 ], "min":[ -5.885629653930664, -0.5046470165252686, -1.1367090940475464 ], "type":"VEC3" }, { "bufferView":444, "componentType":5126, "count":424, "type":"VEC3" }, { "bufferView":445, "componentType":5123, "count":2496, "type":"SCALAR" }, { "bufferView":446, "componentType":5126, "count":110, "max":[ 1.2648602724075317, 9.051706051366182e-09, 0.9511604309082031 ], "min":[ -1.2648602724075317, -8.854934563373718e-09, -0.9311917424201965 ], "type":"VEC3" }, { "bufferView":447, "componentType":5126, "count":110, "type":"VEC3" }, { "bufferView":448, "componentType":5123, "count":540, "type":"SCALAR" }, { "bufferView":449, "componentType":5126, "count":353, "max":[ 0.12745577096939087, 0.08690422028303146, 2.3370938301086426 ], "min":[ -0.12745580077171326, -0.15926377475261688, -2.3370931148529053 ], "type":"VEC3" }, { "bufferView":450, "componentType":5126, "count":353, "type":"VEC3" }, { "bufferView":451, "componentType":5123, "count":2064, "type":"SCALAR" }, { "bufferView":452, "componentType":5126, "count":876, "max":[ 1.2632966041564941, 7.536156587661935e-09, 0.9429534673690796 ], "min":[ -1.2632967233657837, -0.5335023999214172, -0.9311918020248413 ], "type":"VEC3" }, { "bufferView":453, "componentType":5126, "count":876, "type":"VEC3" }, { "bufferView":454, "componentType":5123, "count":4752, "type":"SCALAR" }, { "bufferView":455, "componentType":5126, "count":2560, "max":[ 1.2643764019012451, 0.2753285765647888, 0.9475919008255005 ], "min":[ -1.2643764019012451, 0.2506295144557953, -0.9368728995323181 ], "type":"VEC3" }, { "bufferView":456, "componentType":5126, "count":2560, "type":"VEC3" }, { "bufferView":457, "componentType":5123, "count":15360, "type":"SCALAR" }, { "bufferView":458, "componentType":5126, "count":640, "max":[ 1.6644946336746216, -0.32771193981170654, 22.462453842163086 ], "min":[ -1.6644943952560425, -0.3600073456764221, -22.719451904296875 ], "type":"VEC3" }, { "bufferView":459, "componentType":5126, "count":640, "type":"VEC3" }, { "bufferView":460, "componentType":5123, "count":3840, "type":"SCALAR" }, { "bufferView":461, "componentType":5126, "count":650, "max":[ 1.2243093252182007, 4.99021179933834e-09, 2.2311463356018066 ], "min":[ -1.2243093252182007, -0.42578771710395813, -0.677730917930603 ], "type":"VEC3" }, { "bufferView":462, "componentType":5126, "count":650, "type":"VEC3" }, { "bufferView":463, "componentType":5123, "count":3888, "type":"SCALAR" }, { "bufferView":464, "componentType":5126, "count":4, "max":[ 0.08039847761392593, -0.004371070768684149, 0.10489880293607712 ], "min":[ -0.08039848506450653, -0.0043710870668292046, -0.13081341981887817 ], "type":"VEC3" }, { "bufferView":465, "componentType":5126, "count":4, "type":"VEC3" }, { "bufferView":466, "componentType":5126, "count":4, "type":"VEC2" }, { "bufferView":467, "componentType":5126, "count":300, "max":[ 0.006832329090684652, -7.933522283565253e-06, 0.006860257126390934 ], "min":[ -0.0068323309533298016, -0.02199321798980236, -0.006860255263745785 ], "type":"VEC3" }, { "bufferView":468, "componentType":5126, "count":300, "type":"VEC3" }, { "bufferView":469, "componentType":5123, "count":1728, "type":"SCALAR" }, { "bufferView":470, "componentType":5126, "count":700, "max":[ 0.18859754502773285, 0.057214487344026566, 0.008845220319926739 ], "min":[ -0.18859755992889404, -0.0034839953295886517, -0.054255563765764236 ], "type":"VEC3" }, { "bufferView":471, "componentType":5126, "count":700, "type":"VEC3" }, { "bufferView":472, "componentType":5123, "count":3972, "type":"SCALAR" }, { "bufferView":473, "componentType":5126, "count":317, "max":[ 0.19292476773262024, 0.23947319388389587, 0.009282649494707584 ], "min":[ -0.19292476773262024, -0.005541855935007334, -0.002669593319296837 ], "type":"VEC3" }, { "bufferView":474, "componentType":5126, "count":317, "type":"VEC3" }, { "bufferView":475, "componentType":5123, "count":1554, "type":"SCALAR" }, { "bufferView":476, "componentType":5126, "count":4080, "max":[ 0.017140550538897514, 0.03083072043955326, 0.1839798539876938 ], "min":[ 0.0014622606104239821, -0.0016518529737368226, -0.1839798539876938 ], "type":"VEC3" }, { "bufferView":477, "componentType":5126, "count":4080, "type":"VEC3" }, { "bufferView":478, "componentType":5123, "count":24192, "type":"SCALAR" }, { "bufferView":479, "componentType":5126, "count":116, "max":[ 0.01459871418774128, -8.859956142259762e-05, 0.1814749836921692 ], "min":[ 0.004253401421010494, -0.11362061649560928, -0.18147499859333038 ], "type":"VEC3" }, { "bufferView":480, "componentType":5126, "count":116, "type":"VEC3" }, { "bufferView":481, "componentType":5123, "count":672, "type":"SCALAR" }, { "bufferView":482, "componentType":5126, "count":701, "max":[ 0.19292476773262024, 0.059158556163311005, 0.009282649494707584 ], "min":[ -0.19292476773262024, -0.005541855935007334, -0.009235925041139126 ], "type":"VEC3" }, { "bufferView":483, "componentType":5126, "count":701, "type":"VEC3" }, { "bufferView":484, "componentType":5123, "count":3972, "type":"SCALAR" }, { "bufferView":485, "componentType":5126, "count":1044, "max":[ 0.18989714980125427, 0.05946918576955795, 0.11615249514579773 ], "min":[ -0.18989714980125427, 9.935706657415722e-06, -0.005730097647756338 ], "type":"VEC3" }, { "bufferView":486, "componentType":5126, "count":1044, "type":"VEC3" }, { "bufferView":487, "componentType":5123, "count":6216, "type":"SCALAR" }, { "bufferView":488, "componentType":5126, "count":241, "max":[ 0.2911272943019867, 2.181886316066084e-08, 0.6213991641998291 ], "min":[ -0.29112735390663147, -1.1645550301864205e-08, -0.40438249707221985 ], "type":"VEC3" }, { "bufferView":489, "componentType":5126, "count":241, "type":"VEC3" }, { "bufferView":490, "componentType":5123, "count":1152, "type":"SCALAR" }, { "bufferView":491, "componentType":5126, "count":2522, "max":[ 0.26504117250442505, 0.1980399191379547, 0.5879448056221008 ], "min":[ -0.2650412321090698, -0.018674420192837715, -0.2089131623506546 ], "type":"VEC3" }, { "bufferView":492, "componentType":5126, "count":2522, "type":"VEC3" }, { "bufferView":493, "componentType":5123, "count":14016, "type":"SCALAR" }, { "bufferView":494, "componentType":5126, "count":1480, "max":[ 0.026884857565164566, 0.4139226973056793, 0.14664645493030548 ], "min":[ -0.02788403630256653, -0.41392260789871216, -0.037943124771118164 ], "type":"VEC3" }, { "bufferView":495, "componentType":5126, "count":1480, "type":"VEC3" }, { "bufferView":496, "componentType":5123, "count":8712, "type":"SCALAR" }, { "bufferView":497, "componentType":5126, "count":12, "max":[ 0.3310958445072174, 1.1071453094482422, 0.5224316716194153 ], "min":[ -0.08596266806125641, 1.097179651260376, -0.5224316716194153 ], "type":"VEC3" }, { "bufferView":498, "componentType":5126, "count":12, "type":"VEC3" }, { "bufferView":499, "componentType":5123, "count":60, "type":"SCALAR" }, { "bufferView":500, "componentType":5126, "count":128, "max":[ 0.3586115539073944, 1.1417394876480103, 0.5487160086631775 ], "min":[ -0.38212496042251587, 1.087152361869812, -0.5487160086631775 ], "type":"VEC3" }, { "bufferView":501, "componentType":5126, "count":128, "type":"VEC3" }, { "bufferView":502, "componentType":5123, "count":228, "type":"SCALAR" }, { "bufferView":503, "componentType":5126, "count":361, "max":[ 0.13963429629802704, 1.1167515516281128, 0.5015658736228943 ], "min":[ -0.08627023547887802, 1.087152361869812, -0.5015658736228943 ], "type":"VEC3" }, { "bufferView":504, "componentType":5126, "count":361, "type":"VEC3" }, { "bufferView":505, "componentType":5123, "count":516, "type":"SCALAR" }, { "bufferView":506, "componentType":5126, "count":554, "max":[ 0.05097965896129608, 1.18086576461792, 0.46035489439964294 ], "min":[ -0.08596266806125641, 3.5204434175760066e-14, -0.46035489439964294 ], "type":"VEC3" }, { "bufferView":507, "componentType":5126, "count":554, "type":"VEC3" }, { "bufferView":508, "componentType":5123, "count":972, "type":"SCALAR" }, { "bufferView":509, "componentType":5126, "count":16256, "max":[ 0.9999998807907104, 1, 0.9999997615814209 ], "min":[ -0.9999997019767761, -1, -1 ], "type":"VEC3" }, { "bufferView":510, "componentType":5126, "count":16256, "type":"VEC3" }, { "bufferView":511, "componentType":5126, "count":16256, "type":"VEC2" }, { "bufferView":512, "componentType":5123, "count":24192, "type":"SCALAR" }, { "bufferView":513, "componentType":5126, "count":58080, "max":[ 0.09569022059440613, 0.10698522627353668, 0.09144195914268494 ], "min":[ -0.09569022804498672, -0.10698520392179489, -0.09144195914268494 ], "type":"VEC3" }, { "bufferView":514, "componentType":5126, "count":58080, "type":"VEC3" }, { "bufferView":515, "componentType":5126, "count":58080, "type":"VEC2" }, { "bufferView":516, "componentType":5123, "count":87120, "type":"SCALAR" }, { "bufferView":517, "componentType":5126, "count":58080, "max":[ 0.09569022059440613, 0.10698522627353668, 0.09144195914268494 ], "min":[ -0.09569022804498672, -0.10698520392179489, -0.09144195914268494 ], "type":"VEC3" }, { "bufferView":518, "componentType":5126, "count":58080, "type":"VEC3" }, { "bufferView":519, "componentType":5126, "count":58080, "type":"VEC2" }, { "bufferView":520, "componentType":5123, "count":87120, "type":"SCALAR" }, { "bufferView":521, "componentType":5126, "count":287, "max":[ 1, 1, 1 ], "min":[ -0.9999999403953552, -1, -0.9999999403953552 ], "type":"VEC3" }, { "bufferView":522, "componentType":5126, "count":287, "type":"VEC3" }, { "bufferView":523, "componentType":5126, "count":287, "type":"VEC2" }, { "bufferView":524, "componentType":5123, "count":1440, "type":"SCALAR" }, { "bufferView":525, "componentType":5126, "count":58080, "max":[ 0.09569022059440613, 0.10698522627353668, 0.09144195914268494 ], "min":[ -0.09569022804498672, -0.10698520392179489, -0.09144195914268494 ], "type":"VEC3" }, { "bufferView":526, "componentType":5126, "count":58080, "type":"VEC3" }, { "bufferView":527, "componentType":5126, "count":58080, "type":"VEC2" }, { "bufferView":528, "componentType":5126, "count":58080, "max":[ 0.09569022059440613, 0.10698522627353668, 0.09144195914268494 ], "min":[ -0.09569022804498672, -0.10698520392179489, -0.09144195914268494 ], "type":"VEC3" }, { "bufferView":529, "componentType":5126, "count":58080, "type":"VEC3" }, { "bufferView":530, "componentType":5126, "count":58080, "type":"VEC2" }, { "bufferView":531, "componentType":5126, "count":287, "max":[ 1, 1, 1 ], "min":[ -0.9999999403953552, -1, -0.9999999403953552 ], "type":"VEC3" }, { "bufferView":532, "componentType":5126, "count":287, "type":"VEC3" }, { "bufferView":533, "componentType":5126, "count":287, "type":"VEC2" }, { "bufferView":534, "componentType":5126, "count":637, "max":[ 1.25, 0.25, 1.25 ], "min":[ -1.25, -0.25, -1.25 ], "type":"VEC3" }, { "bufferView":535, "componentType":5126, "count":637, "type":"VEC3" }, { "bufferView":536, "componentType":5126, "count":637, "type":"VEC2" }, { "bufferView":537, "componentType":5123, "count":3456, "type":"SCALAR" }, { "bufferView":538, "componentType":5126, "count":2304, "max":[ 1.1200000047683716, 0.11999999731779099, 1.1200000047683716 ], "min":[ -1.1200000047683716, -0.11999999731779099, -1.1200000047683716 ], "type":"VEC3" }, { "bufferView":539, "componentType":5126, "count":2304, "type":"VEC3" }, { "bufferView":540, "componentType":5126, "count":2304, "type":"VEC2" }, { "bufferView":541, "componentType":5123, "count":3456, "type":"SCALAR" }, { "bufferView":542, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":543, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":544, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":545, "componentType":5123, "count":36, "type":"SCALAR" }, { "bufferView":546, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":547, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":548, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":549, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":550, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":551, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":552, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":553, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":554, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":555, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":556, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":557, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":558, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":559, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":560, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":561, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":562, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":563, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":564, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":565, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":566, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":567, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":568, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":569, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":570, "componentType":5126, "count":24, "max":[ 1, 1, 1 ], "min":[ -1, -1, -1 ], "type":"VEC3" }, { "bufferView":571, "componentType":5126, "count":24, "type":"VEC3" }, { "bufferView":572, "componentType":5126, "count":24, "type":"VEC2" }, { "bufferView":573, "componentType":5126, "count":16256, "max":[ 0.9999998807907104, 1, 0.9999997615814209 ], "min":[ -0.9999997019767761, -1, -1 ], "type":"VEC3" }, { "bufferView":574, "componentType":5126, "count":16256, "type":"VEC3" }, { "bufferView":575, "componentType":5126, "count":16256, "type":"VEC2" } ], "bufferViews":[ { "buffer":0, "byteLength":7776, "byteOffset":0, "target":34962 }, { "buffer":0, "byteLength":7776, "byteOffset":7776, "target":34962 }, { "buffer":0, "byteLength":1872, "byteOffset":15552, "target":34963 }, { "buffer":0, "byteLength":576, "byteOffset":17424, "target":34962 }, { "buffer":0, "byteLength":576, "byteOffset":18000, "target":34962 }, { "buffer":0, "byteLength":384, "byteOffset":18576, "target":34962 }, { "buffer":0, "byteLength":396, "byteOffset":18960, "target":34963 }, { "buffer":0, "byteLength":576, "byteOffset":19356, "target":34962 }, { "buffer":0, "byteLength":576, "byteOffset":19932, "target":34962 }, { "buffer":0, "byteLength":384, "byteOffset":20508, "target":34962 }, { "buffer":0, "byteLength":396, "byteOffset":20892, "target":34963 }, { "buffer":0, "byteLength":11400, "byteOffset":21288, "target":34962 }, { "buffer":0, "byteLength":11400, "byteOffset":32688, "target":34962 }, { "buffer":0, "byteLength":3432, "byteOffset":44088, "target":34963 }, { "buffer":0, "byteLength":28608, "byteOffset":47520, "target":34962 }, { "buffer":0, "byteLength":28608, "byteOffset":76128, "target":34962 }, { "buffer":0, "byteLength":7560, "byteOffset":104736, "target":34963 }, { "buffer":0, "byteLength":22368, "byteOffset":112296, "target":34962 }, { "buffer":0, "byteLength":22368, "byteOffset":134664, "target":34962 }, { "buffer":0, "byteLength":7548, "byteOffset":157032, "target":34963 }, { "buffer":0, "byteLength":151680, "byteOffset":164580, "target":34962 }, { "buffer":0, "byteLength":151680, "byteOffset":316260, "target":34962 }, { "buffer":0, "byteLength":37320, "byteOffset":467940, "target":34963 }, { "buffer":0, "byteLength":151740, "byteOffset":505260, "target":34962 }, { "buffer":0, "byteLength":151740, "byteOffset":657000, "target":34962 }, { "buffer":0, "byteLength":37320, "byteOffset":808740, "target":34963 }, { "buffer":0, "byteLength":151944, "byteOffset":846060, "target":34962 }, { "buffer":0, "byteLength":151944, "byteOffset":998004, "target":34962 }, { "buffer":0, "byteLength":37320, "byteOffset":1149948, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":1187268, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1187316, "target":34962 }, { "buffer":0, "byteLength":12, "byteOffset":1187364, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":1187376, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1187424, "target":34962 }, { "buffer":0, "byteLength":4488, "byteOffset":1187472, "target":34962 }, { "buffer":0, "byteLength":4488, "byteOffset":1191960, "target":34962 }, { "buffer":0, "byteLength":4296, "byteOffset":1196448, "target":34963 }, { "buffer":0, "byteLength":6648, "byteOffset":1200744, "target":34962 }, { "buffer":0, "byteLength":6648, "byteOffset":1207392, "target":34962 }, { "buffer":0, "byteLength":6624, "byteOffset":1214040, "target":34963 }, { "buffer":0, "byteLength":7224, "byteOffset":1220664, "target":34962 }, { "buffer":0, "byteLength":7224, "byteOffset":1227888, "target":34962 }, { "buffer":0, "byteLength":7200, "byteOffset":1235112, "target":34963 }, { "buffer":0, "byteLength":7800, "byteOffset":1242312, "target":34962 }, { "buffer":0, "byteLength":7800, "byteOffset":1250112, "target":34962 }, { "buffer":0, "byteLength":7776, "byteOffset":1257912, "target":34963 }, { "buffer":0, "byteLength":1248, "byteOffset":1265688, "target":34962 }, { "buffer":0, "byteLength":1248, "byteOffset":1266936, "target":34962 }, { "buffer":0, "byteLength":832, "byteOffset":1268184, "target":34962 }, { "buffer":0, "byteLength":456, "byteOffset":1269016, "target":34963 }, { "buffer":0, "byteLength":5508, "byteOffset":1269472, "target":34962 }, { "buffer":0, "byteLength":5508, "byteOffset":1274980, "target":34962 }, { "buffer":0, "byteLength":3672, "byteOffset":1280488, "target":34962 }, { "buffer":0, "byteLength":4608, "byteOffset":1284160, "target":34963 }, { "buffer":0, "byteLength":25368, "byteOffset":1288768, "target":34962 }, { "buffer":0, "byteLength":25368, "byteOffset":1314136, "target":34962 }, { "buffer":0, "byteLength":25344, "byteOffset":1339504, "target":34963 }, { "buffer":0, "byteLength":216, "byteOffset":1364848, "target":34962 }, { "buffer":0, "byteLength":216, "byteOffset":1365064, "target":34962 }, { "buffer":0, "byteLength":96, "byteOffset":1365280, "target":34963 }, { "buffer":0, "byteLength":1152, "byteOffset":1365376, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1366528, "target":34962 }, { "buffer":0, "byteLength":264, "byteOffset":1367680, "target":34963 }, { "buffer":0, "byteLength":1152, "byteOffset":1367944, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1369096, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1370248, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1371400, "target":34962 }, { "buffer":0, "byteLength":264, "byteOffset":1372552, "target":34963 }, { "buffer":0, "byteLength":1152, "byteOffset":1372816, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1373968, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1375120, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1376272, "target":34962 }, { "buffer":0, "byteLength":264, "byteOffset":1377424, "target":34963 }, { "buffer":0, "byteLength":9480, "byteOffset":1377688, "target":34962 }, { "buffer":0, "byteLength":9480, "byteOffset":1387168, "target":34962 }, { "buffer":0, "byteLength":4416, "byteOffset":1396648, "target":34963 }, { "buffer":0, "byteLength":10224, "byteOffset":1401064, "target":34962 }, { "buffer":0, "byteLength":10224, "byteOffset":1411288, "target":34962 }, { "buffer":0, "byteLength":4104, "byteOffset":1421512, "target":34963 }, { "buffer":0, "byteLength":8448, "byteOffset":1425616, "target":34962 }, { "buffer":0, "byteLength":8448, "byteOffset":1434064, "target":34962 }, { "buffer":0, "byteLength":4224, "byteOffset":1442512, "target":34963 }, { "buffer":0, "byteLength":10992, "byteOffset":1446736, "target":34962 }, { "buffer":0, "byteLength":10992, "byteOffset":1457728, "target":34962 }, { "buffer":0, "byteLength":5064, "byteOffset":1468720, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":1473784, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1473832, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":1473880, "target":34962 }, { "buffer":0, "byteLength":12, "byteOffset":1473912, "target":34963 }, { "buffer":0, "byteLength":3072, "byteOffset":1473924, "target":34962 }, { "buffer":0, "byteLength":3072, "byteOffset":1476996, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":1480068, "target":34963 }, { "buffer":0, "byteLength":3072, "byteOffset":1480836, "target":34962 }, { "buffer":0, "byteLength":3072, "byteOffset":1483908, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1486980, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1487028, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":1487076, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1487108, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1487156, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":1487204, "target":34962 }, { "buffer":0, "byteLength":3072, "byteOffset":1487236, "target":34962 }, { "buffer":0, "byteLength":3072, "byteOffset":1490308, "target":34962 }, { "buffer":0, "byteLength":7668, "byteOffset":1493380, "target":34962 }, { "buffer":0, "byteLength":7668, "byteOffset":1501048, "target":34962 }, { "buffer":0, "byteLength":5112, "byteOffset":1508716, "target":34962 }, { "buffer":0, "byteLength":5034, "byteOffset":1513828, "target":34963 }, { "buffer":0, "byteLength":768, "byteOffset":1518864, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":1519632, "target":34962 }, { "buffer":0, "byteLength":612, "byteOffset":1520400, "target":34963 }, { "buffer":0, "byteLength":864, "byteOffset":1521012, "target":34962 }, { "buffer":0, "byteLength":864, "byteOffset":1521876, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":1522740, "target":34963 }, { "buffer":0, "byteLength":144, "byteOffset":1523508, "target":34962 }, { "buffer":0, "byteLength":144, "byteOffset":1523652, "target":34962 }, { "buffer":0, "byteLength":72, "byteOffset":1523796, "target":34963 }, { "buffer":0, "byteLength":2304, "byteOffset":1523868, "target":34962 }, { "buffer":0, "byteLength":2304, "byteOffset":1526172, "target":34962 }, { "buffer":0, "byteLength":2304, "byteOffset":1528476, "target":34963 }, { "buffer":0, "byteLength":22512, "byteOffset":1530780, "target":34962 }, { "buffer":0, "byteLength":22512, "byteOffset":1553292, "target":34962 }, { "buffer":0, "byteLength":15008, "byteOffset":1575804, "target":34962 }, { "buffer":0, "byteLength":16128, "byteOffset":1590812, "target":34963 }, { "buffer":0, "byteLength":3072, "byteOffset":1606940, "target":34962 }, { "buffer":0, "byteLength":3072, "byteOffset":1610012, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1613084, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1613612, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1614140, "target":34962 }, { "buffer":0, "byteLength":168, "byteOffset":1614492, "target":34963 }, { "buffer":0, "byteLength":528, "byteOffset":1614660, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1615188, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1615716, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1616068, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1616596, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1617124, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1617476, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1618004, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1618532, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1618884, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1619412, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1619940, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1620292, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1620820, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1621348, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1621700, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1622228, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1622756, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1623108, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1623636, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1624164, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1624516, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1625044, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1625572, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1625924, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1626452, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1626980, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1627332, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1627860, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1628388, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1628740, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1629268, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1629796, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1630148, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1630676, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1631204, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1631556, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1632084, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1632612, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1632964, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1633492, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1634020, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1634372, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1634900, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1635428, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1635780, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1636308, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1636836, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1637188, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1637716, "target":34962 }, { "buffer":0, "byteLength":352, "byteOffset":1638244, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1638596, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1639748, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":1640900, "target":34962 }, { "buffer":0, "byteLength":264, "byteOffset":1641668, "target":34963 }, { "buffer":0, "byteLength":2304, "byteOffset":1641932, "target":34962 }, { "buffer":0, "byteLength":2304, "byteOffset":1644236, "target":34962 }, { "buffer":0, "byteLength":1536, "byteOffset":1646540, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1648076, "target":34963 }, { "buffer":0, "byteLength":1152, "byteOffset":1648604, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":1649756, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":1650908, "target":34962 }, { "buffer":0, "byteLength":38148, "byteOffset":1651676, "target":34962 }, { "buffer":0, "byteLength":38148, "byteOffset":1689824, "target":34962 }, { "buffer":0, "byteLength":10320, "byteOffset":1727972, "target":34963 }, { "buffer":0, "byteLength":2304, "byteOffset":1738292, "target":34962 }, { "buffer":0, "byteLength":2304, "byteOffset":1740596, "target":34962 }, { "buffer":0, "byteLength":1536, "byteOffset":1742900, "target":34962 }, { "buffer":0, "byteLength":528, "byteOffset":1744436, "target":34963 }, { "buffer":0, "byteLength":4608, "byteOffset":1744964, "target":34962 }, { "buffer":0, "byteLength":4608, "byteOffset":1749572, "target":34962 }, { "buffer":0, "byteLength":3072, "byteOffset":1754180, "target":34962 }, { "buffer":0, "byteLength":1056, "byteOffset":1757252, "target":34963 }, { "buffer":0, "byteLength":768, "byteOffset":1758308, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":1759076, "target":34962 }, { "buffer":0, "byteLength":252, "byteOffset":1759844, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":1760096, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":1760144, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":1760192, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":1760480, "target":34962 }, { "buffer":0, "byteLength":72, "byteOffset":1760768, "target":34963 }, { "buffer":0, "byteLength":7716, "byteOffset":1760840, "target":34962 }, { "buffer":0, "byteLength":7716, "byteOffset":1768556, "target":34962 }, { "buffer":0, "byteLength":5144, "byteOffset":1776272, "target":34962 }, { "buffer":0, "byteLength":5034, "byteOffset":1781416, "target":34963 }, { "buffer":0, "byteLength":13200, "byteOffset":1786452, "target":34962 }, { "buffer":0, "byteLength":13200, "byteOffset":1799652, "target":34962 }, { "buffer":0, "byteLength":13056, "byteOffset":1812852, "target":34963 }, { "buffer":0, "byteLength":49776, "byteOffset":1825908, "target":34962 }, { "buffer":0, "byteLength":49776, "byteOffset":1875684, "target":34962 }, { "buffer":0, "byteLength":48768, "byteOffset":1925460, "target":34963 }, { "buffer":0, "byteLength":44808, "byteOffset":1974228, "target":34962 }, { "buffer":0, "byteLength":44808, "byteOffset":2019036, "target":34962 }, { "buffer":0, "byteLength":44544, "byteOffset":2063844, "target":34963 }, { "buffer":0, "byteLength":7812, "byteOffset":2108388, "target":34962 }, { "buffer":0, "byteLength":7812, "byteOffset":2116200, "target":34962 }, { "buffer":0, "byteLength":5208, "byteOffset":2124012, "target":34962 }, { "buffer":0, "byteLength":5034, "byteOffset":2129220, "target":34963 }, { "buffer":0, "byteLength":15492, "byteOffset":2134256, "target":34962 }, { "buffer":0, "byteLength":15492, "byteOffset":2149748, "target":34962 }, { "buffer":0, "byteLength":10328, "byteOffset":2165240, "target":34962 }, { "buffer":0, "byteLength":13512, "byteOffset":2175568, "target":34963 }, { "buffer":0, "byteLength":1152, "byteOffset":2189080, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":2190232, "target":34962 }, { "buffer":0, "byteLength":4608, "byteOffset":2191384, "target":34962 }, { "buffer":0, "byteLength":4608, "byteOffset":2195992, "target":34962 }, { "buffer":0, "byteLength":1056, "byteOffset":2200600, "target":34963 }, { "buffer":0, "byteLength":2496, "byteOffset":2201656, "target":34962 }, { "buffer":0, "byteLength":2496, "byteOffset":2204152, "target":34962 }, { "buffer":0, "byteLength":648, "byteOffset":2206648, "target":34963 }, { "buffer":0, "byteLength":3672, "byteOffset":2207296, "target":34962 }, { "buffer":0, "byteLength":3672, "byteOffset":2210968, "target":34962 }, { "buffer":0, "byteLength":1032, "byteOffset":2214640, "target":34963 }, { "buffer":0, "byteLength":1152, "byteOffset":2215672, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":2216824, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":2217976, "target":34962 }, { "buffer":0, "byteLength":264, "byteOffset":2218744, "target":34963 }, { "buffer":0, "byteLength":33372, "byteOffset":2219008, "target":34962 }, { "buffer":0, "byteLength":33372, "byteOffset":2252380, "target":34962 }, { "buffer":0, "byteLength":26880, "byteOffset":2285752, "target":34963 }, { "buffer":0, "byteLength":14976, "byteOffset":2312632, "target":34962 }, { "buffer":0, "byteLength":14976, "byteOffset":2327608, "target":34962 }, { "buffer":0, "byteLength":7992, "byteOffset":2342584, "target":34963 }, { "buffer":0, "byteLength":2736, "byteOffset":2350576, "target":34962 }, { "buffer":0, "byteLength":2736, "byteOffset":2353312, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":2356048, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":2356816, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":2356864, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":2356912, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":2356944, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":2356992, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":2357040, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":2357088, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":2357136, "target":34962 }, { "buffer":0, "byteLength":3072, "byteOffset":2357168, "target":34962 }, { "buffer":0, "byteLength":3072, "byteOffset":2360240, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":2363312, "target":34963 }, { "buffer":0, "byteLength":3024, "byteOffset":2364080, "target":34962 }, { "buffer":0, "byteLength":3024, "byteOffset":2367104, "target":34962 }, { "buffer":0, "byteLength":2880, "byteOffset":2370128, "target":34963 }, { "buffer":0, "byteLength":144, "byteOffset":2373008, "target":34962 }, { "buffer":0, "byteLength":144, "byteOffset":2373152, "target":34962 }, { "buffer":0, "byteLength":72, "byteOffset":2373296, "target":34963 }, { "buffer":0, "byteLength":576, "byteOffset":2373368, "target":34962 }, { "buffer":0, "byteLength":576, "byteOffset":2373944, "target":34962 }, { "buffer":0, "byteLength":168, "byteOffset":2374520, "target":34963 }, { "buffer":0, "byteLength":1056, "byteOffset":2374688, "target":34962 }, { "buffer":0, "byteLength":1056, "byteOffset":2375744, "target":34962 }, { "buffer":0, "byteLength":336, "byteOffset":2376800, "target":34963 }, { "buffer":0, "byteLength":8184, "byteOffset":2377136, "target":34962 }, { "buffer":0, "byteLength":8184, "byteOffset":2385320, "target":34962 }, { "buffer":0, "byteLength":2304, "byteOffset":2393504, "target":34963 }, { "buffer":0, "byteLength":13392, "byteOffset":2395808, "target":34962 }, { "buffer":0, "byteLength":13392, "byteOffset":2409200, "target":34962 }, { "buffer":0, "byteLength":3600, "byteOffset":2422592, "target":34963 }, { "buffer":0, "byteLength":34392, "byteOffset":2426192, "target":34962 }, { "buffer":0, "byteLength":34392, "byteOffset":2460584, "target":34962 }, { "buffer":0, "byteLength":8832, "byteOffset":2494976, "target":34963 }, { "buffer":0, "byteLength":2328, "byteOffset":2503808, "target":34962 }, { "buffer":0, "byteLength":2328, "byteOffset":2506136, "target":34962 }, { "buffer":0, "byteLength":1536, "byteOffset":2508464, "target":34963 }, { "buffer":0, "byteLength":2328, "byteOffset":2510000, "target":34962 }, { "buffer":0, "byteLength":2328, "byteOffset":2512328, "target":34962 }, { "buffer":0, "byteLength":34896, "byteOffset":2514656, "target":34962 }, { "buffer":0, "byteLength":34896, "byteOffset":2549552, "target":34962 }, { "buffer":0, "byteLength":8832, "byteOffset":2584448, "target":34963 }, { "buffer":0, "byteLength":13392, "byteOffset":2593280, "target":34962 }, { "buffer":0, "byteLength":13392, "byteOffset":2606672, "target":34962 }, { "buffer":0, "byteLength":8184, "byteOffset":2620064, "target":34962 }, { "buffer":0, "byteLength":8184, "byteOffset":2628248, "target":34962 }, { "buffer":0, "byteLength":1056, "byteOffset":2636432, "target":34962 }, { "buffer":0, "byteLength":1056, "byteOffset":2637488, "target":34962 }, { "buffer":0, "byteLength":576, "byteOffset":2638544, "target":34962 }, { "buffer":0, "byteLength":576, "byteOffset":2639120, "target":34962 }, { "buffer":0, "byteLength":144, "byteOffset":2639696, "target":34962 }, { "buffer":0, "byteLength":144, "byteOffset":2639840, "target":34962 }, { "buffer":0, "byteLength":3024, "byteOffset":2639984, "target":34962 }, { "buffer":0, "byteLength":3024, "byteOffset":2643008, "target":34962 }, { "buffer":0, "byteLength":3024, "byteOffset":2646032, "target":34962 }, { "buffer":0, "byteLength":3024, "byteOffset":2649056, "target":34962 }, { "buffer":0, "byteLength":144, "byteOffset":2652080, "target":34962 }, { "buffer":0, "byteLength":144, "byteOffset":2652224, "target":34962 }, { "buffer":0, "byteLength":576, "byteOffset":2652368, "target":34962 }, { "buffer":0, "byteLength":576, "byteOffset":2652944, "target":34962 }, { "buffer":0, "byteLength":1056, "byteOffset":2653520, "target":34962 }, { "buffer":0, "byteLength":1056, "byteOffset":2654576, "target":34962 }, { "buffer":0, "byteLength":8304, "byteOffset":2655632, "target":34962 }, { "buffer":0, "byteLength":8304, "byteOffset":2663936, "target":34962 }, { "buffer":0, "byteLength":2304, "byteOffset":2672240, "target":34963 }, { "buffer":0, "byteLength":13104, "byteOffset":2674544, "target":34962 }, { "buffer":0, "byteLength":13104, "byteOffset":2687648, "target":34962 }, { "buffer":0, "byteLength":3600, "byteOffset":2700752, "target":34963 }, { "buffer":0, "byteLength":32652, "byteOffset":2704352, "target":34962 }, { "buffer":0, "byteLength":32652, "byteOffset":2737004, "target":34962 }, { "buffer":0, "byteLength":8832, "byteOffset":2769656, "target":34963 }, { "buffer":0, "byteLength":3456, "byteOffset":2778488, "target":34962 }, { "buffer":0, "byteLength":3456, "byteOffset":2781944, "target":34962 }, { "buffer":0, "byteLength":3072, "byteOffset":2785400, "target":34963 }, { "buffer":0, "byteLength":18072, "byteOffset":2788472, "target":34962 }, { "buffer":0, "byteLength":18072, "byteOffset":2806544, "target":34962 }, { "buffer":0, "byteLength":17808, "byteOffset":2824616, "target":34963 }, { "buffer":0, "byteLength":24624, "byteOffset":2842424, "target":34962 }, { "buffer":0, "byteLength":24624, "byteOffset":2867048, "target":34962 }, { "buffer":0, "byteLength":24384, "byteOffset":2891672, "target":34963 }, { "buffer":0, "byteLength":26532, "byteOffset":2916056, "target":34962 }, { "buffer":0, "byteLength":26532, "byteOffset":2942588, "target":34962 }, { "buffer":0, "byteLength":26112, "byteOffset":2969120, "target":34963 }, { "buffer":0, "byteLength":18024, "byteOffset":2995232, "target":34962 }, { "buffer":0, "byteLength":18024, "byteOffset":3013256, "target":34962 }, { "buffer":0, "byteLength":17808, "byteOffset":3031280, "target":34963 }, { "buffer":0, "byteLength":49896, "byteOffset":3049088, "target":34962 }, { "buffer":0, "byteLength":49896, "byteOffset":3098984, "target":34962 }, { "buffer":0, "byteLength":48768, "byteOffset":3148880, "target":34963 }, { "buffer":0, "byteLength":15840, "byteOffset":3197648, "target":34962 }, { "buffer":0, "byteLength":15840, "byteOffset":3213488, "target":34962 }, { "buffer":0, "byteLength":15660, "byteOffset":3229328, "target":34963 }, { "buffer":0, "byteLength":7680, "byteOffset":3244988, "target":34962 }, { "buffer":0, "byteLength":7680, "byteOffset":3252668, "target":34962 }, { "buffer":0, "byteLength":5120, "byteOffset":3260348, "target":34962 }, { "buffer":0, "byteLength":5034, "byteOffset":3265468, "target":34963 }, { "buffer":0, "byteLength":5640, "byteOffset":3270504, "target":34962 }, { "buffer":0, "byteLength":5640, "byteOffset":3276144, "target":34962 }, { "buffer":0, "byteLength":3760, "byteOffset":3281784, "target":34962 }, { "buffer":0, "byteLength":4128, "byteOffset":3285544, "target":34963 }, { "buffer":0, "byteLength":1152, "byteOffset":3289672, "target":34962 }, { "buffer":0, "byteLength":1152, "byteOffset":3290824, "target":34962 }, { "buffer":0, "byteLength":768, "byteOffset":3291976, "target":34962 }, { "buffer":0, "byteLength":3672, "byteOffset":3292744, "target":34962 }, { "buffer":0, "byteLength":3672, "byteOffset":3296416, "target":34962 }, { "buffer":0, "byteLength":1032, "byteOffset":3300088, "target":34963 }, { "buffer":0, "byteLength":31824, "byteOffset":3301120, "target":34962 }, { "buffer":0, "byteLength":31824, "byteOffset":3332944, "target":34962 }, { "buffer":0, "byteLength":21216, "byteOffset":3364768, "target":34962 }, { "buffer":0, "byteLength":16278, "byteOffset":3385984, "target":34963 }, { "buffer":0, "byteLength":576, "byteOffset":3402264, "target":34962 }, { "buffer":0, "byteLength":576, "byteOffset":3402840, "target":34962 }, { "buffer":0, "byteLength":384, "byteOffset":3403416, "target":34962 }, { "buffer":0, "byteLength":144, "byteOffset":3403800, "target":34963 }, { "buffer":0, "byteLength":8064, "byteOffset":3403944, "target":34962 }, { "buffer":0, "byteLength":8064, "byteOffset":3412008, "target":34962 }, { "buffer":0, "byteLength":7680, "byteOffset":3420072, "target":34963 }, { "buffer":0, "byteLength":8064, "byteOffset":3427752, "target":34962 }, { "buffer":0, "byteLength":8064, "byteOffset":3435816, "target":34962 }, { "buffer":0, "byteLength":20352, "byteOffset":3443880, "target":34962 }, { "buffer":0, "byteLength":20352, "byteOffset":3464232, "target":34962 }, { "buffer":0, "byteLength":19968, "byteOffset":3484584, "target":34963 }, { "buffer":0, "byteLength":2916, "byteOffset":3504552, "target":34962 }, { "buffer":0, "byteLength":2916, "byteOffset":3507468, "target":34962 }, { "buffer":0, "byteLength":2304, "byteOffset":3510384, "target":34963 }, { "buffer":0, "byteLength":43968, "byteOffset":3512688, "target":34962 }, { "buffer":0, "byteLength":43968, "byteOffset":3556656, "target":34962 }, { "buffer":0, "byteLength":39552, "byteOffset":3600624, "target":34963 }, { "buffer":0, "byteLength":55008, "byteOffset":3640176, "target":34962 }, { "buffer":0, "byteLength":55008, "byteOffset":3695184, "target":34962 }, { "buffer":0, "byteLength":50304, "byteOffset":3750192, "target":34963 }, { "buffer":0, "byteLength":24900, "byteOffset":3800496, "target":34962 }, { "buffer":0, "byteLength":24900, "byteOffset":3825396, "target":34962 }, { "buffer":0, "byteLength":23568, "byteOffset":3850296, "target":34963 }, { "buffer":0, "byteLength":36756, "byteOffset":3873864, "target":34962 }, { "buffer":0, "byteLength":36756, "byteOffset":3910620, "target":34962 }, { "buffer":0, "byteLength":35364, "byteOffset":3947376, "target":34963 }, { "buffer":0, "byteLength":62460, "byteOffset":3982740, "target":34962 }, { "buffer":0, "byteLength":62460, "byteOffset":4045200, "target":34962 }, { "buffer":0, "byteLength":55908, "byteOffset":4107660, "target":34963 }, { "buffer":0, "byteLength":4644, "byteOffset":4163568, "target":34962 }, { "buffer":0, "byteLength":4644, "byteOffset":4168212, "target":34962 }, { "buffer":0, "byteLength":4320, "byteOffset":4172856, "target":34963 }, { "buffer":0, "byteLength":64416, "byteOffset":4177176, "target":34962 }, { "buffer":0, "byteLength":64416, "byteOffset":4241592, "target":34962 }, { "buffer":0, "byteLength":61056, "byteOffset":4306008, "target":34963 }, { "buffer":0, "byteLength":9132, "byteOffset":4367064, "target":34962 }, { "buffer":0, "byteLength":9132, "byteOffset":4376196, "target":34962 }, { "buffer":0, "byteLength":8748, "byteOffset":4385328, "target":34963 }, { "buffer":0, "byteLength":27876, "byteOffset":4394076, "target":34962 }, { "buffer":0, "byteLength":27876, "byteOffset":4421952, "target":34962 }, { "buffer":0, "byteLength":27300, "byteOffset":4449828, "target":34963 }, { "buffer":0, "byteLength":28440, "byteOffset":4477128, "target":34962 }, { "buffer":0, "byteLength":28440, "byteOffset":4505568, "target":34962 }, { "buffer":0, "byteLength":28416, "byteOffset":4534008, "target":34963 }, { "buffer":0, "byteLength":11808, "byteOffset":4562424, "target":34962 }, { "buffer":0, "byteLength":11808, "byteOffset":4574232, "target":34962 }, { "buffer":0, "byteLength":11784, "byteOffset":4586040, "target":34963 }, { "buffer":0, "byteLength":35028, "byteOffset":4597824, "target":34962 }, { "buffer":0, "byteLength":35028, "byteOffset":4632852, "target":34962 }, { "buffer":0, "byteLength":34902, "byteOffset":4667880, "target":34963 }, { "buffer":0, "byteLength":57996, "byteOffset":4702784, "target":34962 }, { "buffer":0, "byteLength":57996, "byteOffset":4760780, "target":34962 }, { "buffer":0, "byteLength":57600, "byteOffset":4818776, "target":34963 }, { "buffer":0, "byteLength":23412, "byteOffset":4876376, "target":34962 }, { "buffer":0, "byteLength":23412, "byteOffset":4899788, "target":34962 }, { "buffer":0, "byteLength":21120, "byteOffset":4923200, "target":34963 }, { "buffer":0, "byteLength":34704, "byteOffset":4944320, "target":34962 }, { "buffer":0, "byteLength":34704, "byteOffset":4979024, "target":34962 }, { "buffer":0, "byteLength":23136, "byteOffset":5013728, "target":34962 }, { "buffer":0, "byteLength":25812, "byteOffset":5036864, "target":34963 }, { "buffer":0, "byteLength":5496, "byteOffset":5062676, "target":34962 }, { "buffer":0, "byteLength":5496, "byteOffset":5068172, "target":34962 }, { "buffer":0, "byteLength":3664, "byteOffset":5073668, "target":34962 }, { "buffer":0, "byteLength":4608, "byteOffset":5077332, "target":34963 }, { "buffer":0, "byteLength":2304, "byteOffset":5081940, "target":34962 }, { "buffer":0, "byteLength":2304, "byteOffset":5084244, "target":34962 }, { "buffer":0, "byteLength":1536, "byteOffset":5086548, "target":34962 }, { "buffer":0, "byteLength":1536, "byteOffset":5088084, "target":34963 }, { "buffer":0, "byteLength":5376, "byteOffset":5089620, "target":34962 }, { "buffer":0, "byteLength":5376, "byteOffset":5094996, "target":34962 }, { "buffer":0, "byteLength":4608, "byteOffset":5100372, "target":34963 }, { "buffer":0, "byteLength":6156, "byteOffset":5104980, "target":34962 }, { "buffer":0, "byteLength":6156, "byteOffset":5111136, "target":34962 }, { "buffer":0, "byteLength":5760, "byteOffset":5117292, "target":34963 }, { "buffer":0, "byteLength":9264, "byteOffset":5123052, "target":34962 }, { "buffer":0, "byteLength":9264, "byteOffset":5132316, "target":34962 }, { "buffer":0, "byteLength":9216, "byteOffset":5141580, "target":34963 }, { "buffer":0, "byteLength":22752, "byteOffset":5150796, "target":34962 }, { "buffer":0, "byteLength":22752, "byteOffset":5173548, "target":34962 }, { "buffer":0, "byteLength":19458, "byteOffset":5196300, "target":34963 }, { "buffer":0, "byteLength":5088, "byteOffset":5215760, "target":34962 }, { "buffer":0, "byteLength":5088, "byteOffset":5220848, "target":34962 }, { "buffer":0, "byteLength":4992, "byteOffset":5225936, "target":34963 }, { "buffer":0, "byteLength":1320, "byteOffset":5230928, "target":34962 }, { "buffer":0, "byteLength":1320, "byteOffset":5232248, "target":34962 }, { "buffer":0, "byteLength":1080, "byteOffset":5233568, "target":34963 }, { "buffer":0, "byteLength":4236, "byteOffset":5234648, "target":34962 }, { "buffer":0, "byteLength":4236, "byteOffset":5238884, "target":34962 }, { "buffer":0, "byteLength":4128, "byteOffset":5243120, "target":34963 }, { "buffer":0, "byteLength":10512, "byteOffset":5247248, "target":34962 }, { "buffer":0, "byteLength":10512, "byteOffset":5257760, "target":34962 }, { "buffer":0, "byteLength":9504, "byteOffset":5268272, "target":34963 }, { "buffer":0, "byteLength":30720, "byteOffset":5277776, "target":34962 }, { "buffer":0, "byteLength":30720, "byteOffset":5308496, "target":34962 }, { "buffer":0, "byteLength":30720, "byteOffset":5339216, "target":34963 }, { "buffer":0, "byteLength":7680, "byteOffset":5369936, "target":34962 }, { "buffer":0, "byteLength":7680, "byteOffset":5377616, "target":34962 }, { "buffer":0, "byteLength":7680, "byteOffset":5385296, "target":34963 }, { "buffer":0, "byteLength":7800, "byteOffset":5392976, "target":34962 }, { "buffer":0, "byteLength":7800, "byteOffset":5400776, "target":34962 }, { "buffer":0, "byteLength":7776, "byteOffset":5408576, "target":34963 }, { "buffer":0, "byteLength":48, "byteOffset":5416352, "target":34962 }, { "buffer":0, "byteLength":48, "byteOffset":5416400, "target":34962 }, { "buffer":0, "byteLength":32, "byteOffset":5416448, "target":34962 }, { "buffer":0, "byteLength":3600, "byteOffset":5416480, "target":34962 }, { "buffer":0, "byteLength":3600, "byteOffset":5420080, "target":34962 }, { "buffer":0, "byteLength":3456, "byteOffset":5423680, "target":34963 }, { "buffer":0, "byteLength":8400, "byteOffset":5427136, "target":34962 }, { "buffer":0, "byteLength":8400, "byteOffset":5435536, "target":34962 }, { "buffer":0, "byteLength":7944, "byteOffset":5443936, "target":34963 }, { "buffer":0, "byteLength":3804, "byteOffset":5451880, "target":34962 }, { "buffer":0, "byteLength":3804, "byteOffset":5455684, "target":34962 }, { "buffer":0, "byteLength":3108, "byteOffset":5459488, "target":34963 }, { "buffer":0, "byteLength":48960, "byteOffset":5462596, "target":34962 }, { "buffer":0, "byteLength":48960, "byteOffset":5511556, "target":34962 }, { "buffer":0, "byteLength":48384, "byteOffset":5560516, "target":34963 }, { "buffer":0, "byteLength":1392, "byteOffset":5608900, "target":34962 }, { "buffer":0, "byteLength":1392, "byteOffset":5610292, "target":34962 }, { "buffer":0, "byteLength":1344, "byteOffset":5611684, "target":34963 }, { "buffer":0, "byteLength":8412, "byteOffset":5613028, "target":34962 }, { "buffer":0, "byteLength":8412, "byteOffset":5621440, "target":34962 }, { "buffer":0, "byteLength":7944, "byteOffset":5629852, "target":34963 }, { "buffer":0, "byteLength":12528, "byteOffset":5637796, "target":34962 }, { "buffer":0, "byteLength":12528, "byteOffset":5650324, "target":34962 }, { "buffer":0, "byteLength":12432, "byteOffset":5662852, "target":34963 }, { "buffer":0, "byteLength":2892, "byteOffset":5675284, "target":34962 }, { "buffer":0, "byteLength":2892, "byteOffset":5678176, "target":34962 }, { "buffer":0, "byteLength":2304, "byteOffset":5681068, "target":34963 }, { "buffer":0, "byteLength":30264, "byteOffset":5683372, "target":34962 }, { "buffer":0, "byteLength":30264, "byteOffset":5713636, "target":34962 }, { "buffer":0, "byteLength":28032, "byteOffset":5743900, "target":34963 }, { "buffer":0, "byteLength":17760, "byteOffset":5771932, "target":34962 }, { "buffer":0, "byteLength":17760, "byteOffset":5789692, "target":34962 }, { "buffer":0, "byteLength":17424, "byteOffset":5807452, "target":34963 }, { "buffer":0, "byteLength":144, "byteOffset":5824876, "target":34962 }, { "buffer":0, "byteLength":144, "byteOffset":5825020, "target":34962 }, { "buffer":0, "byteLength":120, "byteOffset":5825164, "target":34963 }, { "buffer":0, "byteLength":1536, "byteOffset":5825284, "target":34962 }, { "buffer":0, "byteLength":1536, "byteOffset":5826820, "target":34962 }, { "buffer":0, "byteLength":456, "byteOffset":5828356, "target":34963 }, { "buffer":0, "byteLength":4332, "byteOffset":5828812, "target":34962 }, { "buffer":0, "byteLength":4332, "byteOffset":5833144, "target":34962 }, { "buffer":0, "byteLength":1032, "byteOffset":5837476, "target":34963 }, { "buffer":0, "byteLength":6648, "byteOffset":5838508, "target":34962 }, { "buffer":0, "byteLength":6648, "byteOffset":5845156, "target":34962 }, { "buffer":0, "byteLength":1944, "byteOffset":5851804, "target":34963 }, { "buffer":0, "byteLength":195072, "byteOffset":5853748, "target":34962 }, { "buffer":0, "byteLength":195072, "byteOffset":6048820, "target":34962 }, { "buffer":0, "byteLength":130048, "byteOffset":6243892, "target":34962 }, { "buffer":0, "byteLength":48384, "byteOffset":6373940, "target":34963 }, { "buffer":0, "byteLength":696960, "byteOffset":6422324, "target":34962 }, { "buffer":0, "byteLength":696960, "byteOffset":7119284, "target":34962 }, { "buffer":0, "byteLength":464640, "byteOffset":7816244, "target":34962 }, { "buffer":0, "byteLength":174240, "byteOffset":8280884, "target":34963 }, { "buffer":0, "byteLength":696960, "byteOffset":8455124, "target":34962 }, { "buffer":0, "byteLength":696960, "byteOffset":9152084, "target":34962 }, { "buffer":0, "byteLength":464640, "byteOffset":9849044, "target":34962 }, { "buffer":0, "byteLength":174240, "byteOffset":10313684, "target":34963 }, { "buffer":0, "byteLength":3444, "byteOffset":10487924, "target":34962 }, { "buffer":0, "byteLength":3444, "byteOffset":10491368, "target":34962 }, { "buffer":0, "byteLength":2296, "byteOffset":10494812, "target":34962 }, { "buffer":0, "byteLength":2880, "byteOffset":10497108, "target":34963 }, { "buffer":0, "byteLength":696960, "byteOffset":10499988, "target":34962 }, { "buffer":0, "byteLength":696960, "byteOffset":11196948, "target":34962 }, { "buffer":0, "byteLength":464640, "byteOffset":11893908, "target":34962 }, { "buffer":0, "byteLength":696960, "byteOffset":12358548, "target":34962 }, { "buffer":0, "byteLength":696960, "byteOffset":13055508, "target":34962 }, { "buffer":0, "byteLength":464640, "byteOffset":13752468, "target":34962 }, { "buffer":0, "byteLength":3444, "byteOffset":14217108, "target":34962 }, { "buffer":0, "byteLength":3444, "byteOffset":14220552, "target":34962 }, { "buffer":0, "byteLength":2296, "byteOffset":14223996, "target":34962 }, { "buffer":0, "byteLength":7644, "byteOffset":14226292, "target":34962 }, { "buffer":0, "byteLength":7644, "byteOffset":14233936, "target":34962 }, { "buffer":0, "byteLength":5096, "byteOffset":14241580, "target":34962 }, { "buffer":0, "byteLength":6912, "byteOffset":14246676, "target":34963 }, { "buffer":0, "byteLength":27648, "byteOffset":14253588, "target":34962 }, { "buffer":0, "byteLength":27648, "byteOffset":14281236, "target":34962 }, { "buffer":0, "byteLength":18432, "byteOffset":14308884, "target":34962 }, { "buffer":0, "byteLength":6912, "byteOffset":14327316, "target":34963 }, { "buffer":0, "byteLength":288, "byteOffset":14334228, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14334516, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14334804, "target":34962 }, { "buffer":0, "byteLength":72, "byteOffset":14334996, "target":34963 }, { "buffer":0, "byteLength":288, "byteOffset":14335068, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14335356, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14335644, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14335836, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14336124, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14336412, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14336604, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14336892, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14337180, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14337372, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14337660, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14337948, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14338140, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14338428, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14338716, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14338908, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14339196, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14339484, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14339676, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14339964, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14340252, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14340444, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14340732, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14341020, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14341212, "target":34962 }, { "buffer":0, "byteLength":288, "byteOffset":14341500, "target":34962 }, { "buffer":0, "byteLength":192, "byteOffset":14341788, "target":34962 }, { "buffer":0, "byteLength":195072, "byteOffset":14341980, "target":34962 }, { "buffer":0, "byteLength":195072, "byteOffset":14537052, "target":34962 }, { "buffer":0, "byteLength":130048, "byteOffset":14732124, "target":34962 } ], "samplers":[ { "magFilter":9729, "minFilter":9987 } ], "buffers":[ { "byteLength":14862172, "uri":"the-white-room-low.bin" } ] } ================================================ FILE: src/Compiler/GPUKernel.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Compiler/GPUKernelCompiler.h" #include "Compiler/GPUKernel.h" #include "Compiler/GPUKernelCompilerOptions.h" #include "HIPRT-Orochi/HIPRTOrochiUtils.h" #include "Threads/ThreadFunctions.h" #include "Threads/ThreadManager.h" #include "UI/ImGui/ImGuiLogger.h" extern GPUKernelCompiler g_gpu_kernel_compiler; extern ImGuiLogger g_imgui_logger; const std::vector GPUKernel::COMMON_ADDITIONAL_KERNEL_INCLUDE_DIRS = { KERNEL_COMPILER_ADDITIONAL_INCLUDE, DEVICE_INCLUDES_DIRECTORY, OROCHI_INCLUDES_DIRECTORY, "./" }; GPUKernel::GPUKernel() { OROCHI_CHECK_ERROR(oroEventCreate(&m_execution_start_event)); OROCHI_CHECK_ERROR(oroEventCreate(&m_execution_stop_event)); } GPUKernel::GPUKernel(const std::string& kernel_file_path, const std::string& kernel_function_name) : GPUKernel() { m_kernel_file_path = kernel_file_path; m_kernel_function_name = kernel_function_name; } std::string GPUKernel::get_kernel_file_path() const { return m_kernel_file_path; } std::string GPUKernel::get_kernel_function_name() const { return m_kernel_function_name; } void GPUKernel::set_kernel_file_path(const std::string& kernel_file_path) { m_kernel_file_path = kernel_file_path; } void GPUKernel::set_kernel_function_name(const std::string& kernel_function_name) { m_kernel_function_name = kernel_function_name; } void GPUKernel::add_additional_macro_for_compilation(const std::string& name, int value) { m_additional_compilation_macros[name] = value; } std::vector GPUKernel::get_additional_compiler_macros() const { std::vector macros; for (auto macro_key_value : m_additional_compilation_macros) macros.push_back("-D " + macro_key_value.first + "=" + std::to_string(macro_key_value.second)); return macros; } void GPUKernel::compile(std::shared_ptr hiprt_ctx, std::vector func_name_sets, bool use_cache, bool silent) { if (m_option_macro_invalidated) parse_option_macros_used(); std::string cache_key = g_gpu_kernel_compiler.get_additional_cache_key(*this); m_kernel_function = g_gpu_kernel_compiler.compile_kernel(*this, m_compiler_options, hiprt_ctx, func_name_sets.data(), /* num geom */1, /* num ray */ func_name_sets.size() == 0 ? 0 : 1, use_cache, cache_key, silent); } int GPUKernel::get_kernel_attribute(oroFunction compiled_kernel, oroFunction_attribute attribute) { int numRegs = 0; if (compiled_kernel == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to get an attribute of a kernel that wasn't compiled yet."); return 0; } OROCHI_CHECK_ERROR(oroFuncGetAttribute(&numRegs, attribute, compiled_kernel)); return numRegs; } int GPUKernel::get_kernel_attribute(oroFunction_attribute attribute) const { int numRegs = 0; if (m_kernel_function == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to get an attribute of a kernel that wasn't compiled yet."); return 0; } OROCHI_CHECK_ERROR(oroFuncGetAttribute(&numRegs, attribute, m_kernel_function)); return numRegs; } GPUKernelCompilerOptions& GPUKernel::get_kernel_options() { return m_compiler_options; } const GPUKernelCompilerOptions& GPUKernel::get_kernel_options() const { return m_compiler_options; } void GPUKernel::synchronize_options_with(std::shared_ptr other_options, const std::unordered_set& options_excluded) { for (auto macro_to_value : other_options->get_options_macro_map()) { const std::string& macro_name = macro_to_value.first; int macro_value = *macro_to_value.second; if (options_excluded.find(macro_name) == options_excluded.end()) // Option is not excluded m_compiler_options.set_pointer_to_macro(macro_name, other_options->get_pointer_to_macro_value(macro_name)); } // Same thing with the custom macros for (auto macro_to_value : other_options->get_custom_macro_map()) { const std::string& macro_name = macro_to_value.first; int macro_value = *macro_to_value.second; if (options_excluded.find(macro_name) == options_excluded.end()) // Option is not excluded m_compiler_options.set_pointer_to_macro(macro_name, other_options->get_pointer_to_macro_value(macro_name)); } } void GPUKernel::launch(int block_size_x, int block_size_y, int nb_threads_x, int nb_threads_y, void** launch_args, oroStream_t stream) { launch_3D(block_size_x, block_size_y, 1, nb_threads_x, nb_threads_y, 1, launch_args, stream); } void GPUKernel::launch_3D(int block_size_x, int block_size_y, int block_size_z, int nb_threads_x, int nb_threads_y, int nb_threads_z, void** launch_args, oroStream_t stream) { int3 nb_groups; nb_groups.x = std::ceil(static_cast(nb_threads_x) / block_size_x); nb_groups.y = std::ceil(static_cast(nb_threads_y) / block_size_y); nb_groups.z = std::ceil(static_cast(nb_threads_z) / block_size_z); OROCHI_CHECK_ERROR(oroModuleLaunchKernel(m_kernel_function, nb_groups.x, nb_groups.y, nb_groups.z, block_size_x, block_size_y, block_size_z, 0, stream, launch_args, 0)); m_launched_at_least_once = true; } void GPUKernel::launch_synchronous(int block_size_x, int block_size_y, int nb_threads_x, int nb_threads_y, void** launch_args, float* execution_time_out) { OROCHI_CHECK_ERROR(oroEventRecord(m_execution_start_event, 0)); launch(block_size_x, block_size_y, nb_threads_x, nb_threads_y, launch_args, 0); OROCHI_CHECK_ERROR(oroEventRecord(m_execution_stop_event, 0)); OROCHI_CHECK_ERROR(oroEventSynchronize(m_execution_stop_event)); if (execution_time_out != nullptr) OROCHI_CHECK_ERROR(oroEventElapsedTime(execution_time_out, m_execution_start_event, m_execution_stop_event)); } void GPUKernel::parse_option_macros_used() { m_used_option_macros = g_gpu_kernel_compiler.get_option_macros_used_by_kernel(*this); m_option_macro_invalidated = false; } bool GPUKernel::uses_macro(const std::string& name) const { return m_used_option_macros.find(name) != m_used_option_macros.end(); } float GPUKernel::compute_execution_time() { if (!m_launched_at_least_once) return 0.0f; float out; OROCHI_CHECK_ERROR(oroEventElapsedTime(&out, m_execution_start_event, m_execution_stop_event)); m_last_execution_time = out; return out; } float GPUKernel::get_last_execution_time() { return m_last_execution_time; } bool GPUKernel::has_been_compiled() const { return m_kernel_function != nullptr; } bool GPUKernel::is_precompiled() const { return m_is_precompiled_kernel; } void GPUKernel::set_precompiled(bool precompiled) { m_is_precompiled_kernel = precompiled; } void GPUKernel::launch_asynchronous(int block_size_x, int block_size_y, int nb_threads_x, int nb_threads_y, void** launch_args, oroStream_t stream) { launch_asynchronous_3D(block_size_x, block_size_y, 1, nb_threads_x, nb_threads_y, 1, launch_args, stream); } void GPUKernel::launch_asynchronous_3D(int block_size_x, int block_size_y, int block_size_z, int nb_threads_x, int nb_threads_y, int nb_threads_z, void** launch_args, oroStream_t stream) { OROCHI_CHECK_ERROR(oroEventRecord(m_execution_start_event, stream)); launch_3D(block_size_x, block_size_y, block_size_z, nb_threads_x, nb_threads_y, nb_threads_z, launch_args, stream); OROCHI_CHECK_ERROR(oroEventRecord(m_execution_stop_event, stream)); // TODO: There's an issue here on HIP 5.7 + Windows where without the oroLaunchHostFunc below, // this oroEventRecord (or any event after a kernel launch) "blocks" the stream (only on a non-NULL stream) // and oroStreamQuery always (kind of) returns hipErrorDeviceNotReady oroLaunchHostFunc(stream, [](void*) {}, nullptr); } ================================================ FILE: src/Compiler/GPUKernel.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GPU_KERNEL_H #define GPU_KERNEL_H #include "Compiler/GPUKernelCompilerOptions.h" #include "HIPRT-Orochi/HIPRTOrochiCtx.h" #include #include #include #include #include #include #include class GPUKernel { public: static const std::vector COMMON_ADDITIONAL_KERNEL_INCLUDE_DIRS; GPUKernel(); GPUKernel(const std::string& kernel_file_path, const std::string& kernel_function_name); std::string get_kernel_file_path() const; std::string get_kernel_function_name() const; void set_kernel_file_path(const std::string& kernel_file_path); void set_kernel_function_name(const std::string& kernel_function_name); void compile(std::shared_ptr hiprt_ctx, std::vector func_name_sets = {}, bool use_cache = true, bool silent = true); //void compile_silent(std::shared_ptr hiprt_ctx, std::vector func_name_sets = {}, bool use_cache = true); void launch_synchronous(int tile_size_x, int tile_size_y, int res_x, int res_y, void** launch_args, float* execution_time_out = nullptr); void launch_asynchronous(int tile_size_x, int tile_size_y, int res_x, int res_y, void** launch_args, oroStream_t stream); void launch_asynchronous_3D(int tile_size_x, int tile_size_y, int tile_size_z, int res_x, int res_y, int res_z, void** launch_args, oroStream_t stream); /** * Sets an additional macro that will be passed to the GPU compiler when compiling this kernel */ void add_additional_macro_for_compilation(const std::string& name, int value); /** * Returns a vector of strings of the form { -DMacroName=value, ... } from all the additional * macros that were added to this kernel by calling 'add_additional_macro_for_compilation()' */ std::vector get_additional_compiler_macros() const; /** * Reads the kernel file and all of its includes to find what option macros this kernel uses. * * Calling this function update the m_used_option_macros member attribute. */ void parse_option_macros_used(); /** * Given an option macro name ("InteriorStackStrategy", "DirectLightSamplingStrategy", "EnvmapSamplingStrategy", ... * for examples. They are all defined in KernelOptions.h), returns true if the kernel uses that option macro. * False otherwise. * * The kernel "uses" that macro if changing the value of that macro and recompiling the kernel * changes the output of the compiler. For example, the camera ray kernel doesn't care about * which direct lighting sampling strategy we're using. It also doesn't care about our envmap * sampling strategy. So we way that the camera ray kernel doesn't use the * "DirectLightSamplingStrategy" and "EnvmapSamplingStrategy" options macro */ bool uses_macro(const std::string& macro_name) const; /** * Returns the number of GPU register that this kernel is using. This function * must be called after the kernel has been compiled. * This function may also return 0 if the device doesn't support querrying * the number of registers */ int get_kernel_attribute(oroFunction_attribute attribute) const; static int get_kernel_attribute(oroFunction compiled_kernel, oroFunction_attribute attribute); /** * Returns the compiler options of this kernel so that they can be modified */ GPUKernelCompilerOptions& get_kernel_options(); const GPUKernelCompilerOptions& get_kernel_options() const; /** * Synchronizes the value of the options of this kernel with the values of the macros of 'other_options'. * This means that if the value of the macro "MY_MACRO" is modified in 'other_options', the value of 'MY_MACRO' * will also be modified in this kernel options. * * Macros that are in the 'options_excluded" set will not be synchronized. * * Macros that are present in 'other_options' but that are not present in this kernel's option * will be added to this kernel and their vlaue will be synchronized with 'other_options' * * This function can be useful if you want to have a global set of macros shared by multiple kernels. * You can thus synchronize all your kernel with that global set of macros and when it is modified, * all the kernels will use the new values. */ void synchronize_options_with(std::shared_ptr other_options, const std::unordered_set& options_excluded = {}); /** * Returns the time taken for the last execution of this kernel in milliseconds */ float compute_execution_time(); float get_last_execution_time(); /** * Structure used to pass data to the compute_elapsed_time_callback that computes the * elapsed time between the start and end events of this structure and stores the elapsed * time in 'elapsed_time_out' */ struct ComputeElapsedTimeCallbackData { // Start and end events to compute the elapsed time between oroEvent_t start, end; // The elapsed time will be stored in here in milliseconds float* elapsed_time_out; // Needed to set the CUDA/HIP context as current to be able to call // CUDA/HIP functions from the callback HIPRTOrochiCtx* hiprt_orochi_ctx; }; bool has_been_compiled() const; bool is_precompiled() const; void set_precompiled(bool precompiled); private: void launch(int tile_size_x, int tile_size_y, int res_x, int res_y, void** launch_args, oroStream_t stream); void launch_3D(int tile_size_x, int tile_size_y, int tile_size_z, int res_x, int res_y, int res_z, void** launch_args, oroStream_t stream); std::string m_kernel_file_path = ""; std::string m_kernel_function_name = ""; // Whether or not the kernel has been launched at least once // This is used to avoid CUDA/HIP errors when trying to read // the stop/start event elapsed time whereas the kernel has never // been launched bool m_launched_at_least_once = false; // GPU events to time the execution time oroEvent_t m_execution_start_event = nullptr; oroEvent_t m_execution_stop_event = nullptr; float m_last_execution_time = 0.0f; // Whether or not the macros used by this kernel have been modified recently. // Only adding new macros / removing macros invalidate the macros. // Changing the values of macros doesn't invalidate the macros. // This variable is used to determine whether or not we need to parse the kernel // source file to collect the macro actually used during the compilation of the kernel bool m_option_macro_invalidated = true; // Which option macros (as defined in KernelOptions.h) the kernel uses. // // See uses_macro() for some examples of what "use" means. std::unordered_set m_used_option_macros; // An additional map of macros to pass to the compiler for this kernel and their values. // // Example: { "ReSTIR_DI_InitialCandidatesKernel", 1 } std::unordered_map m_additional_compilation_macros; // Options/macros used by the compiler when compiling this kernel GPUKernelCompilerOptions m_compiler_options; oroFunction m_kernel_function = nullptr; // If true, this means that this kernel is only used for precompilation and will be // discarded after it's been compiled // This is used in the GPUKernelCompiler to determine whether or not we should increment // the counter of the ImGuiLoggerLine that counts how many kernels have been precompiled // so far bool m_is_precompiled_kernel = false; }; #endif ================================================ FILE: src/Compiler/GPUKernelCompiler.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Compiler/GPUKernelCompilerOptions.h" #include "Compiler/GPUKernelCompiler.h" #include "HIPRT-Orochi/HIPRTOrochiUtils.h" #include "UI/ImGui/ImGuiLogger.h" #include "Utils/Utils.h" #include #include #include #include GPUKernelCompiler g_gpu_kernel_compiler; extern ImGuiLogger g_imgui_logger; void enable_compilation_warnings(std::shared_ptr hiprt_orochi_ctx, std::vector& compiler_options) { if (std::string(hiprt_orochi_ctx->device_properties.name).find("NVIDIA") == std::string::npos) { // AMD compiler warning suppressors compiler_options.push_back("-Wall"); compiler_options.push_back("-Weverything"); compiler_options.push_back("-Wno-old-style-cast"); compiler_options.push_back("-Wno-reorder-ctor"); compiler_options.push_back("-Wno-c++98-compat"); compiler_options.push_back("-Wno-c++98-compat-pedantic"); compiler_options.push_back("-Wno-reserved-macro-identifier"); compiler_options.push_back("-Wno-extra-semi-stmt"); compiler_options.push_back("-Wno-reserved-identifier"); compiler_options.push_back("-Wno-reserved-identifier"); compiler_options.push_back("-Wno-float-conversion"); compiler_options.push_back("-Wno-implicit-float-conversion"); compiler_options.push_back("-Wno-implicit-int-float-conversion"); compiler_options.push_back("-Wno-deprecated-copy-with-user-provided-copy"); compiler_options.push_back("-Wno-disabled-macro-expansion"); compiler_options.push_back("-Wno-float-equal"); compiler_options.push_back("-Wno-sign-compare"); compiler_options.push_back("-Wno-padded"); compiler_options.push_back("-Wno-sign-conversion"); compiler_options.push_back("-Wno-gnu-zero-variadic-macro-arguments"); compiler_options.push_back("-Wno-missing-variable-declarations"); } } oroFunction_t GPUKernelCompiler::compile_kernel(GPUKernel& kernel, const GPUKernelCompilerOptions& kernel_compiler_options, std::shared_ptr hiprt_orochi_ctx, hiprtFuncNameSet* function_name_sets, int num_geom_types, int num_ray_types, bool use_cache, const std::string& additional_cache_key, bool silent) { std::string kernel_file_path = kernel.get_kernel_file_path(); std::string kernel_function_name = kernel.get_kernel_function_name(); const std::vector& additional_include_dirs = GPUKernel::COMMON_ADDITIONAL_KERNEL_INCLUDE_DIRS; std::vector compiler_options = kernel_compiler_options.get_relevant_macros_as_std_vector_string(&kernel); #ifndef OROCHI_ENABLE_CUEW compiler_options.push_back("-g"); compiler_options.push_back("-ggdb"); #endif // enable_compilation_warnings(hiprt_orochi_ctx, compiler_options); // compiler_options.push_back("-g"); // compiler_options.push_back("-ggdb"); // Locking because neither NVIDIA or AMD can compile kernels on multiple threads at the same time // with their runtime API (but NVCC/HIPCC can compile in parallel with the commandline) so we may as well // lock here to have better control on when to compile a kernel as well as have proper compilation times std::unique_lock lock(m_compile_mutex); auto start = std::chrono::high_resolution_clock::now(); hiprtApiFunction trace_function_out; bool use_shader_cache; if (m_shader_cache_force_usage == GPUKernelCompiler::ShaderCacheUsageOverride::FORCE_SHADER_CACHE_OFF) use_shader_cache = false; else if (m_shader_cache_force_usage == GPUKernelCompiler::ShaderCacheUsageOverride::FORCE_SHADER_CACHE_ON) use_shader_cache = true; else use_shader_cache = use_cache; hiprtError compile_status = HIPPTOrochiUtils::build_trace_kernel(hiprt_orochi_ctx->hiprt_ctx, kernel_file_path, kernel_function_name, trace_function_out, additional_include_dirs, compiler_options, num_geom_types, num_ray_types, use_shader_cache, function_name_sets, additional_cache_key); if (compile_status != hiprtError::hiprtSuccess) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Unable to compile kernel \"%s\". Cannot continue.", kernel_function_name.c_str()); return nullptr; } oroFunction kernel_function = reinterpret_cast(trace_function_out); if (kernel.is_precompiled()) { // Updating the logs m_precompiled_kernels_compilation_ended++; g_imgui_logger.update_line(ImGuiLogger::BACKGROUND_KERNEL_COMPILATION_LINE_NAME, "Compiling kernel permutations in the background... [%d / %d]", m_precompiled_kernels_compilation_ended.load(), m_precompiled_kernels_parsing_started.load()); } auto stop = std::chrono::high_resolution_clock::now(); if (!silent) { // Setting the current context is necessary because getting // functions attributes necessitates calling CUDA/HIP functions // which need their context to be current if not calling from // the main thread (which we are not if we are compiling kernels on multithreads) OROCHI_CHECK_ERROR(oroCtxSetCurrent(hiprt_orochi_ctx->orochi_ctx)); int nb_reg = GPUKernel::get_kernel_attribute(kernel_function, ORO_FUNC_ATTRIBUTE_NUM_REGS); int nb_shared = GPUKernel::get_kernel_attribute(kernel_function, ORO_FUNC_ATTRIBUTE_SHARED_SIZE_BYTES); int nb_local = GPUKernel::get_kernel_attribute(kernel_function, ORO_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES); g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_INFO, "Kernel \"%s\" compiled in %ldms.\n\t[Reg, Shared, Local] = [%d, %d, %d]\n", kernel_function_name.c_str(), std::chrono::duration_cast(stop - start).count(), nb_reg, nb_shared, nb_local); } return kernel_function; } std::string GPUKernelCompiler::find_in_include_directories(const std::string& include_name, const std::vector& include_directories) { for (const std::string& include_directory : include_directories) { std::string add_slash = include_directory[include_directory.length() - 1] != '/' ? "/" : ""; std::string file_path = include_directory + add_slash + include_name; std::ifstream try_open_file(file_path); if (try_open_file.is_open()) return file_path; } return ""; } void GPUKernelCompiler::read_includes_of_file(const std::string& include_file_path, const std::vector& include_directories, std::unordered_set& output_includes) { std::ifstream include_file(include_file_path); if (include_file.is_open()) { std::string line; while (std::getline(include_file, line)) { if (line.starts_with("#include ")) { size_t find_start = line.find('<'); if (find_start == std::string::npos) { // Trying to find a quote instead find_start = line.find('"'); if (find_start == std::string::npos) // Couldn't find a quote either, ill-formed include continue; } size_t find_end = line.rfind('>'); if (find_end == std::string::npos) { // Trying to find a quote instead find_end = line.rfind('"'); if (find_end == std::string::npos) // Couldn't find a quote either, ill-formed include continue; } // We found the include string, now we're going to check whether it can be found // in the given includes directories (which contain the only includes that we're // interested in) // Include file with leading Device/includes/... or whatever folder the include may come from std::string full_include_name = line.substr(find_start + 1, find_end - find_start - 1); // We have only the file name (which looks like "MyInclude.h" for example), let's see // if it can be found in the include directories std::string include_file_path = find_in_include_directories(full_include_name, include_directories); if (!include_file_path.empty()) // Adding to file path that can directly be opened in an std::ifstream output_includes.insert(include_file_path); } else continue; } } else { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Could not generate additional cache key for kernel with path \"%s\": %s", include_file_path.c_str(), strerror(errno)); Utils::debugbreak(); } } std::unordered_set GPUKernelCompiler::read_option_macro_of_file(const std::string& filepath) { std::string file_modification_time; try { std::chrono::time_point modification_time = std::filesystem::last_write_time(filepath); file_modification_time = std::to_string(modification_time.time_since_epoch().count()); } catch (std::filesystem::filesystem_error e) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "HIPKernelCompiler - Unable to open include file \"%s\" for option macros analyzing: %s", filepath.c_str(), e.what()); return std::unordered_set(); } { // We don't to read into the cache while someone may be writing to it (at the end of this function) // so we lock std::lock_guard lock(m_option_macro_cache_mutex); auto cache_timestamp_find = m_filepath_to_options_macros_cache_timestamp.find(filepath); if (cache_timestamp_find != m_filepath_to_options_macros_cache_timestamp.end() && cache_timestamp_find->second == file_modification_time) { // Cache hit return m_filepath_to_option_macros_cache[filepath]; } } std::unordered_set option_macros; std::ifstream include_file(filepath); if (include_file.is_open()) { std::string line; while (std::getline(include_file, line)) for (const std::string& existing_macro_option : GPUKernelCompilerOptions::ALL_MACROS_NAMES) if (line.find(existing_macro_option) != std::string::npos) option_macros.insert(existing_macro_option); } else g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Could not open file \"%s\" for reading option macros used by that file: %s", filepath.c_str(), strerror(errno)); // The cache is shared to all threads using this GPUKernelCompiler so we're locking that operation // The lock is destroyed when the function returns std::lock_guard lock(m_option_macro_cache_mutex); // Updating the cache m_filepath_to_option_macros_cache[filepath] = option_macros; m_filepath_to_options_macros_cache_timestamp[filepath] = file_modification_time; return option_macros; } std::string GPUKernelCompiler::get_additional_cache_key(GPUKernel& kernel) { m_additional_cache_key_started++; std::unordered_set already_processed_includes; std::deque yet_to_process_includes; yet_to_process_includes.push_back(kernel.get_kernel_file_path()); while (!yet_to_process_includes.empty()) { std::string current_file = yet_to_process_includes.front(); yet_to_process_includes.pop_front(); if (already_processed_includes.find(current_file) != already_processed_includes.end()) // We've already processed that file continue; already_processed_includes.insert(current_file); std::unordered_set new_includes; read_includes_of_file(current_file, GPUKernel::COMMON_ADDITIONAL_KERNEL_INCLUDE_DIRS, new_includes); for (const std::string& new_include : new_includes) yet_to_process_includes.push_back(new_include); } // The cache key is going to be the concatenation of the last modified times of all the includes // that the kernel file we just parsed depends on. That way, if any dependency of this kernel has // been modified, the cache key will be different and the cache will be invalidated. std::string final_cache_key = ""; for (const std::string& include : already_processed_includes) { // TODO this exception here should probably go up a level so that we can know that the kernel compilation failed --> set the kernel function to nullptr --> do try to launch the kernel (otherwise this will probably crash the driver) try { std::chrono::time_point modification_time = std::filesystem::last_write_time(include); final_cache_key += std::to_string(modification_time.time_since_epoch().count()); } catch (std::filesystem::filesystem_error e) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "HIPKernelCompiler - Unable to open include file \"%s\" for shader cache validation: %s", include.c_str(), e.what()); m_additional_cache_key_ended++; // Notifying the condition variable that's used to // avoid exiting the application with ongoing IO operations m_read_macros_cv.notify_all(); return ""; } } m_additional_cache_key_ended++; // Notifying the condition variable that's used to // avoid exiting the application with ongoing IO operations m_read_macros_cv.notify_all(); return final_cache_key; } std::unordered_set GPUKernelCompiler::get_option_macros_used_by_kernel(const GPUKernel& kernel) { if (kernel.is_precompiled()) // If this kernel is being precompiled, we can increment the counter // used for logging m_precompiled_kernels_parsing_started++; // Limiting the number of threads that can get in here at the same time otherwise we may // get some "Too many files open!" error m_read_macros_semaphore.acquire(); std::unordered_set already_processed_includes; std::deque yet_to_process_includes; yet_to_process_includes.push_back(kernel.get_kernel_file_path()); while (!yet_to_process_includes.empty()) { std::string current_file = yet_to_process_includes.front(); yet_to_process_includes.pop_front(); if (already_processed_includes.find(current_file) != already_processed_includes.end()) // We've already processed that file continue; else if (current_file.find("HostDeviceCommon/KernelOptions") != std::string::npos) // Ignoring kernel options files when looking for option macros continue; else if (current_file.find("Device/") == std::string::npos && current_file.find("HostDeviceCommon/") == std::string::npos) // Excluding files that are not in the Device/ or HostDeviceCommon/ folder because we're only // interested in kernel files, not CPU C++ files continue; already_processed_includes.insert(current_file); std::unordered_set new_includes; read_includes_of_file(current_file, GPUKernel::COMMON_ADDITIONAL_KERNEL_INCLUDE_DIRS, new_includes); for (const std::string& new_include : new_includes) yet_to_process_includes.push_back(new_include); } std::unordered_set option_macro_names; for (const std::string& include : already_processed_includes) { std::unordered_set include_option_macros = read_option_macro_of_file(include); for (const std::string& option_macro : include_option_macros) option_macro_names.insert(option_macro); } m_read_macros_semaphore.release(); m_read_macros_cv.notify_all(); if (kernel.is_precompiled()) { // If this kernel is being precompiled, we can increment the counter // used for logging m_precompiled_kernels_parsing_ended++; // And update the log line g_imgui_logger.update_line(ImGuiLogger::BACKGROUND_KERNEL_PARSING_LINE_NAME, "Parsing kernel permutations in the background... [%d / %d]", m_precompiled_kernels_parsing_ended.load(), m_precompiled_kernels_parsing_started.load()); } return option_macro_names; } void GPUKernelCompiler::wait_compiler_file_operations() { std::mutex mutex; std::unique_lock lock(mutex); m_read_macros_cv.wait(lock, [this]() { return m_precompiled_kernels_parsing_started == m_precompiled_kernels_parsing_ended; }); m_read_macros_cv.wait(lock, [this]() { return m_additional_cache_key_started == m_additional_cache_key_ended; }); } GPUKernelCompiler::ShaderCacheUsageOverride GPUKernelCompiler::get_shader_cache_usage_override() const { return m_shader_cache_force_usage; } void GPUKernelCompiler::set_shader_cache_usage_override(GPUKernelCompiler::ShaderCacheUsageOverride override_usage) { m_shader_cache_force_usage = override_usage; } ================================================ FILE: src/Compiler/GPUKernelCompiler.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GPU_KERNEL_COMPILER_H #define GPU_KERNEL_COMPILER_H #include "Compiler/GPUKernel.h" #include #include #include #include #include class GPUKernelCompiler { public: enum ShaderCacheUsageOverride { FORCE_SHADER_CACHE_DEFAULT, FORCE_SHADER_CACHE_OFF, FORCE_SHADER_CACHE_ON, }; oroFunction_t compile_kernel(GPUKernel& kernel, const GPUKernelCompilerOptions& kernel_compiler_options, std::shared_ptr hiprt_orochi_ctx, hiprtFuncNameSet* function_name_sets, int num_geom_types, int num_ray_types, bool use_cache, const std::string& additional_cache_key, bool silent = false); /** * Takes an include name ("Device/includes/MyInclude.h" for example) and a list of include directories. * If the given include can be found in one the given include directories, the concatenation of the * include directory with the include name is returned * * If it cannot be found, the empty string is returned */ std::string find_in_include_directories(const std::string& include_name, const std::vector& include_directories); /** * Reads the given file (include_file_path) and fills the 'output_includes' parameters with the includes (#include "XXX" pr #include ) * used by that file. * * Only includes that can be found in the given 'include_directories' will be added to the output parameter, others will be * ignored. */ void read_includes_of_file(const std::string& include_file_path, const std::vector& include_directories, std::unordered_set& output_includes); /** * Returns a list of the option macro used in the given file. * "Used" means that the option macro is used in #if, #ifdef or equivalent directives */ std::unordered_set read_option_macro_of_file(const std::string& filepath); /** * Returns a string that consists of the concatenation of the include dependencies of the given kernel. * For example, if the given kernel has includes "Include1.h" and "Include2.h" and that Include2.h itself contains "Include3.h", the returned string will be the concatenation of the last modification time (on the hard drive) of these 3 files, which may look something like this: * * "133378594621" + "13334848655" + "1331849841" = "133378594621133348486551331849841". * * Note that the timestamps are "time since epoch" so that's why they're pretty unintelligible. * * The returned string, so-called "additional cache key", can be used to determine whether or not a GPU * shader needs to be recompiled or not by passing it to the HIPRT compiler which will take it into account * into the hash used to determine whether a file is up to date or not. * * Note that only includes that can be found in the additional include directories of the given kernel * (the parameter of this function) are going to be considered for the concatenation of time stamps. Includes * that cannot be found in the kernel's include directories are ignored (this prevents the issue of losing * ourselves in the parsing of stdlib headers for example. stdlib headers will be ignored since they are not * [probably] in the include directories of the kernel). */ std::string get_additional_cache_key(GPUKernel& kernel); /** * Returns a list of the option macro names used by the given kernel. * * For example, this function will return {"DirectLightSamplingStrategy", "EnvmapSamplingStrategy"} * if the given kernel uses this two macros (if the kernel has some "#if == DirectLightSamplingStrategy", "#ifdef DirectLightSamplingStrategy" * directives or similar in its code) */ std::unordered_set get_option_macros_used_by_kernel(const GPUKernel& kernel); /** * For background kernel precompilation, threads compiling kernels actually open the kernel * files on the disk to read the macros used by the kernels. * * If the application is closed while these files are being opened, this can SEGFAULT. * * This function blocks the calling thread until all threads have parsed the kernel files and * thus avoids a crash. * * This function is mostly called when exiting the Renderwindow */ void wait_compiler_file_operations(); /** * Possible values for `override_usage`: * * - GPUKernelCompiler::ShaderCacheUsageOverride::FORCE_SHADER_CACHE_OFF * --> The shader compiler will be forced *not* to use the shader cache * - GPUKernelCompiler::ShaderCacheUsageOverride::FORCE_SHADER_CACHE_ON * --> The shader compiler will be forced to use the shader cache * - GPUKernelCompiler::ShaderCacheUsageOverride::FORCE_SHADER_CACHE_DEFAULT * --> The shader compiler will use whatever 'use_cache' parameter is passed to 'GPUKernelCompiler::compile_kernel()' */ void set_shader_cache_usage_override(ShaderCacheUsageOverride override_usage); ShaderCacheUsageOverride get_shader_cache_usage_override() const; private: // Cache that maps a filepath to the option macros that it contains. // This saves us having to reparse the file to find the options macros // if the file was already parsed for another kernel by this GPUKernelCompiler std::unordered_map> m_filepath_to_option_macros_cache; // Maps filepath to the last modification time of the file pointed by the filepath. // Useful to invalidate the cache if the file was modified (meaning that the option // macros used by that file may have changed so we have to reparse the file) std::unordered_map m_filepath_to_options_macros_cache_timestamp; // Because this GPUKernelCompiler may be used by multiple threads at the same time, // we may use that mutex sometimes to protect from race conditions std::mutex m_option_macro_cache_mutex; std::mutex m_compile_mutex; // Semaphore used by 'get_option_macros_used_by_kernel' so that not too many threads // read kernel files at the same time: this can cause a "Too many files open" error // // Limiting to a number of maximum threads at a time std::counting_semaphore<> m_read_macros_semaphore { 1 }; std::condition_variable m_read_macros_cv; // Counters for logging the progress of background kernel precompilation // These variables are also used for making sure that all threads have completed // their IO operations before exiting the app std::atomic m_precompiled_kernels_parsing_started = 0; std::atomic m_precompiled_kernels_parsing_ended = 0; std::atomic m_additional_cache_key_started = 0; std::atomic m_additional_cache_key_ended = 0; std::atomic m_precompiled_kernels_compilation_ended = 0; ShaderCacheUsageOverride m_shader_cache_force_usage = ShaderCacheUsageOverride::FORCE_SHADER_CACHE_DEFAULT; }; #endif ================================================ FILE: src/Compiler/GPUKernelCompilerOptions.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Compiler/GPUKernel.h" #include "Compiler/GPUKernelCompilerOptions.h" #include "HostDeviceCommon/KernelOptions/KernelOptions.h" #include "Utils/Utils.h" #include /** * Defining the strings that go with the option so that they can be passed to the shader compiler * with the -D= option. * * The strings used here must match the ones used in KernelOptions.h */ const std::string GPUKernelCompilerOptions::USE_SHARED_STACK_BVH_TRAVERSAL = "UseSharedStackBVHTraversal"; const std::string GPUKernelCompilerOptions::SHARED_STACK_BVH_TRAVERSAL_SIZE = "SharedStackBVHTraversalSize"; const std::string GPUKernelCompilerOptions::SHARED_STACK_BVH_TRAVERSAL_BLOCK_SIZE = "KernelWorkgroupThreadCount"; const std::string GPUKernelCompilerOptions::DO_FIRST_BOUNCE_WARP_DIRECTION_REUSE = "DoFirstBounceWarpDirectionReuse"; const std::string GPUKernelCompilerOptions::DISPLAY_ONLY_SAMPLE_N = "DisplayOnlySampleN"; const std::string GPUKernelCompilerOptions::BSDF_OVERRIDE = "BSDFOverride"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DIFFUSE_LOBE = "PrincipledBSDFDiffuseLobe"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_ENERGY_COMPENSATION = "PrincipledBSDFDoEnergyCompensation"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_GLASS_ENERGY_COMPENSATION = "PrincipledBSDFDoGlassEnergyCompensation"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_CLEARCOAT_ENERGY_COMPENSATION = "PrincipledBSDFDoClearcoatEnergyCompensation"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_METALLIC_ENERGY_COMPENSATION = "PrincipledBSDFDoMetallicEnergyCompensation"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_METALLIC_FRESNEL_ENERGY_COMPENSATION = "PrincipledBSDFDoMetallicFresnelEnergyCompensation"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_SPECULAR_ENERGY_COMPENSATION = "PrincipledBSDFDoSpecularEnergyCompensation"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DELTA_DISTRIBUTION_EVALUATION_OPTIMIZATION = "PrincipledBSDFDeltaDistributionEvaluationOptimization"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_SAMPLE_GLOSSY_BASED_ON_FRESNEL = "PrincipledBSDFSampleGlossyBasedOnFresnel"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_SAMPLE_COAT_BASED_ON_FRESNEL = "PrincipledBSDFSampleCoatBasedOnFresnel"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_MICROFACET_REGULARIZATION = "PrincipledBSDFDoMicrofacetRegularization"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_MICROFACET_REGULARIZATION_CONSISTENT_PARAMETERIZATION = "PrincipledBSDFDoMicrofacetRegularizationConsistentParameterization"; const std::string GPUKernelCompilerOptions::PRINCIPLED_BSDF_MICROFACET_REGULARIZATION_DIFFUSION_HEURISTIC= "PrincipledBSDFMicrofacetRegularizationDiffusionHeuristic"; const std::string GPUKernelCompilerOptions::GGX_SAMPLE_FUNCTION = "PrincipledBSDFAnisotropicGGXSampleFunction"; const std::string GPUKernelCompilerOptions::NESTED_DIELETRCICS_STACK_SIZE_OPTION = "NestedDielectricsStackSize"; const std::string GPUKernelCompilerOptions::TRIANGLE_POINT_SAMPLING_STRATEGY = "TrianglePointSamplingStrategy"; const std::string GPUKernelCompilerOptions::REGIR_GRID_FILL_DO_LIGHT_PRESAMPLING = "ReGIR_GridFillDoLightPresampling"; const std::string GPUKernelCompilerOptions::REGIR_GRID_FILL_LIGHT_SAMPLING_BASE_STRATEGY = "ReGIR_GridFillLightSamplingBaseStrategy"; const std::string GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_VISIBILITY = "ReGIR_GridFillTargetFunctionVisibility"; const std::string GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_COSINE_TERM = "ReGIR_GridFillTargetFunctionCosineTerm"; const std::string GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_COSINE_TERM_LIGHT_SOURCE = "ReGIR_GridFillTargetFunctionCosineTermLightSource"; const std::string GPUKernelCompilerOptions::REGIR_GRID_FILL_PRIMARY_HITS_TARGET_FUNCTION_BSDF = "ReGIR_GridFillPrimaryHitsTargetFunctionBSDF"; const std::string GPUKernelCompilerOptions::REGIR_GRID_FILL_SECONDARY_HITS_TARGET_FUNCTION_BSDF = "ReGIR_GridFillSecondaryHitsTargetFunctionBSDF"; const std::string GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_NEE_PLUS_PLUS_VISIBILITY_ESTIMATION = "ReGIR_GridFillTargetFunctionNeePlusPlusVisibilityEstimation"; const std::string GPUKernelCompilerOptions::REGIR_GRID_FILL_SPATIAL_REUSE_ACCUMULATE_PRE_INTEGRATION = "ReGIR_GridFillSpatialReuse_AccumulatePreIntegration"; const std::string GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_TARGET_FUNCTION_VISIBILITY = "ReGIR_ShadingResamplingTargetFunctionVisibility"; const std::string GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_TARGET_FUNCTION_NEE_PLUS_PLUS_VISIBILITY = "ReGIR_ShadingResamplingTargetFunctionNeePlusPlusVisibility"; const std::string GPUKernelCompilerOptions::REGIR_SHADING_RESMAPLING_JITTER_CANONICAL_CANDIDATES = "ReGIR_ShadingResamplingJitterCanonicalCandidates"; const std::string GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_DO_BSDF_MIS = "ReGIR_ShadingResamplingDoBSDFMIS"; const std::string GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_DO_MIS_PAIRWISE_MIS = "ReGIR_ShadingResamplingDoMISPairwiseMIS"; const std::string GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_SHADE_ALL_SAMPLES = "ReGIR_ShadingResamplingShadeAllSamples"; const std::string GPUKernelCompilerOptions::REGIR_FALLBACK_LIGHT_SAMPLING_STRATEGY = "ReGIR_FallbackLightSamplingStrategy"; const std::string GPUKernelCompilerOptions::REGIR_HASH_GRID_COLLISION_RESOLUTION_MODE = "ReGIR_HashGridCollisionResolutionMode"; const std::string GPUKernelCompilerOptions::REGIR_HASH_GRID_COLLISION_RESOLUTION_MAX_STEPS = "ReGIR_HashGridCollisionResolutionMaxSteps"; const std::string GPUKernelCompilerOptions::REGIR_HASH_GRID_HASH_SURFACE_NORMAL = "ReGIR_HashGridHashSurfaceNormal"; const std::string GPUKernelCompilerOptions::REGIR_HASH_GRID_HASH_SURFACE_NORMAL_RESOLUTION_PRIMARY_HITS = "ReGIR_HashGridHashSurfaceNormalResolutionPrimaryHits"; const std::string GPUKernelCompilerOptions::REGIR_HASH_GRID_HASH_SURFACE_NORMAL_RESOLUTION_SECONDARY_HITS = "ReGIR_HashGridHashSurfaceNormalResolutionSecondaryHits"; const std::string GPUKernelCompilerOptions::REGIR_SHADING_JITTER_TRIES = "ReGIR_ShadingJitterTries"; const std::string GPUKernelCompilerOptions::REGIR_HASH_GRID_ADAPTIVE_ROUGHNESS_GRID_PRECISION = "ReGIR_HashGridAdaptiveRoughnessGridPrecision"; const std::string GPUKernelCompilerOptions::REGIR_HASH_GRID_CONSTANT_GRID_CELL_SIZE = "ReGIR_HashGridConstantGridCellSize"; const std::string GPUKernelCompilerOptions::REGIR_DEBUG_MODE = "ReGIR_DebugMode"; const std::string GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_STRATEGY = "DirectLightSamplingStrategy"; const std::string GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_BASE_STRATEGY = "DirectLightSamplingBaseStrategy"; const std::string GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_NEE_SAMPLE_COUNT = "DirectLightSamplingNEESampleCount"; const std::string GPUKernelCompilerOptions::DIRECT_LIGHT_USE_NEE_PLUS_PLUS = "DirectLightUseNEEPlusPlus"; const std::string GPUKernelCompilerOptions::DIRECT_LIGHT_USE_NEE_PLUS_PLUS_RUSSIAN_ROULETTE = "DirectLightUseNEEPlusPlusRR"; const std::string GPUKernelCompilerOptions::DIRECT_LIGHT_NEE_PLUS_PLUS_DISPLAY_SHADOW_RAYS_DISCARDED = "DirectLightNEEPlusPlusDisplayShadowRaysDiscarded"; const std::string GPUKernelCompilerOptions::DIRECT_LIGHT_NEE_PLUS_PLUS_DISPLAY_SHADOW_RAYS_DISCARDED_BOUNCE = "DirectLightNEEPlusPlusDisplayShadowRaysDiscardedBounce"; const std::string GPUKernelCompilerOptions::NEE_PLUS_PLUS_LINEAR_PROBING_STEPS = "NEEPlusPlus_LinearProbingSteps"; const std::string GPUKernelCompilerOptions::NEE_PLUS_PLUS_DEBUG_MODE = "NEEPlusPlusDebugMode"; const std::string GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_BSDF_DELTA_DISTRIBUTION_OPTIMIZATION = "DirectLightSamplingDeltaDistributionOptimization"; const std::string GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_ALLOW_BACKFACING_LIGHTS = "DirectLightSamplingAllowBackfacingLights"; const std::string GPUKernelCompilerOptions::RIS_USE_VISIBILITY_TARGET_FUNCTION = "RISUseVisiblityTargetFunction"; const std::string GPUKernelCompilerOptions::ENVMAP_SAMPLING_STRATEGY = "EnvmapSamplingStrategy"; const std::string GPUKernelCompilerOptions::ENVMAP_SAMPLING_DO_BSDF_MIS = "EnvmapSamplingDoBSDFMIS"; const std::string GPUKernelCompilerOptions::ENVMAP_SAMPLING_DO_BILINEAR_FILTERING = "EnvmapSamplingDoBilinearFiltering"; const std::string GPUKernelCompilerOptions::PATH_SAMPLING_STRATEGY = "PathSamplingStrategy"; const std::string GPUKernelCompilerOptions::RESTIR_DI_INITIAL_TARGET_FUNCTION_VISIBILITY = "ReSTIR_DI_InitialTargetFunctionVisibility"; const std::string GPUKernelCompilerOptions::RESTIR_DI_SPATIAL_TARGET_FUNCTION_VISIBILITY = "ReSTIR_DI_SpatialTargetFunctionVisibility"; const std::string GPUKernelCompilerOptions::RESTIR_DI_DO_VISIBILITY_REUSE = "ReSTIR_DI_DoVisibilityReuse"; const std::string GPUKernelCompilerOptions::RESTIR_DI_BIAS_CORRECTION_USE_VISIBILITY = "ReSTIR_DI_BiasCorrectionUseVisibility"; const std::string GPUKernelCompilerOptions::RESTIR_DI_BIAS_CORRECTION_WEIGHTS = "ReSTIR_DI_BiasCorrectionWeights"; const std::string GPUKernelCompilerOptions::RESTIR_DI_LATER_BOUNCES_SAMPLING_STRATEGY = "ReSTIR_DI_LaterBouncesSamplingStrategy"; const std::string GPUKernelCompilerOptions::RESTIR_DI_DO_LIGHT_PRESAMPLING = "ReSTIR_DI_DoLightPresampling"; const std::string GPUKernelCompilerOptions::RESTIR_DI_LIGHT_PRESAMPLING_STRATEGY = "ReSTIR_DI_LightPresamplingStrategy"; const std::string GPUKernelCompilerOptions::RESTIR_DI_SPATIAL_DIRECTIONAL_REUSE_MASK_BIT_COUNT = "ReSTIR_DI_SpatialDirectionalReuseBitCount"; const std::string GPUKernelCompilerOptions::RESTIR_DI_DO_OPTIMAL_VISIBILITY_SAMPLING = "ReSTIR_DI_DoOptimalVisibilitySampling"; const std::string GPUKernelCompilerOptions::RESTIR_GI_SPATIAL_TARGET_FUNCTION_VISIBILITY = "ReSTIR_GI_SpatialTargetFunctionVisibility"; const std::string GPUKernelCompilerOptions::RESTIR_GI_SPATIAL_DIRECTIONAL_REUSE_MASK_BIT_COUNT = "ReSTIR_GI_SpatialDirectionalReuseBitCount"; const std::string GPUKernelCompilerOptions::RESTIR_GI_BIAS_CORRECTION_USE_VISIBILITY = "ReSTIR_GI_BiasCorrectionUseVisibility"; const std::string GPUKernelCompilerOptions::RESTIR_GI_BIAS_CORRECTION_WEIGHTS = "ReSTIR_GI_BiasCorrectionWeights"; const std::string GPUKernelCompilerOptions::RESTIR_GI_DO_OPTIMAL_VISIBILITY_SAMPLING = "ReSTIR_GI_DoOptimalVisibilitySampling"; const std::string GPUKernelCompilerOptions::GMON_M_SETS_COUNT = "GMoNMSetsCount"; const std::unordered_set GPUKernelCompilerOptions::ALL_MACROS_NAMES = { GPUKernelCompilerOptions::USE_SHARED_STACK_BVH_TRAVERSAL, GPUKernelCompilerOptions::SHARED_STACK_BVH_TRAVERSAL_SIZE, GPUKernelCompilerOptions::SHARED_STACK_BVH_TRAVERSAL_BLOCK_SIZE, GPUKernelCompilerOptions::DO_FIRST_BOUNCE_WARP_DIRECTION_REUSE, GPUKernelCompilerOptions::DISPLAY_ONLY_SAMPLE_N, GPUKernelCompilerOptions::BSDF_OVERRIDE, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DIFFUSE_LOBE, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_ENERGY_COMPENSATION, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_GLASS_ENERGY_COMPENSATION, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_CLEARCOAT_ENERGY_COMPENSATION, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_METALLIC_ENERGY_COMPENSATION, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_METALLIC_FRESNEL_ENERGY_COMPENSATION, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_SPECULAR_ENERGY_COMPENSATION, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DELTA_DISTRIBUTION_EVALUATION_OPTIMIZATION, GPUKernelCompilerOptions::PRINCIPLED_BSDF_SAMPLE_GLOSSY_BASED_ON_FRESNEL, GPUKernelCompilerOptions::PRINCIPLED_BSDF_SAMPLE_COAT_BASED_ON_FRESNEL, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_MICROFACET_REGULARIZATION, GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_MICROFACET_REGULARIZATION_CONSISTENT_PARAMETERIZATION, GPUKernelCompilerOptions::PRINCIPLED_BSDF_MICROFACET_REGULARIZATION_DIFFUSION_HEURISTIC , GPUKernelCompilerOptions::GGX_SAMPLE_FUNCTION, GPUKernelCompilerOptions::NESTED_DIELETRCICS_STACK_SIZE_OPTION, GPUKernelCompilerOptions::TRIANGLE_POINT_SAMPLING_STRATEGY, GPUKernelCompilerOptions::REGIR_GRID_FILL_DO_LIGHT_PRESAMPLING, GPUKernelCompilerOptions::REGIR_GRID_FILL_LIGHT_SAMPLING_BASE_STRATEGY, GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_VISIBILITY, GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_COSINE_TERM, GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_COSINE_TERM_LIGHT_SOURCE, GPUKernelCompilerOptions::REGIR_GRID_FILL_PRIMARY_HITS_TARGET_FUNCTION_BSDF, GPUKernelCompilerOptions::REGIR_GRID_FILL_SECONDARY_HITS_TARGET_FUNCTION_BSDF, GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_NEE_PLUS_PLUS_VISIBILITY_ESTIMATION, GPUKernelCompilerOptions::REGIR_GRID_FILL_SPATIAL_REUSE_ACCUMULATE_PRE_INTEGRATION, GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_TARGET_FUNCTION_VISIBILITY, GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_TARGET_FUNCTION_NEE_PLUS_PLUS_VISIBILITY, GPUKernelCompilerOptions::REGIR_SHADING_RESMAPLING_JITTER_CANONICAL_CANDIDATES, GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_DO_BSDF_MIS, GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_DO_MIS_PAIRWISE_MIS, GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_SHADE_ALL_SAMPLES, GPUKernelCompilerOptions::REGIR_FALLBACK_LIGHT_SAMPLING_STRATEGY, GPUKernelCompilerOptions::REGIR_HASH_GRID_COLLISION_RESOLUTION_MODE, GPUKernelCompilerOptions::REGIR_HASH_GRID_COLLISION_RESOLUTION_MAX_STEPS, GPUKernelCompilerOptions::REGIR_HASH_GRID_HASH_SURFACE_NORMAL, GPUKernelCompilerOptions::REGIR_HASH_GRID_HASH_SURFACE_NORMAL_RESOLUTION_PRIMARY_HITS, GPUKernelCompilerOptions::REGIR_HASH_GRID_HASH_SURFACE_NORMAL_RESOLUTION_SECONDARY_HITS, GPUKernelCompilerOptions::REGIR_SHADING_JITTER_TRIES, GPUKernelCompilerOptions::REGIR_HASH_GRID_ADAPTIVE_ROUGHNESS_GRID_PRECISION, GPUKernelCompilerOptions::REGIR_HASH_GRID_CONSTANT_GRID_CELL_SIZE, GPUKernelCompilerOptions::REGIR_DEBUG_MODE, GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_STRATEGY, GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_BASE_STRATEGY, GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_NEE_SAMPLE_COUNT, GPUKernelCompilerOptions::DIRECT_LIGHT_USE_NEE_PLUS_PLUS, GPUKernelCompilerOptions::DIRECT_LIGHT_USE_NEE_PLUS_PLUS_RUSSIAN_ROULETTE, GPUKernelCompilerOptions::DIRECT_LIGHT_NEE_PLUS_PLUS_DISPLAY_SHADOW_RAYS_DISCARDED, GPUKernelCompilerOptions::DIRECT_LIGHT_NEE_PLUS_PLUS_DISPLAY_SHADOW_RAYS_DISCARDED_BOUNCE, GPUKernelCompilerOptions::NEE_PLUS_PLUS_LINEAR_PROBING_STEPS, GPUKernelCompilerOptions::NEE_PLUS_PLUS_DEBUG_MODE, GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_BSDF_DELTA_DISTRIBUTION_OPTIMIZATION, GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_ALLOW_BACKFACING_LIGHTS, GPUKernelCompilerOptions::RIS_USE_VISIBILITY_TARGET_FUNCTION, GPUKernelCompilerOptions::ENVMAP_SAMPLING_STRATEGY, GPUKernelCompilerOptions::ENVMAP_SAMPLING_DO_BSDF_MIS, GPUKernelCompilerOptions::ENVMAP_SAMPLING_DO_BILINEAR_FILTERING, GPUKernelCompilerOptions::PATH_SAMPLING_STRATEGY, GPUKernelCompilerOptions::RESTIR_DI_INITIAL_TARGET_FUNCTION_VISIBILITY, GPUKernelCompilerOptions::RESTIR_DI_SPATIAL_TARGET_FUNCTION_VISIBILITY, GPUKernelCompilerOptions::RESTIR_DI_DO_VISIBILITY_REUSE, GPUKernelCompilerOptions::RESTIR_DI_BIAS_CORRECTION_USE_VISIBILITY, GPUKernelCompilerOptions::RESTIR_DI_BIAS_CORRECTION_WEIGHTS, GPUKernelCompilerOptions::RESTIR_DI_LATER_BOUNCES_SAMPLING_STRATEGY, GPUKernelCompilerOptions::RESTIR_DI_DO_LIGHT_PRESAMPLING, GPUKernelCompilerOptions::RESTIR_DI_LIGHT_PRESAMPLING_STRATEGY, GPUKernelCompilerOptions::RESTIR_DI_SPATIAL_DIRECTIONAL_REUSE_MASK_BIT_COUNT, GPUKernelCompilerOptions::RESTIR_DI_DO_OPTIMAL_VISIBILITY_SAMPLING, GPUKernelCompilerOptions::RESTIR_GI_SPATIAL_TARGET_FUNCTION_VISIBILITY, GPUKernelCompilerOptions::RESTIR_GI_SPATIAL_DIRECTIONAL_REUSE_MASK_BIT_COUNT, GPUKernelCompilerOptions::RESTIR_GI_BIAS_CORRECTION_USE_VISIBILITY, GPUKernelCompilerOptions::RESTIR_GI_BIAS_CORRECTION_WEIGHTS, GPUKernelCompilerOptions::RESTIR_GI_DO_OPTIMAL_VISIBILITY_SAMPLING, GPUKernelCompilerOptions::GMON_M_SETS_COUNT, }; GPUKernelCompilerOptions::GPUKernelCompilerOptions() { // Mandatory options that every kernel must have so we're // adding them here with their default values m_options_macro_map[GPUKernelCompilerOptions::USE_SHARED_STACK_BVH_TRAVERSAL] = std::make_shared(UseSharedStackBVHTraversal); m_options_macro_map[GPUKernelCompilerOptions::SHARED_STACK_BVH_TRAVERSAL_SIZE] = std::make_shared(SharedStackBVHTraversalSize); m_options_macro_map[GPUKernelCompilerOptions::SHARED_STACK_BVH_TRAVERSAL_BLOCK_SIZE] = std::make_shared(KernelWorkgroupThreadCount); m_options_macro_map[GPUKernelCompilerOptions::DO_FIRST_BOUNCE_WARP_DIRECTION_REUSE] = std::make_shared(DoFirstBounceWarpDirectionReuse); m_options_macro_map[GPUKernelCompilerOptions::DISPLAY_ONLY_SAMPLE_N] = std::make_shared(DisplayOnlySampleN); m_options_macro_map[GPUKernelCompilerOptions::BSDF_OVERRIDE] = std::make_shared(BSDFOverride); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DIFFUSE_LOBE] = std::make_shared(PrincipledBSDFDiffuseLobe); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_ENERGY_COMPENSATION] = std::make_shared(PrincipledBSDFDoEnergyCompensation); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_GLASS_ENERGY_COMPENSATION] = std::make_shared(PrincipledBSDFDoGlassEnergyCompensation); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_CLEARCOAT_ENERGY_COMPENSATION] = std::make_shared(PrincipledBSDFDoClearcoatEnergyCompensation); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_METALLIC_ENERGY_COMPENSATION] = std::make_shared(PrincipledBSDFDoMetallicEnergyCompensation); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_METALLIC_FRESNEL_ENERGY_COMPENSATION] = std::make_shared(PrincipledBSDFDoMetallicFresnelEnergyCompensation); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_SPECULAR_ENERGY_COMPENSATION] = std::make_shared(PrincipledBSDFDoSpecularEnergyCompensation); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DELTA_DISTRIBUTION_EVALUATION_OPTIMIZATION] = std::make_shared(PrincipledBSDFDeltaDistributionEvaluationOptimization); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_SAMPLE_GLOSSY_BASED_ON_FRESNEL] = std::make_shared(PrincipledBSDFSampleGlossyBasedOnFresnel); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_SAMPLE_COAT_BASED_ON_FRESNEL] = std::make_shared(PrincipledBSDFSampleCoatBasedOnFresnel); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_MICROFACET_REGULARIZATION] = std::make_shared(PrincipledBSDFDoMicrofacetRegularization); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_DO_MICROFACET_REGULARIZATION_CONSISTENT_PARAMETERIZATION] = std::make_shared(PrincipledBSDFDoMicrofacetRegularizationConsistentParameterization); m_options_macro_map[GPUKernelCompilerOptions::PRINCIPLED_BSDF_MICROFACET_REGULARIZATION_DIFFUSION_HEURISTIC] = std::make_shared(PrincipledBSDFMicrofacetRegularizationDiffusionHeuristic); m_options_macro_map[GPUKernelCompilerOptions::GGX_SAMPLE_FUNCTION] = std::make_shared(PrincipledBSDFAnisotropicGGXSampleFunction); m_options_macro_map[GPUKernelCompilerOptions::NESTED_DIELETRCICS_STACK_SIZE_OPTION] = std::make_shared(NestedDielectricsStackSize); m_options_macro_map[GPUKernelCompilerOptions::TRIANGLE_POINT_SAMPLING_STRATEGY] = std::make_shared(TrianglePointSamplingStrategy); m_options_macro_map[GPUKernelCompilerOptions::REGIR_GRID_FILL_DO_LIGHT_PRESAMPLING] = std::make_shared(ReGIR_GridFillDoLightPresampling); m_options_macro_map[GPUKernelCompilerOptions::REGIR_GRID_FILL_LIGHT_SAMPLING_BASE_STRATEGY] = std::make_shared(ReGIR_GridFillLightSamplingBaseStrategy); m_options_macro_map[GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_VISIBILITY] = std::make_shared(ReGIR_GridFillTargetFunctionVisibility); m_options_macro_map[GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_COSINE_TERM] = std::make_shared(ReGIR_GridFillTargetFunctionCosineTerm); m_options_macro_map[GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_COSINE_TERM_LIGHT_SOURCE] = std::make_shared(ReGIR_GridFillTargetFunctionCosineTermLightSource); m_options_macro_map[GPUKernelCompilerOptions::REGIR_GRID_FILL_PRIMARY_HITS_TARGET_FUNCTION_BSDF] = std::make_shared(ReGIR_GridFillPrimaryHitsTargetFunctionBSDF); m_options_macro_map[GPUKernelCompilerOptions::REGIR_GRID_FILL_SECONDARY_HITS_TARGET_FUNCTION_BSDF] = std::make_shared(ReGIR_GridFillSecondaryHitsTargetFunctionBSDF); m_options_macro_map[GPUKernelCompilerOptions::REGIR_GRID_FILL_TARGET_FUNCTION_NEE_PLUS_PLUS_VISIBILITY_ESTIMATION] = std::make_shared(ReGIR_GridFillTargetFunctionNeePlusPlusVisibilityEstimation); m_options_macro_map[GPUKernelCompilerOptions::REGIR_GRID_FILL_SPATIAL_REUSE_ACCUMULATE_PRE_INTEGRATION] = std::make_shared(ReGIR_GridFillSpatialReuse_AccumulatePreIntegration); m_options_macro_map[GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_TARGET_FUNCTION_VISIBILITY] = std::make_shared(ReGIR_ShadingResamplingTargetFunctionVisibility); m_options_macro_map[GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_TARGET_FUNCTION_NEE_PLUS_PLUS_VISIBILITY] = std::make_shared(ReGIR_ShadingResamplingTargetFunctionNeePlusPlusVisibility); m_options_macro_map[GPUKernelCompilerOptions::REGIR_SHADING_RESMAPLING_JITTER_CANONICAL_CANDIDATES] = std::make_shared(ReGIR_ShadingResamplingJitterCanonicalCandidates); m_options_macro_map[GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_DO_BSDF_MIS] = std::make_shared(ReGIR_ShadingResamplingDoBSDFMIS); m_options_macro_map[GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_DO_MIS_PAIRWISE_MIS] = std::make_shared(ReGIR_ShadingResamplingDoMISPairwiseMIS); m_options_macro_map[GPUKernelCompilerOptions::REGIR_SHADING_RESAMPLING_SHADE_ALL_SAMPLES] = std::make_shared(ReGIR_ShadingResamplingShadeAllSamples); m_options_macro_map[GPUKernelCompilerOptions::REGIR_FALLBACK_LIGHT_SAMPLING_STRATEGY] = std::make_shared(ReGIR_FallbackLightSamplingStrategy); m_options_macro_map[GPUKernelCompilerOptions::REGIR_HASH_GRID_COLLISION_RESOLUTION_MODE] = std::make_shared(ReGIR_HashGridCollisionResolutionMode); m_options_macro_map[GPUKernelCompilerOptions::REGIR_HASH_GRID_COLLISION_RESOLUTION_MAX_STEPS] = std::make_shared(ReGIR_HashGridCollisionResolutionMaxSteps); m_options_macro_map[GPUKernelCompilerOptions::REGIR_HASH_GRID_HASH_SURFACE_NORMAL] = std::make_shared(ReGIR_HashGridHashSurfaceNormal); m_options_macro_map[GPUKernelCompilerOptions::REGIR_HASH_GRID_HASH_SURFACE_NORMAL_RESOLUTION_PRIMARY_HITS] = std::make_shared(ReGIR_HashGridHashSurfaceNormalResolutionPrimaryHits); m_options_macro_map[GPUKernelCompilerOptions::REGIR_HASH_GRID_HASH_SURFACE_NORMAL_RESOLUTION_SECONDARY_HITS] = std::make_shared(ReGIR_HashGridHashSurfaceNormalResolutionSecondaryHits); m_options_macro_map[GPUKernelCompilerOptions::REGIR_SHADING_JITTER_TRIES] = std::make_shared(ReGIR_ShadingJitterTries); m_options_macro_map[GPUKernelCompilerOptions::REGIR_HASH_GRID_ADAPTIVE_ROUGHNESS_GRID_PRECISION] = std::make_shared(ReGIR_HashGridAdaptiveRoughnessGridPrecision); m_options_macro_map[GPUKernelCompilerOptions::REGIR_HASH_GRID_CONSTANT_GRID_CELL_SIZE] = std::make_shared(ReGIR_HashGridConstantGridCellSize); m_options_macro_map[GPUKernelCompilerOptions::REGIR_DEBUG_MODE] = std::make_shared(ReGIR_DebugMode); m_options_macro_map[GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_STRATEGY] = std::make_shared(DirectLightSamplingStrategy); m_options_macro_map[GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_BASE_STRATEGY] = std::make_shared(DirectLightSamplingBaseStrategy); m_options_macro_map[GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_NEE_SAMPLE_COUNT] = std::make_shared(DirectLightSamplingNEESampleCount); m_options_macro_map[GPUKernelCompilerOptions::DIRECT_LIGHT_USE_NEE_PLUS_PLUS] = std::make_shared(DirectLightUseNEEPlusPlus); m_options_macro_map[GPUKernelCompilerOptions::DIRECT_LIGHT_USE_NEE_PLUS_PLUS_RUSSIAN_ROULETTE] = std::make_shared(DirectLightUseNEEPlusPlusRR); m_options_macro_map[GPUKernelCompilerOptions::DIRECT_LIGHT_NEE_PLUS_PLUS_DISPLAY_SHADOW_RAYS_DISCARDED] = std::make_shared(DirectLightNEEPlusPlusDisplayShadowRaysDiscarded); m_options_macro_map[GPUKernelCompilerOptions::DIRECT_LIGHT_NEE_PLUS_PLUS_DISPLAY_SHADOW_RAYS_DISCARDED_BOUNCE] = std::make_shared(DirectLightNEEPlusPlusDisplayShadowRaysDiscardedBounce); m_options_macro_map[GPUKernelCompilerOptions::NEE_PLUS_PLUS_LINEAR_PROBING_STEPS] = std::make_shared(NEEPlusPlus_LinearProbingSteps); m_options_macro_map[GPUKernelCompilerOptions::NEE_PLUS_PLUS_DEBUG_MODE] = std::make_shared(NEEPlusPlusDebugMode); m_options_macro_map[GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_BSDF_DELTA_DISTRIBUTION_OPTIMIZATION] = std::make_shared(DirectLightSamplingDeltaDistributionOptimization); m_options_macro_map[GPUKernelCompilerOptions::DIRECT_LIGHT_SAMPLING_ALLOW_BACKFACING_LIGHTS] = std::make_shared(DirectLightSamplingAllowBackfacingLights); m_options_macro_map[GPUKernelCompilerOptions::RIS_USE_VISIBILITY_TARGET_FUNCTION] = std::make_shared(RISUseVisiblityTargetFunction); m_options_macro_map[GPUKernelCompilerOptions::ENVMAP_SAMPLING_STRATEGY] = std::make_shared(EnvmapSamplingStrategy); m_options_macro_map[GPUKernelCompilerOptions::ENVMAP_SAMPLING_DO_BSDF_MIS] = std::make_shared(EnvmapSamplingDoBSDFMIS); m_options_macro_map[GPUKernelCompilerOptions::ENVMAP_SAMPLING_DO_BILINEAR_FILTERING] = std::make_shared(EnvmapSamplingDoBilinearFiltering); m_options_macro_map[GPUKernelCompilerOptions::PATH_SAMPLING_STRATEGY] = std::make_shared(PathSamplingStrategy); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_INITIAL_TARGET_FUNCTION_VISIBILITY] = std::make_shared(ReSTIR_DI_InitialTargetFunctionVisibility); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_SPATIAL_TARGET_FUNCTION_VISIBILITY] = std::make_shared(ReSTIR_DI_SpatialTargetFunctionVisibility); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_DO_VISIBILITY_REUSE] = std::make_shared(ReSTIR_DI_DoVisibilityReuse); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_BIAS_CORRECTION_USE_VISIBILITY] = std::make_shared(ReSTIR_DI_BiasCorrectionUseVisibility); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_BIAS_CORRECTION_WEIGHTS] = std::make_shared(ReSTIR_DI_BiasCorrectionWeights); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_LATER_BOUNCES_SAMPLING_STRATEGY] = std::make_shared(ReSTIR_DI_LaterBouncesSamplingStrategy); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_DO_LIGHT_PRESAMPLING] = std::make_shared(ReSTIR_DI_DoLightPresampling); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_LIGHT_PRESAMPLING_STRATEGY] = std::make_shared(ReSTIR_DI_LightPresamplingStrategy); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_SPATIAL_DIRECTIONAL_REUSE_MASK_BIT_COUNT] = std::make_shared(ReSTIR_DI_SpatialDirectionalReuseBitCount); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_DI_DO_OPTIMAL_VISIBILITY_SAMPLING] = std::make_shared(ReSTIR_DI_DoOptimalVisibilitySampling); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_GI_SPATIAL_TARGET_FUNCTION_VISIBILITY] = std::make_shared(ReSTIR_GI_SpatialTargetFunctionVisibility); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_GI_SPATIAL_DIRECTIONAL_REUSE_MASK_BIT_COUNT] = std::make_shared(ReSTIR_GI_SpatialDirectionalReuseBitCount); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_GI_BIAS_CORRECTION_USE_VISIBILITY] = std::make_shared(ReSTIR_GI_BiasCorrectionUseVisibility); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_GI_BIAS_CORRECTION_WEIGHTS] = std::make_shared(ReSTIR_GI_BiasCorrectionWeights); m_options_macro_map[GPUKernelCompilerOptions::RESTIR_GI_DO_OPTIMAL_VISIBILITY_SAMPLING] = std::make_shared(ReSTIR_GI_DoOptimalVisibilitySampling); m_options_macro_map[GPUKernelCompilerOptions::GMON_M_SETS_COUNT] = std::make_shared(GMoNMSetsCount); // Making sure we didn't forget to fill the ALL_MACROS_NAMES vector with all the options that exist if (GPUKernelCompilerOptions::ALL_MACROS_NAMES.size() != m_options_macro_map.size()) Utils::debugbreak(); } GPUKernelCompilerOptions::GPUKernelCompilerOptions(const GPUKernelCompilerOptions& other) { *this = other.deep_copy(); } GPUKernelCompilerOptions& GPUKernelCompilerOptions::operator=(const GPUKernelCompilerOptions& other) { m_options_macro_map = other.m_options_macro_map; m_custom_macro_map = other.m_custom_macro_map; return *this; } GPUKernelCompilerOptions GPUKernelCompilerOptions::deep_copy() const { GPUKernelCompilerOptions out; out.clear(); for (auto& pair : m_options_macro_map) // Creating new shared ptr for the copy out.m_options_macro_map[pair.first] = std::make_shared(*pair.second); for (auto& pair : m_custom_macro_map) // Creating new shared ptr for the copy out.m_custom_macro_map[pair.first] = std::make_shared(*pair.second); return out; } std::vector GPUKernelCompilerOptions::get_all_macros_as_std_vector_string() { std::vector macros; for (auto macro_key_value : m_options_macro_map) macros.push_back("-D " + macro_key_value.first + "=" + std::to_string(*macro_key_value.second)); for (auto macro_key_value : m_custom_macro_map) macros.push_back("-D " + macro_key_value.first + "=" + std::to_string(*macro_key_value.second)); return macros; } std::vector GPUKernelCompilerOptions::get_relevant_macros_as_std_vector_string(const GPUKernel* kernel) const { std::vector macros; // Looping on all the options macros and checking if the kernel uses that option macro, // only adding the macro to the returned vector if the kernel uses that option macro for (auto macro_key_value : m_options_macro_map) if (kernel->uses_macro(macro_key_value.first)) macros.push_back("-D " + macro_key_value.first + "=" + std::to_string(*macro_key_value.second)); // Adding all the custom macros without conditions for (auto macro_key_value : m_custom_macro_map) macros.push_back("-D " + macro_key_value.first + "=" + std::to_string(*macro_key_value.second)); std::vector additional_macros = kernel->get_additional_compiler_macros(); for (const std::string& additional_macro : additional_macros) macros.push_back(additional_macro); return macros; } void GPUKernelCompilerOptions::set_macro_value(const std::string& name, int value) { if (ALL_MACROS_NAMES.find(name) != ALL_MACROS_NAMES.end()) { if (m_options_macro_map.find(name) != m_options_macro_map.end()) // If you could find the name in the options-macro, settings its value *m_options_macro_map[name] = value; else // Otherwise, creating it m_options_macro_map[name] = std::make_shared(value); } else { // Otherwise, this is a user defined macro, putting it in the custom macro map if (m_custom_macro_map.find(name) != m_custom_macro_map.end()) // Updating the macro's alue if it already exists *m_custom_macro_map[name] = value; else // Creating it otherwise m_custom_macro_map[name] = std::make_shared(value); } } void GPUKernelCompilerOptions::remove_macro(const std::string& name) { // Only removing from the custom macro map because we cannot remove the options-macro m_custom_macro_map.erase(name); } bool GPUKernelCompilerOptions::has_macro(const std::string& name) { // Only checking the custom macro map because we cannot remove the options-macro so it makes // no sense to check whether this instance has the macro "InteriorStackStrategy" // for example, it will always be yes return m_custom_macro_map.find(name) != m_custom_macro_map.end(); } int GPUKernelCompilerOptions::get_macro_value(const std::string& name) const { auto find = m_options_macro_map.find(name); if (find == m_options_macro_map.end()) { // Wasn't found in the options-macro, trying in the custom macros auto find_custom = m_custom_macro_map.find(name); if (find_custom == m_custom_macro_map.end()) return std::numeric_limits::min(); else return *find_custom->second; } else return *find->second; } const std::shared_ptr GPUKernelCompilerOptions::get_pointer_to_macro_value(const std::string& name) const { auto find = m_options_macro_map.find(name); if (find == m_options_macro_map.end()) { // Wasn't found in the options-macro, trying in the custom macros auto find_custom = m_custom_macro_map.find(name); if (find_custom == m_custom_macro_map.end()) return nullptr; else return find_custom->second; } else return find->second; } int* GPUKernelCompilerOptions::get_raw_pointer_to_macro_value(const std::string& name) { std::shared_ptr pointer = get_pointer_to_macro_value(name); if (pointer != nullptr) return pointer.get(); return nullptr; } void GPUKernelCompilerOptions::set_pointer_to_macro(const std::string& name, std::shared_ptr pointer_to_value) { auto find = m_options_macro_map.find(name); if (find == m_options_macro_map.end()) // Wasn't found in the options-macro, adding/setting it in the custom macro map m_custom_macro_map[name] = pointer_to_value; else m_options_macro_map[name] = pointer_to_value; } const std::unordered_map>& GPUKernelCompilerOptions::get_options_macro_map() const { return m_options_macro_map; } const std::unordered_map>& GPUKernelCompilerOptions::get_custom_macro_map() const { return m_custom_macro_map; } void GPUKernelCompilerOptions::clear() { m_custom_macro_map.clear(); m_options_macro_map.clear(); } void GPUKernelCompilerOptions::apply_onto(GPUKernelCompilerOptions& other) { for (auto& pair : m_options_macro_map) { if (other.m_options_macro_map.find(pair.first) == other.m_options_macro_map.end()) // The option doesn't exist, we need to create the shared ptr other.m_options_macro_map[pair.first] = std::make_shared(*pair.second); else // No need to create a shared ptr, we can just copy the value *other.m_options_macro_map[pair.first] = *pair.second; } for (auto& pair : m_custom_macro_map) { if (other.m_custom_macro_map.find(pair.first) == other.m_custom_macro_map.end()) // The option doesn't exist, we need to create the shared ptr other.m_custom_macro_map[pair.first] = std::make_shared(*pair.second); else // No need to create a shared ptr, we can just copy the value *other.m_custom_macro_map[pair.first] = *pair.second; } } ================================================ FILE: src/Compiler/GPUKernelCompilerOptions.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GPU_KERNEL_OPTIONS_H #define GPU_KERNEL_OPTIONS_H #include #include #include #include #include class GPUKernel; class GPUKernelCompilerOptions { public: static const std::string USE_SHARED_STACK_BVH_TRAVERSAL; static const std::string SHARED_STACK_BVH_TRAVERSAL_BLOCK_SIZE; static const std::string SHARED_STACK_BVH_TRAVERSAL_SIZE; static const std::string DO_FIRST_BOUNCE_WARP_DIRECTION_REUSE; static const std::string DISPLAY_ONLY_SAMPLE_N; static const std::string BSDF_OVERRIDE; static const std::string PRINCIPLED_BSDF_DIFFUSE_LOBE; static const std::string PRINCIPLED_BSDF_DO_ENERGY_COMPENSATION; static const std::string PRINCIPLED_BSDF_DO_GLASS_ENERGY_COMPENSATION; static const std::string PRINCIPLED_BSDF_DO_CLEARCOAT_ENERGY_COMPENSATION; static const std::string PRINCIPLED_BSDF_DO_METALLIC_ENERGY_COMPENSATION; static const std::string PRINCIPLED_BSDF_DO_METALLIC_FRESNEL_ENERGY_COMPENSATION; static const std::string PRINCIPLED_BSDF_DO_SPECULAR_ENERGY_COMPENSATION; static const std::string PRINCIPLED_BSDF_DELTA_DISTRIBUTION_EVALUATION_OPTIMIZATION; static const std::string PRINCIPLED_BSDF_SAMPLE_GLOSSY_BASED_ON_FRESNEL; static const std::string PRINCIPLED_BSDF_SAMPLE_COAT_BASED_ON_FRESNEL; static const std::string PRINCIPLED_BSDF_DO_MICROFACET_REGULARIZATION; static const std::string PRINCIPLED_BSDF_DO_MICROFACET_REGULARIZATION_CONSISTENT_PARAMETERIZATION; static const std::string PRINCIPLED_BSDF_MICROFACET_REGULARIZATION_DIFFUSION_HEURISTIC; static const std::string GGX_SAMPLE_FUNCTION; static const std::string NESTED_DIELETRCICS_STACK_SIZE_OPTION; static const std::string TRIANGLE_POINT_SAMPLING_STRATEGY; static const std::string REGIR_GRID_FILL_DO_LIGHT_PRESAMPLING; static const std::string REGIR_GRID_FILL_LIGHT_SAMPLING_BASE_STRATEGY; static const std::string REGIR_GRID_FILL_TARGET_FUNCTION_VISIBILITY; static const std::string REGIR_GRID_FILL_TARGET_FUNCTION_COSINE_TERM; static const std::string REGIR_GRID_FILL_TARGET_FUNCTION_COSINE_TERM_LIGHT_SOURCE; static const std::string REGIR_GRID_FILL_PRIMARY_HITS_TARGET_FUNCTION_BSDF; static const std::string REGIR_GRID_FILL_SECONDARY_HITS_TARGET_FUNCTION_BSDF; static const std::string REGIR_GRID_FILL_TARGET_FUNCTION_NEE_PLUS_PLUS_VISIBILITY_ESTIMATION; static const std::string REGIR_GRID_FILL_SPATIAL_REUSE_ACCUMULATE_PRE_INTEGRATION; static const std::string REGIR_SHADING_RESAMPLING_TARGET_FUNCTION_VISIBILITY; static const std::string REGIR_SHADING_RESAMPLING_TARGET_FUNCTION_NEE_PLUS_PLUS_VISIBILITY; static const std::string REGIR_SHADING_RESMAPLING_JITTER_CANONICAL_CANDIDATES; static const std::string REGIR_SHADING_RESAMPLING_DO_BSDF_MIS; static const std::string REGIR_SHADING_RESAMPLING_DO_MIS_PAIRWISE_MIS; static const std::string REGIR_SHADING_RESAMPLING_SHADE_ALL_SAMPLES; static const std::string REGIR_FALLBACK_LIGHT_SAMPLING_STRATEGY; static const std::string REGIR_HASH_GRID_COLLISION_RESOLUTION_MODE; static const std::string REGIR_HASH_GRID_COLLISION_RESOLUTION_MAX_STEPS; static const std::string REGIR_HASH_GRID_HASH_SURFACE_NORMAL; static const std::string REGIR_HASH_GRID_HASH_SURFACE_NORMAL_RESOLUTION_PRIMARY_HITS; static const std::string REGIR_HASH_GRID_HASH_SURFACE_NORMAL_RESOLUTION_SECONDARY_HITS; static const std::string REGIR_SHADING_JITTER_TRIES; static const std::string REGIR_HASH_GRID_ADAPTIVE_ROUGHNESS_GRID_PRECISION; static const std::string REGIR_HASH_GRID_CONSTANT_GRID_CELL_SIZE; static const std::string REGIR_DEBUG_MODE; static const std::string DIRECT_LIGHT_SAMPLING_STRATEGY; static const std::string DIRECT_LIGHT_SAMPLING_BASE_STRATEGY; static const std::string DIRECT_LIGHT_SAMPLING_NEE_SAMPLE_COUNT; static const std::string DIRECT_LIGHT_USE_NEE_PLUS_PLUS; static const std::string DIRECT_LIGHT_USE_NEE_PLUS_PLUS_RUSSIAN_ROULETTE; static const std::string DIRECT_LIGHT_NEE_PLUS_PLUS_DISPLAY_SHADOW_RAYS_DISCARDED; static const std::string DIRECT_LIGHT_NEE_PLUS_PLUS_DISPLAY_SHADOW_RAYS_DISCARDED_BOUNCE; static const std::string NEE_PLUS_PLUS_LINEAR_PROBING_STEPS; static const std::string NEE_PLUS_PLUS_DEBUG_MODE; static const std::string DIRECT_LIGHT_SAMPLING_BSDF_DELTA_DISTRIBUTION_OPTIMIZATION; static const std::string DIRECT_LIGHT_SAMPLING_ALLOW_BACKFACING_LIGHTS; static const std::string RIS_USE_VISIBILITY_TARGET_FUNCTION; static const std::string ENVMAP_SAMPLING_STRATEGY; static const std::string ENVMAP_SAMPLING_DO_BSDF_MIS; static const std::string ENVMAP_SAMPLING_DO_BILINEAR_FILTERING; static const std::string PATH_SAMPLING_STRATEGY; static const std::string RESTIR_DI_INITIAL_TARGET_FUNCTION_VISIBILITY; static const std::string RESTIR_DI_SPATIAL_TARGET_FUNCTION_VISIBILITY; static const std::string RESTIR_DI_DO_VISIBILITY_REUSE; static const std::string RESTIR_DI_BIAS_CORRECTION_USE_VISIBILITY; static const std::string RESTIR_DI_BIAS_CORRECTION_WEIGHTS; static const std::string RESTIR_DI_LATER_BOUNCES_SAMPLING_STRATEGY; static const std::string RESTIR_DI_DO_LIGHT_PRESAMPLING; static const std::string RESTIR_DI_LIGHT_PRESAMPLING_STRATEGY; static const std::string RESTIR_DI_SPATIAL_DIRECTIONAL_REUSE_MASK_BIT_COUNT; static const std::string RESTIR_DI_DO_OPTIMAL_VISIBILITY_SAMPLING; static const std::string RESTIR_GI_SPATIAL_TARGET_FUNCTION_VISIBILITY; static const std::string RESTIR_GI_SPATIAL_DIRECTIONAL_REUSE_MASK_BIT_COUNT; static const std::string RESTIR_GI_BIAS_CORRECTION_USE_VISIBILITY; static const std::string RESTIR_GI_BIAS_CORRECTION_WEIGHTS; static const std::string RESTIR_GI_DO_OPTIMAL_VISIBILITY_SAMPLING; static const std::string GMON_M_SETS_COUNT; static const std::unordered_set ALL_MACROS_NAMES; GPUKernelCompilerOptions(); GPUKernelCompilerOptions(const GPUKernelCompilerOptions& other); /** * Shallow copy of the options of 'other' into 'this' * * The shared_ptr of the options of 'other' will be shared with 'this': * this means that if changing the value of "OPTION_1" in 'other', * the value of "OPTION_1" will also change in 'this'. * * If this is exactly the behavior that you don't want, have a look at 'deep_copy' */ GPUKernelCompilerOptions& operator=(const GPUKernelCompilerOptions& other); /** * Returns a new GPUKernelCompilerOptions object that has the same option values as 'this' * but with different shared_ptr. This means that if changing the value of "OPTION_1" in 'other', * the value of "OPTION_1" will not change in the new object returned by this function. */ GPUKernelCompilerOptions deep_copy() const; /** * Gets a list of all the compiler options of the form { "-D InteriorStackStrategy=1", ... } * that can directly be passed to the kernel compiler. * * The returned options do not contain additional include directories. * Additional include directories are not considered options. */ std::vector get_all_macros_as_std_vector_string(); /** * Same as get_all_macros_as_std_vector_string() but the returned vector doesn't contain * the macros that do not apply to the kernel given in parameter. * * For example, the camera rays kernel doesn't care about whether our direct lighting * strategy is MIS, RIS, ReSTIR DI, ... so if a camera ray kernel is given in parameter * the returned vector will not contain the macro for the direct lighting strategy. * Same logic for the other macros defined in KernelOptions.h * * The returned vector always contain all the "custom" macros manually defined through * a call to 'set_macro_value()' (unless the macro changed through 'set_macro_value()' is an * option macro defined in KernelOptions.h as defined above) * * The returned vector also contains all the additional compiler macro that were added * to the kernel by calling 'kernel.add_additional_macro_for_compilation()' */ std::vector get_relevant_macros_as_std_vector_string(const GPUKernel* kernel) const; /** * Replace the value of the macro if it has already been added previous to this call. * If the macro doesn't exist in these compiler options, it it added to the custom * options map. * * The 'name' parameter is expected to be given without the '-D' macro prefix commonly * given to compilers. * For example, if you want to define a macro "MyMacro" equal to 1, you simply * call set_macro_value("MyMacro", 1). * The addition of the -D prefix will be added internally. */ void set_macro_value(const std::string& name, int value); /** * Removes a macro from the list given to the compiler */ void remove_macro(const std::string& name); /** * Returns true if the given macro is defined. False otherwise */ bool has_macro(const std::string& name); /** * Gets the value of a macro or -1 if the macro isn't set */ int get_macro_value(const std::string& name) const; /** * Returns a pointer to the value of a macro given its name. * * Useful for use with ImGui for example. * * nullptr is returned if the option doesn't exist (set_macro_value() wasn't called yet) */ const std::shared_ptr get_pointer_to_macro_value(const std::string& name) const; int* get_raw_pointer_to_macro_value(const std::string& name); /** * Links the value of the macro 'name' with the given pointer such that if the value at the given * 'pointer_to_value' is modified, the value of the same macro in this instance of GPUKernelCompilerOptions * will also be modified to the same value */ void set_pointer_to_macro(const std::string& name, std::shared_ptr pointer_to_value); /** * Returns the map that stores the macro names with their associated values */ const std::unordered_map>& get_options_macro_map() const; /** * Returns the map that stores the custom macro names with their associated values */ const std::unordered_map>& get_custom_macro_map() const; /** * Removes all options from this instance */ void clear(); /** * Overrides any option value of 'other' with the value of the corresponding option of this instance * If the option doesn't exist in other, it is added */ void apply_onto(GPUKernelCompilerOptions& other); private: // Maps the name of the macro to its value. // Example: ["InteriorStackStrategy", 1] // // This "options macro" map only contains the macro as defined in KernelOptions.h // Those are the macros that control the compilation of the kernels to enable / disable // certain behavior of the path tracer by recompilation (to save registers by eliminating code) // // This macro map and the 'custom_macro_map' contain pointers to int for their values // because we want to be able to synchronize the value of the options with // another instance of GPUKernelCompilerOptions. This requires having the value // of our macro point to the value of the other GPUKernelCompilerOptions instance // and we need pointers for that std::unordered_map> m_options_macro_map; // This "custom macro" map contains the macros given by the user with set_macro_value(). // Any macro that isn't defined in KernelOptions.h will be found in this custom macro map std::unordered_map> m_custom_macro_map; }; #endif ================================================ FILE: src/Device/functions/FilterFunction.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_FUNCTIONS_FILTER_FUNCTION_H #define DEVICE_FUNCTIONS_FILTER_FUNCTION_H #include "Device/functions/FilterFunctionPayload.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Material.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/Xorshift.h" /** * This filter function handles self intersection avoidance and alpha testing * * return FALSE if the intersection is ACCEPTED * return true if the intersection is rejected */ HIPRT_DEVICE HIPRT_INLINE bool filter_function(const hiprtRay&, const void*, void* payld, const hiprtHit& hit) { FilterFunctionPayload* payload = reinterpret_cast(payld); int global_triangle_index_hit; if (payload->simplified_light_ray) // If the ray is shot in the BVH containg only the emissive triangles, the hit.primID is the index of the emissive triangle in that BVH, // not the index of the emissive triangle in the whole scene, which 'payload->last_hit_primitive_index' is // // So we need to 'convert' the hit index in the light BVH to a hit index in the whole scene BVH and do // the comparison against that global_triangle_index_hit = payload->render_data->buffers.emissive_triangles_primitive_indices_and_emissive_textures[hit.primID]; else global_triangle_index_hit = hit.primID; if (global_triangle_index_hit == payload->last_hit_primitive_index) // This is a self-intersection, filtering it out // // Triangles are planar so one given triangle can // never be intersect twice in a row (unless we're absolutely // perfectly parallel to the triangle but let's ignore that...) // // This self-intersection avoidance only works for planar primitives return true; if (!payload->render_data->render_settings.do_alpha_testing) // No alpha testing return false; if (payload->bounce >= payload->render_data->render_settings.alpha_testing_indirect_bounce) // Alpha testing is disabled at the current bounce // // Returning false to indicate an intersection return false; int material_index = payload->render_data->buffers.material_indices[global_triangle_index_hit]; if (payload->render_data->buffers.material_opaque[material_index]) // The material is fully opaque, no need to test further, accept the intersection return false; // Composition both the alpha of the base color texture and the material unsigned short int base_color_texture_index = payload->render_data->buffers.materials_buffer.get_base_color_texture_index(material_index); float base_color_alpha = get_hit_base_color_alpha(*payload->render_data, base_color_texture_index, global_triangle_index_hit, hit.uv); float alpha_opacity = payload->render_data->buffers.materials_buffer.get_alpha_opacity(material_index); float composited_alpha = alpha_opacity * base_color_alpha; if ((*payload->random_number_generator)() < composited_alpha) // Alpha test not passing, the ray is blocked return false; // No tests stopped the ray, that's not a hit. // Returning 'true' to filter out the intersection return true; } #endif ================================================ FILE: src/Device/functions/FilterFunctionPayload.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_FUNCTIONS_FILTER_FUNCTION_PAYLOAD_H #define DEVICE_FUNCTIONS_FILTER_FUNCTION_PAYLOAD_H struct HIPRTRenderData; struct Xorshift32Generator; struct FilterFunctionPayload { // -- Alpha testing payload -- const HIPRTRenderData* render_data; Xorshift32Generator* random_number_generator; // -- Alpha testing payload -- // What bounce the ray being launched currently is at int bounce = 0; // -- Self intersection avoidance payload -- int last_hit_primitive_index; bool simplified_light_ray = false; // Whether or not the ray is shot in the BVH containing only the emissive triangles of the scene // -- Self intersection avoidance payload -- }; #endif ================================================ FILE: src/Device/includes/AdaptiveSampling.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_ADAPTIVE_SAMPLING_H #define DEVICE_ADAPTIVE_SAMPLING_H #include "HostDeviceCommon/RenderData.h" HIPRT_HOST_DEVICE HIPRT_INLINE float get_pixel_confidence_interval(const HIPRTRenderData& render_data, int pixel_index, int pixel_sample_count, float& average_luminance) { float luminance = render_data.buffers.accumulated_ray_colors[pixel_index].luminance(); average_luminance = luminance / (pixel_sample_count + 1); float squared_luminance = render_data.aux_buffers.pixel_squared_luminance[pixel_index]; float pixel_variance = (squared_luminance - luminance * average_luminance) / (pixel_sample_count + 1); return 1.96f * sqrtf(pixel_variance) / sqrtf(pixel_sample_count + 1); } /** * pixel_converged is set to true if the given pixel has reached the noise * threshold given in render_data.render_settings.stop_pixel_percentage_converged. It * is set to false otherwise. * * Returns true if the pixel needs more sample according to adaptive sampling (or if adaptive sampling is disabled). * Returns false otherwise */ HIPRT_HOST_DEVICE HIPRT_INLINE bool adaptive_sampling(const HIPRTRenderData& render_data, int pixel_index, bool& pixel_converged) { const HIPRTRenderSettings& render_settings = render_data.render_settings; const AuxiliaryBuffers& aux_buffers = render_data.aux_buffers; if (!render_settings.has_access_to_adaptive_sampling_buffers()) // Adaptive sampling is not on so returning true to indicate // that this pixel is going to need sampling return true; if (render_settings.enable_adaptive_sampling) { // Computing pixel convergence according to adaptive sampling to // know whether to keep sampling that pixel or not if (aux_buffers.pixel_converged_sample_count[pixel_index] != -1) // Pixel is already converged because we have a value != -1 in the // pixels converged sample count buffer return false; int pixel_sample_count = aux_buffers.pixel_sample_count[pixel_index]; if (pixel_sample_count > render_settings.adaptive_sampling_min_samples) { float average_luminance; float confidence_interval = get_pixel_confidence_interval(render_data, pixel_index, pixel_sample_count, average_luminance); bool pixel_needs_sampling = confidence_interval > render_settings.adaptive_sampling_noise_threshold * average_luminance; if (!pixel_needs_sampling) { if (aux_buffers.pixel_converged_sample_count[pixel_index] == -1) // Indicates no need to sample anymore by indicating that this pixel has converged // only if we hadn't indicated the convergence before already aux_buffers.pixel_converged_sample_count[pixel_index] = pixel_sample_count; return false; } } return true; } // Only counting the convergence of pixels according to // the pixel stop noise threshold if adaptive sampling is not enabled // // The rationale is that if we have both adaptive sampling and pixel stop noise threshold // enabled, we probably want to use adaptive sampling only but also stop rendering after // a certain proportion of pixels have converged and we don't actually want to use the // "stop pixel noise threshold" but only the "stop pixel convergence proportion" else if (render_settings.stop_pixel_noise_threshold > 0.0f && render_settings.use_pixel_stop_noise_threshold) { int pixel_sample_count = aux_buffers.pixel_sample_count[pixel_index]; float average_luminance; float confidence_interval = get_pixel_confidence_interval(render_data, pixel_index, pixel_sample_count, average_luminance); // The value of pixel_converged will be used outside of this function pixel_converged = // Converged enough (confidence_interval <= render_settings.stop_pixel_noise_threshold * average_luminance) // At least 2 samples because we can't evaluate the variance with only 1 sample && (render_settings.sample_number > 1); int current_converged_count = aux_buffers.pixel_converged_sample_count[pixel_index]; if (pixel_converged && current_converged_count == -1) // If the pixel has converged, storing the number of samples at which it has converged. // We're only storing the number of samples if we hadn't already (if the value in the buffer is -1) aux_buffers.pixel_converged_sample_count[pixel_index] = pixel_sample_count; else if (!pixel_converged) // If the pixel hasn't converged aux_buffers.pixel_converged_sample_count[pixel_index] = -1; } return true; } #endif ================================================ FILE: src/Device/includes/AliasTable.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_DEVICE_ALIAS_TABLE_H #define DEVICE_INCLUDES_DEVICE_ALIAS_TABLE_H #include "HostDeviceCommon/Xorshift.h" struct DeviceAliasTable { HIPRT_HOST_DEVICE int sample(Xorshift32Generator& rng) const { int random_index = rng.random_index(size); float probability = alias_table_probas[random_index]; if (rng() > probability) // Picking the alias random_index = alias_table_alias[random_index]; return random_index; } int* alias_table_alias = nullptr; float* alias_table_probas = nullptr; float sum_elements = 0.0f; unsigned int size = 0; }; #endif ================================================ FILE: src/Device/includes/BSDFs/BSDFContext.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_BSDF_CONTEXT_H #define DEVICE_INCLUDES_BSDF_CONTEXT_H #include "Device/includes/BSDFs/BSDFIncidentLightInfo.h" #include "Device/includes/BSDFs/MicrofacetRegularization.h" #include "Device/includes/RayVolumeState.h" struct BSDFContext { const DeviceUnpackedEffectiveMaterial& material; RayVolumeState& volume_state; float3 view_direction = make_float3(-1.0f, -1.0f, -1.0f); float3 shading_normal = make_float3(-1.0f, -1.0f, -1.0f); float3 geometric_normal = make_float3(-1.0f, -1.0f, -1.0f); float3 to_light_direction = make_float3(-1.0f, -1.0f, -1.0f); BSDFIncidentLightInfo& incident_light_info; int current_bounce = 0; float accumulated_path_roughness = 0.0f; // Whether or not to modify the volume state of the ray as the BSDF is sampled / evaluated. // // For example, if the ray is currently refracting out of a glass material, and 'update_ray_volume_state' == true, // the ray volume state of the ray will be updated and the glass object will be popped out of the // nested dielectrics stack bool update_ray_volume_state = false; // Whether or not to regularize the BSDF when sampling/evaluating it MicrofacetRegularization::RegularizationMode bsdf_regularization_mode = MicrofacetRegularization::RegularizationMode::NO_REGULARIZATION; /** * 'to_light_direction' is only needed if evaluating the BSDF // TODO create a separate eval context and sampling context * 'incident_light_info' should be passed as BSDFIncidentLightInfo::NO_INFO if you don't care about what lobe the BSDF sampled of if you don't have the information about * what lobe the 'to_light_direction' comes from (during NEE light sampling for example) */ HIPRT_HOST_DEVICE BSDFContext(const float3& view_direction_, const float3& shading_normal, const float3& geometric_normal, const float3& to_light_direction, BSDFIncidentLightInfo& incident_light_info, RayVolumeState& ray_volume_state, bool update_ray_volume_state, const DeviceUnpackedEffectiveMaterial& material, int current_bounce, float accumulated_path_roughness, MicrofacetRegularization::RegularizationMode regularize_bsdf = MicrofacetRegularization::RegularizationMode::NO_REGULARIZATION) : material(material), volume_state(ray_volume_state), view_direction(view_direction_), shading_normal(shading_normal), geometric_normal(geometric_normal), to_light_direction(to_light_direction), incident_light_info(incident_light_info), update_ray_volume_state(update_ray_volume_state), current_bounce(current_bounce), accumulated_path_roughness(accumulated_path_roughness), bsdf_regularization_mode(regularize_bsdf) {} }; #endif ================================================ FILE: src/Device/includes/BSDFs/BSDFIncidentLightInfo.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_BSDF_EVAL_INCIDENT_LIGHT_INFO_H #define DEVICE_BSDF_EVAL_INCIDENT_LIGHT_INFO_H enum BSDFIncidentLightInfo { // Default value: nothing is assumed about the incident light direction NO_INFO = 0, // The additional information below can be used by the bsdf_eval() function to // avoid evaluating delta lobes (such as a perfectly smooth clearcoat lobe, // glass lobe, specualr lobe, ...) and save some performance. // // In such scenarios, the BSDF evaluation will still be correct because delta distribution // lobes will evaluate to 0 anyways if they are evaluated with a direction that // was not sampled from the lobe itself. // // For example, consider a clearcoat diffuse lobe. If bsdf_eval() is called with an // incident light direction that was sampled from the diffuse lobe, the perfectly smooth clearcoat lobe // is going to have its contribution evaluate to 0 because there is no chance that the sampled // diffuse direction perfectly aligns with the delta of the smooth clearcoat lobe // // Same with all the other lobes that can be delta distributions // // Using bit shifts for the values here so that it can be used easily by ReSTIR DI LIGHT_DIRECTION_SAMPLED_FROM_COAT_LOBE = 1 << 1, LIGHT_DIRECTION_SAMPLED_FROM_FIRST_METAL_LOBE = 1 << 2, LIGHT_DIRECTION_SAMPLED_FROM_SECOND_METAL_LOBE = 1 << 3, LIGHT_DIRECTION_SAMPLED_FROM_SPECULAR_LOBE = 1 << 4, LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFLECT_LOBE = 1 << 5, LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFRACT_LOBE = 1 << 6, LIGHT_DIRECTION_SAMPLED_FROM_DIFFUSE_LOBE = 1 << 7, LIGHT_DIRECTION_SAMPLED_FROM_DIFFUSE_TRANSMISSION_LOBE = 1 << 8, // This can be used if the incident light direction comes from sampling a light in the scene // from example LIGHT_DIRECTION_NOT_SAMPLED_FROM_BSDF }; #endif ================================================ FILE: src/Device/includes/BSDFs/CookTorrance.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_COOK_TORRANCE_H #define DEVICE_COOK_TORRANCE_H #include "HostDeviceCommon/Math.h" #include "HostDeviceCommon/Material/Material.h" #include "Device/includes/Sampling.h" HIPRT_HOST_DEVICE HIPRT_INLINE float GGX_normal_distribution(float alpha, float NoH) { //To avoid numerical instability when NoH basically == 1, i.e when the //material is a perfect mirror and the normal distribution function is a Dirac NoH = hippt::min(NoH, 0.999999f); float alpha2 = alpha * alpha; float NoH2 = NoH * NoH; float b = (NoH2 * (alpha2 - 1.0f) + 1.0f); return alpha2 * M_INV_PI / (b * b); } HIPRT_HOST_DEVICE HIPRT_INLINE float G1_schlick_ggx(float k, float dot_prod) { return dot_prod / (dot_prod * (1.0f - k) + k); } HIPRT_HOST_DEVICE HIPRT_INLINE float GGX_smith_masking_shadowing(float roughness_squared, float NoV, float NoL) { float k = roughness_squared * 0.5f; return G1_schlick_ggx(k, NoL) * G1_schlick_ggx(k, NoV); } HIPRT_HOST_DEVICE HIPRT_INLINE float cook_torrance_brdf_pdf(const DeviceUnpackedEffectiveMaterial& material, const float3& view_direction, const float3& to_light_direction, const float3& surface_normal) { float3 microfacet_normal = hippt::normalize(view_direction + to_light_direction); float alpha = material.roughness * material.roughness; float VoH = hippt::max(0.0f, hippt::dot(view_direction, microfacet_normal)); float NoH = hippt::max(0.0f, hippt::dot(surface_normal, microfacet_normal)); float D = GGX_normal_distribution(alpha, NoH); return D * NoH / (4.0f * VoH); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F cook_torrance_brdf(const DeviceUnpackedEffectiveMaterial& material, const float3& to_light_direction, const float3& view_direction, const float3& surface_normal) { ColorRGB32F brdf_color = ColorRGB32F(0.0f, 0.0f, 0.0f); ColorRGB32F base_color = material.base_color; float3 halfway_vector = hippt::normalize(view_direction + to_light_direction); float NoV = hippt::max(0.0f, hippt::dot(surface_normal, view_direction)); float NoL = hippt::max(0.0f, hippt::dot(surface_normal, to_light_direction)); float NoH = hippt::max(0.0f, hippt::dot(surface_normal, halfway_vector)); float VoH = hippt::max(0.0f, hippt::dot(halfway_vector, view_direction)); if (NoV > 0.0f && NoL > 0.0f && NoH > 0.0f) { float metallic = material.metallic; float roughness = material.roughness; float alpha = roughness * roughness; ////////// Cook Torrance BRDF ////////// ColorRGB32F F; float D, G; //F0 = 0.04 for dielectrics, 1.0 for metals (approximation) ColorRGB32F F0 = ColorRGB32F(0.04f * (1.0f - metallic)) + metallic * base_color; //GGX Distribution function F = fresnel_schlick(F0, VoH); D = GGX_normal_distribution(alpha, NoH); G = GGX_smith_masking_shadowing(alpha, NoV, NoL); ColorRGB32F kD = ColorRGB32F(1.0f - metallic); //Metals do not have a base_color part kD = kD * (ColorRGB32F(1.0f) - F);//Only the transmitted light is diffused ColorRGB32F diffuse_part = kD * base_color * M_INV_PI; ColorRGB32F specular_part = (F * D * G) / (4.0f * NoV * NoL); brdf_color = diffuse_part + specular_part; } return brdf_color; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F cook_torrance_brdf_importance_sample(const DeviceUnpackedEffectiveMaterial& material, const float3& view_direction, const float3& surface_normal, float3& output_direction, float& pdf, Xorshift32Generator& random_number_generator) { pdf = 0.0f; float metallic = material.metallic; float roughness = material.roughness; float alpha = roughness * roughness; float rand1 = random_number_generator(); float rand2 = random_number_generator(); float phi = M_TWO_PI * rand1; float theta = acos((1.0f - rand2) / (rand2 * (alpha * alpha - 1.0f) + 1.0f)); float sin_theta = sin(theta); // The microfacet normal is sampled in its local space, we'll have to bring it to the space // around the surface normal float3 microfacet_normal_local_space = make_float3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos(theta)); float3 microfacet_normal = local_to_world_frame(surface_normal, microfacet_normal_local_space); if (hippt::dot(microfacet_normal, surface_normal) < 0.0f) //The microfacet normal that we sampled was under the surface, this can happen return ColorRGB32F(0.0f); float3 to_light_direction = hippt::normalize(2.0f * hippt::dot(microfacet_normal, view_direction) * microfacet_normal - view_direction); float3 halfway_vector = microfacet_normal; output_direction = to_light_direction; ColorRGB32F brdf_color = ColorRGB32F(0.0f, 0.0f, 0.0f); ColorRGB32F base_color = material.base_color; float NoV = hippt::max(0.0f, hippt::dot(surface_normal, view_direction)); float NoL = hippt::max(0.0f, hippt::dot(surface_normal, to_light_direction)); float NoH = hippt::max(0.0f, hippt::dot(surface_normal, halfway_vector)); float VoH = hippt::max(0.0f, hippt::dot(halfway_vector, view_direction)); if (NoV > 0.0f && NoL > 0.0f && NoH > 0.0f) { /////////// Cook Torrance BRDF ////////// ColorRGB32F F; float D, G; //GGX Distribution function D = GGX_normal_distribution(alpha, NoH); //F0 = 0.04 for dielectrics, 1.0 for metals (approximation) ColorRGB32F F0 = ColorRGB32F(0.04f * (1.0f - metallic)) + metallic * base_color; F = fresnel_schlick(F0, VoH); G = GGX_smith_masking_shadowing(alpha, NoV, NoL); ColorRGB32F kD = ColorRGB32F(1.0f - metallic); //Metals do not have a base_color part kD = kD * (ColorRGB32F(1.0f) - F);//Only the transmitted light is diffused ColorRGB32F diffuse_part = kD * base_color * M_INV_PI; ColorRGB32F specular_part = (F * D * G) / (4.0f * NoV * NoL); pdf = D * NoH / (4.0f * VoH); brdf_color = diffuse_part + specular_part; } return brdf_color; } #endif ================================================ FILE: src/Device/includes/BSDFs/Glass.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_GLASS_H #define DEVICE_GLASS_H #include "HostDeviceCommon/Math.h" #include "HostDeviceCommon/Material/Material.h" #include "Device/includes/Sampling.h" HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F smooth_glass_bsdf(const DeviceUnpackedEffectiveMaterial& material, float3& out_bounce_direction, const float3& ray_direction, float3& surface_normal, float eta_i, float eta_t, float& pdf, Xorshift32Generator& random_generator) { // Clamping here because the dot product can eventually returns values less // than -1 or greater than 1 because of precision errors in the vectors // (in previous calculations) float cos_theta_i = hippt::clamp(-1.0f, 1.0f, hippt::dot(surface_normal, -ray_direction)); if (cos_theta_i < 0.0f) { // We're inside the surface, we're going to flip the eta and the normal for // the calculations that follow // Note that this also flips the normal for the caller of this function // since the normal is passed by reference. This is useful since the normal // will be used for offsetting the new ray origin for example cos_theta_i = -cos_theta_i; surface_normal = -surface_normal; float temp = eta_i; eta_i = eta_t; eta_t = temp; } // Computing the proportion of reflected light using fresnel equations // We're going to use the result to decide whether to refract or reflect the // ray float fresnel_reflect = full_fresnel_dielectric(cos_theta_i, eta_i, eta_t); if (random_generator() <= fresnel_reflect) { // Reflect the ray out_bounce_direction = reflect_ray(-ray_direction, surface_normal); pdf = fresnel_reflect; return ColorRGB32F(fresnel_reflect) / hippt::dot(surface_normal, out_bounce_direction); } else { // Refract the ray float3 refract_direction; refract_ray(-ray_direction, surface_normal, refract_direction, eta_t / eta_i); out_bounce_direction = refract_direction; surface_normal = -surface_normal; pdf = 1.0f - fresnel_reflect; return ColorRGB32F(1.0f - fresnel_reflect) * material.base_color / hippt::dot(out_bounce_direction, surface_normal); } } #endif ================================================ FILE: src/Device/includes/BSDFs/Lambertian.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_LAMBERTIAN_H #define DEVICE_LAMBERTIAN_H #include "Device/includes/ONB.h" #include "Device/includes/Sampling.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F lambertian_brdf_eval(const DeviceUnpackedEffectiveMaterial& material, float NoL, float& pdf) { pdf = 0.0f; if (NoL <= 0.0f) return ColorRGB32F(0.0f); pdf = NoL * M_INV_PI; return material.base_color * M_INV_PI; } HIPRT_HOST_DEVICE HIPRT_INLINE float lambertian_brdf_pdf(const DeviceUnpackedEffectiveMaterial& material, float NoL) { float pdf = 0.0f; if (NoL <= 0.0f) return 0.0f; return NoL * M_INV_PI; } /** * If sampleDirectionOnly is 'true',, this function samples only the BSDF without * evaluating the contribution or the PDF of the BSDF. This function will then always return * ColorRGB32F(0.0f) and the 'pdf' out parameter will always be set to 0.0f */ template HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F lambertian_brdf_sample( const DeviceUnpackedEffectiveMaterial& material, const float3& shading_normal, float3& sampled_direction, float& pdf, Xorshift32Generator& random_number_generator, BSDFIncidentLightInfo& out_sampled_light_info) { sampled_direction = cosine_weighted_sample_around_normal_world_space(shading_normal, random_number_generator); out_sampled_light_info = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_DIFFUSE_LOBE; if constexpr (sampleDirectionOnly) { pdf = 0.0f; return ColorRGB32F(0.0f); } else return lambertian_brdf_eval(material, hippt::dot(shading_normal, sampled_direction), pdf); } #endif ================================================ FILE: src/Device/includes/BSDFs/Microfacet.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_BSDF_MICROFACET_H #define DEVICE_BSDF_MICROFACET_H #include "Device/includes/Sampling.h" #include "Device/includes/BSDFs/MicrofacetEnergyCompensation.h" // Clamping value for dot products when evaluating the GGX distribution // This helps with fireflies due to numerical imprecisions // // 1.0e-3f seems indistinguishable from 1.0e-8f (which is closer to // "ground truth" since we're not clamping as hard) except that 1.0e-8f // has a bunch of fireflies / is not very stable at all. // // So even though 1.0e-3f may seem a bit harsh, it's actually fine #define GGX_DOT_PRODUCTS_CLAMP 1.0e-3f /** * Evaluates the GGX anisotropic normal distribution function */ HIPRT_HOST_DEVICE HIPRT_INLINE float GGX_anisotropic(float alpha_x, float alpha_y, const float3& local_microfacet_normal) { float denom = (local_microfacet_normal.x * local_microfacet_normal.x) / (alpha_x * alpha_x) + (local_microfacet_normal.y * local_microfacet_normal.y) / (alpha_y * alpha_y) + (local_microfacet_normal.z * local_microfacet_normal.z); return 1.0f / (M_PI * alpha_x * alpha_y * denom * denom); } /** * Evaluates the visible normal distribution function with GGX as * the normal disitrbution function * * Reference: [Sampling the GGX Distribution of Visible Normals, Heitz, 2018] * Equation 3 */ HIPRT_HOST_DEVICE HIPRT_INLINE float GGX_anisotropic_vndf(float D, float G1V, const float3& local_view_direction, const float3& local_microfacet_normal) { float HoL = hippt::max(GGX_DOT_PRODUCTS_CLAMP, hippt::dot(local_view_direction, local_microfacet_normal)); return G1V * D * HoL / local_view_direction.z; } /** * Lambda function for the denominator of the G1 Smith masking/shadowing functions */ HIPRT_HOST_DEVICE HIPRT_INLINE float G1_Smith_lambda(float alpha_x, float alpha_y, const float3& local_direction) { float ax = local_direction.x * alpha_x; float ay = local_direction.y * alpha_y; return (-1.0f + sqrt(1.0f + (ax * ax + ay * ay) / (local_direction.z * local_direction.z))) * 0.5f; } /** * G1 Smith masking/shadowing (depending on whether local_direction is wo or wi) function * * Reference: [Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs, Heitz, 2014] * Equation 43 */ HIPRT_HOST_DEVICE HIPRT_INLINE float G1_Smith(float alpha_x, float alpha_y, const float3& local_direction) { float lambda = G1_Smith_lambda(alpha_x, alpha_y, local_direction); return 1.0f / (1.0f + lambda); } /** * 'incident_light_direction_is_from_GGX_sample' should be true if the 'local_to_light_direction' given comes from * sampling the microfacet distribution that is being evaluated by this function call * * false otherwise (if 'local_to_light_direction' comes from light sampling NEE, or sampling another lobe of the BSDF, ...). * This parameter only matters if the BRDF is perfectly smooth: roughness < MaterialConstants::ROUGHNESS_CLAMP */ template HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F torrance_sparrow_GGX_eval_reflect(const HIPRTRenderData& render_data, float material_roughness, float material_anisotropy, bool material_do_energy_compensation, const ColorRGB32F& F, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_halfway_vector, float& out_pdf, MaterialUtils::SpecularDeltaReflectionSampled incident_light_direction_is_from_GGX_sample, int current_bounce) { out_pdf = -1.0f; return ColorRGB32F(-1.0f); } /** * Evaluates the Torrance Sparrow BRDF 'FDG / 4.NoL.NoV' with the * GGX as the microfacet distribution * function with single scattering (no energy compensation) * * 'incident_light_direction_is_from_GGX_sample' should be true if the 'local_to_light_direction' given comes from * sampling the microfacet distribution that is being evaluated by this function call * * false otherwise (if 'local_to_light_direction' comes from light sampling NEE, or sampling another lobe of the BSDF, ...). * This parameter only matters if the BRDF is perfectly smooth: roughness < MaterialConstants::ROUGHNESS_CLAMP * * Reference: [Sampling the GGX Distribution of Visible Normals, Heitz, 2018] * Equation 15 */ template <> HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F torrance_sparrow_GGX_eval_reflect<0>(const HIPRTRenderData& render_data, float material_roughness, float material_anisotropy, bool material_do_energy_compensation, const ColorRGB32F& F, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_halfway_vector, float& out_pdf, MaterialUtils::SpecularDeltaReflectionSampled incident_light_direction_is_from_GGX_sample, int current_bounce) { out_pdf = 0.0f; if (MaterialUtils::is_perfectly_smooth(material_roughness) && PrincipledBSDFDeltaDistributionEvaluationOptimization == KERNEL_OPTION_TRUE) { // Fast path for perfectly specular BRDF if (incident_light_direction_is_from_GGX_sample == MaterialUtils::SpecularDeltaReflectionSampled::SPECULAR_PEAK_NOT_SAMPLED) // For a perfectly smooth GGX distribution (a delta distribution), anything other than a // perfectly sampled reflection direction is going to yield 0 contribution return ColorRGB32F(0.0f); else { if (hippt::dot(reflect_ray(local_view_direction, make_float3(0.0f, 0.0f, 1.0f)), local_to_light_direction) < MaterialConstants::DELTA_DISTRIBUTION_ALIGNEMENT_THRESHOLD) { // Just an additional check that we indeed have the incident light // direction aligned with the perfect reflection direction // // This additional check is mainly useful for ReSTIR where we need // to evaluate the BRDF with a sample that may have been sampled from // a delta distribution at a neighbor (so it checks all the boxes for the shortcut // and we could just quickly return MaterialConstants::DELTA_DISTRIBUTION_HIGH_VALUE // but because that sample wasn't sampled at the current pixel, there // is a good chance that it doesn't actually align with the perfect // reflection direction = it doesn't align with the specular peak = 0 contribution out_pdf = 0.0f; return ColorRGB32F(0.0f); } out_pdf = MaterialConstants::DELTA_DISTRIBUTION_HIGH_VALUE; return ColorRGB32F(MaterialConstants::DELTA_DISTRIBUTION_HIGH_VALUE) * F / hippt::abs(local_to_light_direction.z); } } if (local_to_light_direction.z < 0.0f) // A direction that is below the surface is invalid for a microfacet ** BRDF ** return ColorRGB32F(0.0f); float alpha_x; float alpha_y; MaterialUtils::get_alphas(material_roughness, material_anisotropy, alpha_x, alpha_y); // GGX normal distribution float D = GGX_anisotropic(alpha_x, alpha_y, local_halfway_vector); // GGX visible normal distribution for evaluating the PDF float lambda_V = G1_Smith_lambda(alpha_x, alpha_y, local_view_direction); float G1V = 1.0f / (1.0f + lambda_V); float Dvisible = GGX_anisotropic_vndf(D, G1V, local_view_direction, local_halfway_vector); // Maxing to GGX_DOT_PRODUCTS_CLAMP here to avoid zeros and numerical imprecisions // TODO note that we shouldn't need abs() here because we cannot have the view direction or to light direction below the surface float NoV = hippt::max(GGX_DOT_PRODUCTS_CLAMP, hippt::abs(local_view_direction.z)); float NoL = hippt::max(GGX_DOT_PRODUCTS_CLAMP, hippt::abs(local_to_light_direction.z)); // Because we're exactly sampling the visible normals distribution function, // that's exactly our PDF. // // Additionally, because we need to take into account the reflection operator // that we're going to apply to get our final 'to light direction' and so the // jacobian determinant of that reflection operator is the (4.0f * NoV) in the // denominator out_pdf = Dvisible / (4.0f * hippt::dot(local_view_direction, local_halfway_vector)); if (out_pdf == 0.0f) return ColorRGB32F(0.0f); else { float lambda_L = G1_Smith_lambda(alpha_x, alpha_y, local_to_light_direction); if (render_data.bsdfs_data.GGX_masking_shadowing == GGXMaskingShadowingFlavor::HeightUncorrelated) { float G1L = 1.0f / (1.0f + lambda_L); float G2 = G1V * G1L; return F * D * G2 / (4.0f * NoL * NoV); } else // Default to GGXMaskingShadowingFlavor::HeightCorrelated { float G2HeightCorrelated = 1.0f / (1.0f + lambda_V + lambda_L); return F * D * G2HeightCorrelated / (4.0f * NoL * NoV); } } } /** * 'incident_light_direction_is_from_GGX_sample' should be true if the 'local_to_light_direction' given comes from * sampling the microfacet distribution that is being evaluated by this function call * * false otherwise(if 'local_to_light_direction' comes from light sampling NEE, or sampling another lobe of the BSDF, ...). * This parameter only matters if the BRDF is perfectly smooth: roughness < MaterialConstants::ROUGHNESS_CLAMP */ template <> HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F torrance_sparrow_GGX_eval_reflect<1>(const HIPRTRenderData& render_data, float material_roughness, float material_anisotropy, bool material_do_energy_compensation, const ColorRGB32F& F, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_halfway_vector, float& out_pdf, MaterialUtils::SpecularDeltaReflectionSampled incident_light_direction_is_from_GGX_sample, int current_bounce) { ColorRGB32F ms_compensation_term = get_GGX_energy_compensation_conductors(render_data, F, material_roughness, material_do_energy_compensation, local_view_direction, current_bounce); ColorRGB32F single_scattering = torrance_sparrow_GGX_eval_reflect<0>(render_data, material_roughness, material_anisotropy, false, F, local_view_direction, local_to_light_direction, local_halfway_vector, out_pdf, incident_light_direction_is_from_GGX_sample, current_bounce); return single_scattering * ms_compensation_term; } /** * Returns the PDF of the Torrance Sparrow BRDF 'FDG / 4.NoL.NoV' with the * GGX as the microfacet distribution * * 'incident_light_direction_is_from_GGX_sample' should be true if the 'local_to_light_direction' given comes from * sampling the microfacet distribution that is being evaluated by this function call * * false otherwise (if 'local_to_light_direction' comes from light sampling NEE, or sampling another lobe of the BSDF, ...). * This parameter only matters if the BRDF is perfectly smooth: roughness < MaterialConstants::ROUGHNESS_CLAMP * * Reference: [Sampling the GGX Distribution of Visible Normals, Heitz, 2018] * Equation 15 */ HIPRT_HOST_DEVICE HIPRT_INLINE float torrance_sparrow_GGX_pdf_reflect(const HIPRTRenderData& render_data, float material_roughness, float material_anisotropy, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_halfway_vector, MaterialUtils::SpecularDeltaReflectionSampled incident_light_direction_is_from_GGX_sample) { if (MaterialUtils::is_perfectly_smooth(material_roughness) && PrincipledBSDFDeltaDistributionEvaluationOptimization == KERNEL_OPTION_TRUE) { // Fast path for perfectly specular BRDF if (incident_light_direction_is_from_GGX_sample == MaterialUtils::SpecularDeltaReflectionSampled::SPECULAR_PEAK_NOT_SAMPLED) // For a perfectly smooth GGX distribution (a delta distribution), anything other than a // perfectly sampled reflection direction is going to yield 0 contribution return 0.0f; else { if (hippt::dot(reflect_ray(local_view_direction, make_float3(0.0f, 0.0f, 1.0f)), local_to_light_direction) < MaterialConstants::DELTA_DISTRIBUTION_ALIGNEMENT_THRESHOLD) { // Just an additional check that we indeed have the incident light // direction aligned with the perfect reflection direction // // This additional check is mainly useful for ReSTIR where we need // to evaluate the BRDF with a sample that may have been sampled from // a delta distribution at a neighbor (so it checks all the boxes for the shortcut // and we could just quickly return MaterialConstants::DELTA_DISTRIBUTION_HIGH_VALUE // but because that sample wasn't sampled at the current pixel, there // is a good chance that it doesn't actually align with the perfect // reflection direction = it doesn't align with the specular peak = 0 contribution return 0.0f; } return MaterialConstants::DELTA_DISTRIBUTION_HIGH_VALUE; } } if (local_to_light_direction.z < 0.0f) // A direction that is below the surface is invalid for a microfacet ** BRDF ** return 0.0f; float pdf = 0.0f; float alpha_x; float alpha_y; MaterialUtils::get_alphas(material_roughness, material_anisotropy, alpha_x, alpha_y); // GGX normal distribution float D = GGX_anisotropic(alpha_x, alpha_y, local_halfway_vector); // GGX visible normal distribution for evaluating the PDF float lambda_V = G1_Smith_lambda(alpha_x, alpha_y, local_view_direction); float G1V = 1.0f / (1.0f + lambda_V); float Dvisible = GGX_anisotropic_vndf(D, G1V, local_view_direction, local_halfway_vector); // Because we're exactly sampling the visible normals distribution function, // that's exactly our PDF. // // Additionally, because we need to take into account the reflection operator // that we're going to apply to get our final 'to light direction' and so the // jacobian determinant of that reflection operator is the (4.0f * NoV) in the // denominator return Dvisible / (4.0f * hippt::dot(local_view_direction, local_halfway_vector)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F torrance_sparrow_GGX_eval_refract(const DeviceUnpackedEffectiveMaterial& material, float roughness, float relative_eta, ColorRGB32F fresnel_reflectance, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_halfway_vector, float& out_pdf, BSDFIncidentLightInfo incident_light_info) { float NoL = local_to_light_direction.z; float NoV = local_view_direction.z; float HoL = hippt::dot(local_to_light_direction, local_halfway_vector); float HoV = hippt::dot(local_view_direction, local_halfway_vector); ColorRGB32F color; if (MaterialUtils::is_perfectly_smooth(roughness) && PrincipledBSDFDeltaDistributionEvaluationOptimization == KERNEL_OPTION_TRUE) { // Fast path for specular glass bool incident_direction_is_perfect_refraction = hippt::dot(refract_ray(local_view_direction, make_float3(0.0f, 0.0f, 1.0f), relative_eta), local_to_light_direction) > MaterialConstants::DELTA_DISTRIBUTION_ALIGNEMENT_THRESHOLD; if (incident_light_info == BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFRACT_LOBE && incident_direction_is_perfect_refraction) { // When the glass is perfectly smooth i.e. delta distribution, our only hope is to sample // directly from the glass lobe. If we didn't sample from the glass lobe, this is going to be 0 // contribution // Just some high value because this is a delta distribution // And also, take fresnel into account color = ColorRGB32F(MaterialConstants::DELTA_DISTRIBUTION_HIGH_VALUE) * (ColorRGB32F(1.0f) - fresnel_reflectance) * material.base_color; color /= hippt::abs(NoL); out_pdf = MaterialConstants::DELTA_DISTRIBUTION_HIGH_VALUE; } else { color = ColorRGB32F(0.0f); out_pdf = 0.0f; } } else { float dot_prod = HoL + HoV / relative_eta; float dot_prod2 = dot_prod * dot_prod; float denom = dot_prod2 * NoL * NoV; float alpha_x; float alpha_y; MaterialUtils::get_alphas(roughness, material.anisotropy, alpha_x, alpha_y); float D = GGX_anisotropic(alpha_x, alpha_y, local_halfway_vector); float G1_V = G1_Smith(alpha_x, alpha_y, local_view_direction); float G1_L = G1_Smith(alpha_x, alpha_y, local_to_light_direction); float G2 = G1_V * G1_L; float dwm_dwi = hippt::abs(HoL) / dot_prod2; float D_pdf = G1_V / hippt::abs(NoV) * D * hippt::abs(HoV); out_pdf = dwm_dwi * D_pdf; // We added a check a few lines above to "avoid dividing by 0 later on". This is where. // When NoL is 0, denom is 0 too and we're dividing by 0. // The PDF of this case is as low as 1.0e-9 (light direction sampled perpendicularly to the normal) // so this is an extremely rare case. // The PDF being non-zero, we could actualy compute it, it's valid but not with floats :D color = material.base_color * D * (ColorRGB32F(1.0f) - fresnel_reflectance) * G2 * hippt::abs(HoL * HoV / denom); } // Account for non-symmetric scattering when refracting // Reference: https://www.pbr-book.org/4ed/Reflection_Models/Dielectric_BSDF#Non-SymmetricScatteringandRefraction color /= hippt::square(relative_eta); return color; } HIPRT_HOST_DEVICE HIPRT_INLINE float torrance_sparrow_GGX_pdf_refract(const DeviceUnpackedEffectiveMaterial& material, float roughness, float relative_eta, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_halfway_vector, BSDFIncidentLightInfo incident_light_info) { float NoL = local_to_light_direction.z; float NoV = local_view_direction.z; float HoL = hippt::dot(local_to_light_direction, local_halfway_vector); float HoV = hippt::dot(local_view_direction, local_halfway_vector); ColorRGB32F color; if (MaterialUtils::is_perfectly_smooth(roughness) && PrincipledBSDFDeltaDistributionEvaluationOptimization == KERNEL_OPTION_TRUE) { // Fast path for specular glass bool incident_direction_is_perfect_refraction = hippt::dot(refract_ray(local_view_direction, make_float3(0.0f, 0.0f, 1.0f), relative_eta), local_to_light_direction) > MaterialConstants::DELTA_DISTRIBUTION_ALIGNEMENT_THRESHOLD; if (incident_light_info == BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFRACT_LOBE && incident_direction_is_perfect_refraction) return MaterialConstants::DELTA_DISTRIBUTION_HIGH_VALUE; else return 0.0f; } else { float dot_prod = HoL + HoV / relative_eta; float dot_prod2 = dot_prod * dot_prod; float denom = dot_prod2 * NoL * NoV; float alpha_x; float alpha_y; MaterialUtils::get_alphas(roughness, material.anisotropy, alpha_x, alpha_y); float D = GGX_anisotropic(alpha_x, alpha_y, local_halfway_vector); float G1_V = G1_Smith(alpha_x, alpha_y, local_view_direction); float G1_L = G1_Smith(alpha_x, alpha_y, local_to_light_direction); float G2 = G1_V * G1_L; float dwm_dwi = hippt::abs(HoL) / dot_prod2; float D_pdf = G1_V / hippt::abs(NoV) * D * hippt::abs(HoV); return dwm_dwi * D_pdf; } } /** * Reference: [Sampling the GGX Distribution of Visible Normals, Unity: Heitz ; 2018] */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 GGX_VNDF_sample(const float3 local_view_direction, float alpha_x, float alpha_y, Xorshift32Generator& random_number_generator) { float r1 = random_number_generator(); float r2 = random_number_generator(); // Stretching the ellipsoid to the hemisphere configuration float3 Vh = hippt::normalize(float3{ alpha_x * local_view_direction.x, alpha_y * local_view_direction.y, local_view_direction.z }); // Orthonormal basis construction float lensq = Vh.x * Vh.x + Vh.y * Vh.y; float3 T1 = lensq > 0.0f ? float3{ -Vh.y, Vh.x, 0 } / sqrt(lensq) : float3{ 1.0f, 0.0f, 0.0f }; float3 T2 = hippt::cross(Vh, T1); // Parametrization of the projected area of the hemisphere float r = sqrt(r1); float phi = M_TWO_PI * r2; float t1 = r * cos(phi); float t2 = r * sin(phi); float s = 0.5f * (1.0f + Vh.z); t2 = (1.0f - s) * sqrt(1.0f - t1 * t1) + s * t2; // Sampling the hemisphere float3 Nh = t1 * T1 + t2 * T2 + sqrt(hippt::max(0.0f, 1.0f - t1 * t1 - t2 * t2)) * Vh; // Un-stretching back to our ellipsoid return hippt::normalize(float3{ alpha_x * Nh.x, alpha_y * Nh.y, hippt::max(0.0f, Nh.z) }); } /** * Sample the distribution anisotropic GGX of visible normals using * the spherical caps formulation which is slightly faster than the traditional * VNDF sampling by Heitz 2018. * * Reference: [Sampling Visible GGX Normals with Spherical Caps, Dupuy, Benyoub, 2023] */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 GGX_VNDF_spherical_caps_sample(const float3 local_view_direction, float alpha_x, float alpha_y, Xorshift32Generator& random_number_generator) { float r1 = random_number_generator(); float r2 = random_number_generator(); // Stretching the ellipsoid to the hemisphere configuration float3 Vh = hippt::normalize(make_float3(alpha_x * local_view_direction.x, alpha_y * local_view_direction.y, local_view_direction.z)); // Sample a spherical cap in (-wi.z, 1] float phi = M_TWO_PI * r1; float z = (1.0f - r2) * (1.0f + Vh.z) - Vh.z; float sinTheta = sqrtf(hippt::clamp(0.0f, 1.0f, 1.0f - z * z)); float x = sinTheta * cos(phi); float y = sinTheta * sin(phi); float3 c = make_float3(x, y, z); // Compute microfacet normal float3 Nh = c + Vh; // Un-stretching back to our ellipsoid return hippt::normalize(make_float3(alpha_x * Nh.x, alpha_y * Nh.y, Nh.z)); } /** * Samples a microfacet normal from the distribution of visible normals of * the GGX normal function distribution */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 GGX_anisotropic_sample_microfacet(const float3& local_view_direction, float alpha_x, float alpha_y, Xorshift32Generator& random_number_generator) { if (alpha_x <= MaterialConstants::ROUGHNESS_CLAMP && alpha_y <= MaterialConstants::ROUGHNESS_CLAMP) // For delta GGX distribution, the sampled normal is always the same as the surface normal // (so (0, 0, 1) in local space // // This is basically a small optimization to avoid to whole sampling routine return make_float3(0.0f, 0.0f, 1.0f); #if PrincipledBSDFAnisotropicGGXSampleFunction == GGX_VNDF_SAMPLING return GGX_VNDF_sample(local_view_direction, alpha_x, alpha_y, random_number_generator); #elif PrincipledBSDFAnisotropicGGXSampleFunction == GGX_VNDF_SPHERICAL_CAPS return GGX_VNDF_spherical_caps_sample(local_view_direction, alpha_x, alpha_y, random_number_generator); #elif PrincipledBSDFAnisotropicGGXSampleFunction == GGX_VNDF_BOUNDED #else #endif } /* * Samples a microfacet normal from the distribution of visible normals of * the GGX normal function distribution and reflects the given view direction * about that microfacet normal to produce a 'to_light_direction' in local * shading space that is then returned by that function */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 microfacet_GGX_sample_reflection(float roughness, float anisotropy, const float3& local_view_direction, Xorshift32Generator& random_number_generator) { // The view direction can sometimes be below the shading normal hemisphere // because of normal mapping / smooth normals int below_normal = (local_view_direction.z < 0) ? -1 : 1; float alpha_x, alpha_y; MaterialUtils::get_alphas(roughness, anisotropy, alpha_x, alpha_y); if (below_normal == -1) below_normal *= 1.0f; float3 microfacet_normal = GGX_anisotropic_sample_microfacet(local_view_direction * below_normal, alpha_x, alpha_y, random_number_generator); float3 sampled_direction = reflect_ray(local_view_direction, microfacet_normal * below_normal); // Should already be normalized but float imprecisions... return hippt::normalize(sampled_direction); } #endif ================================================ FILE: src/Device/includes/BSDFs/MicrofacetEnergyCompensation.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_BSDF_MICROFACET_ENERGY_COMPENSATION_H #define DEVICE_BSDF_MICROFACET_ENERGY_COMPENSATION_H #include "Device/includes/Fresnel.h" #include "Device/includes/Texture.h" #include "Device/includes/SanityCheck.h" #include "HostDeviceCommon/RenderData.h" // To be able to access GPUBakerConstants::GGX_DIRECTIONAL_ALBEDO_TEXTURE_SIZE && GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE #include "Renderer/Baker/GPUBakerConstants.h" /** * References: * [1] [Practical multiple scattering compensation for microfacet models, Turquin, 2019] * [2] [Revisiting Physically Based Shading at Imageworks, Kulla & Conty, SIGGRAPH 2017] * [3] [Dassault Enterprise PBR 2025 Specification] * [4] [Google - Physically Based Rendering in Filament] * [5] [MaterialX codebase on Github] * [6] [Blender's Cycles codebase on Github] */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F get_GGX_energy_compensation_conductors(const HIPRTRenderData& render_data, const ColorRGB32F& F0, float material_roughness, bool material_do_energy_compensation, const float3& local_view_direction, int current_bounce) { bool max_bounce_reached = current_bounce > render_data.bsdfs_data.metal_energy_compensation_max_bounce && render_data.bsdfs_data.metal_energy_compensation_max_bounce > -1; bool smooth_enough = material_roughness <= render_data.bsdfs_data.energy_compensation_roughness_threshold; bool invalid_view_direction = local_view_direction.z < 0.0f; if (!material_do_energy_compensation || smooth_enough || max_bounce_reached || invalid_view_direction) return ColorRGB32F(1.0f); const void* GGX_directional_albedo_texture_pointer = nullptr; #ifdef __KERNELCC__ GGX_directional_albedo_texture_pointer = &render_data.bsdfs_data.GGX_conductor_directional_albedo; #else GGX_directional_albedo_texture_pointer = render_data.bsdfs_data.GGX_conductor_directional_albedo; #endif // Reading the precomputed directional albedo from the texture float2 uv = make_float2(hippt::max(0.0f, local_view_direction.z), material_roughness); // Flipping the Y manually (and that's why we pass 'false' in the sample call that follow) // because that GGX energy compensation texture is created with a clamp address mode, not wrap // and we have to do the Y-flipping manually when not sampling in wrap mode uv.y = 1.0f - uv.y; float Ess = sample_texture_rgb_32bits(GGX_directional_albedo_texture_pointer, 0, /* is_srgb */ false, uv, /* flip UV-Y */ false).r; // Computing kms, [Practical multiple scattering compensation for microfacet models, Turquin, 2019], Eq. 10 float kms = (1.0f - Ess) / Ess; #if PrincipledBSDFDoMetallicFresnelEnergyCompensation == KERNEL_OPTION_TRUE // [Practical multiple scattering compensation for microfacet models, Turquin, 2019], Eq. 15 ColorRGB32F fresnel_compensation_term = F0; #else // 1.0f F so that the fresnel compensation has no effect ColorRGB32F fresnel_compensation_term = ColorRGB32F(1.0f); #endif // Computing the compensation term and multiplying by the single scattering non-energy conserving base GGX BRDF, // Eq. 9 return ColorRGB32F(1.0f) + kms * fresnel_compensation_term; } /** * References: * [1] [Practical multiple scattering compensation for microfacet models, Turquin, 2019] [Main implementation] * [2] [Revisiting Physically Based Shading at Imageworks, Kulla & Conty, SIGGRAPH 2017] * [3] [Dassault Enterprise PBR 2025 Specification] * [4] [Google - Physically Based Rendering in Filament] * [5] [MaterialX codebase on Github] * [6] [Blender's Cycles codebase on Github] * * The energy compensation LUT for GGX Glass materials is computed by remapping cos_theta * with cos_theta^2.5 * * However cos_theta^2.5 still results in energy gains at grazing angles so we're going to bias * the exponent used for fetching in the table here. * * This means that we store in the LUT during the precomputation but we're going to fetch * from the LUT with an exponent higher than 2.5f to try and force-remove energy gains * * The "ideal" exponent depends primarily on roughness so I've fined tuned some parameters * here to try and get the best white furnace tests * * * -------------------- * If you're reading this code for a reference implementation, read what follows: * In the end, what we're doing here is to fix the unwanted energy gains that we have with * the base implementation as proposed in * [Practical multiple scattering compensation for microfacet models, Turquin, 2019]. * * I don't think that these energy gains are supposed to happen, they are not mentioned * anywhere in the papers. And the papers use 32x32x32 tables. We use 256x16x192. And * we still have issues. I'm lead to believe that the issue is elsewhere in the codebase * but oh well... I can't find where this is coming from so we're fixing the broken code * instead of fixing the root of the issue which probably isn't what you should do if you're * reading this */ HIPRT_HOST_DEVICE HIPRT_INLINE float GGX_glass_energy_compensation_get_correction_exponent(float roughness, float relative_eta) { if (hippt::is_zero(roughness) || hippt::abs(1.0f - relative_eta) < 1.0e-3f) // No correction for these, returning the original 2.5f that is used in the LUT return 2.5f; float lower_relative_eta_bound = 1.01f; float lower_correction = 2.5f; if (relative_eta > 1.01f && relative_eta <= 1.02f) { lower_relative_eta_bound = 1.01f; if (roughness <= 0.0f) lower_correction = 2.5f; else if (roughness <= 0.1f) lower_correction = 2.5f; else if (roughness <= 0.2f) lower_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) lower_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) lower_correction = hippt::lerp(2.4f, 2.45f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) lower_correction = hippt::lerp(2.45f, 2.4665f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) lower_correction = hippt::lerp(2.4665f, 2.52f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) lower_correction = hippt::lerp(2.52f, 2.55f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) lower_correction = 2.55f; else if (roughness <= 0.9f) lower_correction = hippt::lerp(2.55f, 2.585f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) lower_correction = hippt::lerp(2.585f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta > 1.02f && relative_eta <= 1.03f) { lower_relative_eta_bound = 1.02f; if (roughness <= 0.0f) lower_correction = 2.5f; else if (roughness <= 0.1f) lower_correction = 2.5f; else if (roughness <= 0.2f) lower_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) lower_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) lower_correction = hippt::lerp(2.4f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) lower_correction = hippt::lerp(2.475f, 2.51f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) lower_correction = hippt::lerp(2.51f, 2.54f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) lower_correction = hippt::lerp(2.54f, 2.565f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) lower_correction = hippt::lerp(2.565f, 2.57f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) lower_correction = hippt::lerp(2.57f, 2.59f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) lower_correction = hippt::lerp(2.59f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta > 1.03f && relative_eta <= 1.1f) { lower_relative_eta_bound = 1.03f; if (roughness <= 0.0f) lower_correction = 2.5f; else if (roughness <= 0.1f) lower_correction = 2.5f; else if (roughness <= 0.2f) lower_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) lower_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) lower_correction = hippt::lerp(2.4f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) lower_correction = hippt::lerp(2.475f, 2.51f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) lower_correction = hippt::lerp(2.51f, 2.544f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) lower_correction = hippt::lerp(2.544f, 2.565f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) lower_correction = hippt::lerp(2.565f, 2.58f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) lower_correction = hippt::lerp(2.58f, 2.6f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) lower_correction = hippt::lerp(2.6f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta > 1.1f && relative_eta <= 1.2f) { lower_relative_eta_bound = 1.1f; if (roughness <= 0.0f) lower_correction = 2.5f; else if (roughness <= 0.1f) lower_correction = 2.5f; else if (roughness <= 0.2f) lower_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) lower_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) lower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) lower_correction = hippt::lerp(2.475f, 2.54f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) lower_correction = hippt::lerp(2.54f, 2.575f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) lower_correction = hippt::lerp(2.575f, 2.61f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) lower_correction = hippt::lerp(2.61f, 2.63f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) lower_correction = hippt::lerp(2.63f, 2.6f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) lower_correction = hippt::lerp(2.6f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta > 1.2f && relative_eta <= 1.4f) { lower_relative_eta_bound = 1.2f; if (roughness <= 0.0f) lower_correction = 2.5f; else if (roughness <= 0.1f) lower_correction = hippt::lerp(2.5f, 1.8f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) lower_correction = hippt::lerp(1.8f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) lower_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) lower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) lower_correction = hippt::lerp(2.475f, 2.55f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) lower_correction = hippt::lerp(2.55f, 2.65f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) lower_correction = hippt::lerp(2.65f, 2.675f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) lower_correction = hippt::lerp(2.675f, 2.7f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) lower_correction = hippt::lerp(2.7f, 2.675f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) lower_correction = hippt::lerp(2.675f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta > 1.4f && relative_eta <= 1.5f) { lower_relative_eta_bound = 1.4f; if (roughness <= 0.0f) lower_correction = 2.5f; else if (roughness <= 0.1f) lower_correction = hippt::lerp(2.5f, 1.8f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) lower_correction = hippt::lerp(1.8f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) lower_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) lower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) lower_correction = hippt::lerp(2.475f, 2.7f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) lower_correction = hippt::lerp(2.7f, 2.875f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) lower_correction = hippt::lerp(2.875f, 2.925f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) lower_correction = hippt::lerp(2.925f, 2.95f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) lower_correction = hippt::lerp(2.95f, 2.8f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) lower_correction = hippt::lerp(2.8f, 2.55f, (roughness - 0.9f) / 0.1f); } else if (relative_eta > 1.5f && relative_eta <= 2.0f) { lower_relative_eta_bound = 1.5f; if (roughness <= 0.0f) lower_correction = 2.5f; else if (roughness <= 0.1f) lower_correction = hippt::lerp(2.5f, 1.6f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) lower_correction = hippt::lerp(1.6f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) lower_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) lower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) lower_correction = hippt::lerp(2.475f, 2.7f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) lower_correction = hippt::lerp(2.7f, 2.95f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) lower_correction = hippt::lerp(2.95f, 3.1f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) lower_correction = 3.1f; else if (roughness <= 0.9f) lower_correction = hippt::lerp(3.1f, 3.05f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) lower_correction = hippt::lerp(3.05f, 2.57f, (roughness - 0.9f) / 0.1f); } else if (relative_eta > 2.0f && relative_eta <= 2.4f) { lower_relative_eta_bound = 2.0f; if (roughness <= 0.0f) lower_correction = 2.5f; else if (roughness <= 0.1f) lower_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) lower_correction = hippt::lerp(1.5f, 2.2f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) lower_correction = hippt::lerp(2.2f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) lower_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) lower_correction = hippt::lerp(2.475f, 2.75f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) lower_correction = hippt::lerp(2.75f, 3.5f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) lower_correction = hippt::lerp(3.5f, 4.85f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) lower_correction = hippt::lerp(4.85f, 6.0f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) lower_correction = hippt::lerp(6.0f, 7.0f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) lower_correction = hippt::lerp(7.0f, 2.57f, (roughness - 0.9f) / 0.1f); } else if (relative_eta > 2.4f && relative_eta <= 3.0f) { lower_relative_eta_bound = 2.4f; if (roughness <= 0.0f) lower_correction = 2.5f; else if (roughness <= 0.1f) lower_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) lower_correction = hippt::lerp(1.5f, 2.0f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) lower_correction = hippt::lerp(2.0f, 2.44f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) lower_correction = hippt::lerp(2.44f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) lower_correction = hippt::lerp(2.475f, 3.0f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) lower_correction = hippt::lerp(3.0f, 3.8f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) lower_correction = hippt::lerp(3.8f, 7.0f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) lower_correction = hippt::lerp(7.0f, 10.0f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) lower_correction = hippt::lerp(10.0f, 12.0f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) lower_correction = hippt::lerp(12.0f, 3.9f, (roughness - 0.9f) / 0.1f); } float higher_relative_eta_bound = 1.01f; float higher_correction = 2.5f; if (relative_eta <= 1.01f) { higher_relative_eta_bound = 1.01f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = 2.5f; else if (roughness <= 0.2f) higher_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.4f, 2.45f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.45f, 2.4665f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(2.4665f, 2.52f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(2.52f, 2.55f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = 2.55f; else if (roughness <= 0.9f) higher_correction = hippt::lerp(2.55f, 2.585f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(2.585f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta <= 1.02f) { higher_relative_eta_bound = 1.02f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = 2.5f; else if (roughness <= 0.2f) higher_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.4f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.475f, 2.51f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(2.51f, 2.54f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(2.54f, 2.565f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = hippt::lerp(2.565f, 2.57f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) higher_correction = hippt::lerp(2.57f, 2.59f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(2.59f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta <= 1.03f) { higher_relative_eta_bound = 1.03f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = 2.5f; else if (roughness <= 0.2f) higher_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(2.3f, 2.4f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.4f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.475f, 2.51f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(2.51f, 2.544f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(2.544f, 2.565f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = hippt::lerp(2.565f, 2.58f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) higher_correction = hippt::lerp(2.58f, 2.6f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(2.6f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta <= 1.1f) { higher_relative_eta_bound = 1.1f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = 2.5f; else if (roughness <= 0.2f) higher_correction = hippt::lerp(2.5f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.475f, 2.54f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(2.54f, 2.575f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(2.575f, 2.61f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = hippt::lerp(2.61f, 2.63f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) higher_correction = hippt::lerp(2.63f, 2.6f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(2.6f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta <= 1.2f) { higher_relative_eta_bound = 1.2f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = hippt::lerp(2.5f, 1.8f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) higher_correction = hippt::lerp(1.8f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.475f, 2.55f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(2.55f, 2.65f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(2.65f, 2.675f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = hippt::lerp(2.675f, 2.7f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) higher_correction = hippt::lerp(2.7f, 2.675f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(2.675f, 2.5f, (roughness - 0.9f) / 0.1f); } else if (relative_eta <= 1.4f) { higher_relative_eta_bound = 1.4f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = hippt::lerp(2.5f, 1.8f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) higher_correction = hippt::lerp(1.8f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.475f, 2.7f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(2.7f, 2.875f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(2.875f, 2.925f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = hippt::lerp(2.925f, 2.95f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) higher_correction = hippt::lerp(2.95f, 2.8f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(2.8f, 2.55f, (roughness - 0.9f) / 0.1f); } else if (relative_eta <= 1.5f) { higher_relative_eta_bound = 1.5f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = hippt::lerp(2.5f, 1.6f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) higher_correction = hippt::lerp(1.6f, 2.3f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(2.3f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.475f, 2.7f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(2.7f, 2.95f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(2.95f, 3.1f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = 3.1f; else if (roughness <= 0.9f) higher_correction = hippt::lerp(3.1f, 3.05f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(3.05f, 2.57f, (roughness - 0.9f) / 0.1f); } else if (relative_eta <= 2.0f) { higher_relative_eta_bound = 2.0f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) higher_correction = hippt::lerp(1.5f, 2.2f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(2.2f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.475f, 2.75f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(2.75f, 3.5f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(3.5f, 4.85f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = hippt::lerp(4.85f, 6.0f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) higher_correction = hippt::lerp(6.0f, 7.0f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(7.0f, 2.57f, (roughness - 0.9f) / 0.1f); } else if (relative_eta <= 2.4f) { higher_relative_eta_bound = 2.4f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) higher_correction = hippt::lerp(1.5f, 2.0f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(2.0f, 2.44f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.44f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.475f, 3.0f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(3.0f, 3.8f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(3.8f, 7.0f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = hippt::lerp(7.0f, 10.0f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) higher_correction = hippt::lerp(10.0f, 12.0f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(12.0f, 3.9f, (roughness - 0.9f) / 0.1f); } else { higher_relative_eta_bound = 3.0f; if (roughness <= 0.0f) higher_correction = 2.5f; else if (roughness <= 0.1f) higher_correction = hippt::lerp(2.5f, 1.5f, (roughness - 0.0f) / 0.1f); else if (roughness <= 0.2f) higher_correction = hippt::lerp(1.5f, 1.7f, (roughness - 0.1f) / 0.1f); else if (roughness <= 0.3f) higher_correction = hippt::lerp(1.7f, 2.38f, (roughness - 0.2f) / 0.1f); else if (roughness <= 0.4f) higher_correction = hippt::lerp(2.38f, 2.475f, (roughness - 0.3f) / 0.1f); else if (roughness <= 0.5f) higher_correction = hippt::lerp(2.475f, 2.9f, (roughness - 0.4f) / 0.1f); else if (roughness <= 0.6f) higher_correction = hippt::lerp(2.9f, 3.8f, (roughness - 0.5f) / 0.1f); else if (roughness <= 0.7f) higher_correction = hippt::lerp(3.8f, 7.5f, (roughness - 0.6f) / 0.1f); else if (roughness <= 0.8f) higher_correction = hippt::lerp(7.5f, 12.0f, (roughness - 0.7f) / 0.1f); else if (roughness <= 0.9f) higher_correction = hippt::lerp(12.0f, 13.75f, (roughness - 0.8f) / 0.1f); else if (roughness <= 1.0f) higher_correction = hippt::lerp(13.75f, 2.5f, (roughness - 0.9f) / 0.1f); } if (higher_relative_eta_bound == lower_relative_eta_bound) // Arbitrarily returning the lower correction return lower_correction; return hippt::lerp(lower_correction, higher_correction, (relative_eta - lower_relative_eta_bound) / (higher_relative_eta_bound - lower_relative_eta_bound)); } HIPRT_HOST_DEVICE HIPRT_INLINE float get_GGX_energy_compensation_dielectrics(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, float custom_roughness, bool inside_object, float eta_t, float eta_i, float relative_eta, float NoV, int current_bounce) { bool smooth_enough = custom_roughness <= render_data.bsdfs_data.energy_compensation_roughness_threshold; bool max_bounce_reached = current_bounce > render_data.bsdfs_data.glass_energy_compensation_max_bounce && render_data.bsdfs_data.glass_energy_compensation_max_bounce > -1; if (!material.do_glass_energy_compensation || smooth_enough || max_bounce_reached) return 1.0f; float compensation_term = 1.0f; #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoGlassEnergyCompensation == KERNEL_OPTION_TRUE // Not doing energy compensation if the thin-film is fully present // See the // TODO FIX THIS HORROR below // // Also not doing compensation if we already have full compensation on the material // because the energy compensation of the glass lobe here is then redundant if (material.thin_film < 1.0f) { float relative_eta_for_correction = inside_object ? 1.0f / relative_eta : relative_eta; float exponent_correction = 2.5f; if (!material.thin_walled) exponent_correction = GGX_glass_energy_compensation_get_correction_exponent(custom_roughness, relative_eta_for_correction); // We're storing cos_theta_o^2.5 in the LUT so we're retrieving it with pow(1.0f / 2.5f) i.e. // sqrt 2.5 // // We're using a "correction exponent" to forcefully get rid of energy gains at grazing angles due // to float precision issues: storing in the LUT with cos_theta^2.5 but fetching with pow(1.0f / 2.6f) // for example (instead of fetching with pow(1.0f / 2.5f)) darkens the overall appearance and helps remove // energy gains float view_direction_tex_fetch = powf(hippt::max(1.0e-3f, NoV), 1.0f / exponent_correction); float F0 = F0_from_eta(eta_t, eta_i); // sqrt(sqrt()) of F0 here because we're storing F0^4 in the LUT float F0_remapped = sqrt(sqrt(F0)); float3 uvw = make_float3(view_direction_tex_fetch, custom_roughness, F0_remapped); if (material.thin_walled) { void* texture = render_data.bsdfs_data.GGX_thin_glass_directional_albedo; int3 dims = make_int3(GPUBakerConstants::GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O, GPUBakerConstants::GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS, GPUBakerConstants::GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR); compensation_term = sample_texture_3D_rgb_32bits(texture, dims, uvw, render_data.bsdfs_data.use_hardware_tex_interpolation).r; } else { void* texture = inside_object ? render_data.bsdfs_data.GGX_glass_directional_albedo_inverse : render_data.bsdfs_data.GGX_glass_directional_albedo; int3 dims = make_int3(GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O, GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS, GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR); compensation_term = sample_texture_3D_rgb_32bits(texture, dims, uvw, render_data.bsdfs_data.use_hardware_tex_interpolation).r; } // TODO FIX THIS HORROR // This is here because directional albedo for the glass BSDF is tabulated with the standard non-colored Fresnel // This means that the precomputed table is incompatible with the thin-film interference fresnel // // And as a matter of fact, using the energy compensation term (precomputed for the traditional fresnel) // with thin-film interference Fresnel results in noticeable energy gains at grazing angles at high roughnesses // // Blender Cycles doesn't have that issue but I don't understand yet how they avoid it. // // The quick and disgusting solution here is just to disable energy compensation as the thin-film // weight gets stronger. Energy compensation is fully disabled when the thin-film weight is 1.0f // // Because the error is stronger at high roughnesses than at low roughnesses, we can include the roughness // in the lerp such that we use less and less the energy compensation term as the roughness increases compensation_term = hippt::lerp(compensation_term, 1.0f, material.thin_film * custom_roughness); } #endif return compensation_term; } HIPRT_HOST_DEVICE HIPRT_INLINE float get_GGX_energy_compensation_dielectrics(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, bool inside_object, float eta_t, float eta_i, float relative_eta, float NoV, int current_bounce) { return get_GGX_energy_compensation_dielectrics(render_data, material, material.roughness, inside_object, eta_t, eta_i, relative_eta, NoV, current_bounce); } #endif ================================================ FILE: src/Device/includes/BSDFs/MicrofacetRegularization.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDE_MICROFACET_REGULARIZATION_H #define DEVICE_INCLUDE_MICROFACET_REGULARIZATION_H #include "HostDeviceCommon/KernelOptions/PrincipledBSDFKernelOptions.h" #include "HostDeviceCommon/Math.h" #include "HostDeviceCommon/MicrofacetRegularizationSettings.h" struct MicrofacetRegularization { enum class RegularizationMode : unsigned char { NO_REGULARIZATION = 0, REGULARIZATION_CLASSIC = 1, // Should be used when the regularized BSDF PDF isn't going to be used in a MIS weight REGULARIZATION_MIS = 2, // Should be used when the regularized BSDF PDF ** is ** going to be used in a MIS weight or if this is for evaluating a BSDF whose sample comes from MIS sampling }; HIPRT_HOST_DEVICE static float regularize_reflection(const MicrofacetRegularizationSettings& regularization_settings, RegularizationMode regularization_mode, float initial_roughness, float accumulated_path_roughness, int sample_number) { #if PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_FALSE return initial_roughness; #endif if (regularization_mode == RegularizationMode::NO_REGULARIZATION) return initial_roughness; float consistent_tau = MicrofacetRegularization::consistent_tau(regularization_settings.tau_0, sample_number); // Note that the diffusion heuristic that we're using here is not the one proposed in the paper // because the one of the paper requires the mean curvature of the surface and this requires additional // per vertex data to be computed... Sounds a bit heavy just for path regularization // // So instead, we're just using the maximum roughness found on the path so far (which is // 'accumulated_path_roughness') to decide whether or not we should use a strong regularization // or not. // // Caustics only happen on diffuse surfaces (roughness 1). So for such a surface, tau should be // unchanged i.e., we use the full regularization. // // But for smooth surfaces (mirrors, clear glass), we shouldn't regularize anything to keep the sharpness // of the glossy reflections. // // By dividing by a roughness close to 0, tau skyrockets and regularization is essentially disabled float path_diffusion_tau = consistent_tau / hippt::max(hippt::square(accumulated_path_roughness), 1.0e-8f); #if PrincipledBSDFMicrofacetRegularizationDiffusionHeuristic == KERNEL_OPTION_TRUE float final_tau = path_diffusion_tau; #else float final_tau = consistent_tau; #endif float regularized_roughness = sqrtf(sqrtf(1.0f / (final_tau * M_PI))); return hippt::max(regularization_settings.min_roughness, hippt::max(initial_roughness, regularized_roughness)); } HIPRT_HOST_DEVICE static float regularize_refraction(const MicrofacetRegularizationSettings& regularization_settings, RegularizationMode regularization_mode, float initial_roughness, float accumulated_path_roughness, float eta_i, float eta_t, int sample_number) { #if PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_FALSE return initial_roughness; #endif if (regularization_mode == RegularizationMode::NO_REGULARIZATION) return initial_roughness; float consistent_tau = MicrofacetRegularization::consistent_tau(regularization_settings.tau_0, sample_number + 1); // Note that the diffusion heuristic that we're using here is not the one proposed in the paper // because the one of the paper requires the mean curvature of the surface and this requires additional // per vertex data to be computed... Sounds a bit heavy just for path regularization // // So instead, we're just using the maximum roughness found on the path so far (which is // 'accumulated_path_roughness') to decide whether or not we should use a strong regularization // or not. // // Caustics only happen on diffuse surfaces (roughness 1). So for such a surface, tau should be // unchanged i.e., we use the full regularization. // // But for smooth surfaces (mirrors, clear glass), we shouldn't regularize anything to keep the sharpness // of the glossy reflections. // // By dividing by a roughness close to 0, tau skyrockets and regularization is essentially disabled float path_diffusion_tau = consistent_tau / hippt::max(hippt::square(accumulated_path_roughness), 1.0e-8f); #if PrincipledBSDFMicrofacetRegularizationDiffusionHeuristic == KERNEL_OPTION_TRUE float final_tau = path_diffusion_tau; #else float final_tau = consistent_tau; #endif float regularized_roughness = sqrtf(sqrtf(1.0f / (final_tau * M_PI * hippt::square(eta_i - eta_t) / (4.0f * hippt::square(hippt::max(eta_i, eta_t)))))); return hippt::max(regularization_settings.min_roughness, hippt::max(initial_roughness, regularized_roughness)); } HIPRT_HOST_DEVICE static float regularize_mix_reflection_refraction(const MicrofacetRegularizationSettings& regularization_settings, RegularizationMode regularization_mode, float initial_roughness, float accumulated_path_roughness, float eta_i, float eta_t, int sample_number) { #if PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_FALSE return initial_roughness; #endif if (regularization_mode == RegularizationMode::NO_REGULARIZATION) return initial_roughness; float consistent_tau = MicrofacetRegularization::consistent_tau(regularization_settings.tau_0, sample_number); // Note that the diffusion heuristic that we're using here is not the one proposed in the paper // because the one of the paper requires the mean curvature of the surface and this requires additional // per vertex data to be computed... Sounds a bit heavy just for path regularization // // So instead, we're just using the maximum roughness found on the path so far (which is // 'accumulated_path_roughness') to decide whether or not we should use a strong regularization // or not. // // Caustics only happen on diffuse surfaces (roughness 1). So for such a surface, tau should be // unchanged i.e., we use the full regularization. // // But for smooth surfaces (mirrors, clear glass), we shouldn't regularize anything to keep the sharpness // of the glossy reflections. // // By dividing by a roughness close to 0, tau skyrockets and regularization is essentially disabled float path_diffusion_tau = consistent_tau / hippt::max(hippt::square(accumulated_path_roughness), 1.0e-8f); #if PrincipledBSDFMicrofacetRegularizationDiffusionHeuristic == KERNEL_OPTION_TRUE float final_tau = path_diffusion_tau; #else float final_tau = consistent_tau; #endif float regularized_roughness_reflection = sqrtf(sqrtf(1.0f / (final_tau * M_PI))); if (eta_i == eta_t) // Avoiding singularities. // // The refraction regularized roughness will be degenerate here so we're just using the reflection // regularization return regularized_roughness_reflection; float regularized_roughness_refraction = sqrtf(sqrtf(1.0f / (final_tau * M_PI * hippt::square(eta_i - eta_t) / (4.0f * hippt::square(hippt::max(eta_i, eta_t)))))); // Mixing both reflection and refraction regularized roughnesses. // Refraction regularization tends to be stronger (higher resulting roughness). // // We're biasing (75%) towards refraction to bias towards higher regularization to conservatively // reduce variance return hippt::max(regularization_settings.min_roughness, hippt::max(initial_roughness, regularized_roughness_refraction * 0.75f + regularized_roughness_reflection * 0.25f)); } /** * 'sample_number" should be >= 1 */ HIPRT_HOST_DEVICE static float consistent_tau(float tau_0, int sample_number) { #if PrincipledBSDFDoMicrofacetRegularizationConsistentParameterization == KERNEL_OPTION_FALSE return tau_0; #endif // Eq. 16 of the paper return 1.0f / (2.0f * M_PI * (1.0f - cosf(atanf(powf(sample_number + 1, -1.0f / 6.0f) * sqrt(M_FOUR_PI * tau_0 - 1.0f) / (M_TWO_PI * tau_0 - 1.0f))))); } }; #endif ================================================ FILE: src/Device/includes/BSDFs/OrenNayar.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_OREN_NAYAR_H #define DEVICE_OREN_NAYAR_H #include "Device/includes/Sampling.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Math.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" /* References: * [1] [Physically Based Rendering 3rd Edition] https://www.pbr-book.org/3ed-2018/Reflection_Models/Microfacet_Models */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F oren_nayar_brdf_eval(const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& local_to_light_direction, float& pdf) { // sin(theta)^2 = 1.0 - cos(theta)^2 float sin_theta_i = sqrt(1.0f - local_to_light_direction.z * local_to_light_direction.z); float sin_theta_o = sqrt(1.0f - local_view_direction.z * local_view_direction.z); // max_cos here is going to be cos(phi_to_light - phi_view_direction) // but computed as cos(phi_light) * cos(phi_view) + sin(phi_light) * sin(phi_view) // according to cos(a - b) = cos(a) * cos(b) + sin(a) * sin(b) float max_cos = 0; if (sin_theta_i > 1.0e-4f && sin_theta_o > 1.0e-4f) { float sin_phi_i = local_to_light_direction.y / sin_theta_i; float cos_phi_i = local_to_light_direction.x / sin_theta_i; float sin_phi_o = local_view_direction.y / sin_theta_o; float cos_phi_o = local_view_direction.x / sin_theta_o; float d_cos = cos_phi_i * cos_phi_o + sin_phi_i * sin_phi_o; max_cos = hippt::max(0.0f, d_cos); } float sin_alpha, tan_beta; if (hippt::abs(local_to_light_direction.z) > hippt::abs(local_view_direction.z)) { sin_alpha = sin_theta_o; tan_beta = sin_theta_i / hippt::abs(local_to_light_direction.z); } else { sin_alpha = sin_theta_i; tan_beta = sin_theta_o / hippt::abs(local_view_direction.z); } float oren_nayar_A; float oren_nayar_B; MaterialUtils::get_oren_nayar_AB(material.oren_nayar_sigma, oren_nayar_A, oren_nayar_B); pdf = local_to_light_direction.z * M_INV_PI; return material.base_color * M_INV_PI * (oren_nayar_A + oren_nayar_B * max_cos * sin_alpha * tan_beta); } HIPRT_HOST_DEVICE HIPRT_INLINE float oren_nayar_brdf_pdf(const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& local_to_light_direction) { if (local_to_light_direction.z <= 0.0f) return 0.0f; return local_to_light_direction.z * M_INV_PI; } /** * Override of the eval function for world space directions */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F oren_nayar_brdf_eval(const DeviceUnpackedEffectiveMaterial& material, const float3& world_space_view_direction, const float3& surface_normal, const float3& world_space_to_light_direction, float& pdf) { float3 T, B; build_ONB(surface_normal, T, B); // Using local view and light directions to simply following computations float3 local_view_direction = world_to_local_frame(T, B, surface_normal, world_space_view_direction); float3 local_to_light_direction = world_to_local_frame(T, B, surface_normal, world_space_to_light_direction); return oren_nayar_brdf_eval(material, local_view_direction, local_to_light_direction, pdf); } /** * If sampleDirectionOnly is 'true',, this function samples only the BSDF without * evaluating the contribution or the PDF of the BSDF. This function will then always return * ColorRGB32F(0.0f) and the 'pdf' out parameter will always be set to 0.0f */ template HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F oren_nayar_brdf_sample(const DeviceUnpackedEffectiveMaterial& material, const float3& world_space_view_direction, const float3& shading_normal, float3& out_sampled_direction, float& pdf, Xorshift32Generator& random_number_generator, BSDFIncidentLightInfo* out_sampled_light_info = nullptr) { out_sampled_direction = cosine_weighted_sample_around_normal_world_space(shading_normal, random_number_generator); if (out_sampled_light_info != nullptr) *out_sampled_light_info = BSDFIncidentLightInfo::NO_INFO; if constexpr (sampleDirectionOnly) { pdf = 0.0f; return ColorRGB32F(0.0f); } else return oren_nayar_brdf_eval(material, world_space_view_direction, shading_normal, out_sampled_direction, pdf); } #endif ================================================ FILE: src/Device/includes/BSDFs/Principled.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_PRINCIPLED_H #define DEVICE_PRINCIPLED_H #include "Device/includes/Dispersion.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/ONB.h" #include "Device/includes/BSDFs/Lambertian.h" #include "Device/includes/BSDFs/Microfacet.h" #include "Device/includes/BSDFs/MicrofacetRegularization.h" #include "Device/includes/BSDFs/OrenNayar.h" #include "Device/includes/BSDFs/PrincipledEnergyCompensation.h" #include "Device/includes/BSDFs/ThinFilm.h" #include "Device/includes/RayPayload.h" #include "Device/includes/Sampling.h" #include "Device/includes/BSDFs/SheenLTC.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" #include "HostDeviceCommon/Xorshift.h" /** References: * * [1] [CSE 272 University of California San Diego - Disney BSDF Homework] https://cseweb.ucsd.edu/~tzli/cse272/wi2024/homework1.pdf * [2] [GLSL Path Tracer implementation by knightcrawler25] https://github.com/knightcrawler25/GLSL-PathTracer * [3] [SIGGRAPH 2012 Course] https://blog.selfshadow.com/publications/s2012-shading-course/#course_content * [4] [SIGGRAPH 2015 Course] https://blog.selfshadow.com/publications/s2015-shading-course/#course_content * [5] [Burley 2015 Course Notes - Extending the Disney BRDF to a BSDF with Integrated Subsurface Scattering] https://blog.selfshadow.com/publications/s2015-shading-course/burley/s2015_pbs_disney_bsdf_notes.pdf * [6] [PBRT v3 Source Code] https://github.com/mmp/pbrt-v3 * [7] [PBRT v4 Source Code] https://github.com/mmp/pbrt-v4 * [8] [Blender's Cycles Source Code] https://github.com/blender/cycles * [9] [Autodesk Standard Surface] https://autodesk.github.io/standard-surface/ * [10] [Blender Principled BSDF] https://docs.blender.org/manual/fr/dev/render/shader_nodes/shader/principled.html * [11] [Open PBR Specification] https://academysoftwarefoundation.github.io/OpenPBR/ * [12] [Enterprise PBR Specification] https://dassaultsystemes-technology.github.io/EnterprisePBRShadingModel/spec-2025x.md.html * [13] [Arbitrarily Layered Micro-Facet Surfaces, Weidlich, Wilkie] https://www.cg.tuwien.ac.at/research/publications/2007/weidlich_2007_almfs/weidlich_2007_almfs-paper.pdf * [14] [A Practical Extension to Microfacet Theory for the Modeling of Varying Iridescence, Belcour, Barla, 2017] https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html * [15] [MaterialX Implementation Code] https://github.com/AcademySoftwareFoundation/MaterialX * [16] [Khronos GLTF 2.0 KHR_materials_iridescence Implementation Notes] https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_iridescence/README.md * [17] [Khronos GLTF 2.0 KHR_materials_diffuse_transmission Implementation Notes] https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_diffuse_transmission/README.md * * Important note: none of the lobes of this implementation includes the cosine term. * The cosine term NoL needs to be taken into account outside of the BSDF */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_coat_eval(const HIPRTRenderData& render_data, const BSDFContext& bsdf_context, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_halfway_vector, float incident_medium_ior, float& out_pdf) { // The coat lobe is just a microfacet lobe float HoL = hippt::clamp(1.0e-8f, 1.0f, hippt::dot(local_halfway_vector, local_to_light_direction)); float regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, bsdf_context.material.coat_roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); // We're only evaluating the coat lobe if, either: // - The incident light direction was sampled from the clearcoat lobe // - The coat is not a delta distribution (not perfectly smooth) // - The incident light direction was sampled from another perfectly specular lobe // // Because if none of these two conditions are true, the evaluation of the coat will // yield 0.0f anyways // // All the conditions are handled in 'is_specular_delta_reflection_sampled' MaterialUtils::SpecularDeltaReflectionSampled coat_delta_direction_sampled = MaterialUtils::is_specular_delta_reflection_sampled(bsdf_context.material, regularized_roughness, bsdf_context.material.coat_anisotropy, bsdf_context.incident_light_info); ColorRGB32F F = ColorRGB32F(full_fresnel_dielectric(HoL, incident_medium_ior, bsdf_context.material.coat_ior)); return torrance_sparrow_GGX_eval_reflect<0>(render_data, regularized_roughness, bsdf_context.material.coat_anisotropy, false, F, local_view_direction, local_to_light_direction, local_halfway_vector, out_pdf, coat_delta_direction_sampled, bsdf_context.current_bounce); } HIPRT_HOST_DEVICE HIPRT_INLINE float principled_coat_pdf(const HIPRTRenderData& render_data, const BSDFContext& bsdf_context, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_halfway_vector, float incident_medium_ior) { // The coat lobe is just a microfacet lobe float HoL = hippt::clamp(1.0e-8f, 1.0f, hippt::dot(local_halfway_vector, local_to_light_direction)); float regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, bsdf_context.material.coat_roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); // We're only evaluating the coat lobe if, either: // - The incident light direction was sampled from the clearcoat lobe // - The coat is not a delta distribution (not perfectly smooth) // - The incident light direction was sampled from another perfectly specular lobe // // Because if none of these two conditions are true, the evaluation of the coat will // yield 0.0f anyways // // All the conditions are handled in 'is_specular_delta_reflection_sampled' MaterialUtils::SpecularDeltaReflectionSampled coat_delta_direction_sampled = MaterialUtils::is_specular_delta_reflection_sampled(bsdf_context.material, regularized_roughness, bsdf_context.material.coat_anisotropy, bsdf_context.incident_light_info); return torrance_sparrow_GGX_pdf_reflect(render_data, regularized_roughness, bsdf_context.material.coat_anisotropy, local_view_direction, local_to_light_direction, local_halfway_vector, coat_delta_direction_sampled); } /** * The sampled direction is returned in the local shading frame of the basis used for 'local_view_direction' */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 principled_coat_sample(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, const float3& local_view_direction, Xorshift32Generator& random_number_generator) { float regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, bsdf_context.material.coat_roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); return microfacet_GGX_sample_reflection(regularized_roughness, bsdf_context.material.coat_anisotropy, local_view_direction, random_number_generator); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_sheen_eval(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& local_to_light_direction, float& pdf, float& out_sheen_reflectance) { return sheen_ltc_eval(render_data, material, local_to_light_direction, local_view_direction, pdf, out_sheen_reflectance); } HIPRT_HOST_DEVICE HIPRT_INLINE float principled_sheen_pdf(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& local_to_light_direction) { return sheen_ltc_pdf(render_data, material, local_to_light_direction, local_view_direction); } HIPRT_HOST_DEVICE HIPRT_INLINE float3 principled_sheen_sample(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& shading_normal, Xorshift32Generator& random_number_generator) { return sheen_ltc_sample(render_data, material, local_view_direction, shading_normal, random_number_generator); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_metallic_eval(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float roughness, float anisotropy, float incident_ior, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_half_vector, float& pdf) { float regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); MaterialUtils::SpecularDeltaReflectionSampled metal_delta_direction_sampled = MaterialUtils::is_specular_delta_reflection_sampled(bsdf_context.material, regularized_roughness, anisotropy, bsdf_context.incident_light_info); if (metal_delta_direction_sampled == MaterialUtils::SpecularDeltaReflectionSampled::SPECULAR_PEAK_NOT_SAMPLED) { // The distribution isn't worth evaluating because it's specular but we the incident // light direction wasn't sampled from a specular distribution pdf = 0.0f; return ColorRGB32F(0.0f); } float HoL = hippt::clamp(1.0e-8f, 1.0f, hippt::dot(local_half_vector, local_to_light_direction)); ColorRGB32F F_metal = adobe_f82_tint_fresnel(bsdf_context.material.base_color, bsdf_context.material.metallic_F82, bsdf_context.material.metallic_F90, bsdf_context.material.metallic_F90_falloff_exponent, HoL); ColorRGB32F F_thin_film = thin_film_fresnel(bsdf_context.material, incident_ior, HoL); ColorRGB32F F = hippt::lerp(F_metal, F_thin_film, bsdf_context.material.thin_film); return torrance_sparrow_GGX_eval_reflect(render_data, regularized_roughness, anisotropy, bsdf_context.material.do_metallic_energy_compensation, F, local_view_direction, local_to_light_direction, local_half_vector, pdf, metal_delta_direction_sampled, bsdf_context.current_bounce); } HIPRT_HOST_DEVICE HIPRT_INLINE float principled_metallic_pdf(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float roughness, float anisotropy, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_half_vector) { float regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); MaterialUtils::SpecularDeltaReflectionSampled metal_delta_direction_sampled = MaterialUtils::is_specular_delta_reflection_sampled(bsdf_context.material, regularized_roughness, anisotropy, bsdf_context.incident_light_info); if (metal_delta_direction_sampled == MaterialUtils::SpecularDeltaReflectionSampled::SPECULAR_PEAK_NOT_SAMPLED) // The distribution isn't worth evaluating because it's specular but we the incident // light direction wasn't sampled from a specular distribution return 0.0f; return torrance_sparrow_GGX_pdf_reflect(render_data, regularized_roughness, anisotropy, local_view_direction, local_to_light_direction, local_half_vector, metal_delta_direction_sampled); } /** * The sampled direction is returned in the local shading frame of the basis used for 'local_view_direction' */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 principled_metallic_sample(const HIPRTRenderData& render_data, const BSDFContext& bsdf_context, float roughness, float anisotropy, const float3& local_view_direction, Xorshift32Generator& random_number_generator) { float regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); return microfacet_GGX_sample_reflection(regularized_roughness, anisotropy, local_view_direction, random_number_generator); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_diffuse_eval(const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& local_to_light_direction, float& pdf) { // The diffuse lobe is a simple Oren Nayar lobe #if PrincipledBSDFDiffuseLobe == PRINCIPLED_DIFFUSE_LOBE_LAMBERTIAN return lambertian_brdf_eval(material, local_to_light_direction.z, pdf); #elif PrincipledBSDFDiffuseLobe == PRINCIPLED_DIFFUSE_LOBE_OREN_NAYAR return oren_nayar_brdf_eval(material, local_view_direction, local_to_light_direction, pdf); #endif } HIPRT_HOST_DEVICE HIPRT_INLINE float principled_diffuse_pdf(const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& local_to_light_direction) { // The diffuse lobe is a simple Oren Nayar lobe #if PrincipledBSDFDiffuseLobe == PRINCIPLED_DIFFUSE_LOBE_LAMBERTIAN return lambertian_brdf_pdf(material, local_to_light_direction.z); #elif PrincipledBSDFDiffuseLobe == PRINCIPLED_DIFFUSE_LOBE_OREN_NAYAR return oren_nayar_brdf_pdf(material, local_view_direction, local_to_light_direction); #endif } /** * The sampled direction is returned in world space */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 principled_diffuse_sample(const float3& surface_normal, Xorshift32Generator& random_number_generator) { // Our Oren-Nayar diffuse lobe is sampled by a cosine weighted distribution return cosine_weighted_sample_around_normal_world_space(surface_normal, random_number_generator); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_specular_fresnel(const DeviceUnpackedEffectiveMaterial& material, float relative_specular_ior, float cos_theta_i) { // We want the IOR of the layer we're coming from for the thin-film fresnel // // 'relative_specular_IOR' is "A / B" // with A the IOR of the specular layer // and B the IOR of the layer (or medium) above the specular layer // // so the IOR of the layer above is 1.0f / (relative_IOR / specular_ior) = specular_IOR / relative_IOR float layer_above_IOR = material.ior / relative_specular_ior; // Computing the fresnel term // It's either the thin film fresnel for thin film interference or the usual // non colored dielectric/dielectric fresnel. // // We're lerping between the two based on material.thin_film float material_thin_film = material.thin_film; ColorRGB32F F_specular; if (material_thin_film < 1.0f) F_specular = ColorRGB32F(full_fresnel_dielectric(cos_theta_i, relative_specular_ior)); ColorRGB32F F_thin_film = thin_film_fresnel(material, layer_above_IOR, cos_theta_i); ColorRGB32F F = hippt::lerp(F_specular, F_thin_film, material_thin_film); return F; } /** * Returns the relative IOR as "A /B" * with A the IOR of the specular layer * and B the IOR of the layer (or medium) above the specular layer * * 'incident_medium_ior' should be the IOR of the medium in which the object is (i.e. the air most likely) */ HIPRT_HOST_DEVICE HIPRT_INLINE float principled_specular_relative_ior(const DeviceUnpackedEffectiveMaterial& material, float incident_medium_ior) { if (material.coat == 0.0f) return material.ior; // When computing the specular layer, the incident IOR actually isn't always // that of the incident medium because we may have the coat layer above us instead of the medium // so the "proper" IOR to use here is actually the lerp between the medium and the coat // IOR depending on the coat factor float incident_layer_ior = hippt::lerp(incident_medium_ior, material.coat_ior, material.coat); float relative_ior = material.ior / incident_layer_ior; if (relative_ior < 1.0f) // If the coat IOR (which we're coming from) is greater than the IOR // of the base layer (which is the specular layer with IOR material.ior) // then we may hit total internal reflection when entering the specular layer from // the coat layer above. This manifests as a weird ring near grazing angles. // // This weird ring should not happen in reality. It only happens because we're // not bending the rays when refracting into the coat layer: we compute the // fresnel at the specular/coat interface as if the light direction just went // straight through the coat layer without refraction. There will always be // some refraction at the air/coat interface if the coat layer IOR is > 1.0f. // // The proper solution would be to actually bend the ray after it hits the coat layer. // We would then be evaluating the fresnel at the coat/specular interface with a // incident light cosine angle that is different and we wouldn't get total internal reflection. // // This is explained in the [OpenPBR Spec 2024] // https://academysoftwarefoundation.github.io/OpenPBR/#model/coat/totalinternalreflection // // A more computationally efficient solution is to simply invert the IOR as done here. // This is also explained in the OpenPBR spec as well as in // [Novel aspects of the Adobe Standard Material, Kutz, Hasan, Edmondson, 2023] // https://helpx.adobe.com/content/dam/substance-3d/general-knowledge/asm/Adobe%20Standard%20Material%20-%20Technical%20Documentation%20-%20May2023.pdf relative_ior = 1.0f / relative_ior; return relative_ior; } /** * 'relative_ior' is eta_t / eta_i with 'eta_t' the IOR of the glossy layer and * 'eta_i' the IOR of */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_specular_eval(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float relative_ior, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_half_vector, float& pdf) { float regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, bsdf_context.material.roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); MaterialUtils::SpecularDeltaReflectionSampled is_specular_delta_reflection_sampled = MaterialUtils::is_specular_delta_reflection_sampled(bsdf_context.material, regularized_roughness, bsdf_context.material.anisotropy, bsdf_context.incident_light_info); // The specular lobe is just another GGX lobe // // We actually don't want energy compensation here for the specular layer // (hence the torrance_sparrow_GGX_eval_reflect<0>) because energy compensation // for the specular layer is handled for the glossy based (specular + diffuse lobe) // as a whole, not just in the specular layer ColorRGB32F F = principled_specular_fresnel(bsdf_context.material, relative_ior, hippt::dot(local_to_light_direction, local_half_vector)); // No energy compensation on the specular layer because energy compensation is done on the whole diffuse + specular // not just specular. ColorRGB32F specular = torrance_sparrow_GGX_eval_reflect<0>(render_data, regularized_roughness, bsdf_context.material.anisotropy, /* do_energy_compensation */ false, F, local_view_direction, local_to_light_direction, local_half_vector, pdf, is_specular_delta_reflection_sampled, bsdf_context.current_bounce); return specular; } HIPRT_HOST_DEVICE HIPRT_INLINE float principled_specular_pdf(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float relative_ior, const float3& local_view_direction, const float3& local_to_light_direction, const float3& local_half_vector) { float regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, bsdf_context.material.roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); MaterialUtils::SpecularDeltaReflectionSampled is_specular_delta_reflection_sampled = MaterialUtils::is_specular_delta_reflection_sampled(bsdf_context.material, regularized_roughness, bsdf_context.material.anisotropy, bsdf_context.incident_light_info); return torrance_sparrow_GGX_pdf_reflect(render_data, regularized_roughness, bsdf_context.material.anisotropy, local_view_direction, local_to_light_direction, local_half_vector, is_specular_delta_reflection_sampled); } HIPRT_HOST_DEVICE HIPRT_INLINE float3 principled_specular_sample(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float roughness, float anisotropy, const float3& local_view_direction, Xorshift32Generator& random_number_generator) { float regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, bsdf_context.material.roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); return microfacet_GGX_sample_reflection(regularized_roughness, anisotropy, local_view_direction, random_number_generator); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_beer_absorption(const HIPRTRenderData& render_data, RayVolumeState& ray_volume_state) { // Note that we want to use the absorption of the material we finished traveling in. // The BSDF we're evaluating right now is using the new material we're refracting in, this is not // by this material that the ray has been absorbed. The ray has been absorded by the volume // it was in before refracting here, so it's the incident mat index ColorRGB32F absorption_color; if (render_data.bsdfs_data.white_furnace_mode) absorption_color = ColorRGB32F(1.0f); else absorption_color = render_data.buffers.materials_buffer.get_absorption_color(ray_volume_state.incident_mat_index); if (!absorption_color.is_white()) { // Capping the distance to avoid numerical issues at 0 distance // (can happen depending on the geometry of the scene if a ray exits a volume very quickly after entering it) ray_volume_state.distance_in_volume = hippt::max(ray_volume_state.distance_in_volume, 1.0e-6f); // Remapping the absorption coefficient so that it is more intuitive to manipulate // according to Burley, 2015 [5]. // This effectively gives us a "at distance" absorption coefficient. ColorRGB32F absorption_coefficient = log(absorption_color) / render_data.buffers.materials_buffer.get_absorption_at_distance(ray_volume_state.incident_mat_index); return exp(absorption_coefficient * ray_volume_state.distance_in_volume); } return ColorRGB32F(1.0f); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_glass_eval(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, const float3& local_view_direction, const float3& local_to_light_direction, float& pdf) { pdf = 0.0f; float NoV = local_view_direction.z; float NoL = local_to_light_direction.z; if (hippt::abs(NoL) < 1.0e-8f) // Check to avoid dividing by 0 later on return ColorRGB32F(0.0f); // We're in the case of reflection if the view direction and the bounced ray (light direction) are in the same hemisphere bool reflecting = NoL * NoV > 0; // Relative eta = eta_t / eta_i float eta_i = bsdf_context.volume_state.incident_mat_index == NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX ? 1.0f : render_data.buffers.materials_buffer.get_ior(bsdf_context.volume_state.incident_mat_index); float eta_t = bsdf_context.volume_state.outgoing_mat_index == NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX ? 1.0f : render_data.buffers.materials_buffer.get_ior(bsdf_context.volume_state.outgoing_mat_index); float dispersion_abbe_number = bsdf_context.material.dispersion_abbe_number; float dispersion_scale = bsdf_context.material.dispersion_scale; eta_i = compute_dispersion_ior(dispersion_abbe_number, dispersion_scale, eta_i, hippt::abs(bsdf_context.volume_state.sampled_wavelength)); eta_t = compute_dispersion_ior(dispersion_abbe_number, dispersion_scale, eta_t, hippt::abs(bsdf_context.volume_state.sampled_wavelength)); float relative_eta = eta_t / eta_i; // relative_eta can be 1 when refracting from a volume into another volume of the same IOR. // This in conjunction with the view direction and the light direction being the negative of // one another will lead the microfacet normal to be the null vector which then causes // NaNs. // // Example: // The view and light direction can be the negative of one another when looking straight at a // flat window for example. The view direction is aligned with the normal of the window // in this configuration whereas the refracting light direction (and it is very likely to refract // in this configuration) is going to point exactly away from the view direction and the normal. // // We then have // // half_vector = light_dir + relative_eta * view_dir // = light_dir + 1.0f * view_dir // = light_dir + view_dir = (0, 0, 0) // // Normalizing this null vector then leads to a NaNs because of the zero-length. // // We're settings relative_eta to 1.00001f to avoid this issue if (hippt::abs(relative_eta - 1.0f) < 1.0e-5f) relative_eta = 1.0f + 1.0e-5f; bool thin_walled = bsdf_context.material.thin_walled; float scaled_roughness = MaterialUtils::get_thin_walled_roughness(thin_walled, bsdf_context.material.roughness, relative_eta); float3 local_half_vector; if (scaled_roughness <= MaterialConstants::ROUGHNESS_CLAMP && PrincipledBSDFDeltaDistributionEvaluationOptimization == KERNEL_OPTION_TRUE && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_FALSE) { // Fast path for specular glass // // Note that we check for 'PrincipledBSDFDeltaDistributionEvaluationOptimization' because if we're not using the delta distribution // optimizations, any incident light direction given to the BSDF is going to be evaluated // and so the half vector won't necessarily be (0, 0, 1). // // Any incident light direction may also be given with the optimizations ON but // with the optimizations ON, any direction that doesn't align with the perfect // reflection direction will be rejected (and contribute 0) so this is not an issue // // If microfacet regularization is enabled, the smooth glass is going to be roughened so we cannot // assume roughness 0.0f and we fall back to the classical half-vector computation below local_half_vector = make_float3(0.0f, 0.0f, 1.0f); } else { // Computing the generalized (that takes refraction into account) half vector if (reflecting) local_half_vector = local_to_light_direction + local_view_direction; else { if (thin_walled) // Thin walled material refract without light bending (because both refractions interfaces are simulated in one layer of material) // just refract straight through i.e. light_direction = -view_direction // It can be as si local_half_vector = local_to_light_direction * make_float3(1.0f, 1.0f, -1.0f) + local_view_direction; else // We need to take the relative_eta into account when refracting to compute // the half vector (this is the "generalized" half vector) local_half_vector = local_to_light_direction * relative_eta + local_view_direction; } local_half_vector = hippt::normalize(local_half_vector); } if (local_half_vector.z < 0.0f) // Because the rest of the function we're going to compute here assume // that the microfacet normal is in the same hemisphere as the surface // normal, we're going to flip it if needed local_half_vector = -local_half_vector; float HoL = hippt::dot(local_to_light_direction, local_half_vector); float HoV = hippt::dot(local_view_direction, local_half_vector); if (HoL * NoL < 0.0f || HoV * NoV < 0.0f) // Backfacing microfacets when the microfacet normal isn't in the same // hemisphere as the view dir or light dir return ColorRGB32F(0.0f); float thin_film = bsdf_context.material.thin_film; ColorRGB32F F_thin_film = thin_film_fresnel(bsdf_context.material, eta_i, HoV); ColorRGB32F F_no_thin_film; if (thin_film < 1.0f) F_no_thin_film = ColorRGB32F(full_fresnel_dielectric(HoV, relative_eta)); ColorRGB32F F = hippt::lerp(F_no_thin_film, F_thin_film, thin_film); if (thin_walled && F.r < 1.0f && thin_film == 0.0f && scaled_roughness < 0.1f) // If this is not total reflection, adjusting the fresnel term to account for inter-reflections within the thin interface // Not doing this if thin-film is present because that would not be accurate at all. Thin-film // effect require phase shift computations and that's expensive so we're just not doing it here // instead // // Reference: Dielectric BSDF, PBR Book 4ed: https://pbr-book.org/4ed/Reflection_Models/Dielectric_BSDF // // If there is no thin-film, the fresnel reflectance is non-colored and is the same // value for all RGB wavelengths. This means that f_reflect_proba is actually just the fresnel reflection factor // // This fresnel scaling only works at roughness 0 but still using below 0.1f for a close enough approximation F += ColorRGB32F(hippt::square(1.0f - F.r) * F.r / (1.0f - hippt::square(F.r))); float f_reflect_proba = F.luminance(); ColorRGB32F color; if (reflecting) { float regularized_roughness = scaled_roughness; if (bsdf_context.bsdf_regularization_mode == MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE) // If this if for MIS, we want to use the same roughness as for the BSDF sampling so that the MIS weights are correct regularized_roughness = MicrofacetRegularization::regularize_mix_reflection_refraction(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, scaled_roughness, bsdf_context.accumulated_path_roughness, eta_i, eta_t, render_data.render_settings.sample_number); else if (bsdf_context.bsdf_regularization_mode == MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE) regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, scaled_roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); MaterialUtils::SpecularDeltaReflectionSampled delta_glass_direction_sampled = MaterialUtils::is_specular_delta_reflection_sampled(bsdf_context.material, scaled_roughness, bsdf_context.material.anisotropy, bsdf_context.incident_light_info); color = torrance_sparrow_GGX_eval_reflect<0>(render_data, regularized_roughness, bsdf_context.material.anisotropy, false, F, local_view_direction, local_to_light_direction, local_half_vector, pdf, delta_glass_direction_sampled, bsdf_context.current_bounce); // Note: for specular glass, the compensation term will never be evaluated as there is no energy loss. // The function will return very quickly and will return 1.0f float compensation_term = get_GGX_energy_compensation_dielectrics(render_data, bsdf_context.material, bsdf_context.volume_state.inside_material, eta_t, eta_i, relative_eta, local_view_direction.z, bsdf_context.current_bounce); // [Turquin, 2019] Eq. 18 for dielectric microfacet energy compensation color /= compensation_term; // Scaling the PDF by the probability of being here (reflection of the ray and not transmission) pdf *= f_reflect_proba; } else { float regularized_roughness = scaled_roughness; if (bsdf_context.bsdf_regularization_mode == MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE) // If this if for MIS, we want to use the same roughness as for the BSDF sampling so that the MIS weights are correct regularized_roughness = MicrofacetRegularization::regularize_mix_reflection_refraction(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, scaled_roughness, bsdf_context.accumulated_path_roughness, eta_i, eta_t, render_data.render_settings.sample_number); else if (bsdf_context.bsdf_regularization_mode == MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE) regularized_roughness = MicrofacetRegularization::regularize_refraction(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, scaled_roughness, bsdf_context.accumulated_path_roughness, eta_i, eta_t, render_data.render_settings.sample_number); color = torrance_sparrow_GGX_eval_refract(bsdf_context.material, regularized_roughness, relative_eta, F, local_view_direction, local_to_light_direction, local_half_vector, pdf, bsdf_context.incident_light_info); // Taking refraction russian roulette probability into account pdf *= 1.0f - f_reflect_proba; // Note: for specular glass, the compensation term will never be evaluated as there is no energy loss. // The function will return very quickly and will return 1.0f float compensation_term = get_GGX_energy_compensation_dielectrics(render_data, bsdf_context.material, regularized_roughness, bsdf_context.volume_state.inside_material, eta_t, eta_i, relative_eta, local_view_direction.z, bsdf_context.current_bounce); // [Turquin, 2019] Eq. 18 for dielectric microfacet energy compensation color /= compensation_term; if (thin_walled) // Thin materials use the base color squared to represent both the entry and the exit // simultaneously color *= bsdf_context.material.base_color; if (thin_walled && bsdf_context.update_ray_volume_state) // For thin materials, refracting in equals refracting out so we're poping the stack bsdf_context.volume_state.interior_stack.pop(bsdf_context.volume_state.inside_material); else if (bsdf_context.volume_state.incident_mat_index != NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX) { // If we're not coming from the air, this means that we were in a volume and we're currently // refracting out of the volume or into another volume. // This is where we take the absorption of our travel into account using Beer-Lambert's law. color *= principled_beer_absorption(render_data, bsdf_context.volume_state); if (bsdf_context.update_ray_volume_state) { // We changed volume so we're resetting the distance bsdf_context.volume_state.distance_in_volume = 0.0f; if (bsdf_context.volume_state.inside_material) // We refracting out of a volume so we're poping the stack bsdf_context.volume_state.interior_stack.pop(bsdf_context.volume_state.inside_material); } } } return color; } HIPRT_HOST_DEVICE HIPRT_INLINE float principled_glass_pdf(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, const float3& local_view_direction, const float3& local_to_light_direction) { float pdf = 0.0f; float NoV = local_view_direction.z; float NoL = local_to_light_direction.z; if (hippt::abs(NoL) < 1.0e-8f) // Check to avoid dividing by 0 later on return 0.0f; // We're in the case of reflection if the view direction and the bounced ray (light direction) are in the same hemisphere bool reflecting = NoL * NoV > 0; // Relative eta = eta_t / eta_i float eta_i = bsdf_context.volume_state.incident_mat_index == NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX ? 1.0f : render_data.buffers.materials_buffer.get_ior(bsdf_context.volume_state.incident_mat_index); float eta_t = bsdf_context.volume_state.outgoing_mat_index == NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX ? 1.0f : render_data.buffers.materials_buffer.get_ior(bsdf_context.volume_state.outgoing_mat_index); float dispersion_abbe_number = bsdf_context.material.dispersion_abbe_number; float dispersion_scale = bsdf_context.material.dispersion_scale; eta_i = compute_dispersion_ior(dispersion_abbe_number, dispersion_scale, eta_i, hippt::abs(bsdf_context.volume_state.sampled_wavelength)); eta_t = compute_dispersion_ior(dispersion_abbe_number, dispersion_scale, eta_t, hippt::abs(bsdf_context.volume_state.sampled_wavelength)); float relative_eta = eta_t / eta_i; // relative_eta can be 1 when refracting from a volume into another volume of the same IOR. // This in conjunction with the view direction and the light direction being the negative of // one another will lead the microfacet normal to be the null vector which then causes // NaNs. // // Example: // The view and light direction can be the negative of one another when looking straight at a // flat window for example. The view direction is aligned with the normal of the window // in this configuration whereas the refracting light direction (and it is very likely to refract // in this configuration) is going to point exactly away from the view direction and the normal. // // We then have // // half_vector = light_dir + relative_eta * view_dir // = light_dir + 1.0f * view_dir // = light_dir + view_dir = (0, 0, 0) // // Normalizing this null vector then leads to a NaNs because of the zero-length. // // We're settings relative_eta to 1.00001f to avoid this issue if (hippt::abs(relative_eta - 1.0f) < 1.0e-5f) relative_eta = 1.0f + 1.0e-5f; bool thin_walled = bsdf_context.material.thin_walled; float scaled_roughness = MaterialUtils::get_thin_walled_roughness(thin_walled, bsdf_context.material.roughness, relative_eta); float3 local_half_vector; if (scaled_roughness <= MaterialConstants::ROUGHNESS_CLAMP && PrincipledBSDFDeltaDistributionEvaluationOptimization == KERNEL_OPTION_TRUE && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_FALSE) { // Fast path for specular glass // // Note that we check for 'PrincipledBSDFDeltaDistributionEvaluationOptimization' because if we're not using the delta distribution // optimizations, any incident light direction given to the BSDF is going to be evaluated // and so the half vector won't necessarily be (0, 0, 1). // // Any incident light direction may also be given with the optimizations ON but // with the optimizations ON, any direction that doesn't align with the perfect // reflection direction will be rejected (and contribute 0) so this is not an issue // // If microfacet regularization is enabled, the smooth glass is going to be roughened so we cannot // assume roughness 0.0f and we fall back to the classical half-vector computation below local_half_vector = make_float3(0.0f, 0.0f, 1.0f); } else { // Computing the generalized (that takes refraction into account) half vector if (reflecting) local_half_vector = local_to_light_direction + local_view_direction; else { if (thin_walled) // Thin walled material refract without light bending (because both refractions interfaces are simulated in one layer of material) // just refract straight through i.e. light_direction = -view_direction // It can be as si local_half_vector = local_to_light_direction * make_float3(1.0f, 1.0f, -1.0f) + local_view_direction; else // We need to take the relative_eta into account when refracting to compute // the half vector (this is the "generalized" half vector) local_half_vector = local_to_light_direction * relative_eta + local_view_direction; } local_half_vector = hippt::normalize(local_half_vector); } if (local_half_vector.z < 0.0f) // Because the rest of the function we're going to compute here assume // that the microfacet normal is in the same hemisphere as the surface // normal, we're going to flip it if needed local_half_vector = -local_half_vector; float HoL = hippt::dot(local_to_light_direction, local_half_vector); float HoV = hippt::dot(local_view_direction, local_half_vector); if (HoL * NoL < 0.0f || HoV * NoV < 0.0f) // Backfacing microfacets when the microfacet normal isn't in the same // hemisphere as the view dir or light dir return 0.0f; float thin_film = bsdf_context.material.thin_film; ColorRGB32F F_thin_film = thin_film_fresnel(bsdf_context.material, eta_i, HoV); ColorRGB32F F_no_thin_film; if (thin_film < 1.0f) F_no_thin_film = ColorRGB32F(full_fresnel_dielectric(HoV, relative_eta)); ColorRGB32F F = hippt::lerp(F_no_thin_film, F_thin_film, thin_film); if (thin_walled && F.r < 1.0f && thin_film == 0.0f && scaled_roughness < 0.1f) // If this is not total reflection, adjusting the fresnel term to account for inter-reflections within the thin interface // Not doing this if thin-film is present because that would not be accurate at all. Thin-film // effect require phase shift computations and that's expensive so we're just not doing it here // instead // // Reference: Dielectric BSDF, PBR Book 4ed: https://pbr-book.org/4ed/Reflection_Models/Dielectric_BSDF // // If there is no thin-film, the fresnel reflectance is non-colored and is the same // value for all RGB wavelengths. This means that f_reflect_proba is actually just the fresnel reflection factor // // This fresnel scaling only works at roughness 0 but still using below 0.1f for a close enough approximation F += ColorRGB32F(hippt::square(1.0f - F.r) * F.r / (1.0f - hippt::square(F.r))); float f_reflect_proba = F.luminance(); if (reflecting) { float regularized_roughness = scaled_roughness; if (bsdf_context.bsdf_regularization_mode == MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE) // If this if for MIS, we want to use the same roughness as for the BSDF sampling so that the MIS weights are correct regularized_roughness = MicrofacetRegularization::regularize_mix_reflection_refraction(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, scaled_roughness, bsdf_context.accumulated_path_roughness, eta_i, eta_t, render_data.render_settings.sample_number); else if (bsdf_context.bsdf_regularization_mode == MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE) regularized_roughness = MicrofacetRegularization::regularize_reflection(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, scaled_roughness, bsdf_context.accumulated_path_roughness, render_data.render_settings.sample_number); MaterialUtils::SpecularDeltaReflectionSampled delta_glass_direction_sampled = MaterialUtils::is_specular_delta_reflection_sampled(bsdf_context.material, scaled_roughness, bsdf_context.material.anisotropy, bsdf_context.incident_light_info); pdf = torrance_sparrow_GGX_pdf_reflect(render_data, regularized_roughness, bsdf_context.material.anisotropy, local_view_direction, local_to_light_direction, local_half_vector, delta_glass_direction_sampled); // Scaling the PDF by the probability of being here (reflection of the ray and not transmission) pdf *= f_reflect_proba; } else { float regularized_roughness = scaled_roughness; if (bsdf_context.bsdf_regularization_mode == MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE) // If this if for MIS, we want to use the same roughness as for the BSDF sampling so that the MIS weights are correct regularized_roughness = MicrofacetRegularization::regularize_mix_reflection_refraction(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, scaled_roughness, bsdf_context.accumulated_path_roughness, eta_i, eta_t, render_data.render_settings.sample_number); else if (bsdf_context.bsdf_regularization_mode == MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE) regularized_roughness = MicrofacetRegularization::regularize_refraction(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, scaled_roughness, bsdf_context.accumulated_path_roughness, eta_i, eta_t, render_data.render_settings.sample_number); pdf = torrance_sparrow_GGX_pdf_refract(bsdf_context.material, regularized_roughness, relative_eta, local_view_direction, local_to_light_direction, local_half_vector, bsdf_context.incident_light_info); // Taking refraction russian roulette probability into account pdf *= 1.0f - f_reflect_proba; } return pdf; } /** * The sampled direction is returned in the local shading frame of the basis used for 'local_view_direction' */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 principled_glass_sample(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float3 local_view_direction, Xorshift32Generator& random_number_generator) { float eta_i = bsdf_context.volume_state.incident_mat_index == NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX ? 1.0f : render_data.buffers.materials_buffer.get_ior(bsdf_context.volume_state.incident_mat_index); float eta_t = bsdf_context.volume_state.outgoing_mat_index == NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX ? 1.0f : render_data.buffers.materials_buffer.get_ior(bsdf_context.volume_state.outgoing_mat_index); float dispersion_abbe_number = bsdf_context.material.dispersion_abbe_number; float dispersion_scale = bsdf_context.material.dispersion_scale; eta_i = compute_dispersion_ior(dispersion_abbe_number, dispersion_scale, eta_i, hippt::abs(bsdf_context.volume_state.sampled_wavelength)); eta_t = compute_dispersion_ior(dispersion_abbe_number, dispersion_scale, eta_t, hippt::abs(bsdf_context.volume_state.sampled_wavelength)); float relative_eta = eta_t / eta_i; // To avoid sampling directions that would lead to a null half_vector. // Explained in more details in principled_glass_eval. if (hippt::abs(relative_eta - 1.0f) < 1.0e-5f) relative_eta = 1.0f + 1.0e-5f; bool thin_walled = bsdf_context.material.thin_walled; float thin_walled_scaled_roughness = MaterialUtils::get_thin_walled_roughness(thin_walled, bsdf_context.material.roughness, relative_eta); if (bsdf_context.bsdf_regularization_mode == MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS && PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE) // Because we do not know if advance if we're going to reflecct or refract, we do not know // whether we should regularize using the microfacet reflection or refraction bound function. // // So we take the average. This is going to be over-roughened for reflection and under-roughened // for refractions but this should still be effective thin_walled_scaled_roughness = MicrofacetRegularization::regularize_mix_reflection_refraction(render_data.bsdfs_data.microfacet_regularization, bsdf_context.bsdf_regularization_mode, thin_walled_scaled_roughness, bsdf_context.accumulated_path_roughness, eta_i, eta_t, render_data.render_settings.sample_number); float alpha_x, alpha_y; MaterialUtils::get_alphas(thin_walled_scaled_roughness, bsdf_context.material.anisotropy, alpha_x, alpha_y); float3 microfacet_normal = GGX_anisotropic_sample_microfacet(local_view_direction, alpha_x, alpha_y, random_number_generator); float HoV = hippt::dot(local_view_direction, microfacet_normal); float thin_film = bsdf_context.material.thin_film; ColorRGB32F F_thin_film = thin_film_fresnel(bsdf_context.material, eta_i, HoV); ColorRGB32F F_no_thin_film; if (thin_film < 1.0f) F_no_thin_film = ColorRGB32F(full_fresnel_dielectric(HoV, relative_eta)); ColorRGB32F F = hippt::lerp(F_no_thin_film, F_thin_film, thin_film); if (thin_walled && F.r < 1.0f && thin_film == 0.0f && thin_walled_scaled_roughness < 0.1f) // If this is not total reflection, adjusting the fresnel term to account for inter-reflections within the thin interface // Not doing this if thin-film is present because that would not be accurate at all. Thin-film // effect require phase shift computations and that's very expensive so we're just not doing it here // instead // // Reference: Dielectric BSDF, PBR Book 4ed: https://pbr-book.org/4ed/Reflection_Models/Dielectric_BSDF // // If there is no thin-film, the fresnel reflectance is non-colored and is the same // value for all RGB wavelengths. This means that f_reflect_proba is actually just the fresnel reflection factor // // This fresnel scaling only works at roughness 0 but still using below 0.1f for a close enough approximation F += ColorRGB32F(hippt::square(1.0f - F.r) * F.r / (1.0f - hippt::square(F.r))); float f_reflect_proba = F.luminance(); float rand_1 = random_number_generator(); float3 sampled_direction; if (rand_1 < f_reflect_proba) { // Reflection sampled_direction = reflect_ray(local_view_direction, microfacet_normal); bsdf_context.incident_light_info = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFLECT_LOBE; // This is a reflection, we're poping the stack if (bsdf_context.update_ray_volume_state) bsdf_context.volume_state.interior_stack.pop(false); } else { // Refraction bsdf_context.incident_light_info = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFRACT_LOBE; if (hippt::dot(microfacet_normal, local_view_direction) < 0.0f) // For the refraction operation that follows, we want the direction to refract (the view // direction here) to be in the same hemisphere as the normal (the microfacet normal here) // so we're flipping the microfacet normal in case it wasn't in the same hemisphere as // the view direction microfacet_normal = -microfacet_normal; if (thin_walled) { // Because the interface is thin (and so we refract twice, "cancelling" the bending the light), // the refraction direction is just the incoming (view direction) reflected // and flipped about the normal plane float3 reflected = reflect_ray(local_view_direction, microfacet_normal); // Now flipping reflected.z *= -1.0f; // Refraction through the thin walled material. // We're poping the stack because we're not inside the material even // though this is a refraction. A thin material has no inside if (bsdf_context.update_ray_volume_state) bsdf_context.volume_state.interior_stack.pop(false); return reflected; } else sampled_direction = refract_ray(local_view_direction, microfacet_normal, relative_eta); } return sampled_direction; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_diffuse_transmission_eval(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, RayVolumeState& ray_volume_state, bool update_ray_volume_state, const float3& local_view_direction, float3 local_to_light_direction, float& diffuse_transmission_pdf) { diffuse_transmission_pdf = 0.0f; if (local_view_direction.z * local_to_light_direction.z > 0.0f) // Both are in the same hemisphere, incorrect for a transmission only lobe return ColorRGB32F(0.0f); ColorRGB32F color = material.base_color * M_INV_PI; if (ray_volume_state.incident_mat_index != NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX) { // If we're not coming from the air, this means that we were in a volume and we're currently // refracting out of the volume or into another volume. // This is where we take the absorption of our travel into account using Beer-Lambert's law. color *= principled_beer_absorption(render_data, ray_volume_state); if (update_ray_volume_state) { // We changed volume so we're resetting the distance ray_volume_state.distance_in_volume = 0.0f; if (ray_volume_state.inside_material) // We refracting out of a volume so we're poping the stack ray_volume_state.interior_stack.pop(ray_volume_state.inside_material); } } diffuse_transmission_pdf = hippt::abs(local_to_light_direction.z * M_INV_PI); return color; } HIPRT_HOST_DEVICE HIPRT_INLINE float principled_diffuse_transmission_pdf(const float3& local_view_direction, float3 local_to_light_direction) { if (local_view_direction.z * local_to_light_direction.z > 0.0f) // Both are in the same hemisphere, incorrect for a transmission only lobe return 0.0f; return hippt::abs(local_to_light_direction.z * M_INV_PI); } HIPRT_HOST_DEVICE HIPRT_INLINE float3 principled_diffuse_transmission_sample(float3 surface_normal, Xorshift32Generator& random_number_generator) { // Negating the normal here because by convention the surface normal given // to this function is in the same hemisphere as the view direction but we // want to sample a refraction, on the other side of the normal return cosine_weighted_sample_around_normal_world_space(-surface_normal, random_number_generator); } /** * Reference: * * [1] [Open PBR Specification - Coat Darkening] https://academysoftwarefoundation.github.io/OpenPBR/#model/coat/darkening * * 'relative_eta' must be coat_ior / incident_medium_ior */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_coat_compute_darkening(const DeviceUnpackedEffectiveMaterial& material, float relative_eta, float view_dir_fresnel) { if (material.coat_darkening == 0.0f) return ColorRGB32F(1.0f); // Fraction of light that exhibits total internal reflection inside the clearcoat layer, // assuming a perfectly diffuse base float Kr = 1.0f - (1.0f - fresnel_hemispherical_albedo_fit(relative_eta)) / (relative_eta * relative_eta); // Eq. 66 // Fraction of light that exhibits total internal reflection inside the clearcoat layer, // assuming a perfectly smooth base float Ks = view_dir_fresnel; // Eq. 67 // The roughness of the base layer isn't just material.roughness: // // What if material.roughness is 0.0f but there is no specular, or metallic or glass layer. // This means that there is just the diffuse lobe below the clearcoat layer. So even if // material.roughness is 0.0f, because the coat layer is directly on top of the diffuse layer, // the roughness of the base layer is 1.0f // // Now what if we have 0 specular but 1 metallic? Then we must use the roughness of the metallic layer // (which is actually just material.roughness). // // Same for the glass lobe (and specular lobe actually) // // So that's why we have these max() calls below // // The TL;DR is that we must use material.roughness is one of the base layer lobes (metallic/specular/glass) is 1.0f // Otherwise, is all the base layer lobes are 0.0f, then the roughness is 1.0f because this is just the diffuse lobe // And we lerp for the intermediate cases float base_roughness = hippt::lerp(1.0f, material.roughness, hippt::max(material.specular_transmission, hippt::max(material.metallic, material.specular))); // Now because our base, in the general case, isn't perfectly diffuse or perfectly smooth // we're lerping between the two values based on the roughness of the based layer and this gives us a good // approximation of how much total internal reflection we have inside the coat layer float K = hippt::lerp(Ks, Kr, base_roughness); // Eq. 68 // The base albedo is the albedo of the BSDF below the clearcoat. // Because the BSDF below the clearcoat may be composed of many layers, // we're approximating the overall as the blending of the albedos of the individual // lobes. // // Only the base substrate of the BSDF and the sheen layer have albedos so we only // have to mix those two float sheen = material.sheen; ColorRGB32F base_albedo = (material.base_color + material.sheen_color * sheen) / (1.0f + sheen); // This approximation of the amount of total internal reflection can then be used to // compute the darkening of the base caused by the clearcoating ColorRGB32F darkening = (1.0f - K) / (ColorRGB32F(1.0f) - base_albedo * K); // Disabling more or less the darkening based on: // - whether or not we have a coat layer at all // - whether or not we have coat darkening enabled at all or not // - whether or not we have a diffuse transmission lobe below the coat // layer, in which case there is no TIR between the diffuse // transmission lobe and the coat layer because the diffuse // transmission lobe is a BTDF only, it doesn't // reflect light --> no TIR --> no darkening darkening = hippt::lerp(ColorRGB32F(1.0f), darkening, material.coat * material.coat_darkening * (1.0f - material.diffuse_transmission)); return darkening; } /** * 'internal' functions are just so that 'principled_bsdf_eval' looks nicer */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F internal_eval_coat_layer(const HIPRTRenderData& render_data, const BSDFContext& bsdf_context, const float3& local_view_direction, const float3 local_to_light_direction, const float3& local_half_vector, float incident_ior, bool refracting, float coat_weight, float coat_proba, ColorRGB32F& layers_throughput, float& out_cumulative_pdf) { // '|| refracting' here is needed because if we have our coat // lobe on top of the glass lobe, we want to still compute the portion // of light that is left for the glass lobe after going through the coat lobe // so that's why we get into to if() block that does the computation but // we're only going to compute the absorption of the coat layer float coat_ior = bsdf_context.material.coat_ior; if (coat_weight > 0.0f && ((local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) || refracting)) { float coat_pdf = 0.0f; ColorRGB32F contribution; if (!refracting) { // The coat layer only contribtues for light direction in the same // hemisphere as the view direction (so reflections only, not refractions) contribution = principled_coat_eval(render_data, bsdf_context, local_view_direction, local_to_light_direction, local_half_vector, incident_ior, coat_pdf); contribution *= coat_weight; contribution *= layers_throughput; } out_cumulative_pdf += coat_pdf * coat_proba; // We're using hippt::abs() in the fresnel computation that follow because // we may compute these fresnels with incident light directions that are below // the hemisphere (for refractions for example) so that's where we want // the cosine angle not to be negative ColorRGB32F layer_below_attenuation = ColorRGB32F(1.0f); // Only the transmitted portion of the light goes to the layer below // We're using the shading normal here and not the microfacet normal because: // We want the proportion of light that reaches the layer below. // That's given by 1.0f - fresnelReflection. // // But '1.0f - fresnelReflection' needs to be computed with the shading normal, // not the microfacet normal i.e. it needs to be 1.0f - Fresnel(dot(N, L)), // not 1.0f - Fresnel(dot(H, L)) // // By computing 1.0f - Fresnel(dot(H, L)), we're computing the light // that goes through only that one microfacet with the microfacet normal. But light // reaches the layer below through many other microfacets, not just the one with our current // micronormal here (local_half_vector). To compute this correctly, we would actually need // to integrate over the microfacet normals and compute the fresnel transmission portion // (1.0f - Fresnel(dot(H, L))) for each of them and weight that contribution by the // probability given by the normal distribution function for the microfacet normal. // // We can't do that integration online so we're instead using the shading normal to compute // the transmitted portion of light. That's actually either a good approximation or the // exact solution. That was shown in GDC 2017 [PBR Diffuse Lighting for GGX + Smith Microsurfaces] layer_below_attenuation *= 1.0f - full_fresnel_dielectric(hippt::abs(local_to_light_direction.z), incident_ior, coat_ior); // Also, when light reflects off of the layer below the coat layer, some of that reflected light // will hit total internal reflection against the coat/air interface. This means that only // the part of light that does not hit total internal reflection actually reaches the viewer. // // That's why we're computing another fresnel term here to account for that. And additional note: // computing that fresnel with the direction reflected from the base layer or with the viewer direction // is the same, Fresnel is symmetrical. But because we don't have the exact direction reflected from the // base layer, we're using the view direction instead float view_dir_fresnel = full_fresnel_dielectric(hippt::abs(local_view_direction.z), incident_ior, coat_ior); layer_below_attenuation *= 1.0f - view_dir_fresnel; if (!bsdf_context.material.coat_medium_absorption.is_white()) { // Only computing the medium absorption if there is actually // some absorption // Taking the color of the absorbing coat medium into account when the light that got transmitted // travels through it // // The distance traveled into the coat depends on the angle at which we're looking // at it and the angle in which light goes: the grazier the angles, the more the // absorption since we're traveling further in the coat before leaving // // Reference: [11], [13] // // It can happen that 'incident_refracted_angle' or 'outgoing_refracted_angle' // are 0.0f float incident_refracted_angle = hippt::max(1.0e-6f, sqrtf(1.0f - (1.0f - local_to_light_direction.z * local_to_light_direction.z) / (coat_ior * coat_ior))); float outgoing_refracted_angle = hippt::max(1.0e-6f, sqrtf(1.0f - (1.0f - local_view_direction.z * local_view_direction.z) / (coat_ior * coat_ior))); // Reference: [11], [13] float traveled_distance_angle = 1.0f / incident_refracted_angle + 1.0f / outgoing_refracted_angle; ColorRGB32F coat_absorption = exp(-(ColorRGB32F(1.0f) - pow(sqrt(bsdf_context.material.coat_medium_absorption), traveled_distance_angle)) * bsdf_context.material.coat_medium_thickness); layer_below_attenuation *= coat_absorption; } layer_below_attenuation *= principled_coat_compute_darkening(bsdf_context.material, coat_ior / incident_ior, view_dir_fresnel); // If the coat layer has 0 weight, we should not get any light attenuation. // But if the coat layer has 1 weight, we should get the full attenuation that we // computed in 'layer_below_attenuation' so we're lerping between no attenuation // and full attenuation based on the material coat weight. layer_below_attenuation = hippt::lerp(ColorRGB32F(1.0f), layer_below_attenuation, bsdf_context.material.coat); layers_throughput *= layer_below_attenuation; return contribution; } return ColorRGB32F(0.0f); } HIPRT_HOST_DEVICE HIPRT_INLINE float internal_pdf_coat_layer(const HIPRTRenderData& render_data, const BSDFContext& bsdf_context, const float3& local_view_direction, const float3 local_to_light_direction, const float3& local_half_vector, float incident_ior, bool refracting, float coat_weight, float coat_proba) { // '|| refracting' here is needed because if we have our coat // lobe on top of the glass lobe, we want to still compute the portion // of light that is left for the glass lobe after going through the coat lobe // so that's why we get into to if() block that does the computation but // we're only going to compute the absorption of the coat layer float coat_ior = bsdf_context.material.coat_ior; if (coat_weight > 0.0f && ((local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) || refracting)) { float coat_pdf = 0.0f; ColorRGB32F contribution; if (!refracting) { // The coat layer only contribtues for light direction in the same // hemisphere as the view direction (so reflections only, not refractions) coat_pdf = principled_coat_pdf(render_data, bsdf_context, local_view_direction, local_to_light_direction, local_half_vector, incident_ior); } return coat_pdf * coat_proba; } return 0.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F internal_eval_sheen_layer(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& local_to_light_direction, bool refracting, float sheen_weight, float sheen_proba, ColorRGB32F& layers_throughput, float& out_cumulative_pdf) { if ((sheen_weight > 0.0f && local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) || refracting) { float sheen_reflectance; float sheen_pdf; ColorRGB32F contribution = principled_sheen_eval(render_data, material, local_view_direction, local_to_light_direction, sheen_pdf, sheen_reflectance); contribution *= sheen_weight; contribution *= layers_throughput; out_cumulative_pdf += sheen_pdf * sheen_proba; // Same as the coat layer for the sheen: only the refracted light goes into the layer below // // The proportion of light that is reflected is given by the Ri component of AiBiRi // (see 'sheen_ltc_eval') which is returned by 'principled_sheen_eval' in 'sheen_reflectance' layers_throughput *= 1.0f - material.sheen * sheen_reflectance; return contribution; } return ColorRGB32F(0.0f); } HIPRT_HOST_DEVICE HIPRT_INLINE float internal_pdf_sheen_layer(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& local_to_light_direction, bool refracting, float sheen_weight, float sheen_proba) { if ((sheen_weight > 0.0f && local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) || refracting) { float sheen_pdf = principled_sheen_pdf(render_data, material, local_view_direction, local_to_light_direction); return sheen_pdf * sheen_proba; } return 0.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F internal_eval_metal_layer(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float roughness, float anisotropy, const float3& local_view_direction, const float3 local_to_light_direction, const float3& local_half_vector, float incident_ior, float metal_weight, float metal_proba, const ColorRGB32F& layers_throughput, float& out_cumulative_pdf) { if (metal_weight > 0.0f && local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) { float metal_pdf = 0.0f; ColorRGB32F contribution; contribution = principled_metallic_eval(render_data, bsdf_context, roughness, anisotropy, incident_ior, local_view_direction, local_to_light_direction, local_half_vector, metal_pdf); contribution *= metal_weight; contribution *= layers_throughput; out_cumulative_pdf += metal_pdf * metal_proba; // There is nothing below the metal layer so we don't have a // layer_throughput attenuation here // ... return contribution; } return ColorRGB32F(0.0f); } HIPRT_HOST_DEVICE HIPRT_INLINE float internal_pdf_metal_layer(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float roughness, float anisotropy, const float3& local_view_direction, const float3 local_to_light_direction, const float3& local_half_vector, float incident_ior, float metal_weight, float metal_proba) { if (metal_weight > 0.0f && local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) { float metal_pdf = principled_metallic_pdf(render_data, bsdf_context, roughness, anisotropy, local_view_direction, local_to_light_direction, local_half_vector); return metal_pdf * metal_proba; } return 0.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F internal_eval_glass_layer(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, const float3& local_view_direction, const float3 local_to_light_direction, float glass_weight, float glass_proba, const ColorRGB32F& layers_throughput, float& out_cumulative_pdf) { if (glass_weight > 0.0f) { float glass_pdf = 0.0f; ColorRGB32F contribution; contribution = principled_glass_eval(render_data, bsdf_context, local_view_direction, local_to_light_direction, glass_pdf); contribution *= glass_weight; contribution *= layers_throughput; // There is nothing below the glass layer so we don't have a layer_throughput absorption here // ... out_cumulative_pdf += glass_pdf * glass_proba; return contribution; } return ColorRGB32F(0.0f); } HIPRT_HOST_DEVICE HIPRT_INLINE float internal_pdf_glass_layer(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, const float3& local_view_direction, const float3 local_to_light_direction, float glass_weight, float glass_proba) { if (glass_weight > 0.0f) { float glass_pdf = principled_glass_pdf(render_data, bsdf_context, local_view_direction, local_to_light_direction); // There is nothing below the glass layer so we don't have a layer_throughput absorption here // ... return glass_pdf * glass_proba; } return 0.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F internal_eval_diffuse_transmission_layer(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, RayVolumeState& ray_volume_state, bool update_ray_volume_state, const float3& local_view_direction, const float3 local_to_light_direction, float diffuse_transmission_weight, float diffuse_transmission_proba, const ColorRGB32F& layers_throughput, float& out_cumulative_pdf) { if (diffuse_transmission_weight > 0.0f && local_to_light_direction.z < 0.0f) { float diffuse_transmission_pdf = 0.0f; ColorRGB32F contribution = principled_diffuse_transmission_eval(render_data, material, ray_volume_state, update_ray_volume_state, local_view_direction, local_to_light_direction, diffuse_transmission_pdf); contribution *= diffuse_transmission_weight; contribution *= layers_throughput; // There is nothing below the diffuse transmission layer so we don't have a layer_throughput absorption here // ... out_cumulative_pdf += diffuse_transmission_pdf * diffuse_transmission_proba; return contribution; } return ColorRGB32F(0.0f); } HIPRT_HOST_DEVICE HIPRT_INLINE float internal_pdf_diffuse_transmission_layer(const float3& local_view_direction, const float3 local_to_light_direction, float diffuse_transmission_weight, float diffuse_transmission_proba) { if (diffuse_transmission_weight > 0.0f && local_to_light_direction.z < 0.0f) { float diffuse_transmission_pdf = principled_diffuse_transmission_pdf(local_view_direction, local_to_light_direction); return diffuse_transmission_pdf * diffuse_transmission_proba; } return 0.0f; } /** * Reference: * * [1] [Open PBR Specification - Coat Darkening] https://academysoftwarefoundation.github.io/OpenPBR/#model/coat/darkening * * 'relative_eta' must be coat_ior / incident_medium_ior * * This function computes the darkening/increase in saturation that happens * as light is trapped in the specular layer and bounces on the diffuse base. * * This is essentially the same function as 'principled_coat_compute_darkening' * but simplified since we know that only a diffuse base can be below the specular layer * * 'relative_eta' should be specular_ior / coat_ior (or divided by the incident * medium ior if there is no coating) */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_specular_compute_darkening(const DeviceUnpackedEffectiveMaterial& material, float relative_eta, float view_dir_fresnel) { if (material.specular_darkening == 0.0f) return ColorRGB32F(1.0f); // Fraction of light that exhibits total internal reflection inside the clearcoat layer, // assuming a perfectly diffuse base float Kr = 1.0f - (1.0f - fresnel_hemispherical_albedo_fit(relative_eta)) / (relative_eta * relative_eta); // Eq. 66 of OpenPBR // For the specular layer total internal reflection, we know that the base below is diffuse // so K is just Kr float K = Kr; // The base albedo is the albedo of the BSDF below the specular layer. // That's just the diffuse lobe so the base albedo is simple here. ColorRGB32F base_albedo = material.base_color; // This approximation of the amount of total internal reflection can then be used to // compute the darkening of the base caused by the clearcoating ColorRGB32F darkening = (1.0f - K) / (ColorRGB32F(1.0f) - base_albedo * K); // Disabling more or less the darkening based on: // - whether or not we have a specular layer at all // - whether or not we have specular darkening enabled at all or not // - whether or not we have a diffuse transmission lobe below the specular // layer, in which case there is no TIR between the diffuse // transmission lobe and the specular layer because the diffuse // transmission lobe is a BTDF only, it doesn't // reflect light --> no TIR --> no darkening darkening = hippt::lerp(ColorRGB32F(1.0f), darkening, material.specular * material.specular_darkening * (1.0f - material.diffuse_transmission)); return darkening; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F internal_eval_specular_layer(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, const float3& local_view_direction, const float3 local_to_light_direction, const float3& local_half_vector, const float3& shading_normal, float incident_medium_ior, float specular_weight, bool refracting, float specular_proba, ColorRGB32F& layers_throughput, float& out_cumulative_pdf) { // To even attempt the evaluation of the specular lobe, we need the specular weight to be non-zero // // We also need the view and light direction to be above the normal hemisphere because the specular layer // is a BRDF: reflections only. // // However, we may still want to compute the layer throughput of the specular layer if we're given an // incident light direction that comes from the diffuse transmission lobe: such a direction has to go through // the specular layer first before going through the diffuse transmission lobe // The microfacet BRDF will actually evaluate to 0 but the layer throughput will attenuate some light // // This applies to diffuse transmission but doesn't apply to glass though (for example) because the glass layer // isn't "below" the specular layer, it's "adjacent" to it. if (specular_weight > 0.0f && ((local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) || (refracting && bsdf_context.incident_light_info == BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_DIFFUSE_TRANSMISSION_LOBE))) { float relative_ior = principled_specular_relative_ior(bsdf_context.material, incident_medium_ior); float specular_pdf = 0.0f; ColorRGB32F contribution; contribution = principled_specular_eval(render_data, bsdf_context, relative_ior, local_view_direction, local_to_light_direction, local_half_vector, specular_pdf); // Tinting the specular reflection color contribution *= hippt::lerp(ColorRGB32F(1.0f), bsdf_context.material.specular_tint * bsdf_context.material.specular_color, bsdf_context.material.specular); contribution *= specular_weight; contribution *= layers_throughput; ColorRGB32F layer_below_attenuation(1.0f); // Only the transmitted portion of the light goes to the layer below // We're using the shading normal here and not the microfacet normal because: // We want the proportion of light that reaches the layer below. // That's given by 1.0f - fresnelReflection. // // But '1.0f - fresnelReflection' needs to be computed with the shading normal, // not the microfacet normal i.e. it needs to be 1.0f - Fresnel(dot(N, L)), // not 1.0f - Fresnel(dot(H, L)) // // By computing 1.0f - Fresnel(dot(H, L)), we're computing the light // that goes through only that one microfacet with the microfacet normal. But light // reaches the layer below through many other microfacets, not just the one with our current // micronormal here (local_half_vector). To compute this correctly, we would actually need // to integrate over the microfacet normals and compute the fresnel transmission portion // (1.0f - Fresnel(dot(H, L))) for each of them and weight that contribution by the // probability given by the normal distribution function for the microfacet normal. // // We can't do that integration online so we're instead using the shading normal to compute // the transmitted portion of light. That's actually either a good approximation or the // exact solution. That was shown in GDC 2017 [PBR Diffuse Lighting for GGX + Smith Microsurfaces] // // We need the hippt::abs() here because we may be evaluating the fresnel terms with light directions/view // directions that are below the surface because we're evaluating the specular lobe for a refracted direction ColorRGB32F light_dir_fresnel = principled_specular_fresnel(bsdf_context.material, relative_ior, hippt::abs(local_to_light_direction.z)); // If we have a diffuse transmission lobe below the specular instead of the diffuse lobe, then we cannot // have TIR in between the diffuse lobe and the specular lobe (inside the specular layer) because the diffuse // transmission lobe is a BTDF only, it doesn't reflect any light --> no TIR // // We're cancelling the light_dir_fresnel instead of the view_dir_fresnel (which is the one that models the TIR) // though because otherwise it seems to break, not sure why. The handling of fresnel effects when light is coming // from below the specular (or coat lobe) lobe isn't perfect yet light_dir_fresnel *= (1.0f - bsdf_context.material.diffuse_transmission); layer_below_attenuation *= ColorRGB32F(1.0f) - light_dir_fresnel; // Also, when light reflects off of the layer below the specular layer, some of that reflected light // will hit total internal reflection against the specular/[coat or air] interface. This means that only // the part of light that does not hit total internal reflection actually reaches the viewer. // // That's why we're computing another fresnel term here to account for that. And additional note: // computing that fresnel with the direction reflected from the base layer or with the viewer direction // is the same, Fresnel is symmetrical. But because we don't have the exact direction reflected from the // base layer, we're using the view direction instead ColorRGB32F view_dir_fresnel = principled_specular_fresnel(bsdf_context.material, relative_ior, hippt::abs(local_view_direction.z)); layer_below_attenuation *= ColorRGB32F(1.0f) - view_dir_fresnel; // Taking into account the total internal reflection inside the specular layer // (bouncing on the base diffuse layer). We're using the luminance of the fresnel here because // the specular layer may have thin film interference which colors the fresnel but // we're going to assume that the fresnel is non-colored and thus we just take the luminance layer_below_attenuation *= principled_specular_compute_darkening(bsdf_context.material, relative_ior, view_dir_fresnel.luminance()); // If the specular layer has 0 weight, we should not get any light absorption. // But if the specular layer has 1 weight, we should get the full absorption that we // computed in 'layer_below_attenuation' so we're lerping between no absorption // and full absorption based on the material specular weight. layer_below_attenuation = hippt::lerp(ColorRGB32F(1.0f), layer_below_attenuation, bsdf_context.material.specular); layers_throughput *= layer_below_attenuation; out_cumulative_pdf += specular_pdf * specular_proba; return contribution; } return ColorRGB32F(0.0f); } HIPRT_HOST_DEVICE HIPRT_INLINE float internal_pdf_specular_layer(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, const float3& local_view_direction, const float3 local_to_light_direction, const float3& local_half_vector, const float3& shading_normal, float incident_medium_ior, float specular_weight, bool refracting, float specular_proba) { // To even attempt the evaluation of the specular lobe, we need the specular weight to be non-zero // // We also need the view and light direction to be above the normal hemisphere because the specular layer // is a BRDF: reflections only. // // However, we may still want to compute the layer throughput of the specular layer if we're given an // incident light direction that comes from the diffuse transmission lobe: such a direction has to go through // the specular layer first before going through the diffuse transmission lobe // The microfacet BRDF will actually evaluate to 0 but the layer throughput will attenuate some light // // This applies to diffuse transmission but doesn't apply to glass though (for example) because the glass layer // isn't "below" the specular layer, it's "adjacent" to it. if (specular_weight > 0.0f && ((local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) || (refracting && bsdf_context.incident_light_info == BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_DIFFUSE_TRANSMISSION_LOBE))) { float relative_ior = principled_specular_relative_ior(bsdf_context.material, incident_medium_ior); float specular_pdf = principled_specular_pdf(render_data, bsdf_context, relative_ior, local_view_direction, local_to_light_direction, local_half_vector); return specular_pdf * specular_proba; } return 0.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F internal_eval_diffuse_layer(const HIPRTRenderData& render_data, float incident_ior, const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3 local_to_light_direction, float diffuse_weight, float diffuse_proba, ColorRGB32F& layers_throughput, float& out_cumulative_pdf) { if (diffuse_weight > 0.0f && local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) { float diffuse_pdf; ColorRGB32F contribution = principled_diffuse_eval(material, local_view_direction, local_to_light_direction, diffuse_pdf); contribution *= diffuse_weight; contribution *= layers_throughput; // Nothing below the diffuse layer so we don't have a layer throughput // attenuation here out_cumulative_pdf += diffuse_pdf * diffuse_proba; return contribution; } return ColorRGB32F(0.0f); } HIPRT_HOST_DEVICE HIPRT_INLINE float internal_pdf_diffuse_layer(const HIPRTRenderData& render_data, float incident_ior, const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3 local_to_light_direction, float diffuse_weight, float diffuse_proba) { if (diffuse_weight > 0.0f && local_view_direction.z > 0.0f && local_to_light_direction.z > 0.0f) { float diffuse_pdf = principled_diffuse_pdf(material, local_view_direction, local_to_light_direction); return diffuse_pdf * diffuse_proba; } return 0.0f; } /** * The "glossy base" is the combination of a specular GGX layer * on top of a diffuse BRDF. */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F internal_eval_glossy_base(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, const float3& local_view_direction, const float3 local_to_light_direction, const float3& local_half_vector, const float3& local_view_direction_rotated, const float3 local_to_light_direction_rotated, const float3& local_half_vector_rotated, const float3& shading_normal, float incident_medium_ior, float diffuse_weight, float specular_weight, bool refracting, float diffuse_proba_norm, float specular_proba_norm, ColorRGB32F& layers_throughput, float& out_cumulative_pdf) { ColorRGB32F glossy_base_contribution = ColorRGB32F(0.0f); // Evaluating the two components of the glossy base glossy_base_contribution += internal_eval_specular_layer(render_data, bsdf_context, local_view_direction_rotated, local_to_light_direction_rotated, local_half_vector_rotated, shading_normal, incident_medium_ior, specular_weight, refracting, specular_proba_norm, layers_throughput, out_cumulative_pdf); glossy_base_contribution += internal_eval_diffuse_layer(render_data, incident_medium_ior, bsdf_context.material, local_view_direction, local_to_light_direction, diffuse_weight, diffuse_proba_norm, layers_throughput, out_cumulative_pdf); float glossy_base_energy_compensation = get_principled_energy_compensation_glossy_base(render_data, bsdf_context.material, incident_medium_ior, local_view_direction.z, bsdf_context.current_bounce); return glossy_base_contribution / glossy_base_energy_compensation; } HIPRT_HOST_DEVICE HIPRT_INLINE float internal_pdf_glossy_base(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, const float3& local_view_direction, const float3 local_to_light_direction, const float3& local_half_vector, const float3& local_view_direction_rotated, const float3 local_to_light_direction_rotated, const float3& local_half_vector_rotated, const float3& shading_normal, float incident_medium_ior, float diffuse_weight, float specular_weight, bool refracting, float diffuse_proba_norm, float specular_proba_norm) { float pdf = 0.0f; // Evaluating the two components of the glossy base pdf += internal_pdf_specular_layer(render_data, bsdf_context, local_view_direction_rotated, local_to_light_direction_rotated, local_half_vector_rotated, shading_normal, incident_medium_ior, specular_weight, refracting, specular_proba_norm); pdf += internal_pdf_diffuse_layer(render_data, incident_medium_ior, bsdf_context.material, local_view_direction, local_to_light_direction, diffuse_weight, diffuse_proba_norm); return pdf; } /** * Computes the lobes weights for the principled BSDF */ HIPRT_HOST_DEVICE HIPRT_INLINE void principled_bsdf_get_lobes_weights(const DeviceUnpackedEffectiveMaterial& material, bool outside_object, float& out_coat_weight, float& out_sheen_weight, float& out_metal_1_weight, float& out_metal_2_weight, float& out_specular_weight, float& out_diffuse_weight, float& out_glass_weight, float& out_diffuse_transmission_weight) { // Linear blending weights for the lobes // // Everytime we multiply by "outside_object" is because we want to disable // the lobe if we're inside the object // // The layering follows the one of the principled BSDF of blender: // [10] https://docs.blender.org/manual/fr/dev/render/shader_nodes/shader/principled.html out_coat_weight = material.coat * outside_object; out_sheen_weight = material.sheen * outside_object; // Metal 1 and metal 2 are the two metallic lobes for the two roughnesses. // Having 2 roughnesses (linearly blended together) can enable interesting effects // that cannot be achieved with a single GGX metal lobe. // // See [Revisiting Physically Based Shading at Imageworks, Kulla & Conty, SIGGRAPH 2017], // "Double Specular" for more details float metallic = material.metallic; out_metal_1_weight = metallic * outside_object; out_metal_2_weight = metallic * outside_object; float second_roughness_weight = material.second_roughness_weight; out_metal_1_weight = hippt::lerp(out_metal_1_weight, 0.0f, second_roughness_weight); out_metal_2_weight = hippt::lerp(0.0f, out_metal_2_weight, second_roughness_weight); float specular_transmission = material.specular_transmission; float diffuse_transmission = material.diffuse_transmission; out_glass_weight = !outside_object ? (1.0f - diffuse_transmission) : (1.0f - metallic) * (1.0f - diffuse_transmission) * specular_transmission; out_diffuse_transmission_weight = !outside_object ? diffuse_transmission : (1.0f - metallic) * diffuse_transmission; out_specular_weight = (1.0f - metallic) * (1.0f - specular_transmission * (1.0f - diffuse_transmission)) * material.specular * outside_object; out_diffuse_weight = (1.0f - metallic) * (1.0f - specular_transmission) * (1.0f - diffuse_transmission) * outside_object; } HIPRT_HOST_DEVICE HIPRT_INLINE void principled_bsdf_get_lobes_sampling_proba(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, float NoV, float incident_medium_ior, float coat_weight, float sheen_weight, float metal_1_weight, float metal_2_weight, float specular_weight, float diffuse_weight, float glass_weight, float diffuse_transmission_weight, float& out_coat_sampling_proba, float& out_sheen_sampling_proba, float& out_metal_1_sampling_proba, float& out_metal_2_sampling_proba, float& out_specular_sampling_proba, float& out_diffuse_sampling_proba, float& out_glass_sampling_proba, float& out_diffuse_transmission_sampling_proba) { #if PrincipledBSDFSampleGlossyBasedOnFresnel == KERNEL_OPTION_TRUE // Adjusting the probability of sampling the diffuse or specular lobe based on the // fresnel of the specular lobe if (material.specular > 0.0f) { float specular_relative_ior = principled_specular_relative_ior(material, incident_medium_ior); float specular_fresnel = full_fresnel_dielectric(NoV, specular_relative_ior); float specular_fresnel_sampling_weight = specular_fresnel * material.specular; // The specular weight gets affected specular_weight *= specular_fresnel_sampling_weight; // And everything that is below the specular also gets affected diffuse_weight *= 1.0f - specular_fresnel_sampling_weight; diffuse_transmission_weight *= 1.0f - specular_fresnel_sampling_weight; } #endif #if PrincipledBSDFSampleCoatBasedOnFresnel == KERNEL_OPTION_TRUE if (material.coat > 0.0f) { float coat_fresnel = full_fresnel_dielectric(NoV, material.coat_ior / incident_medium_ior); float coat_fresnel_sampling_weight = coat_fresnel * material.coat; // The coat weight gets affected coat_weight *= coat_fresnel_sampling_weight; // And everything that is below the coat also gets affected sheen_weight *= 1.0f - coat_fresnel_sampling_weight; metal_1_weight *= 1.0f - coat_fresnel_sampling_weight; metal_2_weight *= 1.0f - coat_fresnel_sampling_weight; specular_weight *= 1.0f - coat_fresnel_sampling_weight; diffuse_weight *= 1.0f - coat_fresnel_sampling_weight; glass_weight *= 1.0f - coat_fresnel_sampling_weight; diffuse_transmission_weight *= 1.0f - coat_fresnel_sampling_weight; } #endif float normalize_factor = 1.0f / (coat_weight + sheen_weight + metal_1_weight + metal_2_weight + specular_weight + diffuse_weight + glass_weight + diffuse_transmission_weight); out_coat_sampling_proba = coat_weight * normalize_factor; out_sheen_sampling_proba = sheen_weight * normalize_factor; out_metal_1_sampling_proba = metal_1_weight * normalize_factor; out_metal_2_sampling_proba = metal_2_weight * normalize_factor; out_specular_sampling_proba = specular_weight * normalize_factor; out_diffuse_sampling_proba = diffuse_weight * normalize_factor; out_glass_sampling_proba = glass_weight * normalize_factor; out_diffuse_transmission_sampling_proba = diffuse_transmission_weight * normalize_factor; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_bsdf_eval(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float& pdf) { pdf = 0.0f; // Only the glass lobe is considered when evaluating // the BSDF from inside the object so we're going to use that // 'outside_object' flag to nullify the other lobes if we're // inside the object // // Note that we're always outside of thin materials, they have no volume interior bool outside_object = !bsdf_context.volume_state.inside_material; bool refracting = hippt::dot(bsdf_context.shading_normal, bsdf_context.to_light_direction) < 0.0f && outside_object; float3 T, B; build_ONB(bsdf_context.shading_normal, T, B); float3 local_view_direction = world_to_local_frame(T, B, bsdf_context.shading_normal, bsdf_context.view_direction); float3 local_to_light_direction = world_to_local_frame(T, B, bsdf_context.shading_normal, bsdf_context.to_light_direction); float3 local_half_vector = hippt::normalize(local_view_direction + local_to_light_direction); // Rotated ONB for the anisotropic GGX evaluation (metallic/glass lobes for example) float3 TR, BR; build_rotated_ONB(bsdf_context.shading_normal, TR, BR, bsdf_context.material.anisotropy_rotation * M_PI); float3 local_view_direction_rotated = world_to_local_frame(TR, BR, bsdf_context.shading_normal, bsdf_context.view_direction); float3 local_to_light_direction_rotated = world_to_local_frame(TR, BR, bsdf_context.shading_normal, bsdf_context.to_light_direction); float3 local_half_vector_rotated = hippt::normalize(local_view_direction_rotated + local_to_light_direction_rotated); float incident_medium_ior = bsdf_context.volume_state.incident_mat_index == /* air */ NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX ? 1.0f : render_data.buffers.materials_buffer.get_ior(bsdf_context.volume_state.incident_mat_index); float coat_weight, sheen_weight, metal_1_weight, metal_2_weight; float specular_weight, diffuse_weight, glass_weight, diffuse_transmission_weight; principled_bsdf_get_lobes_weights(bsdf_context.material, outside_object, coat_weight, sheen_weight, metal_1_weight, metal_2_weight, specular_weight, diffuse_weight, glass_weight, diffuse_transmission_weight); float coat_proba, sheen_proba, metal_1_proba, metal_2_proba; float specular_proba, diffuse_proba, glass_proba, diffuse_transmission_proba; principled_bsdf_get_lobes_sampling_proba(render_data, bsdf_context.material, local_view_direction.z, incident_medium_ior, coat_weight, sheen_weight, metal_1_weight, metal_2_weight, specular_weight, diffuse_weight, glass_weight, diffuse_transmission_weight, coat_proba, sheen_proba, metal_1_proba, metal_2_proba, specular_proba, diffuse_proba, glass_proba, diffuse_transmission_proba); // Keeps track of the remaining light's energy as we traverse layers ColorRGB32F layers_throughput = ColorRGB32F(1.0f); ColorRGB32F final_color = ColorRGB32F(0.0f); // In the 'internal_eval_coat_layer' function calls below, we're passing // 'weight * !refracting' so that lobes that do not allow refractions // (which is pretty much all of them except glass) do no get evaluated // (because their weight becomes 0) final_color += internal_eval_coat_layer(render_data, bsdf_context, local_view_direction, local_to_light_direction, local_half_vector, incident_medium_ior, refracting, coat_weight, coat_proba, layers_throughput, pdf); final_color += internal_eval_sheen_layer(render_data, bsdf_context.material, local_view_direction, local_to_light_direction, refracting, sheen_weight, sheen_proba, layers_throughput, pdf); final_color += internal_eval_metal_layer(render_data, bsdf_context, bsdf_context.material.roughness, bsdf_context.material.anisotropy, local_view_direction_rotated, local_to_light_direction_rotated, local_half_vector_rotated, incident_medium_ior, metal_1_weight * !refracting, metal_1_proba, layers_throughput, pdf); final_color += internal_eval_metal_layer(render_data, bsdf_context, bsdf_context.material.second_roughness, bsdf_context.material.anisotropy, local_view_direction_rotated, local_to_light_direction_rotated, local_half_vector_rotated, incident_medium_ior, metal_2_weight * !refracting, metal_2_proba, layers_throughput, pdf); // Careful here to evaluate the glass layer before the glossy // base otherwise, layers_throughput is going to be modified // by the specular layer evaluation (in the glossy base) to // take the fresnel of the specular layer into account. // But we don't want that for the glass layer. // The glass layer isn't below the specular layer , it's "next to" // the specular layer so we don't want the specular-layer-fresnel-attenuation // there final_color += internal_eval_glass_layer(render_data, bsdf_context, local_view_direction_rotated, local_to_light_direction_rotated, glass_weight, glass_proba, layers_throughput, pdf); final_color += internal_eval_glossy_base(render_data, bsdf_context, local_view_direction, local_to_light_direction, local_half_vector, local_view_direction_rotated, local_to_light_direction_rotated, local_half_vector_rotated, bsdf_context.shading_normal, incident_medium_ior, diffuse_weight * !refracting, specular_weight, refracting, diffuse_proba, specular_proba, layers_throughput, pdf); final_color += internal_eval_diffuse_transmission_layer(render_data, bsdf_context.material, bsdf_context.volume_state, bsdf_context.update_ray_volume_state, local_view_direction, local_to_light_direction, diffuse_transmission_weight, diffuse_transmission_proba, layers_throughput, pdf); // The clearcoat compensation is done here and not in the clearcoat function // because the clearcoat sits on top of everything else. This means that the clearcoat // closure contains the full BSDF below. So the full BSDF below + the clearcoat (= the whole BSDF actually) // should be compensated, not just the clearcoat lobe. So that's why we're doing // it here, after the full BSDF evaluation so that everything gets compensated final_color /= get_principled_energy_compensation_clearcoat_lobe(render_data, bsdf_context.material, incident_medium_ior, local_view_direction.z, bsdf_context.current_bounce); // TODO compare CPU rendering with and without sanity_check(render_data, final_color, 0, 0); return final_color; } HIPRT_HOST_DEVICE HIPRT_INLINE float principled_bsdf_pdf(const HIPRTRenderData& render_data, BSDFContext& bsdf_context) { float pdf = 0.0f; // Only the glass lobe is considered when evaluating // the BSDF from inside the object so we're going to use that // 'outside_object' flag to nullify the other lobes if we're // inside the object // // Note that we're always outside of thin materials, they have no volume interior bool outside_object = !bsdf_context.volume_state.inside_material; bool refracting = hippt::dot(bsdf_context.shading_normal, bsdf_context.to_light_direction) < 0.0f && outside_object; float3 T, B; build_ONB(bsdf_context.shading_normal, T, B); float3 local_view_direction = world_to_local_frame(T, B, bsdf_context.shading_normal, bsdf_context.view_direction); float3 local_to_light_direction = world_to_local_frame(T, B, bsdf_context.shading_normal, bsdf_context.to_light_direction); float3 local_half_vector = hippt::normalize(local_view_direction + local_to_light_direction); // Rotated ONB for the anisotropic GGX evaluation (metallic/glass lobes for example) float3 TR, BR; build_rotated_ONB(bsdf_context.shading_normal, TR, BR, bsdf_context.material.anisotropy_rotation * M_PI); float3 local_view_direction_rotated = world_to_local_frame(TR, BR, bsdf_context.shading_normal, bsdf_context.view_direction); float3 local_to_light_direction_rotated = world_to_local_frame(TR, BR, bsdf_context.shading_normal, bsdf_context.to_light_direction); float3 local_half_vector_rotated = hippt::normalize(local_view_direction_rotated + local_to_light_direction_rotated); float incident_medium_ior = bsdf_context.volume_state.incident_mat_index == /* air */ NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX ? 1.0f : render_data.buffers.materials_buffer.get_ior(bsdf_context.volume_state.incident_mat_index); float coat_weight, sheen_weight, metal_1_weight, metal_2_weight; float specular_weight, diffuse_weight, glass_weight, diffuse_transmission_weight; principled_bsdf_get_lobes_weights(bsdf_context.material, outside_object, coat_weight, sheen_weight, metal_1_weight, metal_2_weight, specular_weight, diffuse_weight, glass_weight, diffuse_transmission_weight); float coat_proba, sheen_proba, metal_1_proba, metal_2_proba; float specular_proba, diffuse_proba, glass_proba, diffuse_transmission_proba; principled_bsdf_get_lobes_sampling_proba(render_data, bsdf_context.material, local_view_direction.z, incident_medium_ior, coat_weight, sheen_weight, metal_1_weight, metal_2_weight, specular_weight, diffuse_weight, glass_weight, diffuse_transmission_weight, coat_proba, sheen_proba, metal_1_proba, metal_2_proba, specular_proba, diffuse_proba, glass_proba, diffuse_transmission_proba); // In the 'internal_eval_coat_layer' function calls below, we're passing // 'weight * !refracting' so that lobes that do not allow refractions // (which is pretty much all of them except glass) do no get evaluated // (because their weight becomes 0) pdf += internal_pdf_coat_layer(render_data, bsdf_context, local_view_direction, local_to_light_direction, local_half_vector, incident_medium_ior, refracting, coat_weight, coat_proba); pdf += internal_pdf_sheen_layer(render_data, bsdf_context.material, local_view_direction, local_to_light_direction, refracting, sheen_weight, sheen_proba); pdf += internal_pdf_metal_layer(render_data, bsdf_context, bsdf_context.material.roughness, bsdf_context.material.anisotropy, local_view_direction_rotated, local_to_light_direction_rotated, local_half_vector_rotated, incident_medium_ior, metal_1_weight * !refracting, metal_1_proba); pdf += internal_pdf_metal_layer(render_data, bsdf_context, bsdf_context.material.second_roughness, bsdf_context.material.anisotropy, local_view_direction_rotated, local_to_light_direction_rotated, local_half_vector_rotated, incident_medium_ior, metal_2_weight * !refracting, metal_2_proba); // Careful here to evaluate the glass layer before the glossy // base otherwise, layers_throughput is going to be modified // by the specular layer evaluation (in the glossy base) to // take the fresnel of the specular layer into account. // But we don't want that for the glass layer. // The glass layer isn't below the specular layer , it's "next to" // the specular layer so we don't want the specular-layer-fresnel-attenuation // there pdf += internal_pdf_glass_layer(render_data, bsdf_context, local_view_direction_rotated, local_to_light_direction_rotated, glass_weight, glass_proba); pdf += internal_pdf_glossy_base(render_data, bsdf_context, local_view_direction, local_to_light_direction, local_half_vector, local_view_direction_rotated, local_to_light_direction_rotated, local_half_vector_rotated, bsdf_context.shading_normal, incident_medium_ior, diffuse_weight * !refracting, specular_weight, refracting, diffuse_proba, specular_proba); pdf += internal_pdf_diffuse_transmission_layer(local_view_direction, local_to_light_direction, diffuse_transmission_weight, diffuse_transmission_proba); return pdf; } /** * If sampleDirectionOnly is 'true',, this function samples only the BSDF without * evaluating the contribution or the PDF of the BSDF. This function will then always return * ColorRGB32F(0.0f) and the 'pdf' out parameter will always be set to 0.0f */ template HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F principled_bsdf_sample(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float3& output_direction, float& pdf, Xorshift32Generator& random_number_generator) { pdf = 0.0f; // Computing the weights for sampling the lobes bool is_outside_object = !bsdf_context.volume_state.inside_material; float coat_sampling_weight; float sheen_sampling_weight; float metal_1_sampling_weight; float metal_2_sampling_weight; float specular_sampling_weight; float diffuse_sampling_weight; float glass_sampling_weight; float diffuse_transmission_weight; principled_bsdf_get_lobes_weights(bsdf_context.material, is_outside_object, coat_sampling_weight, sheen_sampling_weight, metal_1_sampling_weight, metal_2_sampling_weight, specular_sampling_weight, diffuse_sampling_weight, glass_sampling_weight, diffuse_transmission_weight); float coat_sampling_proba, sheen_sampling_proba, metal_1_sampling_proba; float metal_2_sampling_proba, specular_sampling_proba, diffuse_sampling_proba; float glass_sampling_proba, diffuse_transmission_sampling_proba; float incident_medium_ior = bsdf_context.volume_state.incident_mat_index == /* air */ NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX ? 1.0f : render_data.buffers.materials_buffer.get_ior(bsdf_context.volume_state.incident_mat_index); principled_bsdf_get_lobes_sampling_proba(render_data, bsdf_context.material, hippt::dot(bsdf_context.view_direction, bsdf_context.shading_normal), incident_medium_ior, coat_sampling_weight, sheen_sampling_weight, metal_1_sampling_weight, metal_2_sampling_weight, specular_sampling_weight, diffuse_sampling_weight, glass_sampling_weight, diffuse_transmission_weight, coat_sampling_proba, sheen_sampling_proba, metal_1_sampling_proba, metal_2_sampling_proba, specular_sampling_proba, diffuse_sampling_proba, glass_sampling_proba, diffuse_transmission_sampling_proba); // Not using a float[] array here because array[] are super poorly handled // in general by the HIP compiler on AMD float cdf0 = coat_sampling_proba; float cdf1 = cdf0 + sheen_sampling_proba; float cdf2 = cdf1 + metal_1_sampling_proba; float cdf3 = cdf2 + metal_2_sampling_proba; float cdf4 = cdf3 + specular_sampling_proba; float cdf5 = cdf4 + diffuse_sampling_proba; float cdf6 = cdf5 + diffuse_transmission_sampling_proba; // The last cdf[] is implicitely 1.0f so don't need to include it float rand_1 = random_number_generator(); bool sampling_diffuse_transmission_lobe = rand_1 > cdf5 && rand_1 < cdf6; bool sampling_glass_lobe = rand_1 > cdf6; if (bsdf_context.update_ray_volume_state) if (!sampling_glass_lobe && !sampling_diffuse_transmission_lobe) // We're going to sample a reflective lobe so we're poping the stack // // Note that we may also reflect from glass but the popping for that is done in glass_sample() bsdf_context.volume_state.interior_stack.pop(false); // Rotated ONB for the anisotropic GGX evaluation float3 TR, BR; build_rotated_ONB(bsdf_context.shading_normal, TR, BR, bsdf_context.material.anisotropy_rotation * M_PI); float3 local_view_direction_rotated = world_to_local_frame(TR, BR, bsdf_context.shading_normal, bsdf_context.view_direction); if (rand_1 < cdf0) { // Sampling the coat lobe float3 TR_coat, BR_coat; build_rotated_ONB(bsdf_context.shading_normal, TR_coat, BR_coat, bsdf_context.material.coat_anisotropy_rotation * M_PI); float3 local_view_direction_rotated_coat = world_to_local_frame(TR_coat, BR_coat, bsdf_context.shading_normal, bsdf_context.view_direction); // Giving some information about what the BSDF sampled to the caller bsdf_context.incident_light_info = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_COAT_LOBE; output_direction = local_to_world_frame(TR_coat, BR_coat, bsdf_context.shading_normal, principled_coat_sample(render_data, bsdf_context, local_view_direction_rotated_coat, random_number_generator)); } else if (rand_1 < cdf1) { // Sampling the sheen lobe float3 T, B; build_ONB(bsdf_context.shading_normal, T, B); float3 local_view_direction = world_to_local_frame(T, B, bsdf_context.shading_normal, bsdf_context.view_direction); output_direction = local_to_world_frame(T, B, bsdf_context.shading_normal, principled_sheen_sample(render_data, bsdf_context.material, local_view_direction, bsdf_context.shading_normal, random_number_generator)); } else if (rand_1 < cdf2) { // First metallic lobe sample bsdf_context.incident_light_info = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_FIRST_METAL_LOBE; output_direction = local_to_world_frame(TR, BR, bsdf_context.shading_normal, principled_metallic_sample(render_data, bsdf_context, bsdf_context.material.roughness, bsdf_context.material.anisotropy, local_view_direction_rotated, random_number_generator)); } else if (rand_1 < cdf3) { // Second metallic lobe sample bsdf_context.incident_light_info = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_SECOND_METAL_LOBE; output_direction = local_to_world_frame(TR, BR, bsdf_context.shading_normal, principled_metallic_sample(render_data, bsdf_context, bsdf_context.material.second_roughness, bsdf_context.material.anisotropy, local_view_direction_rotated, random_number_generator)); } else if (rand_1 < cdf4) { // Sampling the specular lobe bsdf_context.incident_light_info = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_SPECULAR_LOBE; output_direction = local_to_world_frame(TR, BR, bsdf_context.shading_normal, principled_specular_sample(render_data, bsdf_context, bsdf_context.material.roughness, bsdf_context.material.anisotropy, local_view_direction_rotated, random_number_generator)); } else if (rand_1 < cdf5) { // No call to local_to_world_frame() since the sample diffuse functions // already returns in world space around the given normal bsdf_context.incident_light_info = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_DIFFUSE_LOBE; output_direction = principled_diffuse_sample(bsdf_context.shading_normal, random_number_generator); } else if (rand_1 < cdf6) { // Diffuse transmission lobe bsdf_context.incident_light_info = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_DIFFUSE_TRANSMISSION_LOBE; output_direction = principled_diffuse_transmission_sample(bsdf_context.shading_normal, random_number_generator); } else // When sampling the glass lobe, if we're reflecting off the glass, we're going to have to pop the stack. // This is handled inside glass_sample because we cannot know from here if we refracted or reflected output_direction = local_to_world_frame(TR, BR, bsdf_context.shading_normal, principled_glass_sample(render_data, bsdf_context, local_view_direction_rotated, random_number_generator)); if (hippt::dot(output_direction, bsdf_context.geometric_normal) < 0.0f && !sampling_glass_lobe && !sampling_diffuse_transmission_lobe) // It can happen that the light direction sampled is below the geometric surface. // // We return 0.0 in this case if we didn't sample the glass lobe // because no lobe other than the glass lobe (or diffuse transmission) allows refractions return ColorRGB32F(0.0f); // Just copying the context to add the incident light info bsdf_context.to_light_direction = output_direction; if constexpr (sampleDirectionOnly) { pdf = 0.0f; return ColorRGB32F(0.0f); } else return principled_bsdf_eval(render_data, bsdf_context, pdf); } #endif ================================================ FILE: src/Device/includes/BSDFs/PrincipledEnergyCompensation.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_PRINCIPLED_ENERGY_COMPENSATION_H #define DEVICE_PRINCIPLED_ENERGY_COMPENSATION_H #include "Device/includes/BSDFs/BSDFContext.h" #include "HostDeviceCommon/Color.h" HIPRT_HOST_DEVICE HIPRT_INLINE float principled_specular_relative_ior(const DeviceUnpackedEffectiveMaterial& material, float incident_medium_ior); HIPRT_HOST_DEVICE HIPRT_INLINE float get_principled_energy_compensation_glossy_base(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, float incident_medium_ior, float NoV, int current_bounce) { bool energy_compensation_disabled = !material.do_specular_energy_compensation; bool roughness_low_enough = material.roughness < render_data.bsdfs_data.energy_compensation_roughness_threshold; // If all we have for the glossy base is the diffuse layer (i.e. no specular // layer because the specular weight is low, then we don't need energy compensation) bool no_specular_layer = material.specular < 1.0e-3f; bool max_bounce_reached = current_bounce > render_data.bsdfs_data.glossy_base_energy_compensation_max_bounce && render_data.bsdfs_data.glossy_base_energy_compensation_max_bounce > -1; bool invalid_view_direction = NoV < 0.0f; if (energy_compensation_disabled || roughness_low_enough || no_specular_layer || max_bounce_reached || invalid_view_direction) return 1.0f; float ms_compensation = 1.0f; #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoSpecularEnergyCompensation == KERNEL_OPTION_TRUE int3 texture_dims = make_int3(GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_COS_THETA_O, GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_ROUGHNESS, GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_IOR); float ior = material.ior; float relative_ior = principled_specular_relative_ior(material, incident_medium_ior); if (hippt::abs(relative_ior - 1.0f) < 1.0e-3f) // If the relative ior is very close to 1.0f, // adding some offset to avoid singularities at 1.0f which cause // fireflies relative_ior += 1.0e-3f; // We're storing cos_theta_o^2.5 in the LUT so we're retrieving with // root 2.5 float view_dir_remapped = pow(NoV, 1.0f / 2.5f); // sqrt(sqrt(F0)) here because we're storing F0^4 in the LUT float F0_remapped = sqrt(sqrt(F0_from_eta_t_and_relative_ior(ior, relative_ior))); float3 uvw = make_float3(view_dir_remapped, material.roughness, F0_remapped); float multiple_scattering_compensation = sample_texture_3D_rgb_32bits(render_data.bsdfs_data.glossy_dielectric_directional_albedo, texture_dims, uvw, render_data.bsdfs_data.use_hardware_tex_interpolation).r; // Applying the compensation term for energy preservation // If material.specular == 1, then we want the full energy compensation // If material.specular == 0, then we only have the diffuse lobe and so we // need no energy compensation at all and so we just divide by 1 to basically do nothing ms_compensation = hippt::lerp(1.0f, multiple_scattering_compensation, material.specular); // Multi scatter compensation is not tabulated to take thin film interference into account. // That's because thin film interference completely modifies the fresnel term and the // tabulated multi scatter compensation only accounts for the usual dielectric fresnel // // So we're progressively disabling ms compensation on the glossy base as the thin-film // is more and more pronounced ms_compensation = hippt::lerp(ms_compensation, 1.0f, material.thin_film); #endif return ms_compensation; } /** * This function gives an approximation of the energy lost by the clearcoat layer * by assuming that whatever is under the clearcoat is lambertian (which *may* * obviously be a very rough approximation, depending on what's the BSDF below the clearcoat) * * This basically treats the clearcoat layer exactly the same as a specular/diffuse * (just like the "glossy base" of the principled BSDF) and so that's why we're using the LUTs * of the glossy base * * The approximation can be harsh but in reasonable scenarios (where we're clearcoating something * quite diffuse: which is usually the case because WHO CLEARCOATS A MIRROR?), it's actually quite good and * it's way better than nothing and cheap compared to the full on-the-fly integration that we * would have to do otherwise (or full interlayer-multiple-scattering simulation) */ HIPRT_HOST_DEVICE HIPRT_INLINE float get_principled_energy_compensation_clearcoat_lobe(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, float incident_medium_ior, float NoV, int current_bounce) { bool energy_compensation_disabled = !material.do_specular_energy_compensation; // If we don't have a clearcoat, let's not compensate energy bool no_coat_layer = material.coat < 1.0e-3f; bool max_bounce_reached = current_bounce > render_data.bsdfs_data.clearcoat_energy_compensation_max_bounce && render_data.bsdfs_data.clearcoat_energy_compensation_max_bounce > -1; bool invalid_view_direction = NoV < 0.0f; if (energy_compensation_disabled || no_coat_layer || max_bounce_reached || invalid_view_direction) return 1.0f; float ms_compensation = 1.0f; #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoClearcoatEnergyCompensation == KERNEL_OPTION_TRUE int3 texture_dims = make_int3(GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_COS_THETA_O, GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_ROUGHNESS, GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_IOR); if (hippt::abs(material.coat_ior / incident_medium_ior - 1.0f) < 1.0e-3f) // If the relative ior is very close to 1.0f, // adding some offset to avoid singularities which cause // fireflies incident_medium_ior += 1.0e-3f; // We're storing cos_theta_o^2.5 in the LUT so we're retrieving with // root 2.5 float view_dir_remapped = pow(NoV, 1.0f / 2.5f); // sqrt(sqrt(F0)) here because we're storing F0^4 in the LUT float F0_remapped = sqrt(sqrt(F0_from_eta(material.coat_ior, incident_medium_ior))); float3 uvw = make_float3(view_dir_remapped, material.coat_roughness, F0_remapped); float multiple_scattering_compensation = sample_texture_3D_rgb_32bits(render_data.bsdfs_data.glossy_dielectric_directional_albedo, texture_dims, uvw, render_data.bsdfs_data.use_hardware_tex_interpolation).r; // Applying the compensation term for energy preservation // If material.coat == 1, then we want the full energy compensation // If material.coat == 0, then we only have the diffuse lobe and so we // need no energy compensation at all and so we just divide by 1 to basically do nothing // // We're also disabling the compensation when the clearcoat is on top of a glass // transmission lobe because the approximation here falls apart and can gain quite a bit // of energy. ms_compensation = hippt::lerp(1.0f, multiple_scattering_compensation, material.coat * (1.0f - material.specular_transmission)); // Multi scatter compensation is not tabulated to take thin film interference into account. // That's because thin film interference completely modifies the fresnel term and the // tabulated multi scatter compensation only accounts for the usual dielectric fresnel // // So we're progressively disabling ms compensation on the glossy base as the thin-film // is more and more pronounced ms_compensation = hippt::lerp(ms_compensation, 1.0f, material.thin_film); #endif return ms_compensation; } #endif ================================================ FILE: src/Device/includes/BSDFs/SheenLTC.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_BSDFS_SHEEN_LTC #define DEVICE_INCLUDES_BSDFS_SHEEN_LTC #include "Device/includes/BSDFs/SheenLTCFittedParameters.h" #include "Device/includes/Texture.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" #include "HostDeviceCommon/RenderData.h" /** * Reference: * * [1] [Practical Multiple-Scattering Sheen Using Linearly Transformed Cosines] https://tizianzeltner.com/projects/Zeltner2022Practical/ * [2] [Real-Time Polygonal-Light Shading with Linearly Transformed Cosines] https://eheitzresearch.wordpress.com/415-2/ * [3] [Blender's Cycles Implementation] https://github.com/blender/cycles/blob/main/src/kernel/closure/bsdf_sheen.h */ HIPRT_DEVICE HIPRT_INLINE float eval_ltc(const float3& to_light_direction_standard, const ColorRGB32F& AiBiRi) { // AiBiRi are the parameters of the LTC such that // { Ai 0 Bi } // M^-1 = { 0 Ai 0 } // { 0 0 1 } // // Bringing the to_light_direction into the "LTC space", // with identity transformation is thus done by multiplying // the direction by the M^-1 matrix float3 light_dir_original = make_float3( to_light_direction_standard.x * AiBiRi.r + to_light_direction_standard.z * AiBiRi.g, to_light_direction_standard.y * AiBiRi.r, to_light_direction_standard.z); float length = hippt::length(light_dir_original); light_dir_original /= length; // Normalization // Determinant of M^-1 float M_inv_determinant = AiBiRi.r * AiBiRi.r; float jacobian = M_inv_determinant / (length * length * length); return light_dir_original.z * M_INV_PI * jacobian; } HIPRT_DEVICE HIPRT_INLINE ColorRGB32F read_LTC_parameters(const HIPRTRenderData& render_data, float roughness, float cos_theta) { const void* ltc_parameters_texture_pointer; #ifdef __KERNELCC__ ltc_parameters_texture_pointer = &render_data.bsdfs_data.sheen_ltc_parameters_texture; #else ltc_parameters_texture_pointer = render_data.bsdfs_data.sheen_ltc_parameters_texture; #endif float2 parameters_uv = make_float2(cos_theta, hippt::clamp(0.0f, 1.0f, roughness)); return sample_texture_rgb_32bits(ltc_parameters_texture_pointer, 0, false, parameters_uv, false); } /** * Returns the phi angle of a direction given in a canonical frame with Z up */ HIPRT_DEVICE HIPRT_INLINE float get_phi(const float3& direction) { float p = atan2(direction.y, direction.x); if (p < 0.0f) p += M_TWO_PI; return p; } /** * Rotates 'u' by 'angle' radians around 'axis' */ HIPRT_DEVICE HIPRT_INLINE float3 rotate_vector(const float3& vec, const float3& axis, float angle) { float sin_angle = sin(angle); float cos_angle = cos(angle); return vec * cos_angle + axis * hippt::dot(vec, axis) * (1.0f - cos_angle) + sin_angle * hippt::cross(axis, vec); } HIPRT_DEVICE float get_sheen_ltc_reflectance(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction) { return read_LTC_parameters(render_data, material.sheen_roughness, local_view_direction.z).b; } HIPRT_DEVICE HIPRT_INLINE ColorRGB32F sheen_ltc_eval(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, const float3& local_to_light_direction, const float3& local_view_direction, float& out_pdf, float& out_sheen_reflectance) { if (local_view_direction.z <= 0.0f || local_to_light_direction.z <= 0.0f) { out_pdf = 0.0f; if (local_view_direction.z > 0.0f) out_sheen_reflectance = get_sheen_ltc_reflectance(render_data, material, local_view_direction); else out_sheen_reflectance = 0.0f; return ColorRGB32F(0.0f); } // The LTC needs to be evaluated in a Z-up coordinate frame with view direction aligned // with phi=0 (so no rotation on the X/Y plane). // // We're thus computing the phi angle and then rotating the to light direction backwards // on that phi angle so that the view direction is at phi=0. float phi = get_phi(local_view_direction); // Rotating the to light direction around z axis such that the view direction is aligned // with phi=0 (because we computed the rotation angle, phi, from the view direction) float3 to_light_standard_frame = rotate_vector(local_to_light_direction, make_float3(0.0f, 0.0f, 1.0f), -phi); ColorRGB32F AiBiRi = read_LTC_parameters(render_data, material.sheen_roughness, local_view_direction.z); float Do = eval_ltc(to_light_standard_frame, AiBiRi); out_pdf = Do; out_sheen_reflectance = AiBiRi.b; // The cosine term is included in the LTC distribution but the renderer expects that // the cosine term isn't included in the BSDFs so we cancel it here. return material.sheen_color * AiBiRi.b * Do / local_to_light_direction.z; } HIPRT_DEVICE HIPRT_INLINE float sheen_ltc_pdf(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, const float3& local_to_light_direction, const float3& local_view_direction) { if (local_view_direction.z <= 0.0f || local_to_light_direction.z <= 0.0f) return 0.0f; // The LTC needs to be evaluated in a Z-up coordinate frame with view direction aligned // with phi=0 (so no rotation on the X/Y plane). // // We're thus computing the phi angle and then rotating the to light direction backwards // on that phi angle so that the view direction is at phi=0. float phi = get_phi(local_view_direction); // Rotating the to light direction around z axis such that the view direction is aligned // with phi=0 (because we computed the rotation angle, phi, from the view direction) float3 to_light_standard_frame = rotate_vector(local_to_light_direction, make_float3(0.0f, 0.0f, 1.0f), -phi); ColorRGB32F AiBiRi = read_LTC_parameters(render_data, material.sheen_roughness, local_view_direction.z); float Do = eval_ltc(to_light_standard_frame, AiBiRi); return Do; } HIPRT_DEVICE HIPRT_INLINE float3 sheen_ltc_sample(const HIPRTRenderData& render_data, const DeviceUnpackedEffectiveMaterial& material, const float3& local_view_direction, const float3& shading_normal, Xorshift32Generator& random_number_generator) { // Sampling a direction in the original space of the LTC float3 cosine_sample = cosine_weighted_sample_z_up_frame(random_number_generator); ColorRGB32F AiBiRi = read_LTC_parameters(render_data, material.sheen_roughness, local_view_direction.z); // And then from the transformation matrix of the LTC, we're going to bring that // sampled direction back to the local space of the BSDF (shading/tangent space) // For that, we need to multiply that standard sampled direction by the matrix M // which is (M^-1)^-1 and we already have M^-1 from AiRiBi, we just to invert it // and its inverse actually is // // { 1/Ai 0 -Bi/Ai } // M = { 0 1/Ai 0 } // { 0 0 1 } // float Ai_inv = 1.0f / AiBiRi.r; float Bi = AiBiRi.g; // Creating the sampled direction in a space at phi=0 float3 sampled_direction_ltc_space = hippt::normalize(make_float3(cosine_sample.x * Ai_inv - cosine_sample.z * Bi * Ai_inv, cosine_sample.y * Ai_inv, cosine_sample.z)); // Bringing out of the phi=0 configuration by rotating return rotate_vector(sampled_direction_ltc_space, make_float3(0.0f, 0.0f, 1.0f), get_phi(local_view_direction)); } #endif ================================================ FILE: src/Device/includes/BSDFs/SheenLTCFittedParameters.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_BSDFS_SHEEN_LTC_PARAMETERS #define DEVICE_INCLUDES_BSDFS_SHEEN_LTC_PARAMETERS #ifndef __KERNELCC__ // This file should not be included on the GPU /** * Reference: * * [1]: [Practical Multiple-Scattering Sheen Using Linearly Transformed Cosines - Github] [https://github.com/tizian/ltc-sheen/blob/master/fitting/python/data/ltc_table_sheen_approx.cpp] */ #include "HostDeviceCommon/Math.h" #include /** * Precomputed parameters for the fitted LTC (Linearly Transformed Cosine) * distribution of an analytic approximation of the reference volumetric SGGX * sheen layer. * * Sampled as [y][x] = float3(Ai, Bi, Ri) with: * y = cos(theta) * x = alpha */ static std::array ltc_parameters_table_approximation = { make_float3(0.10027f, -0.00000f, 0.33971f), make_float3(0.10760f, -0.00000f, 0.35542f), make_float3(0.11991f, 0.00001f, 0.30888f), make_float3(0.13148f, 0.00001f, 0.23195f), make_float3(0.14227f, 0.00001f, 0.15949f), make_float3(0.15231f, -0.00000f, 0.10356f), make_float3(0.16168f, -0.00000f, 0.06466f), make_float3(0.17044f, 0.00000f, 0.03925f), make_float3(0.17867f, 0.00001f, 0.02334f), make_float3(0.18645f, 0.00000f, 0.01366f), make_float3(0.19382f, -0.00000f, 0.00790f), make_float3(0.20084f, -0.00001f, 0.00452f), make_float3(0.20754f, 0.00001f, 0.00257f), make_float3(0.21395f, 0.00000f, 0.00145f), make_float3(0.22011f, 0.00000f, 0.00081f), make_float3(0.22603f, -0.00000f, 0.00045f), make_float3(0.23174f, -0.00001f, 0.00025f), make_float3(0.23726f, 0.00000f, 0.00014f), make_float3(0.24259f, -0.00001f, 0.00008f), make_float3(0.24777f, -0.00001f, 0.00004f), make_float3(0.25279f, -0.00001f, 0.00002f), make_float3(0.25768f, 0.00001f, 0.00001f), make_float3(0.26243f, 0.00001f, 0.00001f), make_float3(0.26707f, -0.00000f, 0.00000f), make_float3(0.27159f, -0.00000f, 0.00000f), make_float3(0.27601f, -0.00000f, 0.00000f), make_float3(0.28033f, 0.00000f, 0.00000f), make_float3(0.28456f, -0.00000f, 0.00000f), make_float3(0.28870f, -0.00001f, 0.00000f), make_float3(0.29276f, -0.00000f, 0.00000f), make_float3(0.29676f, 0.00000f, 0.00000f), make_float3(0.30067f, -0.00001f, 0.00000f), make_float3(0.10068f, -0.00013f, 0.33844f), make_float3(0.10802f, -0.00001f, 0.35438f), make_float3(0.12031f, -0.00000f, 0.30859f), make_float3(0.13190f, 0.00001f, 0.23230f), make_float3(0.14269f, 0.00000f, 0.16016f), make_float3(0.15274f, 0.00001f, 0.10429f), make_float3(0.16211f, -0.00000f, 0.06529f), make_float3(0.17088f, -0.00000f, 0.03975f), make_float3(0.17912f, -0.00000f, 0.02370f), make_float3(0.18691f, -0.00001f, 0.01391f), make_float3(0.19429f, -0.00000f, 0.00807f), make_float3(0.20132f, -0.00000f, 0.00463f), make_float3(0.20803f, 0.00001f, 0.00264f), make_float3(0.21445f, 0.00000f, 0.00149f), make_float3(0.22061f, 0.00000f, 0.00084f), make_float3(0.22655f, -0.00000f, 0.00047f), make_float3(0.23226f, -0.00000f, 0.00026f), make_float3(0.23779f, -0.00001f, 0.00015f), make_float3(0.24314f, -0.00002f, 0.00008f), make_float3(0.24832f, -0.00001f, 0.00004f), make_float3(0.25336f, -0.00002f, 0.00002f), make_float3(0.25824f, -0.00003f, 0.00001f), make_float3(0.26301f, -0.00005f, 0.00001f), make_float3(0.26766f, -0.00008f, 0.00000f), make_float3(0.27220f, -0.00012f, 0.00000f), make_float3(0.27665f, -0.00019f, 0.00000f), make_float3(0.28101f, -0.00026f, 0.00000f), make_float3(0.28532f, -0.00041f, 0.00000f), make_float3(0.28960f, -0.00058f, 0.00000f), make_float3(0.29389f, -0.00080f, 0.00000f), make_float3(0.29830f, -0.00096f, 0.00000f), make_float3(0.30309f, 0.00000f, 0.00000f), make_float3(0.09988f, -0.00743f, 0.33595f), make_float3(0.10928f, -0.00078f, 0.35135f), make_float3(0.12169f, -0.00015f, 0.30790f), make_float3(0.13330f, -0.00006f, 0.23367f), make_float3(0.14412f, -0.00003f, 0.16253f), make_float3(0.15420f, -0.00004f, 0.10680f), make_float3(0.16360f, -0.00002f, 0.06750f), make_float3(0.17240f, -0.00004f, 0.04148f), make_float3(0.18068f, -0.00003f, 0.02497f), make_float3(0.18850f, -0.00004f, 0.01480f), make_float3(0.19591f, -0.00005f, 0.00867f), make_float3(0.20297f, -0.00005f, 0.00503f), make_float3(0.20970f, -0.00008f, 0.00289f), make_float3(0.21616f, -0.00012f, 0.00165f), make_float3(0.22235f, -0.00015f, 0.00094f), make_float3(0.22831f, -0.00020f, 0.00053f), make_float3(0.23407f, -0.00028f, 0.00030f), make_float3(0.23963f, -0.00039f, 0.00017f), make_float3(0.24503f, -0.00056f, 0.00009f), make_float3(0.25028f, -0.00084f, 0.00005f), make_float3(0.25541f, -0.00124f, 0.00003f), make_float3(0.26045f, -0.00184f, 0.00002f), make_float3(0.26545f, -0.00274f, 0.00001f), make_float3(0.27049f, -0.00410f, 0.00001f), make_float3(0.27569f, -0.00612f, 0.00000f), make_float3(0.28127f, -0.00908f, 0.00000f), make_float3(0.28762f, -0.01335f, 0.00000f), make_float3(0.29542f, -0.01917f, 0.00000f), make_float3(0.30593f, -0.02649f, 0.00000f), make_float3(0.32153f, -0.03397f, 0.00000f), make_float3(0.34697f, -0.03669f, 0.00000f), make_float3(0.39704f, -0.00000f, 0.00000f), make_float3(0.08375f, -0.07516f, 0.33643f), make_float3(0.10999f, -0.00776f, 0.34873f), make_float3(0.12392f, -0.00160f, 0.30771f), make_float3(0.13571f, -0.00070f, 0.23660f), make_float3(0.14661f, -0.00044f, 0.16701f), make_float3(0.15676f, -0.00034f, 0.11147f), make_float3(0.16621f, -0.00032f, 0.07157f), make_float3(0.17508f, -0.00029f, 0.04470f), make_float3(0.18341f, -0.00033f, 0.02736f), make_float3(0.19128f, -0.00038f, 0.01648f), make_float3(0.19876f, -0.00045f, 0.00981f), make_float3(0.20587f, -0.00057f, 0.00579f), make_float3(0.21267f, -0.00074f, 0.00339f), make_float3(0.21918f, -0.00096f, 0.00197f), make_float3(0.22544f, -0.00129f, 0.00114f), make_float3(0.23150f, -0.00179f, 0.00066f), make_float3(0.23736f, -0.00249f, 0.00038f), make_float3(0.24308f, -0.00352f, 0.00022f), make_float3(0.24871f, -0.00501f, 0.00012f), make_float3(0.25433f, -0.00721f, 0.00007f), make_float3(0.26003f, -0.01042f, 0.00004f), make_float3(0.26604f, -0.01512f, 0.00002f), make_float3(0.27264f, -0.02192f, 0.00001f), make_float3(0.28039f, -0.03158f, 0.00001f), make_float3(0.29024f, -0.04491f, 0.00000f), make_float3(0.30382f, -0.06232f, 0.00000f), make_float3(0.32395f, -0.08307f, 0.00000f), make_float3(0.35533f, -0.10404f, 0.00000f), make_float3(0.40491f, -0.11883f, 0.00000f), make_float3(0.47816f, -0.11902f, 0.00000f), make_float3(0.56774f, -0.09644f, 0.00000f), make_float3(0.66332f, -0.00000f, 0.00000f), make_float3(0.05655f, -0.31167f, 0.32420f), make_float3(0.10687f, -0.03376f, 0.35090f), make_float3(0.12655f, -0.00804f, 0.31009f), make_float3(0.13914f, -0.00363f, 0.24233f), make_float3(0.15029f, -0.00227f, 0.17455f), make_float3(0.16057f, -0.00177f, 0.11907f), make_float3(0.17014f, -0.00156f, 0.07820f), make_float3(0.17910f, -0.00153f, 0.04999f), make_float3(0.18752f, -0.00162f, 0.03132f), make_float3(0.19549f, -0.00182f, 0.01932f), make_float3(0.20304f, -0.00213f, 0.01178f), make_float3(0.21025f, -0.00261f, 0.00712f), make_float3(0.21716f, -0.00326f, 0.00427f), make_float3(0.22380f, -0.00420f, 0.00255f), make_float3(0.23022f, -0.00554f, 0.00151f), make_float3(0.23648f, -0.00740f, 0.00089f), make_float3(0.24264f, -0.01005f, 0.00053f), make_float3(0.24879f, -0.01381f, 0.00031f), make_float3(0.25510f, -0.01911f, 0.00018f), make_float3(0.26177f, -0.02658f, 0.00011f), make_float3(0.26918f, -0.03698f, 0.00006f), make_float3(0.27795f, -0.05122f, 0.00004f), make_float3(0.28910f, -0.07004f, 0.00002f), make_float3(0.30427f, -0.09359f, 0.00001f), make_float3(0.32606f, -0.12066f, 0.00001f), make_float3(0.35822f, -0.14764f, 0.00001f), make_float3(0.40512f, -0.16863f, 0.00000f), make_float3(0.46849f, -0.17766f, 0.00000f), make_float3(0.54169f, -0.17178f, 0.00000f), make_float3(0.61239f, -0.15052f, 0.00000f), make_float3(0.67350f, -0.11117f, 0.00000f), make_float3(0.73152f, 0.00000f, 0.00000f), make_float3(0.05336f, -0.34864f, 0.38172f), make_float3(0.09920f, -0.08509f, 0.36009f), make_float3(0.12900f, -0.02477f, 0.31816f), make_float3(0.14348f, -0.01195f, 0.25287f), make_float3(0.15525f, -0.00763f, 0.18668f), make_float3(0.16584f, -0.00587f, 0.13092f), make_float3(0.17560f, -0.00512f, 0.08853f), make_float3(0.18472f, -0.00492f, 0.05832f), make_float3(0.19329f, -0.00508f, 0.03768f), make_float3(0.20140f, -0.00555f, 0.02399f), make_float3(0.20910f, -0.00634f, 0.01510f), make_float3(0.21647f, -0.00749f, 0.00942f), make_float3(0.22355f, -0.00914f, 0.00584f), make_float3(0.23039f, -0.01140f, 0.00360f), make_float3(0.23709f, -0.01450f, 0.00221f), make_float3(0.24371f, -0.01877f, 0.00135f), make_float3(0.25039f, -0.02458f, 0.00083f), make_float3(0.25730f, -0.03248f, 0.00051f), make_float3(0.26474f, -0.04313f, 0.00031f), make_float3(0.27313f, -0.05721f, 0.00019f), make_float3(0.28319f, -0.07550f, 0.00012f), make_float3(0.29598f, -0.09825f, 0.00008f), make_float3(0.31310f, -0.12490f, 0.00005f), make_float3(0.33683f, -0.15332f, 0.00003f), make_float3(0.36995f, -0.17964f, 0.00002f), make_float3(0.41497f, -0.19882f, 0.00002f), make_float3(0.47174f, -0.20686f, 0.00001f), make_float3(0.53490f, -0.20242f, 0.00001f), make_float3(0.59635f, -0.18603f, 0.00001f), make_float3(0.65092f, -0.15783f, 0.00001f), make_float3(0.69798f, -0.11426f, 0.00001f), make_float3(0.74494f, 0.00000f, 0.00001f), make_float3(0.05749f, -0.31793f, 0.44455f), make_float3(0.09398f, -0.14133f, 0.37908f), make_float3(0.13152f, -0.05344f, 0.33487f), make_float3(0.14884f, -0.02831f, 0.27078f), make_float3(0.16170f, -0.01861f, 0.20554f), make_float3(0.17282f, -0.01431f, 0.14892f), make_float3(0.18293f, -0.01233f, 0.10431f), make_float3(0.19231f, -0.01159f, 0.07128f), make_float3(0.20110f, -0.01165f, 0.04782f), make_float3(0.20942f, -0.01233f, 0.03163f), make_float3(0.21733f, -0.01364f, 0.02070f), make_float3(0.22491f, -0.01559f, 0.01344f), make_float3(0.23224f, -0.01830f, 0.00867f), make_float3(0.23938f, -0.02200f, 0.00557f), make_float3(0.24643f, -0.02692f, 0.00357f), make_float3(0.25351f, -0.03342f, 0.00228f), make_float3(0.26079f, -0.04192f, 0.00146f), make_float3(0.26852f, -0.05298f, 0.00094f), make_float3(0.27707f, -0.06708f, 0.00060f), make_float3(0.28698f, -0.08468f, 0.00039f), make_float3(0.29907f, -0.10591f, 0.00026f), make_float3(0.31446f, -0.13028f, 0.00017f), make_float3(0.33466f, -0.15629f, 0.00012f), make_float3(0.36155f, -0.18122f, 0.00008f), make_float3(0.39700f, -0.20145f, 0.00006f), make_float3(0.44194f, -0.21352f, 0.00005f), make_float3(0.49484f, -0.21535f, 0.00004f), make_float3(0.55114f, -0.20661f, 0.00003f), make_float3(0.60550f, -0.18774f, 0.00003f), make_float3(0.65477f, -0.15830f, 0.00002f), make_float3(0.69863f, -0.11427f, 0.00002f), make_float3(0.74332f, 0.00000f, 0.00002f), make_float3(0.06502f, -0.28106f, 0.49493f), make_float3(0.09745f, -0.17506f, 0.41308f), make_float3(0.13592f, -0.08778f, 0.36191f), make_float3(0.15585f, -0.05167f, 0.29839f), make_float3(0.17008f, -0.03538f, 0.23353f), make_float3(0.18198f, -0.02741f, 0.17549f), make_float3(0.19260f, -0.02338f, 0.12792f), make_float3(0.20235f, -0.02153f, 0.09115f), make_float3(0.21146f, -0.02104f, 0.06384f), make_float3(0.22006f, -0.02158f, 0.04414f), make_float3(0.22825f, -0.02299f, 0.03022f), make_float3(0.23612f, -0.02528f, 0.02053f), make_float3(0.24374f, -0.02852f, 0.01387f), make_float3(0.25120f, -0.03287f, 0.00933f), make_float3(0.25860f, -0.03853f, 0.00626f), make_float3(0.26608f, -0.04579f, 0.00420f), make_float3(0.27381f, -0.05493f, 0.00282f), make_float3(0.28203f, -0.06627f, 0.00189f), make_float3(0.29109f, -0.08016f, 0.00128f), make_float3(0.30146f, -0.09668f, 0.00087f), make_float3(0.31381f, -0.11579f, 0.00060f), make_float3(0.32900f, -0.13678f, 0.00042f), make_float3(0.34816f, -0.15845f, 0.00030f), make_float3(0.37257f, -0.17874f, 0.00022f), make_float3(0.40358f, -0.19512f, 0.00016f), make_float3(0.44202f, -0.20502f, 0.00013f), make_float3(0.48743f, -0.20660f, 0.00010f), make_float3(0.53748f, -0.19897f, 0.00008f), make_float3(0.58855f, -0.18183f, 0.00007f), make_float3(0.63751f, -0.15424f, 0.00006f), make_float3(0.68300f, -0.11197f, 0.00005f), make_float3(0.72978f, 0.00001f, 0.00005f), make_float3(0.07528f, -0.24711f, 0.53455f), make_float3(0.10863f, -0.18975f, 0.45764f), make_float3(0.14433f, -0.11815f, 0.39949f), make_float3(0.16570f, -0.07688f, 0.33687f), make_float3(0.18121f, -0.05511f, 0.27245f), make_float3(0.19398f, -0.04327f, 0.21288f), make_float3(0.20521f, -0.03673f, 0.16193f), make_float3(0.21545f, -0.03319f, 0.12068f), make_float3(0.22496f, -0.03159f, 0.08855f), make_float3(0.23392f, -0.03136f, 0.06420f), make_float3(0.24244f, -0.03223f, 0.04612f), make_float3(0.25063f, -0.03408f, 0.03290f), make_float3(0.25857f, -0.03690f, 0.02335f), make_float3(0.26634f, -0.04074f, 0.01650f), make_float3(0.27403f, -0.04570f, 0.01163f), make_float3(0.28178f, -0.05195f, 0.00819f), make_float3(0.28972f, -0.05961f, 0.00577f), make_float3(0.29802f, -0.06886f, 0.00406f), make_float3(0.30695f, -0.07982f, 0.00287f), make_float3(0.31682f, -0.09255f, 0.00204f), make_float3(0.32809f, -0.10692f, 0.00146f), make_float3(0.34129f, -0.12259f, 0.00105f), make_float3(0.35714f, -0.13882f, 0.00077f), make_float3(0.37647f, -0.15448f, 0.00057f), make_float3(0.40023f, -0.16805f, 0.00043f), make_float3(0.42931f, -0.17770f, 0.00033f), make_float3(0.46430f, -0.18158f, 0.00026f), make_float3(0.50501f, -0.17811f, 0.00021f), make_float3(0.55015f, -0.16592f, 0.00018f), make_float3(0.59760f, -0.14331f, 0.00015f), make_float3(0.64549f, -0.10572f, 0.00013f), make_float3(0.69674f, 0.00000f, 0.00012f), make_float3(0.08968f, -0.22166f, 0.57013f), make_float3(0.12485f, -0.19606f, 0.50498f), make_float3(0.15755f, -0.13952f, 0.44492f), make_float3(0.17936f, -0.09867f, 0.38452f), make_float3(0.19578f, -0.07393f, 0.32154f), make_float3(0.20935f, -0.05908f, 0.26116f), make_float3(0.22123f, -0.05009f, 0.20725f), make_float3(0.23199f, -0.04467f, 0.16152f), make_float3(0.24197f, -0.04157f, 0.12414f), make_float3(0.25134f, -0.04011f, 0.09438f), make_float3(0.26024f, -0.03986f, 0.07115f), make_float3(0.26879f, -0.04064f, 0.05329f), make_float3(0.27706f, -0.04233f, 0.03970f), make_float3(0.28514f, -0.04488f, 0.02947f), make_float3(0.29311f, -0.04830f, 0.02181f), make_float3(0.30105f, -0.05263f, 0.01611f), make_float3(0.30908f, -0.05790f, 0.01189f), make_float3(0.31731f, -0.06417f, 0.00877f), make_float3(0.32591f, -0.07150f, 0.00648f), make_float3(0.33507f, -0.07986f, 0.00480f), make_float3(0.34502f, -0.08921f, 0.00356f), make_float3(0.35610f, -0.09937f, 0.00266f), make_float3(0.36867f, -0.11001f, 0.00200f), make_float3(0.38323f, -0.12062f, 0.00151f), make_float3(0.40031f, -0.13041f, 0.00116f), make_float3(0.42056f, -0.13839f, 0.00090f), make_float3(0.44465f, -0.14322f, 0.00070f), make_float3(0.47318f, -0.14336f, 0.00056f), make_float3(0.50652f, -0.13700f, 0.00046f), make_float3(0.54470f, -0.12166f, 0.00038f), make_float3(0.58750f, -0.09227f, 0.00032f), make_float3(0.63790f, 0.00000f, 0.00028f), make_float3(0.10928f, -0.20982f, 0.60377f), make_float3(0.14371f, -0.19861f, 0.54677f), make_float3(0.17437f, -0.15359f, 0.48934f), make_float3(0.19613f, -0.11610f, 0.43261f), make_float3(0.21313f, -0.09068f, 0.37256f), make_float3(0.22735f, -0.07402f, 0.31310f), make_float3(0.23983f, -0.06310f, 0.25792f), make_float3(0.25113f, -0.05592f, 0.20914f), make_float3(0.26159f, -0.05128f, 0.16750f), make_float3(0.27141f, -0.04843f, 0.13286f), make_float3(0.28075f, -0.04692f, 0.10458f), make_float3(0.28970f, -0.04644f, 0.08182f), make_float3(0.29835f, -0.04682f, 0.06371f), make_float3(0.30679f, -0.04799f, 0.04942f), make_float3(0.31508f, -0.04984f, 0.03822f), make_float3(0.32329f, -0.05234f, 0.02949f), make_float3(0.33149f, -0.05550f, 0.02272f), make_float3(0.33979f, -0.05931f, 0.01749f), make_float3(0.34827f, -0.06373f, 0.01346f), make_float3(0.35704f, -0.06873f, 0.01036f), make_float3(0.36626f, -0.07428f, 0.00799f), make_float3(0.37608f, -0.08028f, 0.00617f), make_float3(0.38672f, -0.08655f, 0.00478f), make_float3(0.39843f, -0.09283f, 0.00371f), make_float3(0.41148f, -0.09873f, 0.00290f), make_float3(0.42626f, -0.10371f, 0.00228f), make_float3(0.44316f, -0.10703f, 0.00181f), make_float3(0.46265f, -0.10766f, 0.00145f), make_float3(0.48525f, -0.10415f, 0.00117f), make_float3(0.51155f, -0.09426f, 0.00096f), make_float3(0.54238f, -0.07326f, 0.00080f), make_float3(0.58108f, 0.00000f, 0.00068f), make_float3(0.13051f, -0.20682f, 0.62432f), make_float3(0.16330f, -0.20073f, 0.57323f), make_float3(0.19267f, -0.16509f, 0.51965f), make_float3(0.21421f, -0.13200f, 0.46714f), make_float3(0.23154f, -0.10730f, 0.41130f), make_float3(0.24629f, -0.08978f, 0.35487f), make_float3(0.25931f, -0.07747f, 0.30102f), make_float3(0.27115f, -0.06883f, 0.25188f), make_float3(0.28213f, -0.06278f, 0.20849f), make_float3(0.29244f, -0.05862f, 0.17111f), make_float3(0.30225f, -0.05585f, 0.13949f), make_float3(0.31166f, -0.05417f, 0.11309f), make_float3(0.32077f, -0.05341f, 0.09129f), make_float3(0.32964f, -0.05335f, 0.07343f), make_float3(0.33834f, -0.05393f, 0.05890f), make_float3(0.34693f, -0.05508f, 0.04713f), make_float3(0.35546f, -0.05671f, 0.03765f), make_float3(0.36403f, -0.05880f, 0.03004f), make_float3(0.37268f, -0.06133f, 0.02395f), make_float3(0.38149f, -0.06421f, 0.01909f), make_float3(0.39055f, -0.06741f, 0.01521f), make_float3(0.39997f, -0.07082f, 0.01213f), make_float3(0.40987f, -0.07434f, 0.00969f), make_float3(0.42039f, -0.07779f, 0.00775f), make_float3(0.43169f, -0.08092f, 0.00622f), make_float3(0.44398f, -0.08341f, 0.00500f), make_float3(0.45749f, -0.08480f, 0.00404f), make_float3(0.47249f, -0.08439f, 0.00328f), make_float3(0.48934f, -0.08117f, 0.00268f), make_float3(0.50847f, -0.07344f, 0.00220f), make_float3(0.53061f, -0.05739f, 0.00183f), make_float3(0.55814f, -0.00000f, 0.00155f), make_float3(0.15121f, -0.20607f, 0.62947f), make_float3(0.18300f, -0.20354f, 0.58335f), make_float3(0.21155f, -0.17590f, 0.53312f), make_float3(0.23282f, -0.14755f, 0.48447f), make_float3(0.25031f, -0.12455f, 0.43318f), make_float3(0.26543f, -0.10703f, 0.38096f), make_float3(0.27895f, -0.09394f, 0.33029f), make_float3(0.29131f, -0.08418f, 0.28305f), make_float3(0.30281f, -0.07693f, 0.24033f), make_float3(0.31365f, -0.07157f, 0.20255f), make_float3(0.32398f, -0.06766f, 0.16971f), make_float3(0.33391f, -0.06487f, 0.14151f), make_float3(0.34352f, -0.06300f, 0.11754f), make_float3(0.35289f, -0.06189f, 0.09733f), make_float3(0.36206f, -0.06138f, 0.08038f), make_float3(0.37112f, -0.06139f, 0.06624f), make_float3(0.38010f, -0.06186f, 0.05449f), make_float3(0.38905f, -0.06272f, 0.04477f), make_float3(0.39804f, -0.06390f, 0.03675f), make_float3(0.40711f, -0.06534f, 0.03014f), make_float3(0.41635f, -0.06699f, 0.02471f), make_float3(0.42580f, -0.06874f, 0.02026f), make_float3(0.43556f, -0.07051f, 0.01662f), make_float3(0.44571f, -0.07214f, 0.01365f), make_float3(0.45636f, -0.07346f, 0.01122f), make_float3(0.46763f, -0.07423f, 0.00924f), make_float3(0.47968f, -0.07408f, 0.00762f), make_float3(0.49266f, -0.07254f, 0.00631f), make_float3(0.50681f, -0.06883f, 0.00524f), make_float3(0.52242f, -0.06160f, 0.00437f), make_float3(0.54001f, -0.04778f, 0.00367f), make_float3(0.56112f, 0.00000f, 0.00312f), make_float3(0.17197f, -0.20561f, 0.62587f), make_float3(0.20338f, -0.20599f, 0.58408f), make_float3(0.23149f, -0.18515f, 0.53684f), make_float3(0.25251f, -0.16154f, 0.49150f), make_float3(0.27004f, -0.14094f, 0.44437f), make_float3(0.28542f, -0.12426f, 0.39642f), make_float3(0.29931f, -0.11107f, 0.34949f), make_float3(0.31213f, -0.10073f, 0.30512f), make_float3(0.32413f, -0.09263f, 0.26430f), make_float3(0.33550f, -0.08631f, 0.22751f), make_float3(0.34636f, -0.08139f, 0.19485f), make_float3(0.35682f, -0.07760f, 0.16619f), make_float3(0.36695f, -0.07471f, 0.14127f), make_float3(0.37684f, -0.07258f, 0.11976f), make_float3(0.38652f, -0.07106f, 0.10128f), make_float3(0.39607f, -0.07006f, 0.08549f), make_float3(0.40551f, -0.06949f, 0.07205f), make_float3(0.41490f, -0.06928f, 0.06064f), make_float3(0.42427f, -0.06933f, 0.05099f), make_float3(0.43369f, -0.06961f, 0.04283f), make_float3(0.44319f, -0.07004f, 0.03597f), make_float3(0.45283f, -0.07053f, 0.03019f), make_float3(0.46265f, -0.07097f, 0.02535f), make_float3(0.47273f, -0.07127f, 0.02128f), make_float3(0.48314f, -0.07125f, 0.01788f), make_float3(0.49395f, -0.07070f, 0.01504f), make_float3(0.50527f, -0.06936f, 0.01267f), make_float3(0.51721f, -0.06683f, 0.01069f), make_float3(0.52993f, -0.06247f, 0.00904f), make_float3(0.54362f, -0.05516f, 0.00766f), make_float3(0.55866f, -0.04229f, 0.00653f), make_float3(0.57607f, -0.00000f, 0.00561f), make_float3(0.19561f, -0.20369f, 0.62676f), make_float3(0.22742f, -0.20521f, 0.59059f), make_float3(0.25590f, -0.18887f, 0.54809f), make_float3(0.27707f, -0.16911f, 0.50677f), make_float3(0.29477f, -0.15102f, 0.46369f), make_float3(0.31041f, -0.13567f, 0.41959f), make_float3(0.32465f, -0.12298f, 0.37594f), make_float3(0.33788f, -0.11258f, 0.33408f), make_float3(0.35033f, -0.10408f, 0.29491f), make_float3(0.36219f, -0.09713f, 0.25894f), make_float3(0.37355f, -0.09144f, 0.22638f), make_float3(0.38451f, -0.08677f, 0.19721f), make_float3(0.39514f, -0.08296f, 0.17130f), make_float3(0.40551f, -0.07987f, 0.14843f), make_float3(0.41565f, -0.07736f, 0.12835f), make_float3(0.42562f, -0.07533f, 0.11079f), make_float3(0.43546f, -0.07371f, 0.09548f), make_float3(0.44519f, -0.07242f, 0.08219f), make_float3(0.45486f, -0.07138f, 0.07067f), make_float3(0.46449f, -0.07053f, 0.06071f), make_float3(0.47413f, -0.06981f, 0.05212f), make_float3(0.48380f, -0.06914f, 0.04472f), make_float3(0.49355f, -0.06841f, 0.03835f), make_float3(0.50342f, -0.06755f, 0.03289f), make_float3(0.51345f, -0.06643f, 0.02821f), make_float3(0.52370f, -0.06487f, 0.02421f), make_float3(0.53423f, -0.06265f, 0.02078f), make_float3(0.54512f, -0.05946f, 0.01786f), make_float3(0.55646f, -0.05481f, 0.01537f), make_float3(0.56840f, -0.04776f, 0.01325f), make_float3(0.58119f, -0.03618f, 0.01145f), make_float3(0.59545f, -0.00000f, 0.00995f), make_float3(0.22163f, -0.20055f, 0.63021f), make_float3(0.25410f, -0.20231f, 0.59968f), make_float3(0.28326f, -0.18906f, 0.56225f), make_float3(0.30475f, -0.17238f, 0.52492f), make_float3(0.32266f, -0.15662f, 0.48555f), make_float3(0.33852f, -0.14279f, 0.44484f), make_float3(0.35302f, -0.13097f, 0.40410f), make_float3(0.36656f, -0.12093f, 0.36452f), make_float3(0.37937f, -0.11245f, 0.32693f), make_float3(0.39160f, -0.10523f, 0.29188f), make_float3(0.40334f, -0.09909f, 0.25962f), make_float3(0.41470f, -0.09386f, 0.23022f), make_float3(0.42572f, -0.08939f, 0.20363f), make_float3(0.43646f, -0.08557f, 0.17973f), make_float3(0.44696f, -0.08227f, 0.15834f), make_float3(0.45725f, -0.07943f, 0.13927f), make_float3(0.46736f, -0.07697f, 0.12233f), make_float3(0.47733f, -0.07481f, 0.10732f), make_float3(0.48718f, -0.07290f, 0.09404f), make_float3(0.49693f, -0.07117f, 0.08234f), make_float3(0.50661f, -0.06954f, 0.07203f), make_float3(0.51626f, -0.06797f, 0.06298f), make_float3(0.52588f, -0.06635f, 0.05503f), make_float3(0.53552f, -0.06465f, 0.04807f), make_float3(0.54521f, -0.06270f, 0.04198f), make_float3(0.55498f, -0.06041f, 0.03667f), make_float3(0.56487f, -0.05757f, 0.03203f), make_float3(0.57494f, -0.05392f, 0.02799f), make_float3(0.58526f, -0.04906f, 0.02447f), make_float3(0.59592f, -0.04224f, 0.02142f), make_float3(0.60709f, -0.03161f, 0.01879f), make_float3(0.61917f, -0.00000f, 0.01653f), make_float3(0.24798f, -0.19716f, 0.63007f), make_float3(0.28069f, -0.19930f, 0.60332f), make_float3(0.31015f, -0.18883f, 0.56936f), make_float3(0.33172f, -0.17504f, 0.53486f), make_float3(0.34964f, -0.16162f, 0.49830f), make_float3(0.36550f, -0.14954f, 0.46037f), make_float3(0.38006f, -0.13890f, 0.42220f), make_float3(0.39370f, -0.12960f, 0.38484f), make_float3(0.40666f, -0.12148f, 0.34906f), make_float3(0.41907f, -0.11438f, 0.31535f), make_float3(0.43103f, -0.10815f, 0.28399f), make_float3(0.44261f, -0.10266f, 0.25508f), make_float3(0.45385f, -0.09783f, 0.22861f), make_float3(0.46481f, -0.09354f, 0.20450f), make_float3(0.47551f, -0.08974f, 0.18264f), make_float3(0.48599f, -0.08633f, 0.16289f), make_float3(0.49627f, -0.08326f, 0.14510f), make_float3(0.50638f, -0.08047f, 0.12910f), make_float3(0.51633f, -0.07791f, 0.11476f), make_float3(0.52614f, -0.07551f, 0.10192f), make_float3(0.53584f, -0.07321f, 0.09045f), make_float3(0.54546f, -0.07096f, 0.08022f), make_float3(0.55500f, -0.06867f, 0.07111f), make_float3(0.56449f, -0.06628f, 0.06301f), make_float3(0.57397f, -0.06367f, 0.05582f), make_float3(0.58345f, -0.06076f, 0.04944f), make_float3(0.59297f, -0.05731f, 0.04379f), make_float3(0.60257f, -0.05316f, 0.03879f), make_float3(0.61231f, -0.04788f, 0.03438f), make_float3(0.62225f, -0.04079f, 0.03050f), make_float3(0.63252f, -0.03022f, 0.02708f), make_float3(0.64336f, 0.00000f, 0.02411f), make_float3(0.27722f, -0.19199f, 0.63565f), make_float3(0.30981f, -0.19408f, 0.61130f), make_float3(0.33943f, -0.18579f, 0.57961f), make_float3(0.36102f, -0.17447f, 0.54708f), make_float3(0.37889f, -0.16322f, 0.51263f), make_float3(0.39470f, -0.15282f, 0.47688f), make_float3(0.40920f, -0.14347f, 0.44082f), make_float3(0.42282f, -0.13509f, 0.40537f), make_float3(0.43577f, -0.12760f, 0.37118f), make_float3(0.44819f, -0.12084f, 0.33873f), make_float3(0.46018f, -0.11477f, 0.30827f), make_float3(0.47180f, -0.10928f, 0.27992f), make_float3(0.48310f, -0.10430f, 0.25370f), make_float3(0.49411f, -0.09979f, 0.22957f), make_float3(0.50485f, -0.09566f, 0.20745f), make_float3(0.51535f, -0.09188f, 0.18722f), make_float3(0.52564f, -0.08837f, 0.16879f), make_float3(0.53572f, -0.08512f, 0.15202f), make_float3(0.54562f, -0.08205f, 0.13680f), make_float3(0.55536f, -0.07912f, 0.12300f), make_float3(0.56495f, -0.07630f, 0.11052f), make_float3(0.57441f, -0.07350f, 0.09925f), make_float3(0.58375f, -0.07067f, 0.08908f), make_float3(0.59300f, -0.06773f, 0.07992f), make_float3(0.60219f, -0.06459f, 0.07168f), make_float3(0.61132f, -0.06115f, 0.06428f), make_float3(0.62043f, -0.05724f, 0.05763f), make_float3(0.62954f, -0.05265f, 0.05168f), make_float3(0.63871f, -0.04703f, 0.04636f), make_float3(0.64797f, -0.03973f, 0.04161f), make_float3(0.65743f, -0.02917f, 0.03737f), make_float3(0.66725f, -0.00000f, 0.03363f), make_float3(0.30941f, -0.18550f, 0.64456f), make_float3(0.34168f, -0.18712f, 0.62252f), make_float3(0.37132f, -0.18024f, 0.59322f), make_float3(0.39288f, -0.17081f, 0.56272f), make_float3(0.41063f, -0.16130f, 0.53026f), make_float3(0.42627f, -0.15243f, 0.49646f), make_float3(0.44059f, -0.14431f, 0.46224f), make_float3(0.45403f, -0.13689f, 0.42843f), make_float3(0.46680f, -0.13010f, 0.39561f), make_float3(0.47906f, -0.12388f, 0.36425f), make_float3(0.49089f, -0.11815f, 0.33457f), make_float3(0.50236f, -0.11285f, 0.30671f), make_float3(0.51350f, -0.10796f, 0.28072f), make_float3(0.52435f, -0.10341f, 0.25657f), make_float3(0.53493f, -0.09917f, 0.23422f), make_float3(0.54526f, -0.09519f, 0.21359f), make_float3(0.55537f, -0.09146f, 0.19458f), make_float3(0.56525f, -0.08792f, 0.17712f), make_float3(0.57494f, -0.08453f, 0.16109f), make_float3(0.58444f, -0.08126f, 0.14642f), make_float3(0.59377f, -0.07806f, 0.13299f), make_float3(0.60294f, -0.07488f, 0.12073f), make_float3(0.61197f, -0.07164f, 0.10955f), make_float3(0.62087f, -0.06830f, 0.09937f), make_float3(0.62966f, -0.06479f, 0.09010f), make_float3(0.63836f, -0.06097f, 0.08168f), make_float3(0.64699f, -0.05673f, 0.07404f), make_float3(0.65557f, -0.05183f, 0.06711f), make_float3(0.66415f, -0.04599f, 0.06085f), make_float3(0.67274f, -0.03858f, 0.05518f), make_float3(0.68143f, -0.02812f, 0.05008f), make_float3(0.69032f, -0.00000f, 0.04550f), make_float3(0.34235f, -0.17826f, 0.65206f), make_float3(0.37374f, -0.17952f, 0.63152f), make_float3(0.40289f, -0.17391f, 0.60389f), make_float3(0.42408f, -0.16614f, 0.57496f), make_float3(0.44148f, -0.15827f, 0.54412f), make_float3(0.45676f, -0.15084f, 0.51197f), make_float3(0.47071f, -0.14393f, 0.47937f), make_float3(0.48377f, -0.13752f, 0.44704f), make_float3(0.49618f, -0.13155f, 0.41554f), make_float3(0.50808f, -0.12598f, 0.38525f), make_float3(0.51957f, -0.12076f, 0.35644f), make_float3(0.53070f, -0.11585f, 0.32922f), make_float3(0.54151f, -0.11120f, 0.30365f), make_float3(0.55204f, -0.10681f, 0.27973f), make_float3(0.56231f, -0.10265f, 0.25742f), make_float3(0.57233f, -0.09867f, 0.23667f), make_float3(0.58212f, -0.09486f, 0.21741f), make_float3(0.59168f, -0.09120f, 0.19957f), make_float3(0.60106f, -0.08765f, 0.18306f), make_float3(0.61023f, -0.08416f, 0.16782f), make_float3(0.61923f, -0.08072f, 0.15376f), make_float3(0.62806f, -0.07727f, 0.14081f), make_float3(0.63672f, -0.07376f, 0.12890f), make_float3(0.64525f, -0.07012f, 0.11795f), make_float3(0.65365f, -0.06628f, 0.10790f), make_float3(0.66194f, -0.06216f, 0.09869f), make_float3(0.67013f, -0.05760f, 0.09025f), make_float3(0.67825f, -0.05241f, 0.08254f), make_float3(0.68632f, -0.04628f, 0.07549f), make_float3(0.69436f, -0.03862f, 0.06906f), make_float3(0.70244f, -0.02799f, 0.06321f), make_float3(0.71061f, 0.00000f, 0.05791f), make_float3(0.37818f, -0.17017f, 0.65945f), make_float3(0.40866f, -0.17100f, 0.64009f), make_float3(0.43736f, -0.16627f, 0.61399f), make_float3(0.45826f, -0.15979f, 0.58661f), make_float3(0.47535f, -0.15320f, 0.55744f), make_float3(0.49027f, -0.14692f, 0.52704f), make_float3(0.50383f, -0.14103f, 0.49617f), make_float3(0.51647f, -0.13549f, 0.46545f), make_float3(0.52846f, -0.13026f, 0.43541f), make_float3(0.53992f, -0.12530f, 0.40641f), make_float3(0.55095f, -0.12057f, 0.37864f), make_float3(0.56162f, -0.11605f, 0.35226f), make_float3(0.57197f, -0.11171f, 0.32733f), make_float3(0.58204f, -0.10753f, 0.30384f), make_float3(0.59185f, -0.10350f, 0.28179f), make_float3(0.60141f, -0.09961f, 0.26113f), make_float3(0.61074f, -0.09583f, 0.24181f), make_float3(0.61985f, -0.09214f, 0.22378f), make_float3(0.62877f, -0.08853f, 0.20696f), make_float3(0.63747f, -0.08494f, 0.19131f), make_float3(0.64600f, -0.08136f, 0.17675f), make_float3(0.65435f, -0.07776f, 0.16324f), make_float3(0.66254f, -0.07407f, 0.15070f), make_float3(0.67057f, -0.07025f, 0.13908f), make_float3(0.67846f, -0.06622f, 0.12832f), make_float3(0.68622f, -0.06190f, 0.11837f), make_float3(0.69387f, -0.05716f, 0.10919f), make_float3(0.70143f, -0.05182f, 0.10071f), make_float3(0.70890f, -0.04557f, 0.09289f), make_float3(0.71632f, -0.03786f, 0.08570f), make_float3(0.72371f, -0.02730f, 0.07909f), make_float3(0.73113f, -0.00000f, 0.07304f), make_float3(0.41622f, -0.16117f, 0.66557f), make_float3(0.44565f, -0.16160f, 0.64703f), make_float3(0.47380f, -0.15756f, 0.62216f), make_float3(0.49434f, -0.15215f, 0.59618f), make_float3(0.51107f, -0.14662f, 0.56863f), make_float3(0.52562f, -0.14132f, 0.53998f), make_float3(0.53876f, -0.13629f, 0.51088f), make_float3(0.55092f, -0.13152f, 0.48188f), make_float3(0.56240f, -0.12695f, 0.45343f), make_float3(0.57332f, -0.12256f, 0.42585f), make_float3(0.58380f, -0.11831f, 0.39935f), make_float3(0.59391f, -0.11419f, 0.37404f), make_float3(0.60370f, -0.11017f, 0.34998f), make_float3(0.61319f, -0.10625f, 0.32718f), make_float3(0.62242f, -0.10243f, 0.30564f), make_float3(0.63141f, -0.09869f, 0.28533f), make_float3(0.64017f, -0.09501f, 0.26621f), make_float3(0.64871f, -0.09139f, 0.24823f), make_float3(0.65705f, -0.08778f, 0.23136f), make_float3(0.66519f, -0.08420f, 0.21553f), make_float3(0.67315f, -0.08059f, 0.20071f), make_float3(0.68092f, -0.07692f, 0.18683f), make_float3(0.68853f, -0.07317f, 0.17386f), make_float3(0.69599f, -0.06927f, 0.16174f), make_float3(0.70329f, -0.06516f, 0.15044f), make_float3(0.71046f, -0.06075f, 0.13990f), make_float3(0.71751f, -0.05595f, 0.13009f), make_float3(0.72444f, -0.05055f, 0.12095f), make_float3(0.73128f, -0.04430f, 0.11246f), make_float3(0.73804f, -0.03667f, 0.10458f), make_float3(0.74474f, -0.02633f, 0.09728f), make_float3(0.75140f, 0.00000f, 0.09052f), make_float3(0.45566f, -0.15078f, 0.67181f), make_float3(0.48376f, -0.15083f, 0.65404f), make_float3(0.51104f, -0.14734f, 0.63030f), make_float3(0.53106f, -0.14281f, 0.60566f), make_float3(0.54730f, -0.13818f, 0.57963f), make_float3(0.56134f, -0.13375f, 0.55265f), make_float3(0.57395f, -0.12949f, 0.52523f), make_float3(0.58556f, -0.12542f, 0.49788f), make_float3(0.59643f, -0.12148f, 0.47100f), make_float3(0.60673f, -0.11762f, 0.44485f), make_float3(0.61657f, -0.11387f, 0.41962f), make_float3(0.62603f, -0.11017f, 0.39542f), make_float3(0.63516f, -0.10653f, 0.37231f), make_float3(0.64399f, -0.10294f, 0.35030f), make_float3(0.65257f, -0.09939f, 0.32939f), make_float3(0.66090f, -0.09589f, 0.30956f), make_float3(0.66900f, -0.09240f, 0.29078f), make_float3(0.67689f, -0.08893f, 0.27303f), make_float3(0.68459f, -0.08546f, 0.25624f), make_float3(0.69209f, -0.08197f, 0.24041f), make_float3(0.69941f, -0.07844f, 0.22547f), make_float3(0.70656f, -0.07483f, 0.21139f), make_float3(0.71353f, -0.07112f, 0.19815f), make_float3(0.72036f, -0.06724f, 0.18568f), make_float3(0.72704f, -0.06317f, 0.17397f), make_float3(0.73357f, -0.05880f, 0.16298f), make_float3(0.73999f, -0.05404f, 0.15266f), make_float3(0.74629f, -0.04872f, 0.14298f), make_float3(0.75247f, -0.04259f, 0.13392f), make_float3(0.75857f, -0.03514f, 0.12544f), make_float3(0.76459f, -0.02515f, 0.11752f), make_float3(0.77054f, -0.00000f, 0.11012f), make_float3(0.49546f, -0.13868f, 0.67921f), make_float3(0.52189f, -0.13841f, 0.66231f), make_float3(0.54795f, -0.13540f, 0.63977f), make_float3(0.56715f, -0.13163f, 0.61642f), make_float3(0.58273f, -0.12780f, 0.59185f), make_float3(0.59613f, -0.12410f, 0.56640f), make_float3(0.60809f, -0.12056f, 0.54055f), make_float3(0.61905f, -0.11715f, 0.51472f), make_float3(0.62925f, -0.11379f, 0.48927f), make_float3(0.63886f, -0.11049f, 0.46444f), make_float3(0.64800f, -0.10724f, 0.44041f), make_float3(0.65676f, -0.10400f, 0.41727f), make_float3(0.66517f, -0.10079f, 0.39507f), make_float3(0.67330f, -0.09757f, 0.37385f), make_float3(0.68117f, -0.09438f, 0.35358f), make_float3(0.68879f, -0.09119f, 0.33427f), make_float3(0.69620f, -0.08798f, 0.31589f), make_float3(0.70340f, -0.08477f, 0.29841f), make_float3(0.71041f, -0.08153f, 0.28180f), make_float3(0.71724f, -0.07825f, 0.26604f), make_float3(0.72389f, -0.07490f, 0.25108f), make_float3(0.73037f, -0.07146f, 0.23691f), make_float3(0.73669f, -0.06790f, 0.22349f), make_float3(0.74288f, -0.06418f, 0.21078f), make_float3(0.74892f, -0.06025f, 0.19877f), make_float3(0.75481f, -0.05603f, 0.18742f), make_float3(0.76059f, -0.05144f, 0.17669f), make_float3(0.76625f, -0.04631f, 0.16657f), make_float3(0.77180f, -0.04042f, 0.15703f), make_float3(0.77725f, -0.03329f, 0.14803f), make_float3(0.78262f, -0.02378f, 0.13957f), make_float3(0.78790f, -0.00000f, 0.13161f), make_float3(0.53465f, -0.12522f, 0.68615f), make_float3(0.55925f, -0.12477f, 0.67008f), make_float3(0.58386f, -0.12219f, 0.64863f), make_float3(0.60211f, -0.11908f, 0.62649f), make_float3(0.61691f, -0.11598f, 0.60327f), make_float3(0.62960f, -0.11298f, 0.57927f), make_float3(0.64087f, -0.11008f, 0.55488f), make_float3(0.65112f, -0.10725f, 0.53052f), make_float3(0.66062f, -0.10446f, 0.50646f), make_float3(0.66953f, -0.10170f, 0.48294f), make_float3(0.67795f, -0.09893f, 0.46011f), make_float3(0.68598f, -0.09617f, 0.43805f), make_float3(0.69369f, -0.09339f, 0.41682f), make_float3(0.70109f, -0.09059f, 0.39644f), make_float3(0.70824f, -0.08778f, 0.37690f), make_float3(0.71515f, -0.08494f, 0.35821f), make_float3(0.72185f, -0.08209f, 0.34034f), make_float3(0.72836f, -0.07918f, 0.32326f), make_float3(0.73467f, -0.07624f, 0.30696f), make_float3(0.74082f, -0.07324f, 0.29141f), make_float3(0.74679f, -0.07016f, 0.27658f), make_float3(0.75261f, -0.06698f, 0.26246f), make_float3(0.75828f, -0.06367f, 0.24901f), make_float3(0.76381f, -0.06018f, 0.23621f), make_float3(0.76920f, -0.05650f, 0.22404f), make_float3(0.77446f, -0.05253f, 0.21247f), make_float3(0.77961f, -0.04820f, 0.20148f), make_float3(0.78464f, -0.04336f, 0.19105f), make_float3(0.78957f, -0.03781f, 0.18115f), make_float3(0.79440f, -0.03111f, 0.17176f), make_float3(0.79913f, -0.02219f, 0.16287f), make_float3(0.80378f, -0.00000f, 0.15446f), make_float3(0.57256f, -0.11055f, 0.69171f), make_float3(0.59531f, -0.11002f, 0.67633f), make_float3(0.61841f, -0.10792f, 0.65578f), make_float3(0.63563f, -0.10544f, 0.63470f), make_float3(0.64962f, -0.10298f, 0.61271f), make_float3(0.66156f, -0.10060f, 0.59006f), make_float3(0.67214f, -0.09828f, 0.56710f), make_float3(0.68170f, -0.09602f, 0.54415f), make_float3(0.69050f, -0.09375f, 0.52148f), make_float3(0.69871f, -0.09149f, 0.49928f), make_float3(0.70643f, -0.08920f, 0.47768f), make_float3(0.71376f, -0.08689f, 0.45676f), make_float3(0.72074f, -0.08455f, 0.43658f), make_float3(0.72745f, -0.08218f, 0.41714f), make_float3(0.73389f, -0.07976f, 0.39844f), make_float3(0.74011f, -0.07732f, 0.38048f), make_float3(0.74611f, -0.07484f, 0.36325f), make_float3(0.75193f, -0.07230f, 0.34672f), make_float3(0.75756f, -0.06969f, 0.33088f), make_float3(0.76304f, -0.06702f, 0.31571f), make_float3(0.76835f, -0.06427f, 0.30118f), make_float3(0.77352f, -0.06140f, 0.28727f), make_float3(0.77854f, -0.05841f, 0.27397f), make_float3(0.78343f, -0.05525f, 0.26124f), make_float3(0.78818f, -0.05188f, 0.24909f), make_float3(0.79283f, -0.04824f, 0.23747f), make_float3(0.79736f, -0.04426f, 0.22638f), make_float3(0.80178f, -0.03982f, 0.21580f), make_float3(0.80609f, -0.03471f, 0.20571f), make_float3(0.81032f, -0.02854f, 0.19608f), make_float3(0.81445f, -0.02034f, 0.18691f), make_float3(0.81849f, 0.00001f, 0.17818f), make_float3(0.60872f, -0.09464f, 0.69662f), make_float3(0.62966f, -0.09415f, 0.68177f), make_float3(0.65120f, -0.09250f, 0.66197f), make_float3(0.66737f, -0.09060f, 0.64182f), make_float3(0.68051f, -0.08873f, 0.62094f), make_float3(0.69172f, -0.08690f, 0.59955f), make_float3(0.70159f, -0.08513f, 0.57791f), make_float3(0.71047f, -0.08335f, 0.55632f), make_float3(0.71860f, -0.08158f, 0.53498f), make_float3(0.72612f, -0.07978f, 0.51406f), make_float3(0.73318f, -0.07795f, 0.49369f), make_float3(0.73983f, -0.07608f, 0.47393f), make_float3(0.74614f, -0.07417f, 0.45480f), make_float3(0.75217f, -0.07223f, 0.43634f), make_float3(0.75795f, -0.07023f, 0.41855f), make_float3(0.76350f, -0.06820f, 0.40140f), make_float3(0.76885f, -0.06611f, 0.38489f), make_float3(0.77401f, -0.06395f, 0.36901f), make_float3(0.77900f, -0.06175f, 0.35374f), make_float3(0.78383f, -0.05946f, 0.33905f), make_float3(0.78851f, -0.05707f, 0.32494f), make_float3(0.79306f, -0.05459f, 0.31137f), make_float3(0.79745f, -0.05197f, 0.29835f), make_float3(0.80173f, -0.04920f, 0.28584f), make_float3(0.80590f, -0.04623f, 0.27384f), make_float3(0.80993f, -0.04302f, 0.26232f), make_float3(0.81388f, -0.03949f, 0.25127f), make_float3(0.81771f, -0.03554f, 0.24068f), make_float3(0.82144f, -0.03097f, 0.23053f), make_float3(0.82508f, -0.02547f, 0.22081f), make_float3(0.82863f, -0.01815f, 0.21150f), make_float3(0.83210f, 0.00000f, 0.20258f), make_float3(0.64301f, -0.07753f, 0.70123f), make_float3(0.66223f, -0.07712f, 0.68683f), make_float3(0.68223f, -0.07593f, 0.66763f), make_float3(0.69736f, -0.07455f, 0.64827f), make_float3(0.70966f, -0.07320f, 0.62840f), make_float3(0.72015f, -0.07187f, 0.60815f), make_float3(0.72934f, -0.07056f, 0.58775f), make_float3(0.73757f, -0.06924f, 0.56743f), make_float3(0.74506f, -0.06791f, 0.54737f), make_float3(0.75196f, -0.06655f, 0.52771f), make_float3(0.75838f, -0.06515f, 0.50855f), make_float3(0.76441f, -0.06371f, 0.48992f), make_float3(0.77009f, -0.06222f, 0.47188f), make_float3(0.77550f, -0.06069f, 0.45443f), make_float3(0.78065f, -0.05912f, 0.43757f), make_float3(0.78559f, -0.05749f, 0.42129f), make_float3(0.79032f, -0.05583f, 0.40557f), make_float3(0.79488f, -0.05409f, 0.39042f), make_float3(0.79926f, -0.05229f, 0.37579f), make_float3(0.80349f, -0.05042f, 0.36169f), make_float3(0.80758f, -0.04846f, 0.34810f), make_float3(0.81154f, -0.04640f, 0.33499f), make_float3(0.81537f, -0.04423f, 0.32235f), make_float3(0.81906f, -0.04191f, 0.31018f), make_float3(0.82266f, -0.03941f, 0.29845f), make_float3(0.82614f, -0.03670f, 0.28716f), make_float3(0.82952f, -0.03372f, 0.27629f), make_float3(0.83279f, -0.03035f, 0.26582f), make_float3(0.83599f, -0.02647f, 0.25574f), make_float3(0.83908f, -0.02178f, 0.24605f), make_float3(0.84209f, -0.01551f, 0.23672f), make_float3(0.84502f, 0.00001f, 0.22776f), make_float3(0.67520f, -0.05931f, 0.70649f), make_float3(0.69276f, -0.05903f, 0.69240f), make_float3(0.71128f, -0.05822f, 0.67366f), make_float3(0.72537f, -0.05730f, 0.65499f), make_float3(0.73687f, -0.05639f, 0.63599f), make_float3(0.74664f, -0.05549f, 0.61679f), make_float3(0.75518f, -0.05459f, 0.59754f), make_float3(0.76280f, -0.05368f, 0.57840f), make_float3(0.76970f, -0.05276f, 0.55954f), make_float3(0.77600f, -0.05180f, 0.54106f), make_float3(0.78185f, -0.05080f, 0.52303f), make_float3(0.78730f, -0.04977f, 0.50551f), make_float3(0.79243f, -0.04869f, 0.48852f), make_float3(0.79726f, -0.04757f, 0.47207f), make_float3(0.80185f, -0.04641f, 0.45614f), make_float3(0.80622f, -0.04521f, 0.44074f), make_float3(0.81040f, -0.04396f, 0.42583f), make_float3(0.81441f, -0.04265f, 0.41143f), make_float3(0.81826f, -0.04130f, 0.39750f), make_float3(0.82195f, -0.03988f, 0.38404f), make_float3(0.82551f, -0.03837f, 0.37102f), make_float3(0.82892f, -0.03679f, 0.35844f), make_float3(0.83223f, -0.03510f, 0.34628f), make_float3(0.83541f, -0.03331f, 0.33452f), make_float3(0.83849f, -0.03135f, 0.32316f), make_float3(0.84146f, -0.02922f, 0.31218f), make_float3(0.84433f, -0.02686f, 0.30158f), make_float3(0.84711f, -0.02421f, 0.29134f), make_float3(0.84980f, -0.02113f, 0.28144f), make_float3(0.85241f, -0.01739f, 0.27189f), make_float3(0.85493f, -0.01240f, 0.26266f), make_float3(0.85737f, -0.00000f, 0.25376f), make_float3(0.70511f, -0.04013f, 0.71307f), make_float3(0.72112f, -0.03997f, 0.69919f), make_float3(0.73819f, -0.03950f, 0.68079f), make_float3(0.75128f, -0.03897f, 0.66265f), make_float3(0.76198f, -0.03845f, 0.64443f), make_float3(0.77108f, -0.03791f, 0.62615f), make_float3(0.77901f, -0.03737f, 0.60793f), make_float3(0.78606f, -0.03682f, 0.58989f), make_float3(0.79240f, -0.03625f, 0.57212f), make_float3(0.79819f, -0.03565f, 0.55474f), make_float3(0.80350f, -0.03502f, 0.53779f), make_float3(0.80844f, -0.03437f, 0.52130f), make_float3(0.81305f, -0.03367f, 0.50532f), make_float3(0.81738f, -0.03295f, 0.48981f), make_float3(0.82148f, -0.03220f, 0.47479f), make_float3(0.82537f, -0.03142f, 0.46024f), make_float3(0.82906f, -0.03059f, 0.44615f), make_float3(0.83259f, -0.02973f, 0.43250f), make_float3(0.83596f, -0.02882f, 0.41928f), make_float3(0.83918f, -0.02787f, 0.40648f), make_float3(0.84227f, -0.02686f, 0.39407f), make_float3(0.84523f, -0.02578f, 0.38205f), make_float3(0.84808f, -0.02463f, 0.37040f), make_float3(0.85081f, -0.02339f, 0.35913f), make_float3(0.85345f, -0.02205f, 0.34819f), make_float3(0.85597f, -0.02057f, 0.33761f), make_float3(0.85842f, -0.01893f, 0.32734f), make_float3(0.86076f, -0.01707f, 0.31740f), make_float3(0.86302f, -0.01492f, 0.30777f), make_float3(0.86519f, -0.01229f, 0.29845f), make_float3(0.86729f, -0.00878f, 0.28941f), make_float3(0.86932f, -0.00000f, 0.28066f), make_float3(0.73154f, -0.02030f, 0.72031f), make_float3(0.74609f, -0.02024f, 0.70643f), make_float3(0.76177f, -0.02006f, 0.68812f), make_float3(0.77389f, -0.01984f, 0.67034f), make_float3(0.78383f, -0.01961f, 0.65270f), make_float3(0.79228f, -0.01939f, 0.63519f), make_float3(0.79965f, -0.01914f, 0.61784f), make_float3(0.80618f, -0.01889f, 0.60072f), make_float3(0.81203f, -0.01864f, 0.58392f), make_float3(0.81735f, -0.01837f, 0.56751f), make_float3(0.82221f, -0.01807f, 0.55151f), make_float3(0.82672f, -0.01776f, 0.53597f), make_float3(0.83089f, -0.01744f, 0.52087f), make_float3(0.83482f, -0.01710f, 0.50623f), make_float3(0.83850f, -0.01673f, 0.49203f), make_float3(0.84198f, -0.01635f, 0.47828f), make_float3(0.84528f, -0.01595f, 0.46494f), make_float3(0.84842f, -0.01552f, 0.45201f), make_float3(0.85140f, -0.01508f, 0.43946f), make_float3(0.85424f, -0.01460f, 0.42730f), make_float3(0.85696f, -0.01408f, 0.41549f), make_float3(0.85955f, -0.01354f, 0.40404f), make_float3(0.86204f, -0.01296f, 0.39292f), make_float3(0.86442f, -0.01233f, 0.38212f), make_float3(0.86669f, -0.01164f, 0.37165f), make_float3(0.86887f, -0.01087f, 0.36148f), make_float3(0.87097f, -0.01002f, 0.35161f), make_float3(0.87297f, -0.00905f, 0.34203f), make_float3(0.87490f, -0.00791f, 0.33272f), make_float3(0.87673f, -0.00653f, 0.32369f), make_float3(0.87850f, -0.00466f, 0.31492f), make_float3(0.88020f, 0.00001f, 0.30640f), make_float3(0.75486f, -0.00000f, 0.72806f), make_float3(0.76807f, 0.00000f, 0.71395f), make_float3(0.78246f, -0.00000f, 0.69552f), make_float3(0.79366f, -0.00000f, 0.67790f), make_float3(0.80290f, 0.00001f, 0.66069f), make_float3(0.81077f, 0.00001f, 0.64378f), make_float3(0.81763f, -0.00000f, 0.62716f), make_float3(0.82368f, -0.00000f, 0.61086f), make_float3(0.82912f, -0.00000f, 0.59491f), make_float3(0.83404f, -0.00000f, 0.57936f), make_float3(0.83852f, -0.00000f, 0.56423f), make_float3(0.84266f, 0.00000f, 0.54953f), make_float3(0.84649f, 0.00000f, 0.53526f), make_float3(0.85008f, -0.00000f, 0.52142f), make_float3(0.85343f, 0.00000f, 0.50800f), make_float3(0.85660f, -0.00000f, 0.49498f), make_float3(0.85959f, 0.00000f, 0.48235f), make_float3(0.86241f, -0.00000f, 0.47011f), make_float3(0.86510f, 0.00001f, 0.45821f), make_float3(0.86766f, 0.00000f, 0.44666f), make_float3(0.87010f, -0.00001f, 0.43545f), make_float3(0.87242f, 0.00000f, 0.42456f), make_float3(0.87464f, -0.00000f, 0.41398f), make_float3(0.87675f, 0.00000f, 0.40369f), make_float3(0.87877f, 0.00000f, 0.39370f), make_float3(0.88070f, -0.00000f, 0.38398f), make_float3(0.88255f, -0.00000f, 0.37453f), make_float3(0.88431f, 0.00000f, 0.36535f), make_float3(0.88600f, 0.00000f, 0.35642f), make_float3(0.88761f, -0.00000f, 0.34773f), make_float3(0.88915f, -0.00000f, 0.33929f), make_float3(0.89063f, -0.00000f, 0.33107f) }; #endif // __KERNELCC__ #endif ================================================ FILE: src/Device/includes/BSDFs/ThinFilm.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_THIN_FILM_H #define DEVICE_THIN_FILM_H #include "HostDeviceCommon/Material/MaterialUnpacked.h" // Evaluation XYZ sensitivity curves in Fourier space HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F eval_sensitivity(float opd, float shift) { // Use Gaussian fits float phase = 2.0f * M_PI * opd * 1.0e-6f; float3 val = make_float3(5.4856e-13f, 4.4201e-13f, 5.2481e-13f); float3 pos = make_float3(1.6810e+06f, 1.7953e+06f, 2.2084e+06f); float3 var = make_float3(4.3278e+09f, 9.3046e+09f, 6.6121e+09f); float3 xyz = val * hippt::sqrt(2.0f * M_PI * var) * hippt::cos(pos * phase + shift) * hippt::exp(-1.0f * var * phase * phase); xyz.x += 9.7470e-14f * sqrt(2.0f * M_PI * 4.5282e+09f) * cos(2.2399e+06f * phase + shift) * exp(-4.5282e+09f * phase * phase); return ColorRGB32F(xyz / 1.0685e-7f); } /** * Reference: * [1] [A Practical Extension to Microfacet Theory for the Modeling of Varying Iridescence, * Belcour, Barla, 2017, Supplemental document] https://hal.science/hal-01518344v2/file/supp-mat-small%20(1).pdf */ HIPRT_HOST_DEVICE HIPRT_INLINE void fresnel_phase(float cos_theta_i, float eta1, float eta2, float kappa2, float& phi_par, float& phi_perp) { float sinThetaSqr = 1.0f - hippt::square(cos_theta_i); float A = hippt::square(eta2) * (1.0f - hippt::square(kappa2)) - hippt::square(eta1) * sinThetaSqr; float B = sqrt(hippt::square(A) + hippt::square(2 * hippt::square(eta2) * kappa2)); float U = sqrt((A + B) * 0.5); float V = sqrt((B - A) * 0.5f); float phi_perp_y = 2.0f * eta1 * V * cos_theta_i; float phi_perp_x = hippt::square(U) + hippt::square(V) - hippt::square(eta1 * cos_theta_i); phi_perp = atan2(phi_perp_y, phi_perp_x); float phi_par_y = 2.0f * eta1 * hippt::square(eta2) * cos_theta_i * (2.0f * kappa2 * U - (1.0f - hippt::square(kappa2)) * V); float phi_par_x = hippt::square(hippt::square(eta2) * (1.0f + hippt::square(kappa2)) * cos_theta_i) - hippt::square(eta1) * (hippt::square(U) + hippt::square(V)); phi_par = atan2(phi_par_y, phi_par_x); } HIPRT_HOST_DEVICE HIPRT_INLINE void fresnel_conductor(float cos_theta_i, float eta, float k, float& Rp2, float& Rs2) { float cos_theta_i_2 = cos_theta_i * cos_theta_i; float sin_theta_i_2 = 1.0f - cos_theta_i_2; float temp1 = eta * eta - k * k - sin_theta_i_2; float a2pb2 = sqrt(temp1 * temp1 + 4.0f * k * k * eta * eta); float a = sqrt(0.5f * (a2pb2 + temp1)); float term1 = a2pb2 + cos_theta_i_2; float term2 = 2.0f * a * cos_theta_i; Rs2 = (term1 - term2) / (term1 + term2); Rs2 = hippt::clamp(0.0f, 1.0f, Rs2); float term3 = a2pb2 * cos_theta_i_2 + sin_theta_i_2 * sin_theta_i_2; float term4 = term2 * sin_theta_i_2; Rp2 = Rs2 * (term3 - term4) / (term3 + term4); Rp2 = hippt::clamp(0.0f, 1.0f, Rp2); } /** * Reference: https://stackoverflow.com/questions/8507885/shift-hue-of-an-rgb-color */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F RGB_hue_shift(const ColorRGB32F& color, float hue_shift_degrees) { if (hue_shift_degrees == 0.0f) return color; float cosA = cos(hue_shift_degrees / 180.0f * M_PI); float sinA = sin(hue_shift_degrees / 180.0f * M_PI); float3x3 matrix; constexpr float sqrt_1_3 = 0.57735026918962576451f; // sqrtf(1.0f / 3.0f) constexpr float one_over_3 = 1.0f / 3.0f; matrix.m[0][0] = cosA + (1.0f - cosA) / 3.0f; matrix.m[0][1] = one_over_3 * (1.0f - cosA) - sqrt_1_3 * sinA; matrix.m[0][2] = one_over_3 * (1.0f - cosA) + sqrt_1_3 * sinA; matrix.m[1][0] = one_over_3 * (1.0f - cosA) + sqrt_1_3 * sinA; matrix.m[1][1] = cosA + one_over_3 * (1.0f - cosA); matrix.m[1][2] = one_over_3 * (1.0f - cosA) - sqrt_1_3 * sinA; matrix.m[2][0] = one_over_3 * (1.0f - cosA) - sqrt_1_3 * sinA; matrix.m[2][1] = one_over_3 * (1.0f - cosA) + sqrt_1_3 * sinA; matrix.m[2][2] = cosA + one_over_3 * (1.0f - cosA); ColorRGB32F hue_shifted; hue_shifted.r = color.r * matrix.m[0][0] + color.g * matrix.m[0][1] + color.b * matrix.m[0][2]; hue_shifted.g = color.r * matrix.m[1][0] + color.g * matrix.m[1][1] + color.b * matrix.m[1][2]; hue_shifted.b = color.r * matrix.m[2][0] + color.g * matrix.m[2][1] + color.b * matrix.m[2][2]; hue_shifted.clamp(0.0f, 1.0f); return hue_shifted; } /** * References: * * [1] [A Practical Extension to Microfacet Theory for the Modeling of Varying Iridescence, Belcour, Barla, 2017] https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F thin_film_fresnel(const DeviceUnpackedEffectiveMaterial& material, float ambient_IOR, float HoL) { if (material.thin_film == 0.0f) // Quick exit return ColorRGB32F(0.0f); float eta1 = ambient_IOR; float eta2 = material.thin_film_ior; float eta3 = material.thin_film_do_ior_override ? material.thin_film_base_ior_override : material.ior; // If override is not used, just default to 0.0f because the principled BSDF doesn't have // complex IORs support anyways float kappa3 = material.thin_film_do_ior_override ? material.thin_film_kappa_3 : 0.0f; /* Compute the Spectral versions of the Fresnel reflectance and * transmitance for each interface. */ float R12p = 0.0f; float R12s = 0.0f; float T121p = 0.0f; float T121s = 0.0f; float R23p = 0.0f; float R23s = 0.0f; float cos_theta_2 = 0.0f; float cos_theta_transmission_2 = 1.0f - (1.0f - hippt::square(HoL)) * hippt::square(eta1 / eta2); if (cos_theta_transmission_2 <= 0.0f) { // Total internal reflection R12s = 1.0f; R12p = 1.0f; // 0 transmission for total internal reflection T121p = 0.0f; T121s = 0.0f; } else { cos_theta_2 = sqrt(cos_theta_transmission_2); fresnel_conductor(HoL, eta2 / eta1, 0.0f, R12p, R12s); // Reflected part by the base fresnel_conductor(cos_theta_2, eta3 / eta2, kappa3, R23p, R23s); // Compute the transmission coefficients T121p = 1.0f - R12p; T121s = 1.0f - R12s; } /* Optical Path Difference */ // float D = 2.0f * eta2 * film_thickness / 1000.0f * cos_theta_2; float D = material.thin_film_thickness / 1000.0f * cos_theta_2; /* Variables */ float phi21p; float phi21s; float phi23p; float phi23s; /* Evaluate the phase shift */ fresnel_phase(HoL, eta1, eta2, 0.0f, phi21p, phi21s); fresnel_phase(cos_theta_2, eta2, eta3, kappa3, phi23p, phi23s); phi21p = M_PI - phi21p; phi21s = M_PI - phi21s; float r123p = sqrt(R12p * R23p); float r123s = sqrt(R12s * R23s); /* Iridescence term using spectral antialiasing for Parallel polarization */ // Reflectance term for m=0 (DC term amplitude) float Rs = (hippt::square(T121p) * R23p) / (1.0f - R12p * R23p); float C0 = R12p + Rs; ColorRGB32F I = ColorRGB32F(C0); ColorRGB32F Sm; // Reflectance term for m>0 (pairs of diracs) float Cm = Rs - T121p; for (int m = 1; m <= 2; ++m) { Cm *= r123p; Sm = 2.0f * eval_sensitivity(m * D, m * (phi23p + phi21p)); I += Cm * Sm; } /* Iridescence term using spectral antialiasing for Perpendicular polarization */ // Reflectance term for m=0 (DC term amplitude) Rs = (hippt::square(T121s) * R23s) / (1.0f - R12s * R23s); C0 = R12s + Rs; I += ColorRGB32F(C0); // Reflectance term for m>0 (pairs of diracs) Cm = Rs - T121s; for (int m = 1; m <= 2; ++m) { Cm *= r123s; Sm = 2.0f * eval_sensitivity(m * D, m * (phi23s + phi21s)); I += Cm * Sm; } I *= 0.5f; // CIE RGB and CIE XYZ 1931 conversion: // source: https://en.wikipedia.org/wiki/CIE_1931_color_space float r = 2.3646381f * I[0] - 0.8965361f * I[1] - 0.4680737f * I[2]; float g = -0.5151664f * I[0] + 1.4264000f * I[1] + 0.0887608f * I[2]; float b = 0.0052037f * I[0] - 0.0144081f * I[1] + 1.0092106f * I[2]; I = ColorRGB32F(r, g, b); I.clamp(0.0f, 1.0f); return RGB_hue_shift(I, material.thin_film_hue_shift_degrees * 360.0f); } #endif ================================================ FILE: src/Device/includes/Dispatcher.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_DISPATCHER_H #define DEVICE_DISPATCHER_H #include "Device/includes/BSDFs/Lambertian.h" #include "Device/includes/BSDFs/OrenNayar.h" #include "Device/includes/BSDFs/Principled.h" #include "Device/includes/RayPayload.h" /** * The 'random_number_generator' passed here is used only in case * monte-carlo integration of the directional albedo is enabled * * If 'update_ray_volume_state' is passed as true, the givenargument is passed as nullptr, the volume state of the ray won't * be updated by this sample call (i.e. the ray won't track if this sample call made it exit/enter a new material) */ HIPRT_DEVICE HIPRT_INLINE ColorRGB32F bsdf_dispatcher_eval(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float& pdf, Xorshift32Generator& random_number_generator) { #if BSDFOverride == BSDF_NONE || BSDFOverride == BSDF_PRINCIPLED /*switch (brdf_type) { ... ... default: break; }*/ return principled_bsdf_eval(render_data, bsdf_context, pdf); #elif BSDFOverride == BSDF_LAMBERTIAN return lambertian_brdf_eval(bsdf_context.material, hippt::dot(bsdf_context.to_light_direction, bsdf_context.shading_normal), pdf); #elif BSDFOverride == BSDF_OREN_NAYAR return oren_nayar_brdf_eval(bsdf_context.material, bsdf_context.view_direction, bsdf_context.shading_normal, bsdf_context.to_light_direction, pdf); #endif } HIPRT_DEVICE HIPRT_INLINE float bsdf_dispatcher_pdf(const HIPRTRenderData& render_data, BSDFContext& bsdf_context) { #if BSDFOverride == BSDF_NONE || BSDFOverride == BSDF_PRINCIPLED /*switch (brdf_type) { ... ... default: break; }*/ return principled_bsdf_pdf(render_data, bsdf_context); #elif BSDFOverride == BSDF_LAMBERTIAN return lambertian_brdf_pdf(bsdf_context.material, hippt::dot(bsdf_context.to_light_direction, bsdf_context.shading_normal)); #elif BSDFOverride == BSDF_OREN_NAYAR return oren_nayar_brdf_pdf(bsdf_context.material, bsdf_context.view_direction, bsdf_context.shading_normal, bsdf_context.to_light_direction); #endif } /** * If the 'ray_volume_state' argument is passed as nullptr, the volume state of the ray won't * be updated by this sample call (i.e. the ray won't track if this sample call made it exit/enter a new material) * * If sampleDirectionOnly is 'true',, this function samples only the BSDF without * evaluating the contribution or the PDF of the BSDF. This function will then always return * ColorRGB32F(0.0f) and the 'pdf' out parameter will always be set to 0.0f */ template HIPRT_DEVICE HIPRT_INLINE ColorRGB32F bsdf_dispatcher_sample(const HIPRTRenderData& render_data, BSDFContext& bsdf_context, float3& sampled_direction, float& pdf, Xorshift32Generator& random_number_generator) { #if BSDFOverride == BSDF_NONE || BSDFOverride == BSDF_PRINCIPLED /*switch (brdf_type) { ... ... default: break; }*/ return principled_bsdf_sample(render_data, bsdf_context, sampled_direction, pdf, random_number_generator); #elif BSDFOverride == BSDF_LAMBERTIAN return lambertian_brdf_sample(bsdf_context.material, bsdf_context.shading_normal, sampled_direction, pdf, random_number_generator, bsdf_context.incident_light_info); #elif BSDFOverride == BSDF_OREN_NAYAR return oren_nayar_brdf_sample(material, view_direction, surface_normal, sampled_direction, pdf, random_number_generator, out_sampled_light_info); #endif } #endif ================================================ FILE: src/Device/includes/Dispersion.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copXYZ[1]: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_DISPERSION_H #define DEVICE_DISPERSION_H #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Xorshift.h" #define WAVELENGTH_TO_RGB_FIT 0 #define WAVELENGTH_TO_RGB_TABLES 1 #define WavelengthToRGBMethod WAVELENGTH_TO_RGB_FIT #define MIN_SAMPLE_WAVELENGTH 360 #define MAX_SAMPLE_WAVELENGTH 830 // We only need all the code that follows if we're using the lookup tables #if WavelengthToRGBMethod == WAVELENGTH_TO_RGB_TABLES #define CIE_1931_samples 471 #define CIE_1931_MIN 360 const float CIE_X_entries[CIE_1931_samples] = { 0.0001299000f, 0.0001458470f, 0.0001638021f, 0.0001840037f, 0.0002066902f, 0.0002321000f, 0.0002607280f, 0.0002930750f, 0.0003293880f, 0.0003699140f, 0.0004149000f, 0.0004641587f, 0.0005189860f, 0.0005818540f, 0.0006552347f, 0.0007416000f, 0.0008450296f, 0.0009645268f, 0.001094949f, 0.001231154f, 0.001368000f, 0.001502050f, 0.001642328f, 0.001802382f, 0.001995757f, 0.002236000f, 0.002535385f, 0.002892603f, 0.003300829f, 0.003753236f, 0.004243000f, 0.004762389f, 0.005330048f, 0.005978712f, 0.006741117f, 0.007650000f, 0.008751373f, 0.01002888f, 0.01142170f, 0.01286901f, 0.01431000f, 0.01570443f, 0.01714744f, 0.01878122f, 0.02074801f, 0.02319000f, 0.02620736f, 0.02978248f, 0.03388092f, 0.03846824f, 0.04351000f, 0.04899560f, 0.05502260f, 0.06171880f, 0.06921200f, 0.07763000f, 0.08695811f, 0.09717672f, 0.1084063f, 0.1207672f, 0.1343800f, 0.1493582f, 0.1653957f, 0.1819831f, 0.1986110f, 0.2147700f, 0.2301868f, 0.2448797f, 0.2587773f, 0.2718079f, 0.2839000f, 0.2949438f, 0.3048965f, 0.3137873f, 0.3216454f, 0.3285000f, 0.3343513f, 0.3392101f, 0.3431213f, 0.3461296f, 0.3482800f, 0.3495999f, 0.3501474f, 0.3500130f, 0.3492870f, 0.3480600f, 0.3463733f, 0.3442624f, 0.3418088f, 0.3390941f, 0.3362000f, 0.3331977f, 0.3300411f, 0.3266357f, 0.3228868f, 0.3187000f, 0.3140251f, 0.3088840f, 0.3032904f, 0.2972579f, 0.2908000f, 0.2839701f, 0.2767214f, 0.2689178f, 0.2604227f, 0.2511000f, 0.2408475f, 0.2298512f, 0.2184072f, 0.2068115f, 0.1953600f, 0.1842136f, 0.1733273f, 0.1626881f, 0.1522833f, 0.1421000f, 0.1321786f, 0.1225696f, 0.1132752f, 0.1042979f, 0.09564000f, 0.08729955f, 0.07930804f, 0.07171776f, 0.06458099f, 0.05795001f, 0.05186211f, 0.04628152f, 0.04115088f, 0.03641283f, 0.03201000f, 0.02791720f, 0.02414440f, 0.02068700f, 0.01754040f, 0.01470000f, 0.01216179f, 0.009919960f, 0.007967240f, 0.006296346f, 0.004900000f, 0.003777173f, 0.002945320f, 0.002424880f, 0.002236293f, 0.002400000f, 0.002925520f, 0.003836560f, 0.005174840f, 0.006982080f, 0.009300000f, 0.01214949f, 0.01553588f, 0.01947752f, 0.02399277f, 0.02910000f, 0.03481485f, 0.04112016f, 0.04798504f, 0.05537861f, 0.06327000f, 0.07163501f, 0.08046224f, 0.08973996f, 0.09945645f, 0.1096000f, 0.1201674f, 0.1311145f, 0.1423679f, 0.1538542f, 0.1655000f, 0.1772571f, 0.1891400f, 0.2011694f, 0.2133658f, 0.2257499f, 0.2383209f, 0.2510668f, 0.2639922f, 0.2771017f, 0.2904000f, 0.3038912f, 0.3175726f, 0.3314384f, 0.3454828f, 0.3597000f, 0.3740839f, 0.3886396f, 0.4033784f, 0.4183115f, 0.4334499f, 0.4487953f, 0.4643360f, 0.4800640f, 0.4959713f, 0.5120501f, 0.5282959f, 0.5446916f, 0.5612094f, 0.5778215f, 0.5945000f, 0.6112209f, 0.6279758f, 0.6447602f, 0.6615697f, 0.6784000f, 0.6952392f, 0.7120586f, 0.7288284f, 0.7455188f, 0.7621000f, 0.7785432f, 0.7948256f, 0.8109264f, 0.8268248f, 0.8425000f, 0.8579325f, 0.8730816f, 0.8878944f, 0.9023181f, 0.9163000f, 0.9297995f, 0.9427984f, 0.9552776f, 0.9672179f, 0.9786000f, 0.9893856f, 0.9995488f, 1.0090892f, 1.0180064f, 1.0263000f, 1.0339827f, 1.0409860f, 1.0471880f, 1.0524667f, 1.0567000f, 1.0597944f, 1.0617992f, 1.0628068f, 1.0629096f, 1.0622000f, 1.0607352f, 1.0584436f, 1.0552244f, 1.0509768f, 1.0456000f, 1.0390369f, 1.0313608f, 1.0226662f, 1.0130477f, 1.0026000f, 0.9913675f, 0.9793314f, 0.9664916f, 0.9528479f, 0.9384000f, 0.9231940f, 0.9072440f, 0.8905020f, 0.8729200f, 0.8544499f, 0.8350840f, 0.8149460f, 0.7941860f, 0.7729540f, 0.7514000f, 0.7295836f, 0.7075888f, 0.6856022f, 0.6638104f, 0.6424000f, 0.6215149f, 0.6011138f, 0.5811052f, 0.5613977f, 0.5419000f, 0.5225995f, 0.5035464f, 0.4847436f, 0.4661939f, 0.4479000f, 0.4298613f, 0.4120980f, 0.3946440f, 0.3775333f, 0.3608000f, 0.3444563f, 0.3285168f, 0.3130192f, 0.2980011f, 0.2835000f, 0.2695448f, 0.2561184f, 0.2431896f, 0.2307272f, 0.2187000f, 0.2070971f, 0.1959232f, 0.1851708f, 0.1748323f, 0.1649000f, 0.1553667f, 0.1462300f, 0.1374900f, 0.1291467f, 0.1212000f, 0.1136397f, 0.1064650f, 0.09969044f, 0.09333061f, 0.08740000f, 0.08190096f, 0.07680428f, 0.07207712f, 0.06768664f, 0.06360000f, 0.05980685f, 0.05628216f, 0.05297104f, 0.04981861f, 0.04677000f, 0.04378405f, 0.04087536f, 0.03807264f, 0.03540461f, 0.03290000f, 0.03056419f, 0.02838056f, 0.02634484f, 0.02445275f, 0.02270000f, 0.02108429f, 0.01959988f, 0.01823732f, 0.01698717f, 0.01584000f, 0.01479064f, 0.01383132f, 0.01294868f, 0.01212920f, 0.01135916f, 0.01062935f, 0.009938846f, 0.009288422f, 0.008678854f, 0.008110916f, 0.007582388f, 0.007088746f, 0.006627313f, 0.006195408f, 0.005790346f, 0.005409826f, 0.005052583f, 0.004717512f, 0.004403507f, 0.004109457f, 0.003833913f, 0.003575748f, 0.003334342f, 0.003109075f, 0.002899327f, 0.002704348f, 0.002523020f, 0.002354168f, 0.002196616f, 0.002049190f, 0.001910960f, 0.001781438f, 0.001660110f, 0.001546459f, 0.001439971f, 0.001340042f, 0.001246275f, 0.001158471f, 0.001076430f, 0.0009999493f, 0.0009287358f, 0.0008624332f, 0.0008007503f, 0.0007433960f, 0.0006900786f, 0.0006405156f, 0.0005945021f, 0.0005518646f, 0.0005124290f, 0.0004760213f, 0.0004424536f, 0.0004115117f, 0.0003829814f, 0.0003566491f, 0.0003323011f, 0.0003097586f, 0.0002888871f, 0.0002695394f, 0.0002515682f, 0.0002348261f, 0.0002191710f, 0.0002045258f, 0.0001908405f, 0.0001780654f, 0.0001661505f, 0.0001550236f, 0.0001446219f, 0.0001349098f, 0.0001258520f, 0.0001174130f, 0.0001095515f, 0.0001022245f, 0.00009539445f, 0.00008902390f, 0.00008307527f, 0.00007751269f, 0.00007231304f, 0.00006745778f, 0.00006292844f, 0.00005870652f, 0.00005477028f, 0.00005109918f, 0.00004767654f, 0.00004448567f, 0.00004150994f, 0.00003873324f, 0.00003614203f, 0.00003372352f, 0.00003146487f, 0.00002935326f, 0.00002737573f, 0.00002552433f, 0.00002379376f, 0.00002217870f, 0.00002067383f, 0.00001927226f, 0.00001796640f, 0.00001674991f, 0.00001561648f, 0.00001455977f, 0.00001357387f, 0.00001265436f, 0.00001179723f, 0.00001099844f, 0.00001025398f, 0.000009559646f, 0.000008912044f, 0.000008308358f, 0.000007745769f, 0.000007221456f, 0.000006732475f, 0.000006276423f, 0.000005851304f, 0.000005455118f, 0.000005085868f, 0.000004741466f, 0.000004420236f, 0.000004120783f, 0.000003841716f, 0.000003581652f, 0.000003339127f, 0.000003112949f, 0.000002902121f, 0.000002705645f, 0.000002522525f, 0.000002351726f, 0.000002192415f, 0.000002043902f, 0.000001905497f, 0.000001776509f, 0.000001656215f, 0.000001544022f, 0.000001439440f, 0.000001341977f, 0.000001251141f}; const float CIE_Y_entries[CIE_1931_samples] = { 0.000003917000f, 0.000004393581f, 0.000004929604f, 0.000005532136f, 0.000006208245f, 0.000006965000f, 0.000007813219f, 0.000008767336f, 0.000009839844f, 0.00001104323f, 0.00001239000f, 0.00001388641f, 0.00001555728f, 0.00001744296f, 0.00001958375f, 0.00002202000f, 0.00002483965f, 0.00002804126f, 0.00003153104f, 0.00003521521f, 0.00003900000f, 0.00004282640f, 0.00004691460f, 0.00005158960f, 0.00005717640f, 0.00006400000f, 0.00007234421f, 0.00008221224f, 0.00009350816f, 0.0001061361f, 0.0001200000f, 0.0001349840f, 0.0001514920f, 0.0001702080f, 0.0001918160f, 0.0002170000f, 0.0002469067f, 0.0002812400f, 0.0003185200f, 0.0003572667f, 0.0003960000f, 0.0004337147f, 0.0004730240f, 0.0005178760f, 0.0005722187f, 0.0006400000f, 0.0007245600f, 0.0008255000f, 0.0009411600f, 0.001069880f, 0.001210000f, 0.001362091f, 0.001530752f, 0.001720368f, 0.001935323f, 0.002180000f, 0.002454800f, 0.002764000f, 0.003117800f, 0.003526400f, 0.004000000f, 0.004546240f, 0.005159320f, 0.005829280f, 0.006546160f, 0.007300000f, 0.008086507f, 0.008908720f, 0.009767680f, 0.01066443f, 0.01160000f, 0.01257317f, 0.01358272f, 0.01462968f, 0.01571509f, 0.01684000f, 0.01800736f, 0.01921448f, 0.02045392f, 0.02171824f, 0.02300000f, 0.02429461f, 0.02561024f, 0.02695857f, 0.02835125f, 0.02980000f, 0.03131083f, 0.03288368f, 0.03452112f, 0.03622571f, 0.03800000f, 0.03984667f, 0.04176800f, 0.04376600f, 0.04584267f, 0.04800000f, 0.05024368f, 0.05257304f, 0.05498056f, 0.05745872f, 0.06000000f, 0.06260197f, 0.06527752f, 0.06804208f, 0.07091109f, 0.07390000f, 0.07701600f, 0.08026640f, 0.08366680f, 0.08723280f, 0.09098000f, 0.09491755f, 0.09904584f, 0.1033674f, 0.1078846f, 0.1126000f, 0.1175320f, 0.1226744f, 0.1279928f, 0.1334528f, 0.1390200f, 0.1446764f, 0.1504693f, 0.1564619f, 0.1627177f, 0.1693000f, 0.1762431f, 0.1835581f, 0.1912735f, 0.1994180f, 0.2080200f, 0.2171199f, 0.2267345f, 0.2368571f, 0.2474812f, 0.2586000f, 0.2701849f, 0.2822939f, 0.2950505f, 0.3085780f, 0.3230000f, 0.3384021f, 0.3546858f, 0.3716986f, 0.3892875f, 0.4073000f, 0.4256299f, 0.4443096f, 0.4633944f, 0.4829395f, 0.5030000f, 0.5235693f, 0.5445120f, 0.5656900f, 0.5869653f, 0.6082000f, 0.6293456f, 0.6503068f, 0.6708752f, 0.6908424f, 0.7100000f, 0.7281852f, 0.7454636f, 0.7619694f, 0.7778368f, 0.7932000f, 0.8081104f, 0.8224962f, 0.8363068f, 0.8494916f, 0.8620000f, 0.8738108f, 0.8849624f, 0.8954936f, 0.9054432f, 0.9148501f, 0.9237348f, 0.9320924f, 0.9399226f, 0.9472252f, 0.9540000f, 0.9602561f, 0.9660074f, 0.9712606f, 0.9760225f, 0.9803000f, 0.9840924f, 0.9874812f, 0.9903128f, 0.9928116f, 0.9949501f, 0.9967108f, 0.9980983f, 0.9991120f, 0.9997482f, 1.0000000f, 0.9998567f, 0.9993046f, 0.9983255f, 0.9968987f, 0.9950000f, 0.9926005f, 0.9897426f, 0.9864444f, 0.9827241f, 0.9786000f, 0.9740837f, 0.9691712f, 0.9638568f, 0.9581349f, 0.9520000f, 0.9454504f, 0.9384992f, 0.9311628f, 0.9234576f, 0.9154000f, 0.9070064f, 0.8982772f, 0.8892048f, 0.8797816f, 0.8700000f, 0.8598613f, 0.8493920f, 0.8386220f, 0.8275813f, 0.8163000f, 0.8047947f, 0.7930820f, 0.7811920f, 0.7691547f, 0.7570000f, 0.7447541f, 0.7324224f, 0.7200036f, 0.7074965f, 0.6949000f, 0.6822192f, 0.6694716f, 0.6566744f, 0.6438448f, 0.6310000f, 0.6181555f, 0.6053144f, 0.5924756f, 0.5796379f, 0.5668000f, 0.5539611f, 0.5411372f, 0.5283528f, 0.5156323f, 0.5030000f, 0.4904688f, 0.4780304f, 0.4656776f, 0.4534032f, 0.4412000f, 0.4290800f, 0.4170360f, 0.4050320f, 0.3930320f, 0.3810000f, 0.3689184f, 0.3568272f, 0.3447768f, 0.3328176f, 0.3210000f, 0.3093381f, 0.2978504f, 0.2865936f, 0.2756245f, 0.2650000f, 0.2547632f, 0.2448896f, 0.2353344f, 0.2260528f, 0.2170000f, 0.2081616f, 0.1995488f, 0.1911552f, 0.1829744f, 0.1750000f, 0.1672235f, 0.1596464f, 0.1522776f, 0.1451259f, 0.1382000f, 0.1315003f, 0.1250248f, 0.1187792f, 0.1127691f, 0.1070000f, 0.1014762f, 0.09618864f, 0.09112296f, 0.08626485f, 0.08160000f, 0.07712064f, 0.07282552f, 0.06871008f, 0.06476976f, 0.06100000f, 0.05739621f, 0.05395504f, 0.05067376f, 0.04754965f, 0.04458000f, 0.04175872f, 0.03908496f, 0.03656384f, 0.03420048f, 0.03200000f, 0.02996261f, 0.02807664f, 0.02632936f, 0.02470805f, 0.02320000f, 0.02180077f, 0.02050112f, 0.01928108f, 0.01812069f, 0.01700000f, 0.01590379f, 0.01483718f, 0.01381068f, 0.01283478f, 0.01192000f, 0.01106831f, 0.01027339f, 0.009533311f, 0.008846157f, 0.008210000f, 0.007623781f, 0.007085424f, 0.006591476f, 0.006138485f, 0.005723000f, 0.005343059f, 0.004995796f, 0.004676404f, 0.004380075f, 0.004102000f, 0.003838453f, 0.003589099f, 0.003354219f, 0.003134093f, 0.002929000f, 0.002738139f, 0.002559876f, 0.002393244f, 0.002237275f, 0.002091000f, 0.001953587f, 0.001824580f, 0.001703580f, 0.001590187f, 0.001484000f, 0.001384496f, 0.001291268f, 0.001204092f, 0.001122744f, 0.001047000f, 0.0009765896f, 0.0009111088f, 0.0008501332f, 0.0007932384f, 0.0007400000f, 0.0006900827f, 0.0006433100f, 0.0005994960f, 0.0005584547f, 0.0005200000f, 0.0004839136f, 0.0004500528f, 0.0004183452f, 0.0003887184f, 0.0003611000f, 0.0003353835f, 0.0003114404f, 0.0002891656f, 0.0002684539f, 0.0002492000f, 0.0002313019f, 0.0002146856f, 0.0001992884f, 0.0001850475f, 0.0001719000f, 0.0001597781f, 0.0001486044f, 0.0001383016f, 0.0001287925f, 0.0001200000f, 0.0001118595f, 0.0001043224f, 0.00009733560f, 0.00009084587f, 0.00008480000f, 0.00007914667f, 0.00007385800f, 0.00006891600f, 0.00006430267f, 0.00006000000f, 0.00005598187f, 0.00005222560f, 0.00004871840f, 0.00004544747f, 0.00004240000f, 0.00003956104f, 0.00003691512f, 0.00003444868f, 0.00003214816f, 0.00003000000f, 0.00002799125f, 0.00002611356f, 0.00002436024f, 0.00002272461f, 0.00002120000f, 0.00001977855f, 0.00001845285f, 0.00001721687f, 0.00001606459f, 0.00001499000f, 0.00001398728f, 0.00001305155f, 0.00001217818f, 0.00001136254f, 0.00001060000f, 0.000009885877f, 0.000009217304f, 0.000008592362f, 0.000008009133f, 0.000007465700f, 0.000006959567f, 0.000006487995f, 0.000006048699f, 0.000005639396f, 0.000005257800f, 0.000004901771f, 0.000004569720f, 0.000004260194f, 0.000003971739f, 0.000003702900f, 0.000003452163f, 0.000003218302f, 0.000003000300f, 0.000002797139f, 0.000002607800f, 0.000002431220f, 0.000002266531f, 0.000002113013f, 0.000001969943f, 0.000001836600f, 0.000001712230f, 0.000001596228f, 0.000001488090f, 0.000001387314f, 0.000001293400f, 0.000001205820f, 0.000001124143f, 0.000001048009f, 0.0000009770578f, 0.0000009109300f, 0.0000008492513f, 0.0000007917212f, 0.0000007380904f, 0.0000006881098f, 0.0000006415300f, 0.0000005980895f, 0.0000005575746f, 0.0000005198080f, 0.0000004846123f, 0.0000004518100f}; const float CIE_Z_entries[CIE_1931_samples] = { 0.0006061000f, 0.0006808792f, 0.0007651456f, 0.0008600124f, 0.0009665928f, 0.001086000f, 0.001220586f, 0.001372729f, 0.001543579f, 0.001734286f, 0.001946000f, 0.002177777f, 0.002435809f, 0.002731953f, 0.003078064f, 0.003486000f, 0.003975227f, 0.004540880f, 0.005158320f, 0.005802907f, 0.006450001f, 0.007083216f, 0.007745488f, 0.008501152f, 0.009414544f, 0.01054999f, 0.01196580f, 0.01365587f, 0.01558805f, 0.01773015f, 0.02005001f, 0.02251136f, 0.02520288f, 0.02827972f, 0.03189704f, 0.03621000f, 0.04143771f, 0.04750372f, 0.05411988f, 0.06099803f, 0.06785001f, 0.07448632f, 0.08136156f, 0.08915364f, 0.09854048f, 0.1102000f, 0.1246133f, 0.1417017f, 0.1613035f, 0.1832568f, 0.2074000f, 0.2336921f, 0.2626114f, 0.2947746f, 0.3307985f, 0.3713000f, 0.4162091f, 0.4654642f, 0.5196948f, 0.5795303f, 0.6456000f, 0.7184838f, 0.7967133f, 0.8778459f, 0.9594390f, 1.0390501f, 1.1153673f, 1.1884971f, 1.2581233f, 1.3239296f, 1.3856000f, 1.4426352f, 1.4948035f, 1.5421903f, 1.5848807f, 1.6229600f, 1.6564048f, 1.6852959f, 1.7098745f, 1.7303821f, 1.7470600f, 1.7600446f, 1.7696233f, 1.7762637f, 1.7804334f, 1.7826000f, 1.7829682f, 1.7816998f, 1.7791982f, 1.7758671f, 1.7721100f, 1.7682589f, 1.7640390f, 1.7589438f, 1.7524663f, 1.7441000f, 1.7335595f, 1.7208581f, 1.7059369f, 1.6887372f, 1.6692000f, 1.6475287f, 1.6234127f, 1.5960223f, 1.5645280f, 1.5281000f, 1.4861114f, 1.4395215f, 1.3898799f, 1.3387362f, 1.2876400f, 1.2374223f, 1.1878243f, 1.1387611f, 1.0901480f, 1.0419000f, 0.9941976f, 0.9473473f, 0.9014531f, 0.8566193f, 0.8129501f, 0.7705173f, 0.7294448f, 0.6899136f, 0.6521049f, 0.6162000f, 0.5823286f, 0.5504162f, 0.5203376f, 0.4919673f, 0.4651800f, 0.4399246f, 0.4161836f, 0.3938822f, 0.3729459f, 0.3533000f, 0.3348578f, 0.3175521f, 0.3013375f, 0.2861686f, 0.2720000f, 0.2588171f, 0.2464838f, 0.2347718f, 0.2234533f, 0.2123000f, 0.2011692f, 0.1901196f, 0.1792254f, 0.1685608f, 0.1582000f, 0.1481383f, 0.1383758f, 0.1289942f, 0.1200751f, 0.1117000f, 0.1039048f, 0.09666748f, 0.08998272f, 0.08384531f, 0.07824999f, 0.07320899f, 0.06867816f, 0.06456784f, 0.06078835f, 0.05725001f, 0.05390435f, 0.05074664f, 0.04775276f, 0.04489859f, 0.04216000f, 0.03950728f, 0.03693564f, 0.03445836f, 0.03208872f, 0.02984000f, 0.02771181f, 0.02569444f, 0.02378716f, 0.02198925f, 0.02030000f, 0.01871805f, 0.01724036f, 0.01586364f, 0.01458461f, 0.01340000f, 0.01230723f, 0.01130188f, 0.01037792f, 0.009529306f, 0.008749999f, 0.008035200f, 0.007381600f, 0.006785400f, 0.006242800f, 0.005749999f, 0.005303600f, 0.004899800f, 0.004534200f, 0.004202400f, 0.003900000f, 0.003623200f, 0.003370600f, 0.003141400f, 0.002934800f, 0.002749999f, 0.002585200f, 0.002438600f, 0.002309400f, 0.002196800f, 0.002100000f, 0.002017733f, 0.001948200f, 0.001889800f, 0.001840933f, 0.001800000f, 0.001766267f, 0.001737800f, 0.001711200f, 0.001683067f, 0.001650001f, 0.001610133f, 0.001564400f, 0.001513600f, 0.001458533f, 0.001400000f, 0.001336667f, 0.001270000f, 0.001205000f, 0.001146667f, 0.001100000f, 0.001068800f, 0.001049400f, 0.001035600f, 0.001021200f, 0.001000000f, 0.0009686400f, 0.0009299200f, 0.0008868800f, 0.0008425600f, 0.0008000000f, 0.0007609600f, 0.0007236800f, 0.0006859200f, 0.0006454400f, 0.0006000000f, 0.0005478667f, 0.0004916000f, 0.0004354000f, 0.0003834667f, 0.0003400000f, 0.0003072533f, 0.0002831600f, 0.0002654400f, 0.0002518133f, 0.0002400000f, 0.0002295467f, 0.0002206400f, 0.0002119600f, 0.0002021867f, 0.0001900000f, 0.0001742133f, 0.0001556400f, 0.0001359600f, 0.0001168533f, 0.0001000000f, 0.00008613333f, 0.00007460000f, 0.00006500000f, 0.00005693333f, 0.00004999999f, 0.00004416000f, 0.00003948000f, 0.00003572000f, 0.00003264000f, 0.00003000000f, 0.00002765333f, 0.00002556000f, 0.00002364000f, 0.00002181333f, 0.00002000000f, 0.00001813333f, 0.00001620000f, 0.00001420000f, 0.00001213333f, 0.00001000000f, 0.000007733333f, 0.000005400000f, 0.000003200000f, 0.000001333333f, 0.000000000000f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; #define D65_Samples 531 #define D65_MIN 300 #define D65_MAX (D65_MIN + D65_Samples - 1) const float D65_SPD[] = { 0.0341f, 0.36014f, 0.68618f, 1.01222f, 1.33826f, 1.6643f, 1.99034f, 2.31638f, 2.64242f, 2.96846f, 3.2945f, 4.98865f, 6.6828f, 8.37695f, 10.0711f, 11.7652f, 13.4594f, 15.1535f, 16.8477f, 18.5418f, 20.236f, 21.9177f, 23.5995f, 25.2812f, 26.963f, 28.6447f, 30.3265f, 32.0082f, 33.69f, 35.3717f, 37.0535f, 37.343f, 37.6326f, 37.9221f, 38.2116f, 38.5011f, 38.7907f, 39.0802f, 39.3697f, 39.6593f, 39.9488f, 40.4451f, 40.9414f, 41.4377f, 41.934f, 42.4302f, 42.9265f, 43.4228f, 43.9191f, 44.4154f, 44.9117f, 45.0844f, 45.257f, 45.4297f, 45.6023f, 45.775f, 45.9477f, 46.1203f, 46.293f, 46.4656f, 46.6383f, 47.1834f, 47.7285f, 48.2735f, 48.8186f, 49.3637f, 49.9088f, 50.4539f, 50.9989f, 51.544f, 52.0891f, 51.8777f, 51.6664f, 51.455f, 51.2437f, 51.0323f, 50.8209f, 50.6096f, 50.3982f, 50.1869f, 49.9755f, 50.4428f, 50.91f, 51.3773f, 51.8446f, 52.3118f, 52.7791f, 53.2464f, 53.7137f, 54.1809f, 54.6482f, 57.4589f, 60.2695f, 63.0802f, 65.8909f, 68.7015f, 71.5122f, 74.3229f, 77.1336f, 79.9442f, 82.7549f, 83.628f, 84.5011f, 85.3742f, 86.2473f, 87.1204f, 87.9936f, 88.8667f, 89.7398f, 90.6129f, 91.486f, 91.6806f, 91.8752f, 92.0697f, 92.2643f, 92.4589f, 92.6535f, 92.8481f, 93.0426f, 93.2372f, 93.4318f, 92.7568f, 92.0819f, 91.4069f, 90.732f, 90.057f, 89.3821f, 88.7071f, 88.0322f, 87.3572f, 86.6823f, 88.5006f, 90.3188f, 92.1371f, 93.9554f, 95.7736f, 97.5919f, 99.4102f, 101.228f, 103.047f, 104.865f, 106.079f, 107.294f, 108.508f, 109.722f, 110.936f, 112.151f, 113.365f, 114.579f, 115.794f, 117.008f, 117.088f, 117.169f, 117.249f, 117.33f, 117.41f, 117.49f, 117.571f, 117.651f, 117.732f, 117.812f, 117.517f, 117.222f, 116.927f, 116.632f, 116.336f, 116.041f, 115.746f, 115.451f, 115.156f, 114.861f, 114.967f, 115.073f, 115.18f, 115.286f, 115.392f, 115.498f, 115.604f, 115.711f, 115.817f, 115.923f, 115.212f, 114.501f, 113.789f, 113.078f, 112.367f, 111.656f, 110.945f, 110.233f, 109.522f, 108.811f, 108.865f, 108.92f, 108.974f, 109.028f, 109.082f, 109.137f, 109.191f, 109.245f, 109.3f, 109.354f, 109.199f, 109.044f, 108.888f, 108.733f, 108.578f, 108.423f, 108.268f, 108.112f, 107.957f, 107.802f, 107.501f, 107.2f, 106.898f, 106.597f, 106.296f, 105.995f, 105.694f, 105.392f, 105.091f, 104.79f, 105.08f, 105.37f, 105.66f, 105.95f, 106.239f, 106.529f, 106.819f, 107.109f, 107.399f, 107.689f, 107.361f, 107.032f, 106.704f, 106.375f, 106.047f, 105.719f, 105.39f, 105.062f, 104.733f, 104.405f, 104.369f, 104.333f, 104.297f, 104.261f, 104.225f, 104.19f, 104.154f, 104.118f, 104.082f, 104.046f, 103.641f, 103.237f, 102.832f, 102.428f, 102.023f, 101.618f, 101.214f, 100.809f, 100.405f, 100.0f, 99.6334f, 99.2668f, 98.9003f, 98.5337f, 98.1671f, 97.8005f, 97.4339f, 97.0674f, 96.7008f, 96.3342f, 96.2796f, 96.225f, 96.1703f, 96.1157f, 96.0611f, 96.0065f, 95.9519f, 95.8972f, 95.8426f, 95.788f, 95.0778f, 94.3675f, 93.6573f, 92.947f, 92.2368f, 91.5266f, 90.8163f, 90.1061f, 89.3958f, 88.6856f, 88.8177f, 88.9497f, 89.0818f, 89.2138f, 89.3459f, 89.478f, 89.61f, 89.7421f, 89.8741f, 90.0062f, 89.9655f, 89.9248f, 89.8841f, 89.8434f, 89.8026f, 89.7619f, 89.7212f, 89.6805f, 89.6398f, 89.5991f, 89.4091f, 89.219f, 89.029f, 88.8389f, 88.6489f, 88.4589f, 88.2688f, 88.0788f, 87.8887f, 87.6987f, 87.2577f, 86.8167f, 86.3757f, 85.9347f, 85.4936f, 85.0526f, 84.6116f, 84.1706f, 83.7296f, 83.2886f, 83.3297f, 83.3707f, 83.4118f, 83.4528f, 83.4939f, 83.535f, 83.576f, 83.6171f, 83.6581f, 83.6992f, 83.332f, 82.9647f, 82.5975f, 82.2302f, 81.863f, 81.4958f, 81.1285f, 80.7613f, 80.394f, 80.0268f, 80.0456f, 80.0644f, 80.0831f, 80.1019f, 80.1207f, 80.1395f, 80.1583f, 80.177f, 80.1958f, 80.2146f, 80.4209f, 80.6272f, 80.8336f, 81.0399f, 81.2462f, 81.4525f, 81.6588f, 81.8652f, 82.0715f, 82.2778f, 81.8784f, 81.4791f, 81.0797f, 80.6804f, 80.281f, 79.8816f, 79.4823f, 79.0829f, 78.6836f, 78.2842f, 77.4279f, 76.5716f, 75.7153f, 74.859f, 74.0027f, 73.1465f, 72.2902f, 71.4339f, 70.5776f, 69.7213f, 69.9101f, 70.0989f, 70.2876f, 70.4764f, 70.6652f, 70.854f, 71.0428f, 71.2315f, 71.4203f, 71.6091f, 71.8831f, 72.1571f, 72.4311f, 72.7051f, 72.979f, 73.253f, 73.527f, 73.801f, 74.075f, 74.349f, 73.0745f, 71.8f, 70.5255f, 69.251f, 67.9765f, 66.702f, 65.4275f, 64.153f, 62.8785f, 61.604f, 62.4322f, 63.2603f, 64.0885f, 64.9166f, 65.7448f, 66.573f, 67.4011f, 68.2293f, 69.0574f, 69.8856f, 70.4057f, 70.9259f, 71.446f, 71.9662f, 72.4863f, 73.0064f, 73.5266f, 74.0467f, 74.5669f, 75.087f, 73.9376f, 72.7881f, 71.6387f, 70.4893f, 69.3398f, 68.1904f, 67.041f, 65.8916f, 64.7421f, 63.5927f, 61.8752f, 60.1578f, 58.4403f, 56.7229f, 55.0054f, 53.288f, 51.5705f, 49.8531f, 48.1356f, 46.4182f, 48.4569f, 50.4956f, 52.5344f, 54.5731f, 56.6118f, 58.6505f, 60.6892f, 62.728f, 64.7667f, 66.8054f, 66.4631f, 66.1209f, 65.7786f, 65.4364f, 65.0941f, 64.7518f, 64.4096f, 64.0673f, 63.7251f, 63.3828f, 63.4749f, 63.567f, 63.6592f, 63.7513f, 63.8434f, 63.9355f, 64.0276f, 64.1198f, 64.2119f, 64.304f, 63.8188f, 63.3336f, 62.8484f, 62.3632f, 61.8779f, 61.3927f, 60.9075f, 60.4223f, 59.9371f, 59.4519f, 58.7026f, 57.9533f, 57.204f, 56.4547f, 55.7054f, 54.9562f, 54.2069f, 53.4576f, 52.7083f, 51.959f, 52.5072f, 53.0553f, 53.6035f, 54.1516f, 54.6998f, 55.248f, 55.7961f, 56.3443f, 56.8924f, 57.4406f, 57.7278f, 58.015f, 58.3022f, 58.5894f, 58.8765f, 59.1637f, 59.4509f, 59.7381f, 60.0253f, 60.3125f}; /** * Converts an XYZ value to the sRGB color space (linear, no gamma correction is applied) */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F XYZ_to_sRGB(const ColorRGB32F& XYZ) { /** * Reference: https://en.wikipedia.org/wiki/SRGB#Correspondence_to_CIE_XYZ_stimulus */ float r = 3.240479f * XYZ[0] + -1.537150f * XYZ[1] + -0.498535f * XYZ[2]; float g = -0.969256f * XYZ[0] + 1.875991f * XYZ[1] + 0.041556f * XYZ[2]; float b = 0.055648f * XYZ[0] + -0.204043f * XYZ[1] + 1.057311f * XYZ[2]; return ColorRGB32F(r, g, b); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F wavelength_to_XYZ(float wavelength) { ColorRGB32F XYZ; float index_float = wavelength - CIE_1931_MIN; int index_low = hippt::max((int)index_float - 1, 0); int index_high = hippt::min(index_low + 1, CIE_1931_samples - 1); float t = wavelength - (int)wavelength; float x1 = CIE_X_entries[index_low]; float x2 = CIE_X_entries[index_high]; float y1 = CIE_Y_entries[index_low]; float y2 = CIE_Y_entries[index_high]; float z1 = CIE_Z_entries[index_low]; float z2 = CIE_Z_entries[index_high]; XYZ.r = hippt::lerp(x1, x2, t); XYZ.g = hippt::lerp(y1, y2, t); XYZ.b = hippt::lerp(z1, z2, t); // Now scaling by the intensity of the D65 illuminant (which is the point of sRGB) int wavelength_index = hippt::min((int)roundf(wavelength), D65_MAX); float SPD = D65_SPD[wavelength_index - D65_MIN]; XYZ *= SPD; // Average intensity of the D65 illuminant over its wavelengths XYZ /= 22.2175f; return XYZ; } /** * Returns the RGB color of a given wavelength using the CIE 1931 2° Observer CMFs * to get the wavelength to XYZ. * * The XYZ value of the wavelength is then multiplied by the intensity of the D65 * illuminant at that wavelength. * * That scaled XYZ value is then brought to the sRGB color space (linear sRGB, this is not gamma corrected) * using the matrix readily available on Wikipedia: https://en.wikipedia.org/wiki/SRGB#Correspondence_to_CIE_XYZ_stimulus * * The resulting RGB value is clamped to 0 to avoid negative values and then that clamped RGB * is then normalized such that the average of the clamped RGB values of wavelengths * between 360 and 830 is RGB(1.0f, 1.0f, 1.0f) * * For rendering purposes, we should only clamp the average of the non-clamped RGB values. We should not * clamp the individual RGB values themselves. But I found that this introduced so instabilities + we then * have to handle negative values in the system so that's annoying. * * So instead we're clamping the individual values but because that's kind of incorrect practice, we end * up we an average of RGB values that is not RGB(1.0f, 1.0f, 1.0f). So that's why we're * normalizing with the 'scale' factor in the function to bring that average back to RGB(1.0f, 1.0f, 1.0f) * * This normalization trick actually is imperceptible in practice so I guess it's fine and convenient */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F wavelength_to_RGB_clamped(float wavelength) { const ColorRGB32F scale = ColorRGB32F(1.4979f, 1.13591f, 1.13159f); ColorRGB32F RGB = XYZ_to_sRGB(wavelength_to_XYZ(wavelength)); RGB.clamp(0, 1.0e35f); return RGB / scale; } #endif /** * Fitted curves for converting a wavelength to its RGB values. * * This is actually a fit of the 'wavelength_to_RGB_clamped()' such that * we can get the same results as this function but without the need for lookup tables * * This function takes wavelengths between 360 and 830nm and returns RGB values such that * the average of the RGB values of all wavelengths is RGB(1.0f, 1.0f, 1.0f). */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F wavelength_to_RGB_fit(float wavelength) { ColorRGB32F RGB; if( wavelength < 463.0f) { RGB.r = -1.2776028240727566e-01f / (1.0f + exp((wavelength - 4.2680623367293401e+02f) / 8.2197460736637176e+00f)) + -1.3925673552505122e-11f * exp((wavelength - 45.0f) / 1.8175459086411596e+01f); RGB.r += 1.2898689750552100e-01f; } else if (wavelength > 553.0f) { RGB.r = 1.7963649137825513e+01f * ( 1.0f / 2.6577826611702449e+01f) * exp(-0.5f * hippt::square((wavelength - 6.0625724092824566e+02f) * (1.0f / 2.6577826611702449e+01f))); RGB.r += 2.5574660155104657e-03f; } else RGB.r = 0.0f; RGB.g = 3.4962267376163049e+02f * expf(-0.5f * hippt::square((wavelength-5.4209217455705152e+02f) / -2.9598170255834638e+01f)); RGB.g /= wavelength; RGB.b = exp(3.2987659944421112e+03f + (-2.0975839709372405e+05f / wavelength) -4.6368268395094020e+02f * logf(wavelength)); // The fitting process was done with scaled data so the data is actually // fitted such that the average RGB colors of all wavelength is 0.1. But we want 1. // So we multiply by 10. return RGB * 10.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F wavelength_to_RGB(float wavelength) { #if WavelengthToRGBMethod == WAVELENGTH_TO_RGB_FIT return wavelength_to_RGB_fit(wavelength); #elif WavelengthToRGBMethod == WAVELENGTH_TO_RGB_TABLES return wavelength_to_RGB_clamped(wavelength); #endif } HIPRT_HOST_DEVICE HIPRT_INLINE float sample_wavelength_uniformly(Xorshift32Generator& random_number_generator) { float r = random_number_generator(); return r * (MAX_SAMPLE_WAVELENGTH - MIN_SAMPLE_WAVELENGTH) + MIN_SAMPLE_WAVELENGTH; } /** * Reference: * [1] [Open PBR Specification] https://academysoftwarefoundation.github.io/OpenPBR/#model/basesubstrate/translucentbase * * Given the dispersion parameters of material and a base IOR * (assumed to be the IOR of the material measured at 587.6nm), * returns the new IOR of the material but as if measured at the given * 'wavelength' */ HIPRT_HOST_DEVICE HIPRT_INLINE float compute_dispersion_ior(float dispersion_abbe_number, float dispersion_scale, float base_IOR, float wavelength) { if (dispersion_scale == 0.0f) return base_IOR; #define SQUARE_587_6 334777.96f // 587.6^2 #define POW_MIN2_LAMBDA_F_MINUS_LAMBDA_C 0.00000191038851931481f // 486.1^(-2) - 656.3^(-2) float abbe_number = dispersion_abbe_number / dispersion_scale; float B = (base_IOR - 1.0f) / (abbe_number * POW_MIN2_LAMBDA_F_MINUS_LAMBDA_C); float A = base_IOR - B / (SQUARE_587_6); return A + B / (wavelength * wavelength); } /** * Essentially returns the RGB color associated with a wavelength. * * Only returns the color if the given 'wavelength' is negative. * If the wavelength passed is negative, it is negated so that it becomes * positive (hence the passing by reference) * * If the wavelength is positive, this implicitely means that the wavelength * throughput filter has already been applied to the ray and should not * be applied a second time. */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F get_dispersion_ray_color(float& wavelength, float dispersion_scale) { if (dispersion_scale == 0.0f) // No dispersion return ColorRGB32F(1.0f); if (wavelength >= 0.0f) // Wavelength isn't negative, dispersion wavelength throughput filter // has already been applied return ColorRGB32F(1.0f); wavelength *= -1.0f; return wavelength_to_RGB(wavelength); } /** * Below are some utility functions that were used to generate the fit of 'wavelength_to_RGB_fit', * verify the implementation etc... */ #ifndef __KERNELCC__ #include "Image/Image.h" /** * Write CSV files for the R, G and B values at each wavelength. * Used for fitting curves */ static void write_wavelength_to_RGB_data_to_file() { const int nb_samples = 10000; std::ofstream R("wavelength_to_rgb_R_" + std::to_string(nb_samples) + ".csv"); std::ofstream G("wavelength_to_rgb_G_" + std::to_string(nb_samples) + ".csv"); std::ofstream B("wavelength_to_rgb_B_" + std::to_string(nb_samples) + ".csv"); float Y_sum = 0.0f; for (int i = 0; i < nb_samples; i++) { float wavelength = MIN_SAMPLE_WAVELENGTH + i * (MAX_SAMPLE_WAVELENGTH - MIN_SAMPLE_WAVELENGTH) / static_cast(nb_samples); ColorRGB32F RGB = wavelength_to_RGB(wavelength); R << wavelength << ", " << RGB.r << std::endl; G << wavelength << ", " << RGB.g << std::endl; B << wavelength << ", " << RGB.b << std::endl; } } /** * Prints the average of the RGB value of all wavelengths. * This should always output (1.0f, 1.0f, 1.0f) */ static void average_RGB_for_render() { ColorRGB32F average_RGB; const int nb_samples = 1000000; for (int i = 0; i < nb_samples; i++) { float wavelength = MIN_SAMPLE_WAVELENGTH + i * (MAX_SAMPLE_WAVELENGTH - MIN_SAMPLE_WAVELENGTH) / static_cast(nb_samples); average_RGB += wavelength_to_RGB(wavelength) / nb_samples; } std::cout << "Average RGB of all wavelengths for rendering: " << average_RGB << std::endl; } /** * Computes the RGB values of all wavelengths and write that to a file, producing a rainbow * image. */ static void write_rainbow_to_file() { const int width = 1280; const int height = 720; Image32Bit rainbow(width, height, 3); for (int x = 0; x < width; x++) { float t = x / static_cast(width - 1); float wavelength = t * (MAX_SAMPLE_WAVELENGTH - MIN_SAMPLE_WAVELENGTH) + MIN_SAMPLE_WAVELENGTH; ColorRGB32F RGB = wavelength_to_RGB(wavelength); for (int y = 0; y < height; y++) { rainbow[(y * width + x) * 3 + 0] = RGB.r; rainbow[(y * width + x) * 3 + 1] = RGB.g; rainbow[(y * width + x) * 3 + 2] = RGB.b; } } rainbow.write_image_png("rainbow.png"); } #endif #endif ================================================ FILE: src/Device/includes/FixIntellisense.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_FIX_INTELLISENSE_H #define DEVICE_FIX_INTELLISENSE_H /* * All that is in this file is meant to make Visual Studio's intellisense happy * in the kernel code so that we have autocompletion and no * red-underlined-stinky-disgusting intellisense error telling us that no, M_PI is not * defined (even though it is at compile time for the GPU) etc... blah blah blah */ // The HIPRT_KERNEL_SIGNATURE is only useful to help Visual Studio's Intellisense // Without this macro, all kernel functions would be declared as: // extern "C" void __global__ my_function(......) // but Visual Studio doesn't like the 'extern "C" void __global__' part and it // breaks code coloration and autocompletion. It is however required for the shader // compiler // To circumvent this problem, we're only declaring the functions 'void' when in the text editor // (when __KERNELCC__ is not defined) and we're correctly declaring the function with the full // attributes when it's the shader compiler processing the function (when __KERNELCC__ is defined) // We're also defining blockDim, blockIdx and threadIdx because they are udefined otherwise... #ifdef __KERNELCC__ #define GLOBAL_KERNEL_SIGNATURE(returnType) extern "C" returnType __global__ #define DEVICE_KERNEL_SIGNATURE(returnType) extern "C" returnType __device__ #else struct dummyVec3 { int x, y, z; }; static dummyVec3 blockDim, blockIdx, threadIdx, gridDim; #define GLOBAL_KERNEL_SIGNATURE(returnType) returnType #define DEVICE_KERNEL_SIGNATURE(returnType) returnType #define __shared__ #define __restrict__ // TODO move all of this in Math.h inline void __syncthreads() {} inline void __syncwarp() {} inline unsigned int __activemask() { return 1; } inline unsigned int __ballot() { return 1; } // For using printf in Kernels #include #endif // #ifdef __KERNELCC__ #if defined(__KERNELCC__) // GPU #define GPU_CPU_ALIGN(n) __align__(n) #elif defined(__GNUC__) // GCC #define GPU_CPU_ALIGN(n) __attribute__((aligned(n))) #elif defined(_MSC_VER) // MSVC #define GPU_CPU_ALIGN(n) __declspec(align(n)) #else #error "Please provide a definition for GPU_CPU_ALIGN macro for your host compiler!" #endif #endif // FIX_INTELISSENSE_H ================================================ FILE: src/Device/includes/Fresnel.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_FRESNEL_H #define DEVICE_FRESNEL_H #include "HostDeviceCommon/Color.h" HIPRT_HOST_DEVICE HIPRT_INLINE float F0_from_eta(float eta_t, float eta_i) { float nume_F0 = (eta_t - eta_i); float denom_F0 = (eta_t + eta_i); float F0 = (nume_F0 * nume_F0) / (denom_F0 * denom_F0); return F0; } /** * relative_eta here is eta_t / eta_i */ HIPRT_HOST_DEVICE HIPRT_INLINE float F0_from_eta_t_and_relative_ior(float eta_t, float relative_eta) { return F0_from_eta(eta_t, /* eta_i */ eta_t / relative_eta); } /** * Schlick's approximation for dielectric fresnel reflectance */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F fresnel_schlick(ColorRGB32F F0, float angle) { return F0 + (ColorRGB32F(1.0f) - F0) * hippt::pow_5(1.0f - angle); } /** * Full reflectance fresnel dielectric formula * * 'relative_eta' is eta_t / eta_i = transmitted media IOR / incident media IOR */ HIPRT_HOST_DEVICE HIPRT_INLINE float full_fresnel_dielectric(float cos_theta_i, float relative_eta) { if (hippt::abs(1.0f - relative_eta) < 1.0e-4f) // relative_eta of 1, no fresnel return 0.0f; // Computing cos_theta_t float sin_theta_i2 = 1.0f - cos_theta_i * cos_theta_i; float sin_theta_t2 = sin_theta_i2 / (relative_eta * relative_eta); if (sin_theta_t2 >= 1.0f) // Total internal reflection, 0% refraction, all reflection return 1.0f; float cos_theta_t = sqrt(1.0f - sin_theta_t2); float r_parallel = (relative_eta * cos_theta_i - cos_theta_t) / (relative_eta * cos_theta_i + cos_theta_t); float r_perpendicular = (cos_theta_i - relative_eta * cos_theta_t) / (cos_theta_i + relative_eta * cos_theta_t); return (r_parallel * r_parallel + r_perpendicular * r_perpendicular) / 2; } /** * Override of full_fresnel_dielectric with two separate eta */ HIPRT_HOST_DEVICE HIPRT_INLINE float full_fresnel_dielectric(float cos_theta_i, float eta_i, float eta_t) { return full_fresnel_dielectric(cos_theta_i, eta_t / eta_i); } /** * Computes the reflectance at normal incidence from the two * given eta and uses that reflectance to compute the dielectric * fresnel reflectance using schlick's approximation * * This function is basically a shorthand for: * ColorRGB32F F0 = * return schlick(F0, NoL) */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F fresnel_schlick_from_ior(float eta_i, float eta_t, float cos_theta_i) { float F0 = F0_from_eta(eta_t, eta_i); return fresnel_schlick(ColorRGB32F(F0), cos_theta_i); } /** * Overload with normal and light direction */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F fresnel_schlick_from_ior(float eta_i, float eta_t, const float3& normal, const float3& local_to_light_direction) { float NoL = hippt::clamp(1.0e-8f, 1.0f, hippt::dot(normal, local_to_light_direction)); return fresnel_schlick_from_ior(eta_i, eta_t, NoL); } /** * Implementation of [Artist Friendly Metallic Fresnel, Gulbrandsen, 2014] for * computing the complex index of refraction of metals from two intuitive color parameters * 'reflectivity' and 'edge_tint' */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F gulbrandsen_metallic_complex_fresnel(const ColorRGB32F& reflectivity, const ColorRGB32F& edge_tint, float cos_theta_i) { // TODO we should precompute k and n on the CPU from 'reflectivity' and 'edge_tint' // Computing n and k from the 'reflectivity' and 'edge_tint' artist parameters ColorRGB32F one = ColorRGB32F(1.0f); ColorRGB32F sqrt_r = sqrt(reflectivity); ColorRGB32F left_n = edge_tint * ((one - reflectivity) / (one + reflectivity)); ColorRGB32F right_n = (one - edge_tint) * ((one + sqrt_r) / (one - sqrt_r)); ColorRGB32F n = left_n + right_n; ColorRGB32F k_left = n + one; k_left *= k_left; k_left *= reflectivity; ColorRGB32F k_right = n - one; k_right *= k_right; ColorRGB32F k_sqr = (k_left - k_right) / (one - reflectivity); // Computing the approximation for non polarized light based on Rs and Rp // for the perpendicular and parallel components of the light ColorRGB32F Rs_nume = n * n + k_sqr - 2.0f * n * cos_theta_i + ColorRGB32F(cos_theta_i * cos_theta_i); ColorRGB32F Rs_denom = n * n + k_sqr + 2.0f * n * cos_theta_i + ColorRGB32F(cos_theta_i * cos_theta_i); ColorRGB32F Rs = Rs_nume / Rs_denom; ColorRGB32F Rp_nume = (n * n + k_sqr) * cos_theta_i * cos_theta_i - 2.0f * n * cos_theta_i + one; ColorRGB32F Rp_denom = (n * n + k_sqr) * cos_theta_i * cos_theta_i + 2.0f * n * cos_theta_i + one; ColorRGB32F Rp = Rp_nume / Rp_denom; return 0.5f * (Rs + Rp); } /** * Reference: * * [1] [Generalization of Adobe's Fresnel Model, Hoffman, 2023] * [2] [Adobe Standard Material, Technical Documentation, Kutz, Hasan, Edmondson] */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F adobe_f82_tint_fresnel(const ColorRGB32F& F0, const ColorRGB32F& F82, const ColorRGB32F& F90, float F90_falloff_exponent, float cos_theta) { ColorRGB32F base_term = F0 + (F90 - F0) * pow(1.0f - cos_theta, F90_falloff_exponent); if (base_term.max_component() < 1.0e-8f) // Quick exit if the base term is super low to avoid numerical issues with super low // float numbers return ColorRGB32F(0.0f); float lazanyi_correction = cos_theta * hippt::pow_6(1.0f - cos_theta); // cos_theta_max for beta exponent = 6 in the lazanyi correction term constexpr float cos_theta_max = 1.0f / 7.0f; constexpr float denom_a = cos_theta_max * hippt::pow_6(1.0f - cos_theta_max); ColorRGB32F nume_a = (F0 + (F90 - F0) * pow(1.0f - cos_theta_max, F90_falloff_exponent)) * (ColorRGB32F(1.0f) - F82); ColorRGB32F a = nume_a / denom_a; ColorRGB32F F = base_term - a * lazanyi_correction; F.clamp(0.0f, 1.0f); return F; } /** * Reference: * [1] [A Hitchhiker's Guide to Multiple Scattering, Eugene d'Eon] https://eugenedeon.com/hitchhikers * Eq. 11.15 * * Hemispherical albedo (integral of directional albedos over view directions) * of a perfectly smooth dielectric layer. This is an approximated fit. */ HIPRT_HOST_DEVICE HIPRT_INLINE float fresnel_hemispherical_albedo_fit(float relative_eta) { return logf((10893.0f * relative_eta - 1438.2f) / (-774.4f * hippt::square(relative_eta) + 10212.0f * relative_eta + 1.0f)); } #endif ================================================ FILE: src/Device/includes/GBufferDevice.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GBUFFER_DEVICE_H #define GBUFFER_DEVICE_H #include "Device/includes/RayVolumeState.h" #include "HostDeviceCommon/Material/MaterialPacked.h" // Structure of arrays for the data contained in the pixels of the GBuffer // // If you want the roughness of the pixel (X, Y) = [50, 0] for example, // get it at materials[50].get_roughness() struct GBufferDevice { HIPRT_HOST_DEVICE float3 get_view_direction(float3 camera_position, int pixel_index) const { return hippt::normalize(camera_position - primary_hit_position[pixel_index]); } DevicePackedEffectiveMaterial* materials = nullptr; int* first_hit_prim_index = nullptr; float3* primary_hit_position = nullptr; // We need both normals to correct the black fringes from the microfacet // model when used with smooth normals / normal mapping Octahedral24BitNormalPadded32b* shading_normals = nullptr; Octahedral24BitNormalPadded32b* geometric_normals = nullptr; }; #endif ================================================ FILE: src/Device/includes/GMoN/GMoN.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_GMON_H #define DEVICE_INCLUDES_GMON_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/GMoN/GMoNMeansRadixSort.h" #include "Device/includes/GMoN/GMoNDevice.h" #include "HostDeviceCommon/Color.h" // A bunch of macros here to streamline the code between the CPU and GPU #ifdef __KERNELCC__ #define SORTED_MEANS_VARIABLE #define SORTED_MEANS_VARIABLE_WITH_COMMA // Nothing to declare for the sorted means: on the GPU the sorted means are in shared memory, already declared in 'GMoNMeansRadixSort' #define SORTED_MEANS_DECLARATION #define SORTED_MEANS_DECLARATION_WITH_COMMA #define SORTED_MEANS_ASSIGNATION(x) x // Getting the sorted mean of index 'mean_index' (in shared memory on the GPU) #define SORTED_MEANS_FETCH(mean_index) scratch_memory[SCRATCH_MEMORY_INDEX(0, (mean_index))] #define SORTED_INDEX_FETCH(set_index) (sorted_keys[SORTED_KEYS_INDEX(set_index)] & 0xFF) #else // Just a macro for the name of the sorted means std::vector #define SORTED_MEANS_VARIABLE sorted_means #define SORTED_MEANS_VARIABLE_WITH_COMMA ,sorted_means // On the CPU, the sorted means are in a std::vector #define SORTED_MEANS_DECLARATION std::pair, std::vector> SORTED_MEANS_VARIABLE #define SORTED_MEANS_DECLARATION_WITH_COMMA ,std::pair, std::vector> SORTED_MEANS_VARIABLE // Assigning to the sorted means vector #define SORTED_MEANS_ASSIGNATION(x) SORTED_MEANS_VARIABLE = (x) // Getting the sorted mean of index 'mean_index' (in the 'sorted_means' std::vector on the CPU) #define SORTED_MEANS_FETCH(mean_index) SORTED_MEANS_VARIABLE.first[(mean_index)] #define SORTED_INDEX_FETCH(set_index) (SORTED_MEANS_VARIABLE.second[set_index] & 0xFF) #endif HIPRT_HOST_DEVICE HIPRT_INLINE float compute_gini_coefficient(SORTED_MEANS_DECLARATION) { // Applying Eq. 4 of the paper float sum_of_means = 0.0f; float sum_of_means_weighted = 0.0f; for (int j = 1; j <= GMoNMSetsCount; j++) { unsigned int sorted_mean_uint = SORTED_MEANS_FETCH(j - 1); float sorted_mean_float = *reinterpret_cast(&sorted_mean_uint); sum_of_means += sorted_mean_float; sum_of_means_weighted += j * sorted_mean_float; } float nume = 2.0f * sum_of_means_weighted; float denom = GMoNMSetsCount * sum_of_means; if (denom == 0.0f) return 0.0f; return nume / denom - static_cast(GMoNMSetsCount + 1) / GMoNMSetsCount; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F get_median_of_means(GMoNDevice gmon_device, unsigned int pixel_index, int2 render_resolution SORTED_MEANS_DECLARATION_WITH_COMMA) { // Getting the index of the set for the sorted median unsigned short int median_set_index = SORTED_INDEX_FETCH(GMoNMSetsCount / 2); return gmon_device.sets[median_set_index * render_resolution.x * render_resolution.y + pixel_index]; } /** * Computes the median of means over the sets and stores the * result in the 'result_framebuffer' buffer. The result will * be stored scaled by the number of samples rendered by the * path tracer so far such that dividing the 'result_framebuffer' * buffer by the number of samples yields the correct color for * displaying in the viewport */ HIPRT_HOST_DEVICE ColorRGB32F gmon_compute_median_of_means(GMoNDevice gmon_device, uint32_t pixel_index, unsigned int sample_number, int2 render_resolution) { SORTED_MEANS_DECLARATION; SORTED_MEANS_ASSIGNATION(gmon_means_radix_sort(gmon_device.sets, pixel_index, sample_number, render_resolution)); switch (gmon_device.gmon_mode) { case GMoNDevice::GMoNMode::MEDIAN_OF_MEANS: // Multiplying by the number of sets here because (with an example): // - If we have 5 sets // - We rendered 35 samples so far // - Each set has 7 samples // - But the display shader in the viewport expects 35 samples worth of intensity in the framebuffer // - So we need to return the color (which is 7 sample-accumulated) multiplied by the number of sets // to get back our 35 return get_median_of_means(gmon_device, pixel_index, render_resolution SORTED_MEANS_VARIABLE_WITH_COMMA) * GMoNMSetsCount; case GMoNDevice::GMoNMode::BINARY_GMON: { float gini_coefficient = compute_gini_coefficient(SORTED_MEANS_VARIABLE); // Eq. 5 of the paper if (gini_coefficient <= 0.25f) { // Return the mean. We're actually just going to return the sum of the samples and it is the shader that // displays in the viewport that is going to divide that sum by the number of samples rendered so far, // thus giving us the mean ColorRGB32F sum; for (int i = 0; i < GMoNMSetsCount; i++) sum += gmon_device.sets[render_resolution.x * render_resolution.y * i + pixel_index]; return sum; } else // Return the median of means // Multiplying by the number of sets here because (with an example): // - If we have 5 sets // - We rendered 35 samples so far // - Each set has 7 samples // - But the display shader in the viewport expects 35 samples worth of intensity in the framebuffer // - So we need to return the color (which is 7 sample-accumulated) multiplied by the number of sets // to get back our 35 return get_median_of_means(gmon_device, pixel_index, render_resolution SORTED_MEANS_VARIABLE_WITH_COMMA) * GMoNMSetsCount; } case GMoNDevice::GMoNMode::ADAPTIVE_GMON: { // Section 4.3 and Eq. 6 float gini_coefficient = compute_gini_coefficient(SORTED_MEANS_VARIABLE); if (gini_coefficient == 0.0f) return ColorRGB32F(0.0f); int c = gini_coefficient * (GMoNMSetsCount / 2); // Eq. 6 ColorRGB32F sum; for (int i = c; i < GMoNMSetsCount - c; i++) sum += gmon_device.sets[SORTED_INDEX_FETCH(i) * render_resolution.x * render_resolution.y + pixel_index]; // We want this function to return un-averaged colors such that it is // the shader that displays in the viewport that does the averaging. // That's why we have the multiplication by GMoNMSetsCount at the end (which isn't in the paper) return sum / (GMoNMSetsCount - 2 * c) * GMoNMSetsCount; } default: // We shouldn't be here, this means that the GMoNMode used isn't one of the GMoNMode enum return ColorRGB32F(10000.0f, 0.0f, 10000.0f); } // We cannot be here, this would mean that the switch skipped every single case, including the default case return ColorRGB32F(10000.0f, 0.0f, 0.0f); } #endif ================================================ FILE: src/Device/includes/GMoN/GMoNDevice.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_GMON_DEVICE_H #define DEVICE_GMON_DEVICE_H #include "HostDeviceCommon/Color.h" /** * Data structure for the implementation of GMoN * * Reference: * [1] [Firefly removal in Monte Carlo rendering with adaptive Median of meaNs, Buisine et al., 2021] */ struct GMoNDevice { enum GMoNMode { MEDIAN_OF_MEANS = 0, BINARY_GMON = 1, ADAPTIVE_GMON = 2, }; GMoNMode gmon_mode = GMoNMode::ADAPTIVE_GMON; // This is one very big buffer that contains all the sets we accumulate into for GMoN // // For example, for GMoNMSets == 5 and a render resolution of 1280x720, // this is going to be a buffer that is 1280*720*5 elements long ColorRGB32F* sets = nullptr; // This is the buffer that contains the G-median of means result of each pixel and this is going // to be displayed in the viewport instead of the regular framebuffer if GMoN is being used ColorRGB32F* result_framebuffer = nullptr; // Which is the next set that is going to receive the sample unsigned int next_set_to_accumulate = 0; }; #endif ================================================ FILE: src/Device/includes/GMoN/GMoNMeansRadixSort.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_GMON_RADIX_SORT_H #define DEVICE_GMON_RADIX_SORT_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/GMoN/GMoNMeansRadixSortHistogramDeclaration.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/KernelOptions/GMoNOptions.h" #include "HostDeviceCommon/Math.h" // Some macros to make that single function work on the CPU and GPU #ifdef __KERNELCC__ #define GMoNThreadsPerBlock (GMoNComputeMeansKernelThreadBlockSize * GMoNComputeMeansKernelThreadBlockSize) // Allocating enough shared memory for each thread to store the M keys it's going to need for sorting. // We multiply everything * 2 by radix sort isn't in place so we need *1 for the input buffer of keys to sort // and another *1 for the sorted keys __shared__ unsigned int scratch_memory[GMoNThreadsPerBlock * GMoNMSetsCount * 2]; __shared__ unsigned short int sorted_keys[GMoNThreadsPerBlock * GMoNMSetsCount]; #define ThreadIndex1D (threadIdx.x + threadIdx.y * blockDim.x) // The indexing used here tries to avoid bank conflicts #define SCRATCH_MEMORY_INDEX(input_buffer_index, key_index) (ThreadIndex1D + key_index * GMoNThreadsPerBlock + input_buffer_index * GMoNThreadsPerBlock * GMoNMSetsCount) #define SORTED_KEYS_INDEX(key_index) (ThreadIndex1D + key_index * GMoNThreadsPerBlock) #define RETURN_TYPE void #define INITIAL_STORE_KEY_IN_INPUT_BUFFER(key_index, value) scratch_memory[SCRATCH_MEMORY_INDEX(0, key_index)] = value #define READ_KEY(key_index) scratch_memory[SCRATCH_MEMORY_INDEX(input_buffer_index, key_index)] #define STORE_KEY(key_index, value) scratch_memory[SCRATCH_MEMORY_INDEX(!input_buffer_index, key_index)] = value #else // #ifdef __KERNELCC__ #define SCRATCH_MEMORY_INDEX(input_buffer_index, key_index) (key_index) #define SORTED_KEYS_INDEX(key_index) (key_index) #define RETURN_TYPE std::pair, std::vector> #define INITIAL_STORE_KEY_IN_INPUT_BUFFER(key_index, value) keys[key_index] = value #define READ_KEY(key_index) (keys[SCRATCH_MEMORY_INDEX(42, key_index)]) #define STORE_KEY(key_index, value) scratch_memory[SCRATCH_MEMORY_INDEX(42, key_index)] = value #endif HIPRT_HOST_DEVICE HIPRT_INLINE RETURN_TYPE gmon_means_radix_sort(ColorRGB32F* gmon_sets, uint32_t pixel_index, unsigned int sample_number, int2 render_resolution) { #ifndef __KERNELCC__ std::vector keys_vector(GMoNMSetsCount); std::vector scratch_memory_vector(GMoNMSetsCount); std::vector sorted_keys(GMoNMSetsCount); std::vector& out_sorted_indices = sorted_keys; unsigned int* keys = keys_vector.data(); unsigned int* scratch_memory = scratch_memory_vector.data(); #else bool input_buffer_index = false; #endif constexpr unsigned int number_of_keys = GMoNMSetsCount; // Loading in the input scratch memory for (int key_index = 0; key_index < number_of_keys; key_index++) { // Note that this isn't actually the mean, this is just the value of the accumulated samples // If we wanted the mean, we would have to divide everyone by the number of samples // But dividing everyone by the same value isn't going to change the ordering so we don't have to do // that division float mean = gmon_sets[key_index * render_resolution.x * render_resolution.y + pixel_index].luminance(); // Setting the means in the "input buffer" INITIAL_STORE_KEY_IN_INPUT_BUFFER(key_index, *reinterpret_cast(&mean)); } // Initializing the sorted indices // // The sorted indices are 16 bits. // The low 8 bits are the actual sorted indices // The high 16 bits are used for internal machinery // // We only need to initialize the high bits here, the low bits // will be overwritten with the sorted indices for (int i = 0; i < GMoNMSetsCount; i++) sorted_keys[SORTED_KEYS_INDEX(i)] = i << 8; for (int digit = 0; digit < GMoNKeysNbDigitsForRadixSort; digit += GMoNSortRadixSize) { unsigned int radix_extraction_mask = ((1 << GMoNSortRadixSize) - 1) << digit; GMoNRadixSortHistogram histogram; // Computing the histogram for the counting sort for (int key_index = 0; key_index < number_of_keys; key_index++) { unsigned int radix = READ_KEY(key_index) & radix_extraction_mask; radix >>= digit; histogram.increment(radix, 1); } // Computing the prefix sum for stable counting sort for (int i = 1; i < 1 << GMoNSortRadixSize; i++) { unsigned int histogram_i_minus_1_value = histogram.fetch_value(i - 1); histogram.increment(i, histogram_i_minus_1_value); } // Reordering for (int key_index = number_of_keys - 1; key_index >= 0; key_index--) { unsigned int key = READ_KEY(key_index); unsigned int radix = key & radix_extraction_mask; radix >>= digit; histogram.decrement(radix, 1); unsigned int histogram_value = histogram.fetch_value(radix); STORE_KEY(histogram_value, key); // Also sorting a list of indices so that, when returning from this function, // we can find from the caller's code which ColorRGB corresponds to the median // // Clearing the low 8 bits sorted_keys[SORTED_KEYS_INDEX(histogram_value)] &= ~0xFF; // Setting the sorted index in the low 8 bits sorted_keys[SORTED_KEYS_INDEX(histogram_value)] |= (sorted_keys[SORTED_KEYS_INDEX(key_index)] >> 8); } // Bookkeeping to prepare the next sorting pass: copying the sorted indices // (in the low 8 bits) to the high 8 bits for (int i = 0; i < GMoNMSetsCount; i++) { // Clearing the high 8 bits sorted_keys[SORTED_KEYS_INDEX(i)] &= ~(0xFF << 8); // Copying the low 8 bits to the high 8 bits sorted_keys[SORTED_KEYS_INDEX(i)] |= (sorted_keys[SORTED_KEYS_INDEX(i)] & 0xFF) << 8; } #ifdef __KERNELCC__ // Swapping the buffer indices on the GPU input_buffer_index = !input_buffer_index; #else // On the CPU, input/output ping-ponging is just a swap of pointer unsigned int* temp = keys; keys = scratch_memory; scratch_memory = temp; #endif } #ifndef __KERNELCC__ // The result is in keys for 32 digit keys return std::make_pair<>(keys_vector, sorted_keys); #endif } #endif ================================================ FILE: src/Device/includes/GMoN/GMoNMeansRadixSortHistogramDeclaration.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_GMON_RADIX_SORT_HISTROGRAM_DECLARATION_H #define DEVICE_GMON_RADIX_SORT_HISTROGRAM_DECLARATION_H #include "HostDeviceCommon/KernelOptions/GMoNOptions.h" // The maximum number of sets allowed is 31 // This means that the values of the histogram will never go above 31 // // 31 can be encoded with 5 bits // 1 unsigned int is 32 bits // // That makes 6 histogram bins of 5 bits per 32bits uint #define BITS_PER_HISTOGRAM_BIN 5 #define MAX_BINS_PER_HISTOGRAM_UINT 6 #define MAX_BINS_PER_HISTOGRAM_UINT_F 6.0f /** * We're using a class here to compute the histogram for two reasons: * * - Without this class, we would probably use an array unsigned int[HISTOGRAM_SIZE] but arrays like that * behave very poorly with the HIP compiler so we're using simple unsigned int variables instead, not an array * (and that's why we have a big #if, #elif, #endif at the end of the structure to declare the histogram * variables depending on how many we need) * * - We use this class to do some packing since we allow only a maximum of 31 sets, we can make some assumption * about how many bits we need per histogram bins */ struct GMoNRadixSortHistogram { /** * This function adds 'value' to the correct histogram bin */ HIPRT_HOST_DEVICE HIPRT_INLINE void increment(unsigned int index, unsigned int value) { unsigned int histogram_variable_index = static_cast(index / MAX_BINS_PER_HISTOGRAM_UINT_F); unsigned int bin_index = index - histogram_variable_index * MAX_BINS_PER_HISTOGRAM_UINT; switch (histogram_variable_index) { case 0: histogram0 += value << (bin_index * BITS_PER_HISTOGRAM_BIN); break; #if GMoNSortRadixSize >= 4 case 1: histogram1 += value << (bin_index * BITS_PER_HISTOGRAM_BIN); break; case 2: histogram2 += value << (bin_index * BITS_PER_HISTOGRAM_BIN); break; #endif } } /** * This function adds 'value' to the correct histogram bin */ HIPRT_HOST_DEVICE HIPRT_INLINE void decrement(unsigned int index, unsigned int value) { unsigned int histogram_variable_index = static_cast(index / MAX_BINS_PER_HISTOGRAM_UINT_F); unsigned int bin_index = index - histogram_variable_index * MAX_BINS_PER_HISTOGRAM_UINT; // Getting the current value of the bin unsigned int histogram_current_value = fetch_value(index); // Decrementing histogram_current_value -= value; // Clearing before setting clear_bin(index); // Adding (to the bin value that is now 0) increment(index, histogram_current_value); } /** * Returns */ HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int fetch_value(unsigned int index) { unsigned int histogram_variable_index = static_cast(index / MAX_BINS_PER_HISTOGRAM_UINT_F); unsigned int bin_index = index - histogram_variable_index * MAX_BINS_PER_HISTOGRAM_UINT; switch (histogram_variable_index) { case 0: return (histogram0 >> (bin_index * BITS_PER_HISTOGRAM_BIN)) & 31; #if GMoNSortRadixSize >= 4 case 1: return (histogram1 >> (bin_index * BITS_PER_HISTOGRAM_BIN)) & 31; case 2: return (histogram2 >> (bin_index * BITS_PER_HISTOGRAM_BIN)) & 31; #endif default: return -1; } } HIPRT_HOST_DEVICE HIPRT_INLINE void clear_bin(unsigned int index) { unsigned int histogram_variable_index = static_cast(index / MAX_BINS_PER_HISTOGRAM_UINT_F); unsigned int bin_index = index - histogram_variable_index * MAX_BINS_PER_HISTOGRAM_UINT; switch (histogram_variable_index) { case 0: histogram0 &= ~(31 << (bin_index * BITS_PER_HISTOGRAM_BIN)); break; #if GMoNSortRadixSize >= 4 case 1: histogram1 &= ~(31 << (bin_index * BITS_PER_HISTOGRAM_BIN)); break; case 2: histogram2 &= ~(31 << (bin_index * BITS_PER_HISTOGRAM_BIN)); break; #endif } } #if GMoNSortRadixSize == 1 unsigned int histogram0 = 0; #elif GMoNSortRadixSize == 2 unsigned int histogram0 = 0; #elif GMoNSortRadixSize == 4 unsigned int histogram0 = 0, histogram1 = 0, histogram2 = 0; #endif }; #endif ================================================ FILE: src/Device/includes/Hash.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_HASH_H #define DEVICE_HASH_H #include HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int wang_hash(unsigned int seed) { seed = (seed ^ 61) ^ (seed >> 16); seed *= 9; seed = seed ^ (seed >> 4); seed *= 0x27d4eb2d; seed = seed ^ (seed >> 15); return seed; } #endif ================================================ FILE: src/Device/includes/HashGrid.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_HASH_GRID_H #define DEVICE_INCLUDES_HASH_GRID_H #include "HostDeviceCommon/KernelOptions/ReGIROptions.h" #include "HostDeviceCommon/Math.h" struct HashGrid { static constexpr unsigned int UNDEFINED_CHECKSUM_OR_GRID_INDEX = 0xFFFFFFFF; /** * Returns true if the collision was resolved with success and the new hash * (or unchanged if there was no collision) is set in 'in_out_base_hash' * * Returns false if the given 'in_out_hash_cell_index' refers to a hash cell that hasn't been * allocated yet or if there was a collision but it couldn't be resolved and the collision resolution was * aborted because too many iterations */ template HIPRT_DEVICE static bool resolve_collision(AtomicType* checksum_buffer, unsigned int total_number_of_cells, unsigned int& in_out_hash_cell_index, unsigned int checksum, unsigned int opt_existing_checksum = HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { unsigned int existing_checksum; if (opt_existing_checksum != HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) // The current hash key was passed as an argument, no need to fetch from memory existing_checksum = opt_existing_checksum; else existing_checksum = checksum_buffer[in_out_hash_cell_index]; if (existing_checksum == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { // This is refering to a hash cell that hasn't been populated yet if (!isInsertion) { // If we're not inserting, this means that we're querrying an empty cell return false; } else { // This is refering to a hash cell that hasn't been populated yet and we're // inserting into it so we just found an empty cell first try // // Let's try to insert atomically into it unsigned int previous_checksum = hippt::atomic_compare_exchange(&checksum_buffer[in_out_hash_cell_index], HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX, checksum); if (previous_checksum == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { // (and we made sure sure through an atomic CAS that someone else wasn't // also competing for that empty cell) return true; } else if (previous_checksum == checksum) { // Another thread just inserted the same hash key at the same time but this // current thread here wasn't fast enough on the atomic compare exchange above // so the key was already inserted. // This thread has nothing else to do. return true; } else { // Another hash key has been inserted in the same position, we're going to have to // probe for a good position } } } if (existing_checksum != checksum) { // This is a collision unsigned int base_cell_index = in_out_hash_cell_index; unsigned int current_cell_index_collision_resolution = base_cell_index; // Collision resolution for (int i = 1; i <= maxCollisionResolveSteps; i++) { current_cell_index_collision_resolution = collision_resolution_next_cell_index(current_cell_index_collision_resolution, total_number_of_cells); if (current_cell_index_collision_resolution == base_cell_index) // We looped on the whole hash table. Couldn't find an empty cell return false; unsigned int next_cell_checksum = checksum_buffer[current_cell_index_collision_resolution]; if (next_cell_checksum == checksum) { // Stopping if we found our proper cell (with our hash). // // This means that we have resolved the collision in_out_hash_cell_index = current_cell_index_collision_resolution; return true; } else if (next_cell_checksum == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { if (isInsertion) { // Stopping if we found an empty cell for insertion unsigned int previous_checksum = hippt::atomic_compare_exchange(&checksum_buffer[current_cell_index_collision_resolution], HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX, checksum); if (previous_checksum == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { // (and we made sure sure through an atomic CAS that someone else wasn't // also competing for that empty cell) in_out_hash_cell_index = current_cell_index_collision_resolution; return true; } else if (previous_checksum == checksum) { // Another thread just inserted the same hash key at the same time but this // current thread here wasn't fast enough on the atomic compare exchange // above so the key was already inserted. in_out_hash_cell_index = current_cell_index_collision_resolution; // This thread has nothing else to do. return true; } } else { // This is a query but we've hit an empty cell during probing which means that we're querrying // a cell that has never been populated return false; } } } // Linear probing couldn't find a valid position in the hash map return false; } else // This is already our hash, no collision return true; } template HIPRT_DEVICE static unsigned int collision_resolution_next_cell_index(unsigned int current_cell_index_collision_resolution, unsigned int total_number_of_cells) { if constexpr (collisionResolutionMode == REGIR_HASH_GRID_COLLISION_RESOLUTION_MODE_LINEAR_PROBING) { return (current_cell_index_collision_resolution + 1) % total_number_of_cells; } else if constexpr(collisionResolutionMode == REGIR_HASH_GRID_COLLISION_RESOLUTION_MODE_REHASHING) { return wang_hash(current_cell_index_collision_resolution) % total_number_of_cells; } } }; #endif ================================================ FILE: src/Device/includes/HashGridHash.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_HASH_GRID_HASH_H #define DEVICE_INCLUDES_HASH_GRID_HASH_H #include "HostDeviceCommon/HIPRTCamera.h" /** * PCG for the first hash function */ HIPRT_DEVICE HIPRT_INLINE unsigned int h1_pcg(unsigned int seed) { unsigned int state = seed * 747796405u + 2891336453u; unsigned int word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; return (word >> 22u) ^ word; } HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int h1_pcg(float seed) { return h1_pcg(hippt::float_as_uint(seed)); } /** * xxhash32 for the second hash function */ HIPRT_DEVICE HIPRT_INLINE unsigned int h2_xxhash32(unsigned int seed) { constexpr unsigned int PRIME32_2 = 2246822519U; constexpr unsigned int PRIME32_3 = 3266489917U; constexpr unsigned int PRIME32_4 = 668265263U; constexpr unsigned int PRIME32_5 = 374761393U; unsigned int h32 = seed + PRIME32_5; h32 = PRIME32_4 * ((h32 << 17) | (h32 >> (32 - 17))); h32 = PRIME32_2 * (h32 ^ (h32 >> 15)); h32 = PRIME32_3 * (h32 ^ (h32 >> 13)); return h32^(h32 >> 16); } HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int h2_xxhash32(float seed) { return h2_xxhash32(hippt::float_as_uint(seed)); } HIPRT_HOST_DEVICE HIPRT_INLINE float3 hash_periodic_shifting(float3 base_position, float grid_cell_size) { float scaling = 0.1f * grid_cell_size; constexpr float frequency_per_grid_cell = 5.0f; constexpr float frequency_per_grid_cell_inverse = 1.0f / frequency_per_grid_cell; const float frequency = 1.0f / (grid_cell_size * frequency_per_grid_cell_inverse); return make_float3( base_position.x + (hippt::intrin_cosf(base_position.z * frequency) + hippt::intrin_cosf(base_position.y * frequency)) * scaling * 0.5f, base_position.y + (hippt::intrin_cosf(base_position.x * frequency) + hippt::intrin_cosf(base_position.z * frequency)) * scaling * 0.5f, base_position.z + (hippt::intrin_cosf(base_position.y * frequency) + hippt::intrin_cosf(base_position.x * frequency)) * scaling * 0.5f); } /** * The 'precision' factor controls the discretization of the normal. * Higher values mean more discretization steps mean more precision. * * 2 is a default good value for 'precision' */ HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int hash_quantize_normal(float3 normal, unsigned int precision) { float precision_f = precision; unsigned int x = static_cast(normal.x * precision_f) << (2 * precision); unsigned int y = static_cast(normal.y * precision_f) << (1 * precision); unsigned int z = static_cast(normal.z * precision_f); return x | y | z; } /** * Reference: [WORLD-SPACE SPATIOTEMPORAL RESERVOIR REUSE FOR RAY-TRACED GLOBAL ILLUMINATION, Boisse, 2021] */ HIPRT_DEVICE HIPRT_INLINE float compute_adaptive_cell_size(float3 world_position, const HIPRTCamera& current_camera, float target_projected_size, float grid_cell_min_size) { int width = current_camera.sensor_width; int height = current_camera.sensor_height; float cell_size_step = hippt::length(world_position - current_camera.position) * tanf(target_projected_size * current_camera.vertical_fov * hippt::max(1.0f / height, (float)height / hippt::square(width))); float log_step = floorf(log2f(cell_size_step / grid_cell_min_size)); return hippt::max(grid_cell_min_size, grid_cell_min_size * exp2f(log_step)); } /** * Returns the hash cell index of the given world position and camera position. Does not resolve collisions. * The hash key for resolving collision is given in 'out_checksum' */ HIPRT_DEVICE HIPRT_INLINE unsigned int hash_pos_distance_to_camera(unsigned int total_number_of_cells, float3 world_position, const HIPRTCamera& current_camera, float target_projected_size, float grid_cell_min_size, unsigned int& out_checksum) { float cell_size = compute_adaptive_cell_size(world_position, current_camera, target_projected_size, grid_cell_min_size); // Periodic shifting to avoid float precision issues when, for example, rays hit a surface // that is perfectly at Y=0 (the floor of the scene for example). // // In that example, because of float imprecisions, rays hitting the floor will never have // a y=0 hit coordinate but rather be slightly negative or slightly positive, depending // on float imprecisions and this will actually create some noisy patterns where random rays access the hash // grid cell that has Y-negative and some other randoms rays access the Y-positive hash grid cell // // Reference: SIGGRAPH 2022 - Advances in Spatial Hashing world_position = hash_periodic_shifting(world_position, cell_size); unsigned int grid_coord_x = static_cast(floorf(world_position.x / cell_size)); unsigned int grid_coord_y = static_cast(floorf(world_position.y / cell_size)); unsigned int grid_coord_z = static_cast(floorf(world_position.z / cell_size)); // Using two hash functions as proposed in [WORLD-SPACE SPATIOTEMPORAL RESERVOIR REUSE FOR RAY-TRACED GLOBAL ILLUMINATION, Boisse, 2021] out_checksum = h2_xxhash32(cell_size + h2_xxhash32(grid_coord_z + h2_xxhash32(grid_coord_y + h2_xxhash32(grid_coord_x)))); unsigned int cell_hash = h1_pcg(cell_size + h1_pcg(grid_coord_z + h1_pcg(grid_coord_y + h1_pcg(grid_coord_x)))) % total_number_of_cells; return cell_hash; } HIPRT_DEVICE HIPRT_INLINE unsigned int hash_double_position_camera(unsigned int total_number_of_cells, float3 world_position_1, float3 world_position_2, const HIPRTCamera& current_camera, float target_projected_size, float grid_cell_min_size, unsigned int& out_checksum) { float cell_size_1 = compute_adaptive_cell_size(world_position_1, current_camera, target_projected_size, grid_cell_min_size); float cell_size_2 = compute_adaptive_cell_size(world_position_2, current_camera, target_projected_size, grid_cell_min_size); // Periodic shifting to avoid float precision issues when, for example, rays hit a surface // that is perfectly at Y=0 (the floor of the scene for example). // // In that example, because of float imprecisions, rays hitting the floor will never have // a y=0 hit coordinate but rather be slightly negative or slightly positive, depending // on float imprecisions and this will actually create some noisy patterns where random rays access the hash // grid cell that has Y-negative and some other randoms rays access the Y-positive hash grid cell // // Reference: SIGGRAPH 2022 - Advances in Spatial Hashing world_position_1 = hash_periodic_shifting(world_position_1, cell_size_1); world_position_2 = hash_periodic_shifting(world_position_2, cell_size_2); unsigned int grid_coord_x_1 = static_cast(floorf(world_position_1.x / cell_size_1)); unsigned int grid_coord_y_1 = static_cast(floorf(world_position_1.y / cell_size_1)); unsigned int grid_coord_z_1 = static_cast(floorf(world_position_1.z / cell_size_1)); unsigned int grid_coord_x_2 = static_cast(floorf(world_position_2.x / cell_size_2)); unsigned int grid_coord_y_2 = static_cast(floorf(world_position_2.y / cell_size_2)); unsigned int grid_coord_z_2 = static_cast(floorf(world_position_2.z / cell_size_2)); // Using two hash functions as proposed in [WORLD-SPACE SPATIOTEMPORAL RESERVOIR REUSE FOR RAY-TRACED GLOBAL ILLUMINATION, Boisse, 2021] unsigned int hash_1 = h2_xxhash32(cell_size_1 + h2_xxhash32(grid_coord_z_1 + h2_xxhash32(grid_coord_y_1 + h2_xxhash32(grid_coord_x_1)))); unsigned int hash_2 = h2_xxhash32(cell_size_2 + h2_xxhash32(grid_coord_z_2 + h2_xxhash32(grid_coord_y_2 + h2_xxhash32(grid_coord_x_2)))); out_checksum = h2_xxhash32(hash_1 ^ hash_2); unsigned int cell_hash_1 = h1_pcg(cell_size_1 + h1_pcg(grid_coord_z_1 + h1_pcg(grid_coord_y_1 + h1_pcg(grid_coord_x_1)))); unsigned int cell_hash_2 = h1_pcg(cell_size_2 + h1_pcg(grid_coord_z_2 + h1_pcg(grid_coord_y_2 + h1_pcg(grid_coord_x_2)))); unsigned int cell_hash = h1_pcg(cell_hash_1 ^ cell_hash_2) % total_number_of_cells; return cell_hash; } #endif ================================================ FILE: src/Device/includes/Intersect.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INTERSECT_H #define DEVICE_INTERSECT_H #include "Device/includes/Dispersion.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Material.h" #include "Device/includes/ONB.h" #include "Device/includes/RayPayload.h" #include "Device/includes/Sampling.h" #include "Device/includes/Texture.h" #include "Device/includes/TriangleStructures.h" #include "Device/functions/FilterFunction.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/Math.h" #if SharedStackBVHTraversalSize > 0 // This if is necessary to avoid declaring 0 size arrays if the // shared stack traversal sizes are 0 __shared__ static int shared_stack_cache[SharedStackBVHTraversalSize * KernelWorkgroupThreadCount]; #endif //#define __KERNELCC__ #ifdef __KERNELCC__ #if SharedStackBVHTraversalSize > 0 #define DECLARE_SHARED_STACK_BUFFER shared_stack_buffer{ SharedStackBVHTraversalSize, shared_stack_cache } #else #define DECLARE_SHARED_STACK_BUFFER shared_stack_buffer{ 0, nullptr } #endif #if UseSharedStackBVHTraversal == KERNEL_OPTION_TRUE #define CONSTRUCT_HIPRT_CLOSEST_HIT_TRAVERSAL(traversal_variable_name, GPU_BVH_hiprtGeom) hiprtGeomTraversalClosestCustomStack traversal_variable_name(GPU_BVH_hiprtGeom, ray, global_stack, hiprtTraversalHintDefault, &payload, render_data.hiprt_function_table, 0) #define CONSTRUCT_HIPRT_ANY_HIT_TRAVERSAL(traversal_variable_name, GPU_BVH_hiprtGeom) hiprtGeomTraversalAnyHitCustomStack traversal_variable_name(GPU_BVH_hiprtGeom, ray, global_stack, hiprtTraversalHintDefault, &payload, render_data.hiprt_function_table, 0) #else #define CONSTRUCT_HIPRT_CLOSEST_HIT_TRAVERSAL(traversal_variable_name, GPU_BVH_hiprtGeom) hiprtGeomTraversalClosest traversal_variable_name(GPU_BVH_hiprtGeom, ray, hiprtTraversalHintDefault, &payload, render_data.hiprt_function_table, 0); #define CONSTRUCT_HIPRT_ANY_HIT_TRAVERSAL(traversal_variable_name, GPU_BVH_hiprtGeom) hiprtGeomTraversalAnyHit traversal_variable_name(GPU_BVH_hiprtGeom, ray, hiprtTraversalHintDefault, &payload, render_data.hiprt_function_table, 0); #endif #define DECLARE_HIPRT_CLOSEST_ANY_HIT_COMMON(render_data, GPU_BVH_hiprtGeom, ray, last_hit_primitive_index, random_number_generator) \ /* Payload for the alpha testing filter function */ \ FilterFunctionPayload payload; \ payload.render_data = &render_data; \ payload.random_number_generator = &random_number_generator; \ /* Filling the payload with the last hit primitive index to avoid self intersections */ \ /* (avoid that the ray intersects the triangle it is currently sitting on) */ \ payload.last_hit_primitive_index = last_hit_primitive_index; \ payload.simplified_light_ray = GPU_BVH_hiprtGeom == render_data.light_GPU_BVH; \ payload.bounce = bounce; \ \ hiprtSharedStackBuffer DECLARE_SHARED_STACK_BUFFER; \ hiprtGlobalStack global_stack(render_data.global_traversal_stack_buffer, shared_stack_buffer); #define DECLARE_HIPRT_CLOSEST_HIT_TRAVERSAL(traversal_variable_name, render_data, GPU_BVH_hiprtGeom, ray, last_hit_primitive_index, random_number_generator) \ DECLARE_HIPRT_CLOSEST_ANY_HIT_COMMON(render_data, GPU_BVH_hiprtGeom, ray, last_hit_primitive_index, random_number_generator); \ CONSTRUCT_HIPRT_CLOSEST_HIT_TRAVERSAL(traversal_variable_name, GPU_BVH_hiprtGeom); #define DECLARE_HIPRT_ANY_HIT_TRAVERSAL(traversal_variable_name, render_data, GPU_BVH_hiprtGeom, ray, last_hit_primitive_index, random_number_generator) \ DECLARE_HIPRT_CLOSEST_ANY_HIT_COMMON(render_data, GPU_BVH_hiprtGeom, ray, last_hit_primitive_index, random_number_generator); \ CONSTRUCT_HIPRT_ANY_HIT_TRAVERSAL(traversal_variable_name, GPU_BVH_hiprtGeom); #endif /* References: * * [1] [Foundations of Game Engine Development: Rendering - Tangent/Bitangent calculation] http://foundationsofgameenginedev.com/#fged2 */ HIPRT_DEVICE HIPRT_INLINE float3 normal_mapping(const HIPRTRenderData& render_data, int normal_map_texture_index, TriangleIndices triangle_vertex_indices, TriangleTexcoords& texcoords, const float2& interpolated_texcoords, const float3& surface_normal) { // Calculating tangents and bitangents aligned with texture U and V coordinates float2 P0_texcoords = texcoords.x; float2 P1_texcoords = texcoords.y; float2 P2_texcoords = texcoords.z; float2 delta_P1P0_texcoords = P1_texcoords - P0_texcoords; float2 delta_P2P0_texcoords = P2_texcoords - P0_texcoords; float3 P0 = render_data.buffers.vertices_positions[triangle_vertex_indices.x]; float3 P1 = render_data.buffers.vertices_positions[triangle_vertex_indices.y]; float3 P2 = render_data.buffers.vertices_positions[triangle_vertex_indices.z]; float3 edge_P0P1 = P1 - P0; float3 edge_P0P2 = P2 - P0; // To counter degenerate UVs constexpr float det_bias = 1.0e-6f; float det = delta_P1P0_texcoords.x * delta_P2P0_texcoords.y - delta_P1P0_texcoords.y * delta_P2P0_texcoords.x + det_bias; // Check if the det isn't too low to avoid degenerate geometries that can then cause NaNs float det_inverse = 1.0f / det; float3 T = (edge_P0P1 * delta_P2P0_texcoords.y - edge_P0P2 * delta_P1P0_texcoords.y) * det_inverse; float3 B = (edge_P0P2 * delta_P1P0_texcoords.x - edge_P0P1 * delta_P2P0_texcoords.x) * det_inverse; if (hippt::length2(T) < 1.0e-6f || hippt::length2(B) < 1.0e-6f) // The tangent or the bitangent is degenerate return surface_normal; ColorRGB32F normal = sample_texture_rgb_8bits(render_data.buffers.material_textures, normal_map_texture_index, /* is_srgb */ false, interpolated_texcoords); // Bringing the normal in [-x, x]. x doesn't really matter since we normalize the result anyway normal -= ColorRGB32F(0.5f); float3 normal_tangent_space = hippt::normalize(make_float3(normal.r, normal.g, normal.b)); return local_to_world_frame(hippt::normalize(T), hippt::normalize(B), surface_normal, normal_tangent_space); } HIPRT_DEVICE HIPRT_INLINE float3 get_shading_normal(const HIPRTRenderData& render_data, const float3& geometric_normal, TriangleIndices triangle_vertex_indices, TriangleTexcoords triangle_texcoords, int primitive_index, const float2& uv, const float2& interpolated_texcoords) { if (!render_data.render_settings.do_normal_mapping) return geometric_normal; // Do smooth shading first if we have vertex normals float3 surface_normal; if (render_data.buffers.has_vertex_normals[triangle_vertex_indices.x]) // Smooth normal available for the triangle surface_normal = hippt::normalize(uv_interpolate(triangle_vertex_indices, render_data.buffers.vertex_normals, uv)); else surface_normal = geometric_normal; // Do normal mapping if we have a normal map int material_index = render_data.buffers.material_indices[primitive_index]; unsigned short int normal_map_texture_index = render_data.buffers.materials_buffer.get_normal_map_texture_index(material_index); if (normal_map_texture_index != MaterialConstants::NO_TEXTURE) surface_normal = normal_mapping(render_data, normal_map_texture_index, triangle_vertex_indices, triangle_texcoords, interpolated_texcoords, surface_normal); return surface_normal; } /** * Flips the surface normals if necessary such that they are facing us. * * The normals are only flipped if some conditions are met, read the * comment in the function for more details */ HIPRT_DEVICE HIPRT_INLINE void fix_backfacing_normals(HitInfo& hit_info, const float3& view_direction) { if (hippt::dot(view_direction, hit_info.geometric_normal) < 0.0f) { // The geometry isn't front-facing hit_info.geometric_normal *= -1.0f; hit_info.shading_normal *= -1.0f; } if (hippt::dot(view_direction, hit_info.shading_normal) < 0.0f) // Flipping the normal such that the view direction isn't below the shading hemisphere anymore hit_info.shading_normal *= -1.0f; // Now ensuring that a perfectly reflected direction (about the shading normal) doesn't go below the *geometric* surface float3 perfect_reflected_direction = reflect_ray(view_direction, hit_info.shading_normal); if (hippt::dot(perfect_reflected_direction, hit_info.geometric_normal) <= 0.0f) { // The perfectly reflected direction *is* below the geometric normal, // we're going to pull the shading normal towards the geometric normal such that // the perfectly reflected direction now is just an epsilon above the surface // // This is done by first computing a new reflected direction that is just above the surface // and then recomputing the new shading normal as the half vector between the new reflect direction // and the view direction constexpr float epsilon = 0.01; perfect_reflected_direction -= hippt::normalize((hippt::dot(perfect_reflected_direction, hit_info.geometric_normal) - epsilon) * hit_info.geometric_normal); // The new shading normal is the half vector between the pulled up reflected direction // and the view direction hit_info.shading_normal = hippt::normalize(view_direction + perfect_reflected_direction); } } #ifndef __KERNELCC__ #include "Renderer/BVH.h" HIPRT_DEVICE HIPRT_INLINE hiprtHit intersect_scene_cpu(const HIPRTRenderData& render_data, BVH* bvh, const hiprtRay& ray, int last_hit_primitive_index, Xorshift32Generator& random_number_generator) { FilterFunctionPayload filter_function_payload; filter_function_payload.simplified_light_ray = bvh == render_data.cpu_only.light_bvh; filter_function_payload.render_data = &render_data; filter_function_payload.random_number_generator = &random_number_generator; // Filling the payload with the last hit primitive index to avoid self intersections // (avoid that the ray intersects the triangle it is currently sitting on) filter_function_payload.last_hit_primitive_index = last_hit_primitive_index; hiprtHit hiprtHit; bvh->intersect(ray, hiprtHit, &filter_function_payload); return hiprtHit; } #endif /** * Returns true if a hit was found, false otherwise */ HIPRT_DEVICE HIPRT_INLINE bool trace_main_path_ray(const HIPRTRenderData& render_data, hiprtRay ray, RayPayload& in_out_ray_payload, HitInfo& out_hit_info, int last_hit_primitive_index, int bounce, Xorshift32Generator& random_number_generator) { #ifdef __KERNELCC__ if (render_data.GPU_BVH == nullptr) // Empty scene --> no intersection return false; #endif hiprtHit hit; bool skipping_volume_boundary = false; do { #ifdef __KERNELCC__ DECLARE_HIPRT_CLOSEST_HIT_TRAVERSAL(traversal, render_data, render_data.GPU_BVH, ray, last_hit_primitive_index, random_number_generator); hit = traversal.getNextHit(); #else hit = intersect_scene_cpu(render_data, render_data.cpu_only.bvh, ray, last_hit_primitive_index, random_number_generator); #endif if (!hit.hasHit()) return false; TriangleIndices triangle_vertex_indices = load_triangle_vertex_indices(render_data.buffers.triangles_indices, hit.primID); TriangleTexcoords triangle_texcoords = load_triangle_texcoords(render_data.buffers.texcoords, triangle_vertex_indices); out_hit_info.inter_point = ray.origin + hit.t * ray.direction; out_hit_info.primitive_index = hit.primID; out_hit_info.texcoords = uv_interpolate(triangle_texcoords, hit.uv); // TODO hit.normal is in object space, this simple approach will not work if using // multiple-levels BVH (TLAS/BLAS). We'll have to transform by the BLAS transform out_hit_info.geometric_normal = hippt::normalize(hit.normal); out_hit_info.shading_normal = get_shading_normal(render_data, out_hit_info.geometric_normal, triangle_vertex_indices, triangle_texcoords, hit.primID, hit.uv, out_hit_info.texcoords); out_hit_info.t = hit.t; int material_index = render_data.buffers.material_indices[hit.primID]; in_out_ray_payload.material = get_intersection_material(render_data, material_index, out_hit_info.texcoords); skipping_volume_boundary = in_out_ray_payload.volume_state.interior_stack.push( in_out_ray_payload.volume_state.incident_mat_index, in_out_ray_payload.volume_state.outgoing_mat_index, in_out_ray_payload.volume_state.inside_material, material_index, in_out_ray_payload.material.get_dielectric_priority()); if (in_out_ray_payload.volume_state.inside_material) // If we're traveling inside a volume, accumulating the distance for Beer's law in_out_ray_payload.volume_state.distance_in_volume += hit.t; fix_backfacing_normals(out_hit_info, -ray.direction); if (skipping_volume_boundary) { // If we're skipping, the boundary, the ray just keeps going on its way ray.origin = out_hit_info.inter_point; // Don't forget to increment the distance traveled // TODO: Are we not double counting the distance here and a few lines above (where we set the .t, .uv, .geometric_normal, ...) in_out_ray_payload.volume_state.distance_in_volume += hit.t; } } while ((skipping_volume_boundary && hit.hasHit())); if (in_out_ray_payload.material.dispersion_scale > 0.0f && in_out_ray_payload.material.specular_transmission > 0.0f && in_out_ray_payload.volume_state.sampled_wavelength == 0.0f) // If we hit a dispersive material, we sample the wavelength that will be used // for computing the wavelength dependent IORs used for dispersion // // We're also not re-doing the sampling if a wavelength has already been sampled for that path // // Negating the wavelength to indicate that the throughput filter of the wavelength // hasn't been applied yet (applied in principled_glass_eval()) in_out_ray_payload.volume_state.sampled_wavelength = -sample_wavelength_uniformly(random_number_generator); return hit.hasHit(); } /** * Returns true if in shadow (a hit was found before 't_max' distance) * Returns false if unoccluded */ HIPRT_DEVICE HIPRT_INLINE bool evaluate_shadow_ray_occluded(const HIPRTRenderData& render_data, hiprtRay ray, float t_max, int last_hit_primitive_index, int bounce, Xorshift32Generator& random_number_generator) { #ifdef __KERNELCC__ if (render_data.GPU_BVH == nullptr) // Empty scene --> no intersection return false; #endif #ifdef __KERNELCC__ ray.maxT = t_max - 1.0e-4f; DECLARE_HIPRT_ANY_HIT_TRAVERSAL(traversal, render_data, render_data.GPU_BVH, ray, last_hit_primitive_index, random_number_generator); hiprtHit shadow_ray_hit = traversal.getNextHit(); if (!shadow_ray_hit.hasHit()) return false; return true; #else float alpha = 1.0f; // The total distance of our ray. Incremented after each hit // (we may find multiple hits if we hit transparent texture // and keep intersecting the scene) float cumulative_t = 0.0f; hiprtHit hit; do { // We should use ray tracing filter functions here instead of re-tracing new rays hit = intersect_scene_cpu(render_data, render_data.cpu_only.bvh, ray, last_hit_primitive_index, random_number_generator); if (!hit.hasHit()) return false; if (render_data.render_settings.do_alpha_testing) alpha = get_hit_base_color_alpha(render_data, hit); else alpha = 1.0f; // Next ray origin ray.origin = ray.origin + ray.direction * hit.t; cumulative_t += hit.t; // We keep going as long as the alpha is < 1.0f meaning that we hit texture transparency } while (alpha < 1.0f && cumulative_t < t_max - 1.0e-4f); // If we found a hit and that it is close enough return hit.hasHit() && cumulative_t < t_max - 1.0e-4f; #endif // __KERNELCC__ } /** * Returns true if in shadow (a hit was found before 't_max' distance * Returns false if unoccluded * * This function also uses NEE++ if enabled in the kernel options and this * function can update the visibility map of NEE++ if enabled in 'render_data.nee_plus_plus' */ HIPRT_DEVICE HIPRT_INLINE bool evaluate_shadow_ray_nee_plus_plus(HIPRTRenderData& render_data, hiprtRay ray, float t_max, int last_hit_primitive_index, NEEPlusPlusContext& nee_plus_plus_context, Xorshift32Generator& random_number_generator, int bounce) { #if DirectLightUseNEEPlusPlusRR == KERNEL_OPTION_TRUE && DirectLightUseNEEPlusPlus == KERNEL_OPTION_TRUE bool shadow_ray_discarded = false; bool shadow_ray_occluded = false; if (render_data.nee_plus_plus.do_update_shadow_rays_traced_statistics) // Updating the statistics hippt::atomic_fetch_add(render_data.nee_plus_plus.total_shadow_ray_queries, 1ull); bool nee_plus_plus_envmap_rr_disabled = nee_plus_plus_context.envmap && !render_data.nee_plus_plus.m_enable_nee_plus_plus_RR_for_envmap; bool nee_plus_plus_emissives_rr_disabled = !nee_plus_plus_context.envmap && !render_data.nee_plus_plus.m_enable_nee_plus_plus_RR_for_emissives; if (nee_plus_plus_envmap_rr_disabled || nee_plus_plus_emissives_rr_disabled) { // This is NEE++ RR for envmap sampling but envmap NEE++ RR is disabled nee_plus_plus_context.unoccluded_probability = 1.0f; if (render_data.nee_plus_plus.do_update_shadow_rays_traced_statistics) // Updating the statistics hippt::atomic_fetch_add(render_data.nee_plus_plus.shadow_rays_actually_traced, 1ull); shadow_ray_occluded = evaluate_shadow_ray_occluded(render_data, ray, t_max, last_hit_primitive_index, bounce, random_number_generator); shadow_ray_discarded = false; } // Getting the matrix index from 'estimate_visibility_probability' in case we need to accumulate // visibility in the visibility map with 'accumulate_visibility'. If we do need to do that, // then that matrix index can be reused instead of being recomputed automatically by 'accumulate_visibility' // to save a little bit of computations unsigned int seed_before = random_number_generator.m_state.seed; unsigned int nee_plus_plus_hash_grid_cell_index; float visible_probability = nee_plus_plus_context.unoccluded_probability = render_data.nee_plus_plus.estimate_visibility_probability(nee_plus_plus_context, render_data.current_camera, nee_plus_plus_hash_grid_cell_index); bool likely_visible = random_number_generator() < visible_probability; if (likely_visible) { if (render_data.nee_plus_plus.do_update_shadow_rays_traced_statistics) // Updating the statistics hippt::atomic_fetch_add(render_data.nee_plus_plus.shadow_rays_actually_traced, 1ull); // The shadow ray is likely visible, testing with a shadow ray shadow_ray_occluded = evaluate_shadow_ray_occluded(render_data, ray, t_max, last_hit_primitive_index, bounce, random_number_generator); shadow_ray_discarded = false; if (render_data.nee_plus_plus.m_update_visibility_map) render_data.nee_plus_plus.accumulate_visibility(!shadow_ray_occluded, nee_plus_plus_hash_grid_cell_index); } else { shadow_ray_discarded = true; // NEE++ tells us that these two points are going to be occluded so we're not testing // the shadow ray and assuming occluded instead shadow_ray_occluded = true; } #else // Setting this to 1.0f if not using NEE++ so that is has no effect when the caller // divides by it nee_plus_plus_context.unoccluded_probability = 1.0f; bool shadow_ray_occluded = evaluate_shadow_ray_occluded(render_data, ray, t_max, last_hit_primitive_index, bounce, random_number_generator); // We may still want to update the visibility map if (render_data.nee_plus_plus.m_update_visibility_map && DirectLightUseNEEPlusPlus == KERNEL_OPTION_TRUE) { unsigned int nee_plus_plus_hash_grid_cell_index = render_data.nee_plus_plus.get_visibility_map_index(nee_plus_plus_context, render_data.current_camera); render_data.nee_plus_plus.accumulate_visibility(!shadow_ray_occluded, nee_plus_plus_hash_grid_cell_index); } #endif #if DirectLightNEEPlusPlusDisplayShadowRaysDiscarded == KERNEL_OPTION_TRUE uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; uint32_t seed = blockIdx.x + blockIdx.y * gridDim.x + 1 + (threadIdx.y >= 4) * 1; uint32_t pixel_index = x + y * render_data.render_settings.render_resolution.x; Xorshift32Generator color_random(wang_hash(seed)); ColorRGB32F block_color = ColorRGB32F(color_random(), color_random(), color_random()) * (render_data.render_settings.sample_number + 1); if (bounce == DirectLightNEEPlusPlusDisplayShadowRaysDiscardedBounce) { if (shadow_ray_discarded) render_data.buffers.accumulated_ray_colors[pixel_index] = ColorRGB32F(); else render_data.buffers.accumulated_ray_colors[pixel_index] = block_color; } #endif return shadow_ray_occluded; } /** * This function shoots a BSDF ray in the BVH containing only the emissive triangles of the scene * This may be useful in some algorithms for increased performance * * Returns true if a hit was found, false otherwise */ HIPRT_DEVICE HIPRT_INLINE bool evaluate_bsdf_light_sample_ray_simplified(const HIPRTRenderData& render_data, hiprtRay ray, float t_max, BSDFLightSampleRayHitInfo& out_light_hit_info, int last_hit_primitive_index, int bounce, Xorshift32Generator& random_number_generator) { #ifdef __KERNELCC__ if (render_data.light_GPU_BVH == nullptr) // Empty scene --> no intersection return false; #endif #ifdef __KERNELCC__ ray.maxT = t_max - 1.0e-4f; DECLARE_HIPRT_CLOSEST_HIT_TRAVERSAL(traversal, render_data, render_data.light_GPU_BVH, ray, last_hit_primitive_index, random_number_generator); hiprtHit shadow_ray_hit = traversal.getNextHit(); if (!shadow_ray_hit.hasHit()) return false; // If we're here, this means that we found a hit that is not // alpha-transparent with a distance < t_max so that's a hit and we're shadowed. // Reading the emission of the material int global_triangle_index = render_data.buffers.emissive_triangles_primitive_indices_and_emissive_textures[shadow_ray_hit.primID]; int material_index = render_data.buffers.material_indices[global_triangle_index]; int emission_texture_index = render_data.buffers.materials_buffer.get_emission_texture_index(material_index); TriangleIndices triangle_vertex_indices = load_triangle_vertex_indices(render_data.buffers.triangles_indices, global_triangle_index); TriangleTexcoords triangle_texcoords = load_triangle_texcoords(render_data.buffers.texcoords, triangle_vertex_indices); float2 interpolated_texcoords = uv_interpolate(triangle_texcoords, shadow_ray_hit.uv); if (emission_texture_index != MaterialConstants::NO_TEXTURE) out_light_hit_info.hit_emission = get_material_property(render_data, false, interpolated_texcoords, emission_texture_index); // Getting the shading normal else out_light_hit_info.hit_emission = render_data.buffers.materials_buffer.get_emission(material_index); out_light_hit_info.hit_interpolated_texcoords = interpolated_texcoords; out_light_hit_info.hit_shading_normal = get_shading_normal(render_data, hippt::normalize(shadow_ray_hit.normal), triangle_vertex_indices, triangle_texcoords, global_triangle_index, shadow_ray_hit.uv, interpolated_texcoords); out_light_hit_info.hit_geometric_normal = hippt::normalize(shadow_ray_hit.normal); out_light_hit_info.hit_prim_index = global_triangle_index; out_light_hit_info.hit_material_index = material_index; out_light_hit_info.hit_distance = shadow_ray_hit.t; return true; #else float alpha = 1.0f; // The total distance of our ray. Incremented after each hit // (we may find multiple hits if we hit transparent texture // and keep intersecting the scene) float cumulative_t = 0.0f; int global_triangle_index_hit; hiprtHit shadow_ray_hit; do { // We should use ray tracing filter functions here instead of re-tracing new rays shadow_ray_hit = intersect_scene_cpu(render_data, render_data.cpu_only.light_bvh, ray, last_hit_primitive_index, random_number_generator); if (!shadow_ray_hit.hasHit()) return false; global_triangle_index_hit = render_data.buffers.emissive_triangles_primitive_indices_and_emissive_textures[shadow_ray_hit.primID]; if (render_data.render_settings.do_alpha_testing) alpha = get_hit_base_color_alpha(render_data, global_triangle_index_hit, shadow_ray_hit.uv); else alpha = 1.0f; // Next ray origin ray.origin = ray.origin + ray.direction * shadow_ray_hit.t; cumulative_t += shadow_ray_hit.t; // We keep going as long as the alpha is < 1.0f meaning that we hit texture transparency } while (alpha < 1.0f && cumulative_t < t_max - 1.0e-4f); bool hit_found = shadow_ray_hit.hasHit() && cumulative_t < t_max - 1.0e-4f; if (hit_found) { // If we found a hit and that it is close enough (hit_found conditions) int material_index = render_data.buffers.material_indices[global_triangle_index_hit]; int emission_texture_index = render_data.buffers.materials_buffer.get_emission_texture_index(material_index); TriangleIndices triangle_vertex_indices = load_triangle_vertex_indices(render_data.buffers.triangles_indices, global_triangle_index_hit); TriangleTexcoords triangle_texcoords = load_triangle_texcoords(render_data.buffers.texcoords, triangle_vertex_indices); float2 interpolated_texcoords = uv_interpolate(triangle_texcoords, shadow_ray_hit.uv); if (emission_texture_index != MaterialConstants::NO_TEXTURE) out_light_hit_info.hit_emission = get_material_property(render_data, false, interpolated_texcoords, emission_texture_index); else out_light_hit_info.hit_emission = render_data.buffers.materials_buffer.get_emission(material_index); out_light_hit_info.hit_interpolated_texcoords = interpolated_texcoords; out_light_hit_info.hit_shading_normal = get_shading_normal(render_data, hippt::normalize(shadow_ray_hit.normal), triangle_vertex_indices, triangle_texcoords, global_triangle_index_hit, shadow_ray_hit.uv, interpolated_texcoords); out_light_hit_info.hit_geometric_normal = hippt::normalize(shadow_ray_hit.normal); out_light_hit_info.hit_prim_index = global_triangle_index_hit; out_light_hit_info.hit_material_index = material_index; out_light_hit_info.hit_distance = cumulative_t; return true; } else return false; #endif // __KERNELCC__ } /** * Returns true if in shadow, false otherwise. * * Also, if a hit was found, outputs the emission of the material at the hit point in 'out_hit_emission' */ HIPRT_DEVICE HIPRT_INLINE bool evaluate_bsdf_light_sample_ray(const HIPRTRenderData& render_data, hiprtRay ray, float t_max, BSDFLightSampleRayHitInfo& out_light_hit_info, int last_hit_primitive_index, int bounce, Xorshift32Generator& random_number_generator) { #ifdef __KERNELCC__ if (render_data.GPU_BVH == nullptr) // Empty scene --> no intersection return false; #endif #ifdef __KERNELCC__ ray.maxT = t_max - 1.0e-4f; DECLARE_HIPRT_CLOSEST_HIT_TRAVERSAL(traversal, render_data, render_data.GPU_BVH, ray, last_hit_primitive_index, random_number_generator); hiprtHit shadow_ray_hit = traversal.getNextHit(); if (!shadow_ray_hit.hasHit()) return false; // If we're here, this means that we found a hit that is not // alpha-transparent with a distance < t_max so that's a hit and we're shadowed. // Reading the emission of the material int material_index = render_data.buffers.material_indices[shadow_ray_hit.primID]; int emission_texture_index = render_data.buffers.materials_buffer.get_emission_texture_index(material_index); TriangleIndices triangle_vertex_indices = load_triangle_vertex_indices(render_data.buffers.triangles_indices, shadow_ray_hit.primID); TriangleTexcoords triangle_texcoords = load_triangle_texcoords(render_data.buffers.texcoords, triangle_vertex_indices); float2 interpolated_texcoords = uv_interpolate(triangle_texcoords, shadow_ray_hit.uv); if (emission_texture_index != MaterialConstants::NO_TEXTURE) out_light_hit_info.hit_emission = get_material_property(render_data, false, interpolated_texcoords, emission_texture_index); // Getting the shading normal else out_light_hit_info.hit_emission = render_data.buffers.materials_buffer.get_emission(material_index); out_light_hit_info.hit_interpolated_texcoords = interpolated_texcoords; out_light_hit_info.hit_shading_normal = get_shading_normal(render_data, hippt::normalize(shadow_ray_hit.normal), triangle_vertex_indices, triangle_texcoords, shadow_ray_hit.primID, shadow_ray_hit.uv, interpolated_texcoords); out_light_hit_info.hit_geometric_normal = hippt::normalize(shadow_ray_hit.normal); out_light_hit_info.hit_prim_index = shadow_ray_hit.primID; out_light_hit_info.hit_material_index = material_index; out_light_hit_info.hit_distance = shadow_ray_hit.t; return true; #else float alpha = 1.0f; // The total distance of our ray. Incremented after each hit // (we may find multiple hits if we hit transparent texture // and keep intersecting the scene) float cumulative_t = 0.0f; hiprtHit shadow_ray_hit; do { // We should use ray tracing filter functions here instead of re-tracing new rays shadow_ray_hit = intersect_scene_cpu(render_data, render_data.cpu_only.bvh, ray, last_hit_primitive_index, random_number_generator); if (!shadow_ray_hit.hasHit()) return false; if (render_data.render_settings.do_alpha_testing) alpha = get_hit_base_color_alpha(render_data, shadow_ray_hit); else alpha = 1.0f; // Next ray origin ray.origin = ray.origin + ray.direction * shadow_ray_hit.t; cumulative_t += shadow_ray_hit.t; // We keep going as long as the alpha is < 1.0f meaning that we hit texture transparency } while (alpha < 1.0f && cumulative_t < t_max - 1.0e-4f); bool hit_found = shadow_ray_hit.hasHit() && cumulative_t < t_max - 1.0e-4f; if (hit_found) { // If we found a hit and that it is close enough (hit_found conditions) int material_index = render_data.buffers.material_indices[shadow_ray_hit.primID]; int emission_texture_index = render_data.buffers.materials_buffer.get_emission_texture_index(material_index); TriangleIndices triangle_vertex_indices = load_triangle_vertex_indices(render_data.buffers.triangles_indices, shadow_ray_hit.primID); TriangleTexcoords triangle_texcoords = load_triangle_texcoords(render_data.buffers.texcoords, triangle_vertex_indices); float2 interpolated_texcoords = uv_interpolate(triangle_texcoords, shadow_ray_hit.uv); if (emission_texture_index != MaterialConstants::NO_TEXTURE) out_light_hit_info.hit_emission = get_material_property(render_data, false, interpolated_texcoords, emission_texture_index); else out_light_hit_info.hit_emission = render_data.buffers.materials_buffer.get_emission(material_index); out_light_hit_info.hit_interpolated_texcoords = interpolated_texcoords; out_light_hit_info.hit_shading_normal = get_shading_normal(render_data, hippt::normalize(shadow_ray_hit.normal), triangle_vertex_indices, triangle_texcoords, shadow_ray_hit.primID, shadow_ray_hit.uv, interpolated_texcoords); out_light_hit_info.hit_geometric_normal = hippt::normalize(shadow_ray_hit.normal); out_light_hit_info.hit_prim_index = shadow_ray_hit.primID; out_light_hit_info.hit_material_index = material_index; out_light_hit_info.hit_distance = cumulative_t; return true; } else return false; #endif // __KERNELCC__ } HIPRT_DEVICE hiprtHit simple_closest_hit(const HIPRTRenderData& render_data, hiprtRay ray, int last_primitive_index, Xorshift32Generator& random_number_generator) { hiprtHit hit; #ifdef __KERNELCC__ // Payload for the alpha testing filter function FilterFunctionPayload payload; payload.render_data = &render_data; payload.random_number_generator = &random_number_generator; payload.last_hit_primitive_index = last_primitive_index; #if UseSharedStackBVHTraversal == KERNEL_OPTION_TRUE #if SharedStackBVHTraversalSize > 0 hiprtSharedStackBuffer shared_stack_buffer{ SharedStackBVHTraversalSize, shared_stack_cache }; #else hiprtSharedStackBuffer shared_stack_buffer{ 0, nullptr }; #endif hiprtGlobalStack global_stack(render_data.global_traversal_stack_buffer, shared_stack_buffer); hiprtGeomTraversalClosestCustomStack traversal(render_data.GPU_BVH, ray, global_stack, hiprtTraversalHintDefault, &payload, render_data.hiprt_function_table, 0); #else hiprtGeomTraversalClosest traversal(render_data.GPU_BVH, ray, hiprtTraversalHintDefault, &payload, render_data.hiprt_function_table, 0); #endif hit = traversal.getNextHit(); #else hit = intersect_scene_cpu(render_data, render_data.cpu_only.bvh, ray, last_primitive_index, random_number_generator); #endif return hit; } #endif ================================================ FILE: src/Device/includes/LightSampling/Envmap.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_ENVMAP_H #define DEVICE_ENVMAP_H #include "Device/includes/Dispatcher.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Intersect.h" #include "Device/includes/Sampling.h" #include "Device/includes/Texture.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/Xorshift.h" /** * References: * * [1] [GLSL Path Tracer implementation by knightcrawler25] https://github.com/knightcrawler25/GLSL-PathTracer * [2] [PBR Book 3rd Ed - Infinite Light Sampling] https://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Sampling_Light_Sources */ /** * This function expects 'direction' to be in world space */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F eval_envmap_no_pdf(const WorldSettings& world_settings, const float3& direction) { // Bringing the direction in envmap space for sampling the envmap float3 rotated_direction = matrix_X_vec(world_settings.world_to_envmap_matrix, direction); float u = 0.5f + atan2(rotated_direction.z, rotated_direction.x) * M_INV_2_PI; float v = 0.5f + asin(rotated_direction.y) * M_INV_PI; return sample_environment_map_texture(world_settings, make_float2(u, v)); } HIPRT_HOST_DEVICE HIPRT_INLINE void envmap_cdf_search(const WorldSettings& world_settings, float value, int& x, int& y) { //First searching a line to sample unsigned int lower = 0; int upper = world_settings.envmap_height - 1; int x_index = world_settings.envmap_width - 1; while (lower < upper) { int y_index = static_cast(floorf((lower + upper) * 0.5f)); int env_map_index = y_index * world_settings.envmap_width + x_index; if (value < world_settings.envmap_cdf[env_map_index]) upper = y_index; else lower = y_index + 1; } y = hippt::max(hippt::min(lower, world_settings.envmap_height), 0u); //Then sampling the line itself lower = 0; upper = world_settings.envmap_width - 1; int y_index = y; while (lower < upper) { int x_idx = static_cast(floorf((lower + upper) * 0.5f)); int env_map_index = y_index * world_settings.envmap_width + x_idx; if (value < world_settings.envmap_cdf[env_map_index]) upper = x_idx; else lower = x_idx + 1; } x = hippt::max(hippt::min(lower, world_settings.envmap_width), 0u); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F envmap_sample(const WorldSettings& world_settings, float3& sampled_direction, float& envmap_pdf, Xorshift32Generator& random_number_generator) { #if EnvmapSamplingStrategy == ESS_NO_SAMPLING envmap_pdf = 0.0f; return ColorRGB32F(); #endif int x, y; float env_map_total_sum = world_settings.envmap_total_sum; #if EnvmapSamplingStrategy == ESS_BINARY_SEARCH // Importance sampling a texel of the envmap with a binary search on the CDF envmap_cdf_search(world_settings, random_number_generator() * env_map_total_sum, x, y); #elif EnvmapSamplingStrategy == ESS_ALIAS_TABLE int random_index = world_settings.envmap_alias_table.sample(random_number_generator); y = static_cast(floorf(random_index / static_cast(world_settings.envmap_width))); x = static_cast(floorf(random_index - y * static_cast(world_settings.envmap_width))); #endif // Converting to UV coordinates float u = static_cast(x) / world_settings.envmap_width; float v = static_cast(y) / world_settings.envmap_height; // Converting to polar coordinates float phi = u * M_TWO_PI; // Clamping because a theta of 0.0f would mean straight up which means singularity // which means not good for numerical stability float theta = hippt::max(1.0e-5f, v * M_PI); // Convert to cartesian coordinates float cos_theta = cos(theta); float sin_theta = sin(theta); // Using this formula here instead of the usual (sin_theta * cos(phi), sin_theta * sin(phi), cos_theta) // because we want our envmap to be Y-up sampled_direction = make_float3(-sin_theta * cos(phi), -cos_theta, -sin_theta * sin(phi)); // Taking envmap rotation into account to bring the direction in world space sampled_direction = matrix_X_vec(world_settings.envmap_to_world_matrix, sampled_direction); ColorRGB32F env_map_radiance = sample_environment_map_texture(world_settings, make_float2(u, v)); // Computing envmap PDF envmap_pdf = 1.0f; #if EnvmapSamplingStrategy == ESS_BINARY_SEARCH || EnvmapSamplingStrategy == ESS_ALIAS_TABLE // The texel was sampled according to its luminance envmap_pdf = env_map_radiance.luminance() / (env_map_total_sum * world_settings.envmap_intensity); // Account for the fact that the envmap texels have some area in the world envmap_pdf *= world_settings.envmap_width * world_settings.envmap_height; #endif // Converting the PDF from area measure on the envmap to solid angle measure envmap_pdf /= (M_TWO_PI_SQUARED * sin_theta); return env_map_radiance; } /** * This function expects the given direction to be in world space i.e. * the direction is already rotated by the envmap rotation matrix */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F envmap_eval(const HIPRTRenderData& render_data, const float3& direction, float& pdf) { #if EnvmapSamplingStrategy == ESS_NO_SAMPLING pdf = 0.0f; return ColorRGB32F(); #endif const WorldSettings& world_settings = render_data.world_settings; ColorRGB32F envmap_radiance = eval_envmap_no_pdf(world_settings, direction); float envmap_total_sum = world_settings.envmap_total_sum; float theta_bsdf_dir = acos(-direction.y); float sin_theta = sin(theta_bsdf_dir); #if EnvmapSamplingStrategy == ESS_BINARY_SEARCH || EnvmapSamplingStrategy == ESS_ALIAS_TABLE // The texel was sampled according to its luminance pdf = envmap_radiance.luminance() / (envmap_total_sum * render_data.world_settings.envmap_intensity); // Account for the fact that the envmap texels have some area in the world pdf *= world_settings.envmap_width * world_settings.envmap_height; #endif // Converting from "texel on envmap measure" to solid angle pdf /= (M_TWO_PI_SQUARED * sin_theta); return envmap_radiance; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F sample_environment_map_with_mis(HIPRTRenderData& render_data, RayPayload& ray_payload, HitInfo& closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { float envmap_pdf; float3 sampled_direction; ColorRGB32F envmap_color = envmap_sample(render_data.world_settings, sampled_direction, envmap_pdf, random_number_generator); ColorRGB32F envmap_mis_contribution; if (MaterialUtils::can_do_light_sampling(ray_payload.material)) { // Sampling the envmap with MIS float cosine_term = hippt::dot(closest_hit_info.shading_normal, sampled_direction); if (envmap_pdf > 0.0f && cosine_term > 0.0f) { hiprtRay shadow_ray; shadow_ray.origin = closest_hit_info.inter_point; shadow_ray.direction = sampled_direction; NEEPlusPlusContext nee_plus_plus_context; nee_plus_plus_context.shaded_point = closest_hit_info.inter_point; nee_plus_plus_context.point_on_light = sampled_direction; nee_plus_plus_context.envmap = true; bool in_shadow = evaluate_shadow_ray_nee_plus_plus(render_data, shadow_ray, 1.0e35f, closest_hit_info.primitive_index, nee_plus_plus_context, random_number_generator, ray_payload.bounce); if (!in_shadow) { float bsdf_pdf; BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, sampled_direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, EnvmapSamplingDoBSDFMIS ? MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS : MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC); ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); #if EnvmapSamplingDoBSDFMIS float mis_weight = balance_heuristic(envmap_pdf, bsdf_pdf); #else float mis_weight = 1.0f; #endif envmap_mis_contribution = bsdf_color * cosine_term * mis_weight * envmap_color / envmap_pdf / nee_plus_plus_context.unoccluded_probability; } } } #if EnvmapSamplingDoBSDFMIS float bsdf_sample_pdf; float3 bsdf_sampled_dir; ColorRGB32F bsdf_color; ColorRGB32F bsdf_mis_contribution; BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, make_float3(0.0f, 0.0f, 0.0f), incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); bsdf_color = bsdf_dispatcher_sample(render_data, bsdf_context, bsdf_sampled_dir, bsdf_sample_pdf, random_number_generator); // Sampling the BSDF with MIS float cosine_term = hippt::abs(hippt::dot(closest_hit_info.shading_normal, bsdf_sampled_dir)); if (bsdf_sample_pdf > 0.0f) { hiprtRay shadow_ray; shadow_ray.origin = closest_hit_info.inter_point; shadow_ray.direction = bsdf_sampled_dir; bool in_shadow = evaluate_shadow_ray_occluded(render_data, shadow_ray, 1.0e35f, closest_hit_info.primitive_index, ray_payload.bounce, random_number_generator); if (!in_shadow) { float envmap_eval_pdf; ColorRGB32F envmap_radiance = envmap_eval(render_data, bsdf_sampled_dir, envmap_eval_pdf); if (envmap_eval_pdf > 0.0f) { float mis_weight = balance_heuristic(bsdf_sample_pdf, envmap_eval_pdf); bsdf_mis_contribution = envmap_radiance * mis_weight * cosine_term * bsdf_color / bsdf_sample_pdf; } } } return bsdf_mis_contribution + envmap_mis_contribution; #else return envmap_mis_contribution; #endif } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F sample_environment_map(HIPRTRenderData& render_data, RayPayload& ray_payload, HitInfo& closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { const WorldSettings& world_settings = render_data.world_settings; if (world_settings.ambient_light_type != AmbientLightType::ENVMAP || render_data.bsdfs_data.white_furnace_mode) // Not using the envmap return ColorRGB32F(0.0f); if (world_settings.envmap_intensity <= 0.0f) // No need to sample the envmap if the user has set the intensity to 0 return ColorRGB32F(0.0f); if (ray_payload.bounce == 0 && DirectLightSamplingStrategy == LSS_RESTIR_DI) // The envmap lighting is handled by ReSTIR DI on the first bounce return ColorRGB32F(0.0f); #if EnvmapSamplingStrategy == ESS_NO_SAMPLING return ColorRGB32F(0.0f); #else return sample_environment_map_with_mis(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #endif } #endif ================================================ FILE: src/Device/includes/LightSampling/LightUtils.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_LIGHT_UTILS_H #define DEVICE_LIGHT_UTILS_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/LightSampling/PDFConversion.h" #include "Device/includes/ReSTIR/ReGIR/Settings.h" #include "Device/includes/ReSTIR/ReGIR/TargetFunction.h" #include "Device/includes/ReSTIR/ReGIR/ShadingAdditionalInfo.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/RenderData.h" HIPRT_DEVICE HIPRT_INLINE float3 get_triangle_normal_not_normalized(const HIPRTRenderData& render_data, int triangle_index) { int triangle_index_start = triangle_index * 3; float3 vertex_A = render_data.buffers.vertices_positions[render_data.buffers.triangles_indices[triangle_index_start + 0]]; float3 vertex_B = render_data.buffers.vertices_positions[render_data.buffers.triangles_indices[triangle_index_start + 1]]; float3 vertex_C = render_data.buffers.vertices_positions[render_data.buffers.triangles_indices[triangle_index_start + 2]]; float3 AB = vertex_B - vertex_A; float3 AC = vertex_C - vertex_A; return hippt::cross(AB, AC); } HIPRT_DEVICE HIPRT_INLINE float triangle_area(const HIPRTRenderData& render_data, int triangle_index) { return render_data.buffers.triangles_areas[triangle_index]; } HIPRT_DEVICE ColorRGB32F get_emission_of_triangle_from_index(const HIPRTRenderData& render_data, int triangle_index) { return render_data.buffers.materials_buffer.get_emission(render_data.buffers.material_indices[triangle_index]); } /** * Returns the PDF (area measure) of the light sampler for the given triangle_hit_info * * 'primitive_index' is the index of the emissive triangle hit * 'shading_normal' is the shading normal at the intersection point of the emissive triangle hit * 'hit_distance' is the distance to the intersection point on the hit triangle * 'ray_direction' is the direction of the ray that hit the triangle. The direction points towards the triangle. */ template HIPRT_DEVICE HIPRT_INLINE float pdf_of_emissive_triangle_hit_area_measure(const HIPRTRenderData& render_data, float light_area, ColorRGB32F light_emission) { float hit_distance = 1.0f; float area_measure_pdf; // Note that for ReGIR, we cannot have the exact light PDF since ReGIR is based on RIS so we're // faking it with whatever base strategy ReGIR is using if constexpr (lightSamplingStrategy == LSS_BASE_UNIFORM) { // Surface area PDF of hitting that point on that triangle in the scene area_measure_pdf = 1.0f / light_area; area_measure_pdf /= render_data.buffers.emissive_triangles_count; } else if constexpr (lightSamplingStrategy == LSS_BASE_POWER) { area_measure_pdf = 1.0f / light_area; area_measure_pdf *= (light_emission.luminance() * light_area) / render_data.buffers.emissives_power_alias_table.sum_elements; } else if constexpr (lightSamplingStrategy == LSS_BASE_REGIR) // Faking the ReGIR PDF with the PDF of its base sampling strategy area_measure_pdf = pdf_of_emissive_triangle_hit_area_measure(render_data, light_area, light_emission); return area_measure_pdf; } template HIPRT_DEVICE HIPRT_INLINE float pdf_of_emissive_triangle_hit_area_measure(const HIPRTRenderData& render_data, int hit_primitive_index, ColorRGB32F light_emission) { return pdf_of_emissive_triangle_hit_area_measure(render_data, triangle_area(render_data, hit_primitive_index), light_emission); } template HIPRT_DEVICE HIPRT_INLINE float pdf_of_emissive_triangle_hit_area_measure(const HIPRTRenderData& render_data, const BSDFLightSampleRayHitInfo& light_hit_info) { return pdf_of_emissive_triangle_hit_area_measure(render_data, light_hit_info.hit_prim_index, light_hit_info.hit_emission); } /** * Returns the PDF (solid angle measure) of the light sampler for the given 'light_hit_info' * * Note that for light samplers that cannot be point-evaluated (ReGIR for example: we cannot compute a RIS PDF), * the returned PDF is an approximation * * 'primitive_index' is the index of the emissive triangle hit * 'shading_normal' is the shading normal at the intersection point of the emissive triangle hit * 'hit_distance' is the distance to the intersection point on the hit triangle * 'to_light_direction' is the direction of the ray that hit the triangle. The direction points towards the triangle. */ template HIPRT_DEVICE HIPRT_INLINE float pdf_of_emissive_triangle_hit_solid_angle(const HIPRTRenderData& render_data, float light_area, ColorRGB32F light_emission, float3 light_surface_normal, float hit_distance, float3 to_light_direction) { // abs() here to allow backfacing lights // Without abs() here: // - We could be hitting the back of an emissive triangle (think of quad light hanging in the air) // --> triangle normal not facing the same way // --> cos_angle negative float cosine_light_source = compute_cosine_term_at_light_source(light_surface_normal, -to_light_direction); float pdf_area_measure = pdf_of_emissive_triangle_hit_area_measure(render_data, light_area, light_emission); return area_to_solid_angle_pdf(pdf_area_measure, hit_distance, cosine_light_source); } template HIPRT_DEVICE HIPRT_INLINE float pdf_of_emissive_triangle_hit_solid_angle(const HIPRTRenderData& render_data, int hit_primitive_index, ColorRGB32F light_emission, float3 light_surface_normal, float hit_distance, float3 to_light_direction) { return pdf_of_emissive_triangle_hit_solid_angle(render_data, triangle_area(render_data, hit_primitive_index), light_emission, light_surface_normal, hit_distance, to_light_direction); } template HIPRT_DEVICE HIPRT_INLINE float pdf_of_emissive_triangle_hit_solid_angle(const HIPRTRenderData& render_data, const BSDFLightSampleRayHitInfo& light_hit_info, float3 to_light_direction) { return pdf_of_emissive_triangle_hit_solid_angle(render_data, light_hit_info.hit_prim_index, light_hit_info.hit_emission, light_hit_info.hit_geometric_normal, light_hit_info.hit_distance, to_light_direction); } /** * Reference: [A Low-Distortion Map Between Triangle and Square, Heitz, 2019] * * Maps a point in a square to a point in an arbitrary triangle */ HIPRT_DEVICE HIPRT_INLINE float2 square_to_triangle(float& x, float& y) { if (y > x) { x *= 0.5f; y -= x; } else { y *= 0.5f; x -= y; } return make_float2(x, y); } /** * Samples a point uniformly on the given triangle (given with the triangle index) * * Returns true if the sampling was successful, false otherwise (can fail if the triangle is way too small or degenerate) */ HIPRT_DEVICE HIPRT_INLINE bool sample_point_on_generic_triangle(int global_triangle_index, const float3* vertices_positions, const int* triangles_indices, Xorshift32Generator& rng, float3& out_sample_point, float3& out_sampled_triangle_normal, float& out_triangle_area) { float3 vertex_A = vertices_positions[triangles_indices[global_triangle_index * 3 + 0]]; float3 vertex_B = vertices_positions[triangles_indices[global_triangle_index * 3 + 1]]; float3 vertex_C = vertices_positions[triangles_indices[global_triangle_index * 3 + 2]]; float rand_1 = rng(); float rand_2 = rng(); #if TrianglePointSamplingStrategy == TRIANGLE_POINT_SAMPLING_TURK_1990 float sqrt_r1 = sqrt(rand_1); float u = 1.0f - sqrt_r1; float v = (1.0f - rand_2) * sqrt_r1; #elif TrianglePointSamplingStrategy == TRIANGLE_POINT_SAMPLING_HEITZ_2019 float2 remapped = square_to_triangle(rand_1, rand_2); float u = remapped.x; float v = remapped.y; #endif float3 AB = vertex_B - vertex_A; float3 AC = vertex_C - vertex_A; float3 normal = hippt::cross(AB, AC); float length_normal = hippt::length(normal); // TODO the normal length check used to be for some NaNs that occured on degenerate triangles but doesn't seem to happen anymore /*if (length_normal <= 1.0e-6f) return false;*/ float3 random_point_on_triangle = vertex_A + AB * u + AC * v; out_sample_point = random_point_on_triangle; out_sampled_triangle_normal = normal / length_normal; out_triangle_area = 0.5f * length_normal; return true; } /** * Samples a point uniformly on the given emissive triangle. * The given 'emissive_triangle_index' must come from reading the 'emissive_triangle_indices' buffer of the scene. * * Returns true if the sampling was successful, false otherwise (can fail if the triangle is way too small or degenerate) * * !!!!! This function is a remnant of some tests. It's actually less performant than * sample_point_on_generic_triangle() because of more cache misses for some reason !!!!! */ HIPRT_DEVICE HIPRT_INLINE bool sample_point_on_emissive_triangle(int emissive_triangle_index, const PrecomputedEmissiveTrianglesDataSoADevice& petd, Xorshift32Generator& rng, float3& out_sample_point, float3& out_sampled_triangle_normal, float& out_triangle_area) { float rand_1 = rng(); float rand_2 = rng(); #if TrianglePointSamplingStrategy == TRIANGLE_POINT_SAMPLING_TURK_1990 float sqrt_r1 = sqrt(rand_1); float u = 1.0f - sqrt_r1; float v = (1.0f - rand_2) * sqrt_r1; #elif TrianglePointSamplingStrategy == TRIANGLE_POINT_SAMPLING_HEITZ_2019 float2 remapped = square_to_triangle(rand_1, rand_2); float u = remapped.x; float v = remapped.y; #endif float3 vertex_A = petd.triangles_A[emissive_triangle_index]; float3 AB = petd.triangles_AB[emissive_triangle_index]; float3 AC = petd.triangles_AC[emissive_triangle_index]; float3 normal = hippt::cross(AB, AC); float length_normal = hippt::length(normal); if (length_normal <= 1.0e-6f) return false; float3 random_point_on_triangle = vertex_A + AB * u + AC * v; out_sample_point = random_point_on_triangle; out_sampled_triangle_normal = normal / length_normal; out_triangle_area = 0.5f * length_normal; return true; } /** * The PDF is computed in area measure */ HIPRT_DEVICE HIPRT_INLINE LightSampleInformation sample_one_emissive_triangle_uniform(const HIPRTRenderData& render_data, Xorshift32Generator& random_number_generator) { if (render_data.buffers.emissive_triangles_count == 0) return LightSampleInformation(); LightSampleInformation light_sample; int random_emissive_triangle_index = random_number_generator.random_index(render_data.buffers.emissive_triangles_count); int triangle_index = render_data.buffers.emissive_triangles_primitive_indices[random_emissive_triangle_index]; float sampled_triangle_area; float3 sampled_triangle_normal; float3 random_point_on_triangle; if (!sample_point_on_generic_triangle(triangle_index, render_data.buffers.vertices_positions, render_data.buffers.triangles_indices, random_number_generator, random_point_on_triangle, sampled_triangle_normal, sampled_triangle_area)) return LightSampleInformation(); light_sample.emissive_triangle_index = triangle_index; light_sample.light_source_normal = sampled_triangle_normal; light_sample.light_area = sampled_triangle_area; light_sample.emission = render_data.buffers.materials_buffer.get_emission(render_data.buffers.material_indices[triangle_index]); light_sample.point_on_light = random_point_on_triangle; // PDF of that point on that triangle light_sample.area_measure_pdf = 1.0f / sampled_triangle_area; // PDF of that triangle sampled uniformly amongst all emissive triangles light_sample.area_measure_pdf /= render_data.buffers.emissive_triangles_count; return light_sample; } HIPRT_DEVICE HIPRT_INLINE LightSampleInformation sample_one_emissive_triangle_power(const HIPRTRenderData& render_data, Xorshift32Generator& random_number_generator) { if (render_data.buffers.emissive_triangles_count == 0) return LightSampleInformation(); LightSampleInformation out_sample; int random_emissive_triangle_index = render_data.buffers.emissives_power_alias_table.sample(random_number_generator); int triangle_index = render_data.buffers.emissive_triangles_primitive_indices[random_emissive_triangle_index]; float sampled_triangle_area; float3 sampled_triangle_normal; float3 random_point_on_triangle; if (!sample_point_on_generic_triangle(triangle_index, render_data.buffers.vertices_positions, render_data.buffers.triangles_indices, random_number_generator, random_point_on_triangle, sampled_triangle_normal, sampled_triangle_area)) return LightSampleInformation(); out_sample.emissive_triangle_index = triangle_index; out_sample.light_source_normal = sampled_triangle_normal; out_sample.light_area = sampled_triangle_area; out_sample.emission = render_data.buffers.materials_buffer.get_emission(render_data.buffers.material_indices[triangle_index]); out_sample.point_on_light = random_point_on_triangle; // PDF of that point on that triangle out_sample.area_measure_pdf = 1.0f / sampled_triangle_area; // PDF of sampling that triangle according to its power out_sample.area_measure_pdf *= (out_sample.emission.luminance() * sampled_triangle_area) / render_data.buffers.emissives_power_alias_table.sum_elements; return out_sample; } // Forward declaration for use in 'sample_one_emissive_triangle_regir' below template HIPRT_DEVICE HIPRT_INLINE LightSampleInformation sample_one_emissive_triangle(const HIPRTRenderData& render_data, const float3& shading_point, const float3& view_direction, const float3& shading_normal, const float3& geometric_normal, int last_hit_primitive_index, RayPayload& ray_payload, Xorshift32Generator& random_number_generator); template HIPRT_DEVICE float ReGIR_get_reservoir_sample_ReGIR_PDF(const HIPRTRenderData& render_data, const ReGIRGridFillSurface& surface, bool primary_hit, float PDF_normalization, float3 point_on_light, float3 light_source_normal, ColorRGB32F emission, Xorshift32Generator& random_number_generator) { float sample_PDF_unnormalized; if constexpr (canonicalPDF) sample_PDF_unnormalized = ReGIR_grid_fill_evaluate_canonical_target_function(render_data, surface, primary_hit, emission, light_source_normal, point_on_light, random_number_generator); else sample_PDF_unnormalized = ReGIR_grid_fill_evaluate_non_canonical_target_function(render_data, surface, primary_hit, emission, light_source_normal, point_on_light, random_number_generator); return sample_PDF_unnormalized / PDF_normalization; } template HIPRT_DEVICE float ReGIR_get_reservoir_sample_ReGIR_PDF(const HIPRTRenderData& render_data, const ReGIRGridFillSurface& surface, unsigned int grid_cell_index, bool primary_hit, float3 point_on_light, float3 light_source_normal, ColorRGB32F emission, Xorshift32Generator& random_number_generator) { float RIS_integral; if constexpr (canonicalPDF) RIS_integral = render_data.render_settings.regir_settings.get_canonical_pre_integration_factor(grid_cell_index, primary_hit); else RIS_integral = render_data.render_settings.regir_settings.get_non_canonical_pre_integration_factor(grid_cell_index, primary_hit); if (RIS_integral == 0.0f) RIS_integral = 1.0f; if (!render_data.render_settings.regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) RIS_integral = 1.0f; return ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, surface, primary_hit, RIS_integral, point_on_light, light_source_normal, emission, random_number_generator); } template HIPRT_DEVICE float ReGIR_get_reservoir_sample_ReGIR_PDF(const HIPRTRenderData& render_data, float3 point_on_light, float3 light_source_normal, ColorRGB32F emission, unsigned int grid_cell_index, bool primary_hit, Xorshift32Generator& random_number_generator) { if (emission.is_black()) return 0.0f; ReGIRGridFillSurface surface = ReGIR_get_cell_surface(render_data, grid_cell_index, primary_hit); return ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, surface, grid_cell_index, primary_hit, point_on_light, light_source_normal, emission, random_number_generator); } template HIPRT_DEVICE float ReGIR_get_reservoir_sample_ReGIR_PDF(const HIPRTRenderData& render_data, float3 point_on_light, float3 light_source_normal, ColorRGB32F emission, unsigned int grid_cell_index, float RIS_integral, bool primary_hit, Xorshift32Generator& random_number_generator) { if (emission.is_black()) return 0.0f; ReGIRGridFillSurface surface = ReGIR_get_cell_surface(render_data, grid_cell_index, primary_hit); return ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, surface, primary_hit, RIS_integral, point_on_light, light_source_normal, emission, random_number_generator); } template HIPRT_DEVICE float ReGIR_get_reservoir_sample_ReGIR_PDF(const HIPRTRenderData& render_data, const ReGIRReservoir& reservoir, unsigned int grid_cell_index, bool primary_hit, Xorshift32Generator& random_number_generator) { if (reservoir.UCW <= 0.0f) return 0.0f; float3 point_on_light = reservoir.sample.point_on_light; float3 light_source_normal = hippt::normalize(get_triangle_normal_not_normalized(render_data, reservoir.sample.emissive_triangle_index)); ColorRGB32F emission = get_emission_of_triangle_from_index(render_data, reservoir.sample.emissive_triangle_index); return ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, point_on_light, light_source_normal, emission, grid_cell_index, primary_hit, random_number_generator); } template HIPRT_DEVICE float ReGIR_get_reservoir_sample_ReGIR_PDF(const HIPRTRenderData& render_data, const ReGIRReservoir& reservoir, unsigned int grid_cell_index, float RIS_integral, bool primary_hit, Xorshift32Generator& random_number_generator) { if (reservoir.UCW <= 0.0f) return 0.0f; float3 point_on_light = reservoir.sample.point_on_light; float3 light_source_normal = hippt::normalize(get_triangle_normal_not_normalized(render_data, reservoir.sample.emissive_triangle_index)); ColorRGB32F emission = get_emission_of_triangle_from_index(render_data, reservoir.sample.emissive_triangle_index); return ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, point_on_light, light_source_normal, emission, grid_cell_index, RIS_integral, primary_hit, random_number_generator); } HIPRT_DEVICE float ReGIR_get_reservoir_sample_BSDF_PDF(const HIPRTRenderData& render_data, float3 point_on_light, float3 light_source_normal, ColorRGB32F emission, float3 view_direction, float3 shading_point, float3 shading_normal, float3 geometric_normal, BSDFIncidentLightInfo incident_light_info, RayPayload& ray_payload, int last_hit_primitive_index) { if (emission.is_black()) return 0.0f; float3 to_light_direction = point_on_light - shading_point; float distance_to_light = hippt::length(to_light_direction); to_light_direction /= distance_to_light; // Normalization BSDFContext bsdf_context(view_direction, shading_normal, geometric_normal, to_light_direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); float bsdf_pdf = bsdf_dispatcher_pdf(render_data, bsdf_context); float area_measure_bsdf_pdf = solid_angle_to_area_pdf(bsdf_pdf, distance_to_light, compute_cosine_term_at_light_source(light_source_normal, -to_light_direction)); return area_measure_bsdf_pdf; } HIPRT_DEVICE float ReGIR_get_reservoir_sample_BSDF_PDF(const HIPRTRenderData& render_data, const ReGIRReservoir& reservoir, float3 view_direction, float3 shading_point, float3 shading_normal, float3 geometric_normal, BSDFIncidentLightInfo incident_light_info, RayPayload& ray_payload, int last_hit_primitive_index, Xorshift32Generator& random_number_generator) { if (reservoir.UCW <= 0.0f) return 0.0f; float3 point_on_light = reservoir.sample.point_on_light; float3 light_source_normal = hippt::normalize(get_triangle_normal_not_normalized(render_data, reservoir.sample.emissive_triangle_index)); ColorRGB32F emission = get_emission_of_triangle_from_index(render_data, reservoir.sample.emissive_triangle_index); return ReGIR_get_reservoir_sample_BSDF_PDF(render_data, point_on_light, light_source_normal, emission, view_direction, shading_point, shading_normal, geometric_normal, incident_light_info, ray_payload, last_hit_primitive_index); } struct ReGIRPairwiseMIS { HIPRT_DEVICE float compute_MIS_weight_normalization(const HIPRTRenderData& render_data, unsigned int valid_non_canonical_neighbors) { unsigned int number_of_samples = 0; number_of_samples += valid_non_canonical_neighbors * render_data.render_settings.regir_settings.shading.reservoir_tap_count_per_neighbor; // non canonical samples if (number_of_samples == 0) return 0.0f; return 1.0f / number_of_samples; } HIPRT_DEVICE void sum_non_canonical_sample_to_canonical_weights(const HIPRTRenderData& render_data, float3 canonical_technique_1_point_on_light, float3 canonical_technique_1_light_normal, ColorRGB32F canonical_technique_1_emission, float3 canonical_technique_2_point_on_light, float3 canonical_technique_2_light_normal, ColorRGB32F canonical_technique_2_emission, float3 canonical_technique_3_point_on_light, float3 canonical_technique_3_light_normal, ColorRGB32F canonical_technique_3_emission, float canonical_technique_1_canonical_reservoir_1_pdf, float canonical_technique_1_canonical_reservoir_2_pdf, float canonical_technique_1_canonical_reservoir_3_pdf, float canonical_technique_2_canonical_reservoir_1_pdf, float canonical_technique_2_canonical_reservoir_2_pdf, float canonical_technique_2_canonical_reservoir_3_pdf, float canonical_technique_3_canonical_reservoir_1_pdf, float canonical_technique_3_canonical_reservoir_2_pdf, float canonical_technique_3_canonical_reservoir_3_pdf, float mis_weight_normalization, ReGIRGridFillSurface neighbor_surface, float neighbor_non_canonical_RIS_integral, bool is_primary_hit, Xorshift32Generator& random_number_generator) { float non_canonical_neighbor_technique_canonical_reservoir_1_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, neighbor_surface, is_primary_hit, neighbor_non_canonical_RIS_integral, canonical_technique_1_point_on_light, canonical_technique_1_light_normal, canonical_technique_1_emission, random_number_generator); m_sum_canonical_weight_1 += canonical_technique_1_canonical_reservoir_1_pdf * mis_weight_normalization / (non_canonical_neighbor_technique_canonical_reservoir_1_pdf + canonical_technique_1_canonical_reservoir_1_pdf * mis_weight_normalization + canonical_technique_2_canonical_reservoir_1_pdf * mis_weight_normalization + canonical_technique_3_canonical_reservoir_1_pdf * mis_weight_normalization); float non_canonical_neighbor_technique_canonical_reservoir_2_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, neighbor_surface, is_primary_hit, neighbor_non_canonical_RIS_integral, canonical_technique_2_point_on_light, canonical_technique_2_light_normal, canonical_technique_2_emission, random_number_generator); m_sum_canonical_weight_2 += canonical_technique_2_canonical_reservoir_2_pdf * mis_weight_normalization / (non_canonical_neighbor_technique_canonical_reservoir_2_pdf + canonical_technique_1_canonical_reservoir_2_pdf * mis_weight_normalization + canonical_technique_2_canonical_reservoir_2_pdf * mis_weight_normalization + canonical_technique_3_canonical_reservoir_2_pdf * mis_weight_normalization); #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE float non_canonical_neighbor_technique_canonical_reservoir_3_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, neighbor_surface, is_primary_hit, neighbor_non_canonical_RIS_integral, canonical_technique_3_point_on_light, canonical_technique_3_light_normal, canonical_technique_3_emission, random_number_generator); m_sum_canonical_weight_3 += canonical_technique_3_canonical_reservoir_3_pdf * mis_weight_normalization / (non_canonical_neighbor_technique_canonical_reservoir_3_pdf + canonical_technique_1_canonical_reservoir_3_pdf * mis_weight_normalization + canonical_technique_2_canonical_reservoir_3_pdf * mis_weight_normalization + canonical_technique_3_canonical_reservoir_3_pdf * mis_weight_normalization); #endif } HIPRT_DEVICE float compute_MIS_weight_for_non_canonical_sample(const HIPRTRenderData& render_data, float3 sample_point_on_light, float3 sample_light_source_normal, ColorRGB32F sample_emission, float3 canonical_technique_1_point_on_light, float3 canonical_technique_1_light_normal, ColorRGB32F canonical_technique_1_emission, float3 canonical_technique_2_point_on_light, float3 canonical_technique_2_light_normal, ColorRGB32F canonical_technique_2_emission, float3 canonical_technique_3_point_on_light, float3 canonical_technique_3_light_normal, ColorRGB32F canonical_technique_3_emission, const ReGIRGridFillSurface& center_grid_cell_surface, bool primary_hit, float canonical_technique_1_canonical_reservoir_1_pdf, float canonical_technique_1_canonical_reservoir_2_pdf, float canonical_technique_1_canonical_reservoir_3_pdf, float canonical_technique_2_canonical_reservoir_1_pdf, float canonical_technique_2_canonical_reservoir_2_pdf, float canonical_technique_2_canonical_reservoir_3_pdf, float canonical_technique_3_canonical_reservoir_1_pdf, float canonical_technique_3_canonical_reservoir_2_pdf, float canonical_technique_3_canonical_reservoir_3_pdf, float mis_weight_normalization, float non_canonical_RIS_integral_center_grid_cell, float canonical_RIS_integral_center_grid_cell, float non_canonical_sample_PDF, ReGIRGridFillSurface neighbor_surface, float neighbor_non_canonical_RIS_integral, float3 view_direction, float3 shading_point, float3 shading_normal, float3 geometric_normal, RayPayload& ray_payload, int last_hit_primitive_index, Xorshift32Generator& random_number_generator) { float non_canonical_PDF = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, center_grid_cell_surface, primary_hit, non_canonical_RIS_integral_center_grid_cell, sample_point_on_light, sample_light_source_normal, sample_emission, random_number_generator); float canonical_PDF = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, center_grid_cell_surface, primary_hit, canonical_RIS_integral_center_grid_cell, sample_point_on_light, sample_light_source_normal, sample_emission, random_number_generator); #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE float bsdf_PDF = ReGIR_get_reservoir_sample_BSDF_PDF(render_data, sample_point_on_light, sample_light_source_normal, sample_emission, view_direction, shading_point, shading_normal, geometric_normal, BSDFIncidentLightInfo::NO_INFO, ray_payload, last_hit_primitive_index); #else float bsdf_PDF = 0.0f; #endif float mis_weight = mis_weight_normalization * (non_canonical_sample_PDF / (non_canonical_sample_PDF + non_canonical_PDF * mis_weight_normalization + canonical_PDF * mis_weight_normalization + bsdf_PDF * mis_weight_normalization)); // Summing the weights for the canonical MIS weight computation float non_canonical_neighbor_technique_canonical_reservoir_1_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, neighbor_surface, primary_hit, neighbor_non_canonical_RIS_integral, canonical_technique_1_point_on_light, canonical_technique_1_light_normal, canonical_technique_1_emission, random_number_generator); m_sum_canonical_weight_1 += canonical_technique_1_canonical_reservoir_1_pdf * mis_weight_normalization / (non_canonical_neighbor_technique_canonical_reservoir_1_pdf + canonical_technique_1_canonical_reservoir_1_pdf * mis_weight_normalization + canonical_technique_2_canonical_reservoir_1_pdf * mis_weight_normalization + canonical_technique_3_canonical_reservoir_1_pdf * mis_weight_normalization); float non_canonical_neighbor_technique_canonical_reservoir_2_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, neighbor_surface, primary_hit, neighbor_non_canonical_RIS_integral, canonical_technique_2_point_on_light, canonical_technique_2_light_normal, canonical_technique_2_emission, random_number_generator); m_sum_canonical_weight_2 += canonical_technique_2_canonical_reservoir_2_pdf * mis_weight_normalization / (non_canonical_neighbor_technique_canonical_reservoir_2_pdf + canonical_technique_1_canonical_reservoir_2_pdf * mis_weight_normalization + canonical_technique_2_canonical_reservoir_2_pdf * mis_weight_normalization + canonical_technique_3_canonical_reservoir_2_pdf * mis_weight_normalization); float non_canonical_neighbor_technique_canonical_reservoir_3_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, neighbor_surface, primary_hit, neighbor_non_canonical_RIS_integral, canonical_technique_3_point_on_light, canonical_technique_3_light_normal, canonical_technique_3_emission, random_number_generator); m_sum_canonical_weight_3 += canonical_technique_3_canonical_reservoir_3_pdf * mis_weight_normalization / (non_canonical_neighbor_technique_canonical_reservoir_3_pdf + canonical_technique_1_canonical_reservoir_3_pdf * mis_weight_normalization + canonical_technique_2_canonical_reservoir_3_pdf * mis_weight_normalization + canonical_technique_3_canonical_reservoir_3_pdf * mis_weight_normalization); return mis_weight; } HIPRT_DEVICE float get_canonical_MIS_weight_1(float canonical_technique_1_canonical_reservoir_1_pdf, float canonical_technique_2_canonical_reservoir_1_pdf, float canonical_technique_3_canonical_reservoir_1_pdf, float mis_weight_normalization) { if (mis_weight_normalization == 0.0f) // We only have the canonical techniques available, we're going to go for a balance heuristic between them return canonical_technique_1_canonical_reservoir_1_pdf / (canonical_technique_1_canonical_reservoir_1_pdf + canonical_technique_2_canonical_reservoir_1_pdf + canonical_technique_3_canonical_reservoir_1_pdf); return m_sum_canonical_weight_1 * mis_weight_normalization; } HIPRT_DEVICE float get_canonical_MIS_weight_2(float canonical_technique_1_canonical_reservoir_2_pdf, float canonical_technique_2_canonical_reservoir_2_pdf, float canonical_technique_3_canonical_reservoir_2_pdf, float mis_weight_normalization) { if (mis_weight_normalization == 0.0f) // We only have the canonical techniques available, we're going to go for a balance heuristic between them return canonical_technique_2_canonical_reservoir_2_pdf / (canonical_technique_1_canonical_reservoir_2_pdf + canonical_technique_2_canonical_reservoir_2_pdf + canonical_technique_3_canonical_reservoir_2_pdf); return m_sum_canonical_weight_2 * mis_weight_normalization; } HIPRT_DEVICE float get_canonical_MIS_weight_3(float canonical_technique_1_canonical_reservoir_3_pdf, float canonical_technique_2_canonical_reservoir_3_pdf, float canonical_technique_3_canonical_reservoir_3_pdf, float mis_weight_normalization) { if (mis_weight_normalization == 0.0f) // We only have the canonical techniques available, we're going to go for a balance heuristic between them return canonical_technique_3_canonical_reservoir_3_pdf / (canonical_technique_1_canonical_reservoir_3_pdf + canonical_technique_2_canonical_reservoir_3_pdf + canonical_technique_3_canonical_reservoir_3_pdf); return m_sum_canonical_weight_3 * mis_weight_normalization; } // 1st is non-canonical samples float m_sum_canonical_weight_1 = 0.0f; // 2nd technique is canonical samples float m_sum_canonical_weight_2 = 0.0f; // 3rd technique is BSDF samples float m_sum_canonical_weight_3 = 0.0f; }; HIPRT_DEVICE HIPRT_INLINE LightSampleInformation sample_one_emissive_triangle_regir_with_info( const HIPRTRenderData& render_data, const float3& shading_point, const float3& view_direction, const float3& shading_normal, const float3& geometric_normal, int last_hit_primitive_index, RayPayload& ray_payload, bool& out_need_fallback_sampling, Xorshift32Generator& random_number_generator, ReGIRShadingAdditionalInfo& out_infos) { // Starting with this at true and if we find a single good neighbor, // this will be set to false out_need_fallback_sampling = true; float3 selected_point_on_light = make_float3(0.0f, 0.0f, 0.0f); float3 selected_light_source_normal = make_float3(0.0f, 0.0f, 0.0f); float selected_light_source_area = 0.0f; BSDFIncidentLightInfo selected_incident_light_info = BSDFIncidentLightInfo::NO_INFO; ColorRGB32F selected_emission; ReGIRReservoir out_reservoir; // Some random seed to generate to positions of the neighbors (when jittering) // XORing here because not XORing was causing RNG correlations issues... // not sure how that works but more randomness here seems to be getting rid of those correlations issues unsigned neighbor_rng_seed = random_number_generator.xorshift32() ^ random_number_generator.xorshift32(); unsigned non_cano_neighbor_rng_seed = neighbor_rng_seed ^ random_number_generator.xorshift32(); Xorshift32Generator non_canonical_neighbor_rng(non_cano_neighbor_rng_seed); Xorshift32Generator neighbor_rng(neighbor_rng_seed); const ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; unsigned int valid_non_canonical_neighbors = 0; for (int neighbor = 0; neighbor < regir_settings.shading.number_of_neighbors; neighbor++) { unsigned int neighbor_grid_cell_index = regir_settings.find_valid_jittered_neighbor_cell_index( shading_point, geometric_normal, render_data.current_camera, ray_payload.material.roughness, regir_settings.compute_is_primary_hit(ray_payload), regir_settings.shading.get_do_cell_jittering(regir_settings.compute_is_primary_hit(ray_payload)), regir_settings.shading.jittering_radius, non_canonical_neighbor_rng); if (neighbor_grid_cell_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) // Not a valid neighbor continue; else valid_non_canonical_neighbors++; } // Resetting the seed after the counting of the neighbors non_canonical_neighbor_rng.m_state.seed = non_cano_neighbor_rng_seed; #if ReGIR_ShadingResamplingDoMISPairwiseMIS { ReGIRPairwiseMIS pairwise; unsigned int canonical_grid_cell_index = regir_settings.find_valid_jittered_neighbor_cell_index( shading_point, geometric_normal, render_data.current_camera, ray_payload.material.roughness, regir_settings.compute_is_primary_hit(ray_payload), ReGIR_ShadingResamplingJitterCanonicalCandidates, regir_settings.shading.jittering_radius, neighbor_rng); float UCW_1 = 0.0f, UCW_2 = 0.0f; int triangle_index_1 = -1, triangle_index_2 = -1, triangle_index_3 = -1; float3 point_on_light_1, point_on_light_2, point_on_light_3 = make_float3(0.0f, 0.0f, 0.0f); float3 light_source_normal_1, light_source_normal_2, light_source_normal_3 = make_float3(0.0f, 0.0f, 0.0f); ColorRGB32F emission_1, emission_2, emission_3; BSDFIncidentLightInfo canonical_technique_3_sample_ili = BSDFIncidentLightInfo::NO_INFO; ReGIRGridFillSurface center_cell_surface; float canonical_technique_1_canonical_reservoir_1_pdf = 0.0f; float canonical_technique_1_canonical_reservoir_2_pdf = 0.0f; float canonical_technique_1_canonical_reservoir_3_pdf = 0.0f; float canonical_technique_2_canonical_reservoir_1_pdf = 0.0f; float canonical_technique_2_canonical_reservoir_2_pdf = 0.0f; float canonical_technique_2_canonical_reservoir_3_pdf = 0.0f; float canonical_technique_3_canonical_reservoir_1_pdf = 0.0f; float canonical_technique_3_canonical_reservoir_2_pdf = 0.0f; float canonical_technique_3_canonical_reservoir_3_pdf = 0.0f; float mis_weight_normalization = pairwise.compute_MIS_weight_normalization(render_data, valid_non_canonical_neighbors); float non_canonical_RIS_integral_center_grid_cell; float canonical_RIS_integral_center_grid_cell; // Fetching the center cell should never fail because the center cell always exists but it may actually fail in case of collisions // that cannot be resolved if (canonical_grid_cell_index != HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { // We found at least one good sample so we're not going to need a fallback on another light sampling strategy than ReGIR out_need_fallback_sampling = false; // Producing the canonical techniques samples { ReGIRReservoir canonical_technique_1_reservoir = regir_settings.get_random_reservoir_in_grid_cell_for_shading(canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), random_number_generator); ReGIRReservoir canonical_technique_2_reservoir = regir_settings.get_random_reservoir_in_grid_cell_for_shading(canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), random_number_generator); if constexpr (ReGIR_ShadingResamplingDoBSDFMIS) { float bsdf_sample_pdf; float3 sampled_bsdf_direction; BSDFContext bsdf_context(view_direction, shading_normal, geometric_normal, make_float3(0.0f, 0.0f, 0.0f), canonical_technique_3_sample_ili, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); bsdf_dispatcher_sample(render_data, bsdf_context, sampled_bsdf_direction, bsdf_sample_pdf, random_number_generator); bool intersection_found = false; BSDFLightSampleRayHitInfo shadow_light_ray_hit_info; if (bsdf_sample_pdf > 0.0f) { hiprtRay new_ray; new_ray.origin = shading_point; new_ray.direction = sampled_bsdf_direction; intersection_found = evaluate_bsdf_light_sample_ray_simplified(render_data, new_ray, 1.0e35f, shadow_light_ray_hit_info, last_hit_primitive_index, ray_payload.bounce, random_number_generator); // Checking that we did hit something and if we hit something, // it needs to be emissive if (intersection_found && !shadow_light_ray_hit_info.hit_emission.is_black()) { triangle_index_3 = shadow_light_ray_hit_info.hit_prim_index; point_on_light_3 = shading_point + shadow_light_ray_hit_info.hit_distance * sampled_bsdf_direction; light_source_normal_3 = shadow_light_ray_hit_info.hit_geometric_normal; emission_3 = shadow_light_ray_hit_info.hit_emission; // We want ReGIR to produce PDFs that are in area measure so we're converting from solid angle to area measure here canonical_technique_3_canonical_reservoir_3_pdf = solid_angle_to_area_pdf(bsdf_sample_pdf, shadow_light_ray_hit_info.hit_distance, compute_cosine_term_at_light_source(shadow_light_ray_hit_info.hit_geometric_normal, -sampled_bsdf_direction)); } } } // Extracting the data of the canonical reservoirs 1 and 2 if (canonical_technique_1_reservoir.UCW > 0.0f) { UCW_1 = canonical_technique_1_reservoir.UCW; triangle_index_1 = canonical_technique_1_reservoir.sample.emissive_triangle_index; point_on_light_1 = canonical_technique_1_reservoir.sample.point_on_light; light_source_normal_1 = hippt::normalize(get_triangle_normal_not_normalized(render_data, canonical_technique_1_reservoir.sample.emissive_triangle_index)); emission_1 = get_emission_of_triangle_from_index(render_data, canonical_technique_1_reservoir.sample.emissive_triangle_index); } if (canonical_technique_2_reservoir.UCW > 0.0f) { UCW_2 = canonical_technique_2_reservoir.UCW; triangle_index_2 = canonical_technique_2_reservoir.sample.emissive_triangle_index; point_on_light_2 = canonical_technique_2_reservoir.sample.point_on_light; light_source_normal_2 = hippt::normalize(get_triangle_normal_not_normalized(render_data, canonical_technique_2_reservoir.sample.emissive_triangle_index)); emission_2 = get_emission_of_triangle_from_index(render_data, canonical_technique_2_reservoir.sample.emissive_triangle_index); } } // Computing all the PDFs of the canonical techniques that we're going to need for pairwise MIS { if (!emission_1.is_black()) { // TODO we already have the canonical / non-canonical PDF normalization (fetched below) so we can use them because otherwise, that function fetches them again canonical_technique_1_canonical_reservoir_1_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, point_on_light_1, light_source_normal_1, emission_1, canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), random_number_generator); canonical_technique_2_canonical_reservoir_1_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, point_on_light_1, light_source_normal_1, emission_1, canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), random_number_generator); #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE canonical_technique_3_canonical_reservoir_1_pdf = ReGIR_get_reservoir_sample_BSDF_PDF(render_data, point_on_light_1, light_source_normal_1, emission_1, view_direction, shading_point, shading_normal, geometric_normal, BSDFIncidentLightInfo::NO_INFO, ray_payload, last_hit_primitive_index); #endif } if (!emission_2.is_black()) { canonical_technique_1_canonical_reservoir_2_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, point_on_light_2, light_source_normal_2, emission_2, canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), random_number_generator); canonical_technique_2_canonical_reservoir_2_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, point_on_light_2, light_source_normal_2, emission_2, canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), random_number_generator); #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE canonical_technique_3_canonical_reservoir_2_pdf = ReGIR_get_reservoir_sample_BSDF_PDF(render_data, point_on_light_2, light_source_normal_2, emission_2, view_direction, shading_point, shading_normal, geometric_normal, BSDFIncidentLightInfo::NO_INFO, ray_payload, last_hit_primitive_index); #endif } #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE if (!emission_3.is_black()) { canonical_technique_1_canonical_reservoir_3_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, point_on_light_3, light_source_normal_3, emission_3, canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), random_number_generator); canonical_technique_2_canonical_reservoir_3_pdf = ReGIR_get_reservoir_sample_ReGIR_PDF(render_data, point_on_light_3, light_source_normal_3, emission_3, canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), random_number_generator); // This one has already been computed when sampling the BSDF sample // canonical_technique_3_canonical_reservoir_3_pdf.... } #endif } { non_canonical_RIS_integral_center_grid_cell = regir_settings.get_non_canonical_pre_integration_factor(canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); if (non_canonical_RIS_integral_center_grid_cell == 0.0f) non_canonical_RIS_integral_center_grid_cell = 1.0f; if (!regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) non_canonical_RIS_integral_center_grid_cell = 1.0f; canonical_RIS_integral_center_grid_cell = regir_settings.get_canonical_pre_integration_factor(canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); if (canonical_RIS_integral_center_grid_cell == 0.0f) canonical_RIS_integral_center_grid_cell = 1.0f; if (!regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) canonical_RIS_integral_center_grid_cell = 1.0f; } center_cell_surface = ReGIR_get_cell_surface(render_data, canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); } else { // The center grid cell is invalid (must be because of hash grid collisions that couldn't be resolved) out_need_fallback_sampling = true; return LightSampleInformation(); } for (int neighbor = 0; neighbor < regir_settings.shading.number_of_neighbors; neighbor++) { unsigned int neighbor_grid_cell_index = regir_settings.find_valid_jittered_neighbor_cell_index( shading_point, geometric_normal, render_data.current_camera, ray_payload.material.roughness, regir_settings.compute_is_primary_hit(ray_payload), regir_settings.shading.get_do_cell_jittering(regir_settings.compute_is_primary_hit(ray_payload)), regir_settings.shading.jittering_radius, non_canonical_neighbor_rng); if (neighbor_grid_cell_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) // Couldn't find a valid neighbor continue; else out_need_fallback_sampling = false; ReGIRGridFillSurface neighbor_surface = ReGIR_get_cell_surface(render_data, neighbor_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); float neighbor_RIS_integral = regir_settings.get_non_canonical_pre_integration_factor(neighbor_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); if (neighbor_RIS_integral == 0.0f) neighbor_RIS_integral = 1.0f; if (!regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) neighbor_RIS_integral = 1.0f; for (int i = 0; i < regir_settings.shading.reservoir_tap_count_per_neighbor; i++) { // Will be set to true if the jittering causes the current shading point to be jittered out of the scene ReGIRReservoir non_canonical_reservoir = regir_settings.get_random_reservoir_in_grid_cell_for_shading(neighbor_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), neighbor_rng); if (non_canonical_reservoir.UCW <= 0.0f) { // No valid sample in that reservoir pairwise.sum_non_canonical_sample_to_canonical_weights(render_data, point_on_light_1, light_source_normal_1, emission_1, point_on_light_2, light_source_normal_2, emission_2, point_on_light_3, light_source_normal_3, emission_3, canonical_technique_1_canonical_reservoir_1_pdf, canonical_technique_1_canonical_reservoir_2_pdf, canonical_technique_1_canonical_reservoir_3_pdf, canonical_technique_2_canonical_reservoir_1_pdf, canonical_technique_2_canonical_reservoir_2_pdf, canonical_technique_2_canonical_reservoir_3_pdf, canonical_technique_3_canonical_reservoir_1_pdf, canonical_technique_3_canonical_reservoir_2_pdf, canonical_technique_3_canonical_reservoir_3_pdf, mis_weight_normalization, neighbor_surface, neighbor_RIS_integral, regir_settings.compute_is_primary_hit(ray_payload), random_number_generator); continue; } float3 point_on_light = non_canonical_reservoir.sample.point_on_light; float3 light_source_normal = get_triangle_normal_not_normalized(render_data, non_canonical_reservoir.sample.emissive_triangle_index); float light_source_area = hippt::length(light_source_normal) * 0.5f; light_source_normal /= light_source_area * 2.0f; ColorRGB32F emission = get_emission_of_triangle_from_index(render_data, non_canonical_reservoir.sample.emissive_triangle_index); ColorRGB32F sample_radiance; float target_function = ReGIR_shading_evaluate_target_function< ReGIR_ShadingResamplingTargetFunctionVisibility || ReGIR_ShadingResamplingShadeAllSamples, ReGIR_ShadingResamplingTargetFunctionNeePlusPlusVisibility>(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, point_on_light, light_source_normal, emission, random_number_generator, sample_radiance); float non_canonical_sample_PDF_unnormalized = ReGIR_grid_fill_evaluate_non_canonical_target_function(render_data, neighbor_surface, regir_settings.compute_is_primary_hit(ray_payload), emission, light_source_normal, point_on_light, random_number_generator); float current_sample_PDF = non_canonical_sample_PDF_unnormalized / neighbor_RIS_integral; float mis_weight = pairwise.compute_MIS_weight_for_non_canonical_sample(render_data, point_on_light, light_source_normal, emission, point_on_light_1, light_source_normal_1, emission_1, point_on_light_2, light_source_normal_2, emission_2, point_on_light_3, light_source_normal_3, emission_3, center_cell_surface, regir_settings.compute_is_primary_hit(ray_payload), canonical_technique_1_canonical_reservoir_1_pdf, canonical_technique_1_canonical_reservoir_2_pdf, canonical_technique_1_canonical_reservoir_3_pdf, canonical_technique_2_canonical_reservoir_1_pdf, canonical_technique_2_canonical_reservoir_2_pdf, canonical_technique_2_canonical_reservoir_3_pdf, canonical_technique_3_canonical_reservoir_1_pdf, canonical_technique_3_canonical_reservoir_2_pdf, canonical_technique_3_canonical_reservoir_3_pdf, mis_weight_normalization, non_canonical_RIS_integral_center_grid_cell, canonical_RIS_integral_center_grid_cell, current_sample_PDF, neighbor_surface, neighbor_RIS_integral, view_direction, shading_point, shading_normal, geometric_normal, ray_payload, last_hit_primitive_index, random_number_generator); if (out_reservoir.stream_reservoir(mis_weight, target_function, non_canonical_reservoir, random_number_generator)) { selected_point_on_light = point_on_light; selected_light_source_normal = light_source_normal; selected_light_source_area = light_source_area; selected_emission = emission; #if ReGIR_ShadingResamplingShadeAllSamples == KERNEL_OPTION_FALSE out_infos.sample_radiance = sample_radiance; #endif } #if ReGIR_ShadingResamplingShadeAllSamples == KERNEL_OPTION_TRUE out_infos.sample_radiance += sample_radiance * non_canonical_reservoir.UCW * mis_weight; #endif } } if (canonical_grid_cell_index != HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { if (!emission_1.is_black()) { float light_source_area = hippt::length(get_triangle_normal_not_normalized(render_data, triangle_index_1)) * 0.5f; { // Adding visibility in the canonical sample target function's if we have visibility in the grid fill target function // or if we wwant visibility in the target function during shading resampling // or if we're shading all candidates because then we want the target function to produce // the radiance towards the shading point directly which means that we need the visibility in the target function ColorRGB32F sample_radiance; float target_function = ReGIR_shading_evaluate_target_function(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, point_on_light_1, light_source_normal_1, emission_1, random_number_generator, sample_radiance); float RIS_integral = regir_settings.get_non_canonical_pre_integration_factor(canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); if (RIS_integral == 0.0f) RIS_integral = 1.0f; if (!regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) RIS_integral = 1.0f; float non_canonical_sample_PDF_unnormalized = ReGIR_grid_fill_evaluate_non_canonical_target_function(render_data, canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), emission_1, light_source_normal_1, point_on_light_1, random_number_generator); float non_canonical_sample_PDF = non_canonical_sample_PDF_unnormalized / RIS_integral; float mis_weight = pairwise.get_canonical_MIS_weight_1(canonical_technique_1_canonical_reservoir_1_pdf, canonical_technique_2_canonical_reservoir_1_pdf, canonical_technique_3_canonical_reservoir_1_pdf, mis_weight_normalization); ReGIRReservoir canonical_technique_1_reservoir; canonical_technique_1_reservoir.sample.emissive_triangle_index = triangle_index_1; canonical_technique_1_reservoir.sample.point_on_light = point_on_light_1; canonical_technique_1_reservoir.UCW = UCW_1; if (out_reservoir.stream_reservoir(mis_weight, target_function, canonical_technique_1_reservoir, random_number_generator)) { selected_point_on_light = point_on_light_1; selected_light_source_normal = light_source_normal_1; selected_light_source_area = light_source_area; selected_emission = emission_1; #if ReGIR_ShadingResamplingShadeAllSamples == KERNEL_OPTION_FALSE out_infos.sample_radiance = sample_radiance; #endif } #if ReGIR_ShadingResamplingShadeAllSamples == KERNEL_OPTION_TRUE out_infos.sample_radiance += sample_radiance * canonical_technique_1_reservoir.UCW * mis_weight; #endif } } } // Incorporating a canonical candidate if doing visibility reuse because visibility reuse // may cause the grid cell to produce no valid reservoir at all so we need canonical samples to // cover those cases for unbiased results // // Fetching the center cell should never fail because the center cell always exists but it may actually fail in case of collisions // that cannot be resolved if (canonical_grid_cell_index != HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { if (!emission_2.is_black()) { float light_source_area = hippt::length(get_triangle_normal_not_normalized(render_data, triangle_index_2)) * 0.5f; { // Adding visibility in the canonical sample target function's if we have visibility in the grid fill target function // or if we wwant visibility in the target function during shading resampling // or if we're shading all candidates because then we want the target function to produce // the radiance towards the shading point directly which means that we need the visibility in the target function ColorRGB32F sample_radiance; float target_function = ReGIR_shading_evaluate_target_function(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, point_on_light_2, light_source_normal_2, emission_2, random_number_generator, sample_radiance); float RIS_integral = regir_settings.get_canonical_pre_integration_factor(canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); if (RIS_integral == 0.0f) RIS_integral = 1.0f; if (!regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) RIS_integral = 1.0f; float canonical_sample_PDF_unnormalized = ReGIR_grid_fill_evaluate_canonical_target_function(render_data, canonical_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), emission_2, light_source_normal_2, point_on_light_2, random_number_generator); float canonical_sample_PDF = canonical_sample_PDF_unnormalized / RIS_integral; float mis_weight = pairwise.get_canonical_MIS_weight_2(canonical_technique_1_canonical_reservoir_2_pdf, canonical_technique_2_canonical_reservoir_2_pdf, canonical_technique_3_canonical_reservoir_2_pdf, mis_weight_normalization); ReGIRReservoir canonical_technique_2_reservoir; canonical_technique_2_reservoir.sample.emissive_triangle_index = triangle_index_2; canonical_technique_2_reservoir.sample.point_on_light = point_on_light_2; canonical_technique_2_reservoir.UCW = UCW_2; if (out_reservoir.stream_reservoir(mis_weight, target_function, canonical_technique_2_reservoir, random_number_generator)) { selected_point_on_light = point_on_light_2; selected_light_source_normal = light_source_normal_2; selected_light_source_area = light_source_area; selected_emission = emission_2; #if ReGIR_ShadingResamplingShadeAllSamples == KERNEL_OPTION_FALSE out_infos.sample_radiance = sample_radiance; #endif } #if ReGIR_ShadingResamplingShadeAllSamples == KERNEL_OPTION_TRUE out_infos.sample_radiance += sample_radiance * canonical_technique_2_reservoir.UCW * mis_weight; #endif } } } #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE if (canonical_technique_3_canonical_reservoir_3_pdf > 0.0f) { float mis_weight = pairwise.get_canonical_MIS_weight_3(canonical_technique_1_canonical_reservoir_3_pdf, canonical_technique_2_canonical_reservoir_3_pdf, canonical_technique_3_canonical_reservoir_3_pdf, mis_weight_normalization); ColorRGB32F sample_radiance; float target_function = ReGIR_shading_evaluate_target_function(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, point_on_light_3, light_source_normal_3, emission_3, random_number_generator, sample_radiance, canonical_technique_3_sample_ili); if (out_reservoir.stream_sample_raw(mis_weight, target_function, canonical_technique_3_canonical_reservoir_3_pdf, triangle_index_3, point_on_light_3, random_number_generator)) { selected_point_on_light = point_on_light_3; selected_light_source_normal = light_source_normal_3; selected_light_source_area = hippt::length(get_triangle_normal_not_normalized(render_data, triangle_index_3)) * 0.5f; selected_emission = emission_3; selected_incident_light_info = canonical_technique_3_sample_ili; #if ReGIR_ShadingResamplingShadeAllSamples == KERNEL_OPTION_FALSE out_infos.sample_radiance = sample_radiance; #endif } #if ReGIR_ShadingResamplingShadeAllSamples == KERNEL_OPTION_TRUE out_infos.sample_radiance += sample_radiance / canonical_technique_3_canonical_reservoir_3_pdf * mis_weight; #endif } #endif if (out_reservoir.weight_sum == 0.0f || out_need_fallback_sampling) return LightSampleInformation(); out_reservoir.finalize_resampling(1.0f, 1.0f); } #else { int selected_neighbor = -1; for (int neighbor = 0; neighbor < render_data.render_settings.regir_settings.shading.number_of_neighbors; neighbor++) { unsigned int neighbor_grid_cell_index = render_data.render_settings.regir_settings.find_valid_jittered_neighbor_cell_index( shading_point, geometric_normal, render_data.current_camera, ray_payload.material.roughness, regir_settings.compute_is_primary_hit(ray_payload), render_data.render_settings.regir_settings.shading.get_do_cell_jittering(regir_settings.compute_is_primary_hit(ray_payload)), render_data.render_settings.regir_settings.shading.jittering_radius, neighbor_rng); if (neighbor_grid_cell_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) // Couldn't find a valid neighbor continue; else out_need_fallback_sampling = false; for (int i = 0; i < render_data.render_settings.regir_settings.shading.reservoir_tap_count_per_neighbor; i++) { // Will be set to true if the jittering causes the current shading point to be jittered out of the scene ReGIRReservoir non_canonical_reservoir = render_data.render_settings.regir_settings.get_random_reservoir_in_grid_cell_for_shading(neighbor_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), neighbor_rng); if (non_canonical_reservoir.UCW <= 0.0f) // No valid sample in that reservoir continue; float3 point_on_light = non_canonical_reservoir.sample.point_on_light; float3 light_source_normal = get_triangle_normal_not_normalized(render_data, non_canonical_reservoir.sample.emissive_triangle_index); float light_source_area = hippt::length(light_source_normal) * 0.5f; light_source_normal /= light_source_area * 2.0f; ColorRGB32F emission = get_emission_of_triangle_from_index(render_data, non_canonical_reservoir.sample.emissive_triangle_index); float target_function = ReGIR_shading_evaluate_target_function< ReGIR_ShadingResamplingTargetFunctionVisibility, ReGIR_ShadingResamplingTargetFunctionNeePlusPlusVisibility>(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, point_on_light, light_source_normal, emission, random_number_generator); float mis_weight = 1.0f; if (out_reservoir.stream_reservoir(mis_weight, target_function, non_canonical_reservoir, random_number_generator)) { selected_neighbor = neighbor; selected_point_on_light = point_on_light; selected_light_source_normal = light_source_normal; selected_light_source_area = light_source_area; selected_emission = emission; } } } out_need_fallback_sampling = false; float bsdf_sample_pdf; float3 sampled_bsdf_direction; BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE BSDFContext bsdf_context(view_direction, shading_normal, geometric_normal, make_float3(0.0f, 0.0f, 0.0f), incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); ColorRGB32F bsdf_color = bsdf_dispatcher_sample(render_data, bsdf_context, sampled_bsdf_direction, bsdf_sample_pdf, random_number_generator); bool intersection_found = false; BSDFLightSampleRayHitInfo shadow_light_ray_hit_info; if (bsdf_sample_pdf > 0.0f) { hiprtRay new_ray; new_ray.origin = shading_point; new_ray.direction = sampled_bsdf_direction; intersection_found = evaluate_bsdf_light_sample_ray_simplified(render_data, new_ray, 1.0e35f, shadow_light_ray_hit_info, last_hit_primitive_index, ray_payload.bounce, random_number_generator); // Checking that we did hit something and if we hit something, // it needs to be emissive if (intersection_found && !shadow_light_ray_hit_info.hit_emission.is_black()) { LightSampleInformation light_sample; light_sample.emission = shadow_light_ray_hit_info.hit_emission; light_sample.emissive_triangle_index = shadow_light_ray_hit_info.hit_prim_index; light_sample.light_area = triangle_area(render_data, shadow_light_ray_hit_info.hit_prim_index); light_sample.light_source_normal = shadow_light_ray_hit_info.hit_geometric_normal; light_sample.point_on_light = shading_point + shadow_light_ray_hit_info.hit_distance * sampled_bsdf_direction; float mis_weight = 1.0f; float target_function = ReGIR_shading_evaluate_target_function< ReGIR_ShadingResamplingTargetFunctionVisibility, ReGIR_ShadingResamplingTargetFunctionNeePlusPlusVisibility>(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, light_sample.point_on_light, light_sample.light_source_normal, light_sample.emission, random_number_generator, incident_light_info); float area_measure_bsdf_pdf = solid_angle_to_area_pdf(bsdf_sample_pdf, shadow_light_ray_hit_info.hit_distance, compute_cosine_term_at_light_source(shadow_light_ray_hit_info.hit_geometric_normal, -sampled_bsdf_direction)); if (out_reservoir.stream_sample(mis_weight, target_function, area_measure_bsdf_pdf, light_sample, random_number_generator)) { selected_neighbor = render_data.render_settings.regir_settings.shading.number_of_neighbors; selected_point_on_light = light_sample.point_on_light; selected_light_source_normal = shadow_light_ray_hit_info.hit_geometric_normal; selected_light_source_area = light_sample.light_area; selected_emission = shadow_light_ray_hit_info.hit_emission; selected_incident_light_info = incident_light_info; } } } #endif bool need_canonical = (ReGIR_GridFillTargetFunctionVisibility || ReGIR_GridFillTargetFunctionCosineTerm || ReGIR_GridFillTargetFunctionCosineTermLightSource); if (need_canonical) { // Will be set to true if the jittering causes the current shading point to be jittered out of the scene unsigned int neighbor_grid_cell_index = render_data.render_settings.regir_settings.find_valid_jittered_neighbor_cell_index( shading_point, geometric_normal, render_data.current_camera, ray_payload.material.roughness, regir_settings.compute_is_primary_hit(ray_payload), false, // render_data.render_settings.regir_settings.shading.do_cell_jittering, render_data.render_settings.regir_settings.shading.jittering_radius, neighbor_rng); // Fetching the center cell should never fail because the center cell always exists but it may actually fail in case of collisions // that cannot be resolved if (neighbor_grid_cell_index != HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { // We found at least one good sample so we're not going to need a fallback on another light sampling strategy than ReGIR out_need_fallback_sampling = false; ReGIRReservoir canonical_reservoir = render_data.render_settings.regir_settings.get_random_reservoir_in_grid_cell_for_shading(neighbor_grid_cell_index, regir_settings.compute_is_primary_hit(ray_payload), neighbor_rng); if (canonical_reservoir.UCW > 0.0f && canonical_reservoir.UCW != ReGIRReservoir::UNDEFINED_UCW) { ColorRGB32F emission = get_emission_of_triangle_from_index(render_data, canonical_reservoir.sample.emissive_triangle_index); float3 point_on_light = canonical_reservoir.sample.point_on_light; float3 light_source_normal = get_triangle_normal_not_normalized(render_data, canonical_reservoir.sample.emissive_triangle_index); float light_source_area = hippt::length(light_source_normal) * 0.5f; light_source_normal /= light_source_area * 2.0f; { // Adding visibility in the canonical sample target function's if we have visibility in the grid fill target function // or if we wwant visibility in the target function during shading resampling // or if we're shading all candidates because then we want the target function to produce // the radiance towards the shading point directly which means that we need the visibility in the target function float target_function = ReGIR_shading_evaluate_target_function(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, point_on_light, light_source_normal, emission, random_number_generator); float mis_weight = 1.0f; if (out_reservoir.stream_reservoir(mis_weight, target_function, canonical_reservoir, random_number_generator)) { selected_neighbor = render_data.render_settings.regir_settings.shading.number_of_neighbors + ReGIR_ShadingResamplingDoBSDFMIS; selected_point_on_light = point_on_light; selected_light_source_normal = light_source_normal; selected_light_source_area = light_source_area; selected_emission = emission; } } } } } if (out_reservoir.weight_sum == 0.0f || out_need_fallback_sampling) return LightSampleInformation(); neighbor_rng.m_state.seed = neighbor_rng_seed; float normalization_denominator = 0.0f; float normalization_numerator = 0.0f; for (int i = 0; i < render_data.render_settings.regir_settings.shading.number_of_neighbors + need_canonical + ReGIR_ShadingResamplingDoBSDFMIS; i++) { bool is_bsdf_sample = (i == render_data.render_settings.regir_settings.shading.number_of_neighbors && ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE); bool is_canonical = i == render_data.render_settings.regir_settings.shading.number_of_neighbors + ReGIR_ShadingResamplingDoBSDFMIS && need_canonical; unsigned int neighbor_cell_index; if (is_bsdf_sample) { neighbor_cell_index = render_data.render_settings.regir_settings.get_neighbor_replay_hash_grid_cell_index_for_shading( shading_point, geometric_normal, render_data.current_camera, ray_payload.material.roughness, regir_settings.compute_is_primary_hit(ray_payload), false, false, render_data.render_settings.regir_settings.shading.jittering_radius, neighbor_rng); } else { neighbor_cell_index = render_data.render_settings.regir_settings.get_neighbor_replay_hash_grid_cell_index_for_shading( shading_point, geometric_normal, render_data.current_camera, ray_payload.material.roughness, regir_settings.compute_is_primary_hit(ray_payload), is_canonical, render_data.render_settings.regir_settings.shading.get_do_cell_jittering(regir_settings.compute_is_primary_hit(ray_payload)), render_data.render_settings.regir_settings.shading.jittering_radius, neighbor_rng); } if (neighbor_cell_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) // Outside of the alive grid // // Note that this also applies for the canonical sample because canonical samples are gathered // from neighbors. But if the neighbor is outside of the grid (or in a non-alive grid cell), then // we have no canonical neighbor to count in the MIS weights continue; if (selected_neighbor == i) { if (is_canonical) { float RIS_integral = render_data.render_settings.regir_settings.get_canonical_pre_integration_factor(neighbor_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); if (RIS_integral == 0.0f) RIS_integral = 1.0f; if (!render_data.render_settings.regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) RIS_integral = 1.0f; float light_sample_PDF_unnormalized = ReGIR_grid_fill_evaluate_canonical_target_function(render_data, neighbor_cell_index, regir_settings.compute_is_primary_hit(ray_payload), selected_emission, selected_light_source_normal, selected_point_on_light, random_number_generator); float light_sample_PDF = light_sample_PDF_unnormalized / RIS_integral; normalization_numerator = light_sample_PDF; normalization_denominator += light_sample_PDF; } else if (is_bsdf_sample) { float bsdf_pdf; BSDFContext bsdf_context(view_direction, shading_normal, geometric_normal, hippt::normalize(out_reservoir.sample.point_on_light - shading_point), incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); bsdf_pdf = solid_angle_to_area_pdf(bsdf_pdf, hippt::length(out_reservoir.sample.point_on_light - shading_point), compute_cosine_term_at_light_source(selected_light_source_normal, -hippt::normalize(out_reservoir.sample.point_on_light - shading_point))); normalization_numerator = bsdf_pdf; normalization_denominator += bsdf_pdf; } else { // Non-canonical sample float RIS_integral = render_data.render_settings.regir_settings.get_non_canonical_pre_integration_factor(neighbor_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); if (RIS_integral == 0.0f) RIS_integral = 1.0f; if (!render_data.render_settings.regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) RIS_integral = 1.0f; float light_sample_PDF_unnormalized = ReGIR_grid_fill_evaluate_non_canonical_target_function(render_data, neighbor_cell_index, regir_settings.compute_is_primary_hit(ray_payload), selected_emission, selected_light_source_normal, selected_point_on_light, random_number_generator); float light_sample_PDF = light_sample_PDF_unnormalized / RIS_integral; normalization_numerator = light_sample_PDF; normalization_denominator += light_sample_PDF * render_data.render_settings.regir_settings.shading.reservoir_tap_count_per_neighbor; } continue; } else { if (is_canonical) { float RIS_integral = render_data.render_settings.regir_settings.get_canonical_pre_integration_factor(neighbor_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); if (RIS_integral == 0.0f) RIS_integral = 1.0f; if (!render_data.render_settings.regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) RIS_integral = 1.0f; float light_sample_PDF_unnormalized = ReGIR_grid_fill_evaluate_canonical_target_function(render_data, neighbor_cell_index, regir_settings.compute_is_primary_hit(ray_payload), selected_emission, selected_light_source_normal, selected_point_on_light, random_number_generator); float light_sample_PDF = light_sample_PDF_unnormalized / RIS_integral; normalization_denominator += light_sample_PDF; } else if (is_bsdf_sample) { float bsdf_pdf; BSDFContext bsdf_context(view_direction, shading_normal, geometric_normal, hippt::normalize(out_reservoir.sample.point_on_light - shading_point), incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); bsdf_pdf = solid_angle_to_area_pdf(bsdf_pdf, hippt::length(out_reservoir.sample.point_on_light - shading_point), compute_cosine_term_at_light_source(selected_light_source_normal, -hippt::normalize(out_reservoir.sample.point_on_light - shading_point))); normalization_denominator += bsdf_pdf; } else { // Non-canonical sample float RIS_integral = render_data.render_settings.regir_settings.get_non_canonical_pre_integration_factor(neighbor_cell_index, regir_settings.compute_is_primary_hit(ray_payload)); if (RIS_integral == 0.0f) RIS_integral = 1.0f; if (!render_data.render_settings.regir_settings.DEBUG_DO_RIS_INTEGRAL_NORMALIZATION) RIS_integral = 1.0f; float light_sample_PDF_unnormalized = ReGIR_grid_fill_evaluate_non_canonical_target_function(render_data, neighbor_cell_index, regir_settings.compute_is_primary_hit(ray_payload), selected_emission, selected_light_source_normal, selected_point_on_light, random_number_generator); float light_sample_PDF = light_sample_PDF_unnormalized / RIS_integral; normalization_denominator += light_sample_PDF * render_data.render_settings.regir_settings.shading.reservoir_tap_count_per_neighbor; } } } out_reservoir.finalize_resampling(normalization_numerator, normalization_denominator); } #endif LightSampleInformation out_sample; // The UCW is the inverse of the PDF but we expect the PDF to be in 'area_measure_pdf', not the inverse PDF (UCW), so we invert it out_sample.area_measure_pdf = 1.0f / out_reservoir.UCW; out_sample.emissive_triangle_index = out_reservoir.sample.emissive_triangle_index; out_sample.emission = selected_emission; out_sample.light_area = selected_light_source_area; out_sample.light_source_normal = selected_light_source_normal; out_sample.point_on_light = selected_point_on_light; out_sample.incident_light_info = selected_incident_light_info; return out_sample; } HIPRT_DEVICE HIPRT_INLINE LightSampleInformation sample_one_emissive_triangle_regir( const HIPRTRenderData& render_data, const float3& shading_point, const float3& view_direction, const float3& shading_normal, const float3& geometric_normal, int last_hit_primitive_index, RayPayload& ray_payload, bool& out_need_fallback_sampling, Xorshift32Generator& random_number_generator) { ReGIRShadingAdditionalInfo trash_info; return sample_one_emissive_triangle_regir_with_info(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, out_need_fallback_sampling, random_number_generator, trash_info); } template HIPRT_DEVICE HIPRT_INLINE LightSampleInformation sample_one_emissive_triangle(const HIPRTRenderData& render_data, const float3& shading_point, const float3& view_direction, const float3& shading_normal, const float3& geometric_normal, int last_hit_primitive_index, RayPayload& ray_payload, Xorshift32Generator& random_number_generator) { if constexpr (samplingStrategy == LSS_BASE_UNIFORM) { return sample_one_emissive_triangle_uniform(render_data, random_number_generator); } else if constexpr (samplingStrategy == LSS_BASE_POWER) { return sample_one_emissive_triangle_power(render_data, random_number_generator); } else if constexpr (samplingStrategy == LSS_BASE_REGIR) { bool point_outside_grid = false; LightSampleInformation light_sample = sample_one_emissive_triangle_regir(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, point_outside_grid, random_number_generator); if (!point_outside_grid) return light_sample; else { #if ReGIR_FallbackLightSamplingStrategy == LSS_BASE_REGIR // Invalid fallback strategy invalid ReGIR light sampling fallback strategy #endif // Fallback method as the point was outside of the ReGIR grid return sample_one_emissive_triangle(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, random_number_generator); } } } /** * Overload of the function used when sampling lights without a world shading point (as in ReSTIR DI light presampling for example) * * This means that positional light sampling schemes such as ReGIR or light trees cannot be used as the template argument here * and will produced incorrect results if used anyways */ template HIPRT_DEVICE HIPRT_INLINE LightSampleInformation sample_one_emissive_triangle(const HIPRTRenderData& render_data, Xorshift32Generator& random_number_generator) { RayPayload dummy_ray_payload; return sample_one_emissive_triangle(render_data, make_float3(0.0f, 0.0f, 0.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(0.0f, 0.0f, 0.0f), -1, dummy_ray_payload, random_number_generator); } /** * 'clamp_condition' is an additional condition that needs to be met * for clamping to occur. If the additional condition is not met (the boolean * 'clamp_condition' is false, then the 'light_contribution' parameter is returned * untouched */ HIPRT_DEVICE HIPRT_INLINE ColorRGB32F clamp_light_contribution(ColorRGB32F light_contribution, float clamp_max_value, bool clamp_condition) { if (!light_contribution.has_nan() && clamp_max_value > 0.0f && clamp_condition) // We don't want to clamp NaNs because that's UB (kind of) and the NaNs get // immediately clamped to 'clamp_max_value' in my experience // // Not clamping the negatives to 0 because // spectral rendering (for dispersion for example) may produce negative values // and we don't want to clamp those to 0 light_contribution.clamp(-clamp_max_value, clamp_max_value); return light_contribution; } /** * Returns true if the given contribution satisfies the minimum light contribution * required for a light to be */ HIPRT_DEVICE HIPRT_INLINE bool check_minimum_light_contribution(float minimum_contribution, const ColorRGB32F& contribution) { if (minimum_contribution > 0.0f) { if (contribution.r < minimum_contribution && contribution.g < minimum_contribution && contribution.b < minimum_contribution) // The light doesn't contribute enough return false; else // The light contributes enough return true; } else // Minimum light contribution threshold disabled return true; } HIPRT_DEVICE HIPRT_INLINE bool check_minimum_light_contribution(float minimum_contribution, float contribution) { if (minimum_contribution > 0.0f) { if (contribution < minimum_contribution) // The light doesn't contribute enough return false; else // The light contributes enough return true; } else // Minimum light contribution threshold disabled return true; } #endif ================================================ FILE: src/Device/includes/LightSampling/Lights.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_LIGHTS_H #define DEVICE_LIGHTS_H #include "Device/includes/BSDFs/MicrofacetRegularization.h" #include "Device/includes/Dispatcher.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/ReSTIR/DI/Reservoir.h" #include "Device/includes/ReSTIR/DI/FinalShading.h" #include "Device/includes/ReSTIR/ReGIR/FinalShading.h" #include "Device/includes/RIS/RIS.h" #include "Device/includes/Sampling.h" #include "Device/includes/SanityCheck.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/KernelOptions/KernelOptions.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/Xorshift.h" HIPRT_DEVICE HIPRT_INLINE ColorRGB32F sample_one_light_no_MIS(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { if (!MaterialUtils::can_do_light_sampling(ray_payload.material)) return ColorRGB32F(0.0f); LightSampleInformation light_sample = sample_one_emissive_triangle(render_data, closest_hit_info.inter_point, view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, closest_hit_info.primitive_index, ray_payload, random_number_generator); if (light_sample.area_measure_pdf <= 0.0f) // Can happen for very small triangles return ColorRGB32F(0.0f); float3 shadow_ray_origin = closest_hit_info.inter_point; float3 shadow_ray_direction = light_sample.point_on_light - shadow_ray_origin; float distance_to_light = hippt::length(shadow_ray_direction); float3 shadow_ray_direction_normalized = shadow_ray_direction / distance_to_light; hiprtRay shadow_ray; shadow_ray.origin = shadow_ray_origin; shadow_ray.direction = shadow_ray_direction_normalized; ColorRGB32F light_source_radiance; // abs() here to allow backfacing light sources float dot_light_source = compute_cosine_term_at_light_source(light_sample.light_source_normal, -shadow_ray.direction); if (dot_light_source > 0.0f) { NEEPlusPlusContext nee_plus_plus_context; nee_plus_plus_context.point_on_light = light_sample.point_on_light; nee_plus_plus_context.shaded_point = shadow_ray_origin; bool in_shadow = evaluate_shadow_ray_nee_plus_plus(render_data, shadow_ray, distance_to_light, closest_hit_info.primitive_index, nee_plus_plus_context, random_number_generator, ray_payload.bounce); if (!in_shadow) { float bsdf_pdf; BSDFIncidentLightInfo incident_light_info = light_sample.incident_light_info; #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE && DirectLightSamplingBaseStrategy == LSS_BASE_REGIR BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, shadow_ray.direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); #else BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, shadow_ray.direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC); #endif ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); if (bsdf_pdf != 0.0f) { // Conversion to solid angle from surface area measure float light_sample_solid_angle_pdf = area_to_solid_angle_pdf(light_sample.area_measure_pdf, distance_to_light, dot_light_source); if (light_sample_solid_angle_pdf > 0.0f) { float cosine_term = hippt::abs(hippt::dot(closest_hit_info.shading_normal, shadow_ray.direction)); light_source_radiance = light_sample.emission * cosine_term * bsdf_color / light_sample_solid_angle_pdf / nee_plus_plus_context.unoccluded_probability; // Just a CPU-only sanity check sanity_check(render_data, light_source_radiance, 0, 0); } } } } return light_source_radiance; } HIPRT_DEVICE HIPRT_INLINE ColorRGB32F sample_one_light_bsdf(const HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { float bsdf_sample_pdf; float3 sampled_bsdf_direction; BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, make_float3(0.0f, 0.0f, 0.0f), incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC); ColorRGB32F bsdf_color = bsdf_dispatcher_sample(render_data, bsdf_context, sampled_bsdf_direction, bsdf_sample_pdf, random_number_generator); ColorRGB32F bsdf_radiance = ColorRGB32F(0.0f); if (bsdf_sample_pdf > 0.0f) { hiprtRay new_ray; new_ray.origin = closest_hit_info.inter_point; new_ray.direction = sampled_bsdf_direction; BSDFLightSampleRayHitInfo shadow_light_ray_hit_info; bool intersection_found = evaluate_bsdf_light_sample_ray(render_data, new_ray, 1.0e35f, shadow_light_ray_hit_info, closest_hit_info.primitive_index, ray_payload.bounce, random_number_generator); // Checking that we did hit something and if we hit something, // it needs to be emissive if (intersection_found && !shadow_light_ray_hit_info.hit_emission.is_black() && compute_cosine_term_at_light_source(shadow_light_ray_hit_info.hit_geometric_normal, -sampled_bsdf_direction) > 0.0f) { float cosine_term = hippt::abs(hippt::dot(closest_hit_info.shading_normal, sampled_bsdf_direction)); bsdf_radiance = bsdf_color * cosine_term * shadow_light_ray_hit_info.hit_emission / bsdf_sample_pdf; // Just a CPU-only sanity check sanity_check(render_data, bsdf_radiance, 0, 0); } } return bsdf_radiance; } HIPRT_DEVICE HIPRT_INLINE ColorRGB32F sample_one_light_MIS(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { ColorRGB32F light_source_radiance_mis; if (MaterialUtils::can_do_light_sampling(ray_payload.material)) { LightSampleInformation light_sample = sample_one_emissive_triangle(render_data, closest_hit_info.inter_point, view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, closest_hit_info.primitive_index, ray_payload, random_number_generator); // Can happen for very small triangles that the PDF of the sampled triangle couldn't be computed if (light_sample.area_measure_pdf > 0.0f) { float3 shadow_ray_direction = light_sample.point_on_light - closest_hit_info.inter_point; float distance_to_light = hippt::length(shadow_ray_direction); float3 shadow_ray_direction_normalized = shadow_ray_direction / distance_to_light; hiprtRay shadow_ray; shadow_ray.origin = closest_hit_info.inter_point; shadow_ray.direction = shadow_ray_direction_normalized; NEEPlusPlusContext nee_plus_plus_context; nee_plus_plus_context.point_on_light = light_sample.point_on_light; nee_plus_plus_context.shaded_point = shadow_ray.origin; bool in_shadow = evaluate_shadow_ray_nee_plus_plus(render_data, shadow_ray, distance_to_light, closest_hit_info.primitive_index, nee_plus_plus_context, random_number_generator, ray_payload.bounce); if (!in_shadow) { float bsdf_pdf; BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, shadow_ray.direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); if (bsdf_pdf > 0.0f) { float cos_theta_at_light_source = compute_cosine_term_at_light_source(light_sample.light_source_normal, -shadow_ray.direction); // Preventing division by 0 in the conversion to solid angle here if (cos_theta_at_light_source > 1.0e-5f) { float light_sample_solid_angle_pdf = area_to_solid_angle_pdf(light_sample.area_measure_pdf, distance_to_light, cos_theta_at_light_source); float mis_weight = balance_heuristic(light_sample_solid_angle_pdf, bsdf_pdf); float cosine_term = hippt::abs(hippt::dot(closest_hit_info.shading_normal, shadow_ray.direction)); light_source_radiance_mis = bsdf_color * cosine_term * light_sample.emission * mis_weight / light_sample_solid_angle_pdf / nee_plus_plus_context.unoccluded_probability; // Just a CPU-only sanity check sanity_check(render_data, light_source_radiance_mis, 0, 0); } } } } } float bsdf_sample_pdf; float3 sampled_bsdf_direction; float3 bsdf_shadow_ray_origin = closest_hit_info.inter_point; BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; ColorRGB32F bsdf_radiance_mis; unsigned int previous_seed = random_number_generator.m_state.seed; random_number_generator.m_state.seed = previous_seed; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, make_float3(0.0f, 0.0f, 0.0f), incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); ColorRGB32F bsdf_color = bsdf_dispatcher_sample(render_data, bsdf_context, sampled_bsdf_direction, bsdf_sample_pdf, random_number_generator); if (bsdf_sample_pdf > 0.0f) { hiprtRay new_ray; new_ray.origin = bsdf_shadow_ray_origin; new_ray.direction = sampled_bsdf_direction; BSDFLightSampleRayHitInfo shadow_light_ray_hit_info; bool intersection_found = evaluate_bsdf_light_sample_ray(render_data, new_ray, 1.0e35f, shadow_light_ray_hit_info, closest_hit_info.primitive_index, ray_payload.bounce, random_number_generator); // Checking that we did hit something and if we hit something, // it needs to be emissive // // We're also checking if the light is backfacing maybe with compute_cosine_term() if (intersection_found && !shadow_light_ray_hit_info.hit_emission.is_black() && compute_cosine_term_at_light_source(shadow_light_ray_hit_info.hit_geometric_normal, -sampled_bsdf_direction) > 0.0f) { float light_pdf_solid_angle = pdf_of_emissive_triangle_hit_solid_angle(render_data, shadow_light_ray_hit_info, sampled_bsdf_direction); float mis_weight = balance_heuristic(bsdf_sample_pdf, light_pdf_solid_angle); // Using abs here because we want the dot product to be positive. // You may be thinking that if we're doing this, then we're not going to discard BSDF // sampled direction that are below the surface (whereas we should discard them). // That would be correct but bsdf_dispatcher_sample return a PDF == 0.0f if a bad // direction was sampled and if the PDF is 0.0f, we never get to this line of code // you're reading. If we are here, this is because we sampled a direction that is // correct for the BSDF. Even if the direction is correct, the dot product may be // negative in the case of refractions / total internal reflections and so in this case, // we'll need to negative the dot product for it to be positive float cosine_term = hippt::abs(hippt::dot(closest_hit_info.shading_normal, sampled_bsdf_direction)); bsdf_radiance_mis = bsdf_color * cosine_term * shadow_light_ray_hit_info.hit_emission * mis_weight / bsdf_sample_pdf; // Just a CPU-only sanity check sanity_check(render_data, bsdf_radiance_mis, 0, 0); } } return light_source_radiance_mis + bsdf_radiance_mis; } HIPRT_DEVICE HIPRT_INLINE ColorRGB32F sample_multiple_emissive_geometry(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { ColorRGB32F direct_light_contribution; // Any of these light sampling strategy support sampling multiple lights // per each shading point, effectively "amortizing" camera and bounce rays for (int i = 0; i < DirectLightSamplingNEESampleCount; i++) { #if DirectLightSamplingBaseStrategy == LSS_BASE_REGIR && DirectLightSamplingStrategy != LSS_BSDF // ReGIR has its own special path to optimize things a bit. // // Also, BSDF sampling only can be handled by the usual path because then // ReGIR isn't used direct_light_contribution += sample_one_light_ReGIR(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #else // Not ReGIR #if DirectLightSamplingStrategy == LSS_ONE_LIGHT direct_light_contribution += sample_one_light_no_MIS(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #elif DirectLightSamplingStrategy == LSS_BSDF direct_light_contribution += sample_one_light_bsdf(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #elif DirectLightSamplingStrategy == LSS_MIS_LIGHT_BSDF direct_light_contribution += sample_one_light_MIS(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #elif DirectLightSamplingStrategy == LSS_RIS_BSDF_AND_LIGHT direct_light_contribution += sample_lights_RIS(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #endif #endif // #if ReGIR } return direct_light_contribution / DirectLightSamplingNEESampleCount; } HIPRT_DEVICE HIPRT_INLINE ColorRGB32F sample_one_light_ReSTIR_DI(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator, int2 pixel_coords) { // ReSTIR DI doesn't support explicitely looping to sample // multiple lights per shading point so that's why we don't // have a loop for it ColorRGB32F direct_light_contribution; if (ray_payload.bounce == 0) // Can only do ReSTIR DI on the first bounce direct_light_contribution = sample_light_ReSTIR_DI(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator, pixel_coords); else { // ReSTIR DI isn't used for the secondary/tertiary/... bounces // so there we can take multiple light samples per path vertex for (int i = 0; i < DirectLightSamplingNEESampleCount; i++) { #if ReSTIR_DI_LaterBouncesSamplingStrategy == RESTIR_DI_LATER_BOUNCES_UNIFORM_ONE_LIGHT direct_light_contribution += sample_one_light_no_MIS(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #elif ReSTIR_DI_LaterBouncesSamplingStrategy == RESTIR_DI_LATER_BOUNCES_BSDF direct_light_contribution += sample_one_light_bsdf(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #elif ReSTIR_DI_LaterBouncesSamplingStrategy == RESTIR_DI_LATER_BOUNCES_MIS_LIGHT_BSDF direct_light_contribution += sample_one_light_MIS(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #elif ReSTIR_DI_LaterBouncesSamplingStrategy == RESTIR_DI_LATER_BOUNCES_RIS_BSDF_AND_LIGHT direct_light_contribution += sample_lights_RIS(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #endif } direct_light_contribution /= DirectLightSamplingNEESampleCount; } return direct_light_contribution; } /** * Importance sample lights in the scene with NEE * * Just a random note for myself and maybe future readers * that are wondering the same: * * In the case where we shot a ray (camera ray or indirect bounce ray, doesn't matter) * and we hit an emissive material, we should still estimate NEE at that point. i.e. we * should also do NEE when standing on emissive materials because emissive materials can * reflect light just fine (unless they are blackbodies). * * Consider a glowing light bulb for example: this is just metal so hot that it glows * but because this is metal, it also reflects light. * * I think the better morale to remember is that the material being emissive doesn't matter at * all. As long as the material itself reflects light, then we should do NEE. */ HIPRT_DEVICE ColorRGB32F sample_emissive_geometry(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator, int2 pixel_coords) { if (render_data.buffers.emissive_triangles_count == 0 && !(render_data.world_settings.ambient_light_type == AmbientLightType::ENVMAP && DirectLightSamplingStrategy == LSS_RESTIR_DI)) // No emissive geometry in the scene to sample // And we're not sampling the envmap with ReSTIR DI which means // that we're not sampling anything so return black return ColorRGB32F(0.0f); if (render_data.bsdfs_data.white_furnace_mode && render_data.bsdfs_data.white_furnace_mode_turn_off_emissives) return ColorRGB32F(0.0f); ColorRGB32F material_self_textured_emission; if (ray_payload.material.emissive_texture_used) // If the material is using an emissive texture, we will add its emission to the NEE estimation // because we're not importance sampling emissive textures so we're doing it the brute force // way for now (there are some things about light warping I think to properly sample emissive // textures but haven't read too much of that) material_self_textured_emission = ray_payload.material.emission; ColorRGB32F direct_light_contribution; #if DirectLightSamplingStrategy == LSS_NO_DIRECT_LIGHT_SAMPLING direct_light_contribution = ColorRGB32F(0.0f); #else // A light sampling strategy is used #if DirectLightSamplingStrategy != LSS_RESTIR_DI // A light sampling strategy that is not ReSTIR DI // meaning that we can sample more than 1 light per // path vertex direct_light_contribution = sample_multiple_emissive_geometry(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); #elif DirectLightSamplingStrategy == LSS_RESTIR_DI direct_light_contribution = sample_one_light_ReSTIR_DI(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator, pixel_coords); #endif #endif return direct_light_contribution + material_self_textured_emission; } HIPRT_DEVICE ColorRGB32F clamp_direct_lighting_estimation(ColorRGB32F direct_lighting_contribution, float direct_contribution_clamp, int bounce) { return clamp_light_contribution(direct_lighting_contribution, direct_contribution_clamp, bounce > 0); } /** * The x & y parameters are only used if using ReSTIR DI (they are for fetching the ReSTIR DI reservoir). * They can be ignored if not using ReSTIR DI */ HIPRT_DEVICE ColorRGB32F estimate_direct_lighting(HIPRTRenderData& render_data, RayPayload& ray_payload, ColorRGB32F custom_ray_throughput, HitInfo& closest_hit_info, float3 view_direction, int x, int y, Xorshift32Generator& random_number_generator) { ColorRGB32F total_direct_lighting; ColorRGB32F emissive_geometry_direct_contribution = sample_emissive_geometry(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator, make_int2(x, y)); ColorRGB32F envmap_direct_contribution = sample_environment_map(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); // Clamping direct lighting emissive_geometry_direct_contribution = clamp_light_contribution(emissive_geometry_direct_contribution, render_data.render_settings.direct_contribution_clamp, ray_payload.bounce == 0); envmap_direct_contribution = clamp_light_contribution(envmap_direct_contribution, render_data.render_settings.envmap_contribution_clamp, ray_payload.bounce == 0); #if DirectLightSamplingStrategy == LSS_NO_DIRECT_LIGHT_SAMPLING // No direct light sampling ColorRGB32F hit_emission = ray_payload.material.emission; hit_emission = clamp_light_contribution(hit_emission, render_data.render_settings.indirect_contribution_clamp, ray_payload.bounce > 0); total_direct_lighting += hit_emission * custom_ray_throughput; #else if (ray_payload.bounce == 0) // If we do have emissive geometry sampling, we only want to take // it into account on the first bounce, otherwise we would be // accounting for direct light sampling twice (bounce on emissive // geometry + direct light sampling). Otherwise, we don't check for bounce == 0 total_direct_lighting += ray_payload.material.emission; // Clamped indirect lighting ColorRGB32F direct_lighting_contribution = (emissive_geometry_direct_contribution + envmap_direct_contribution) * custom_ray_throughput; total_direct_lighting += direct_lighting_contribution; #endif return total_direct_lighting; } /** * The x & y parameters are only used if using ReSTIR DI (they are for fetching the ReSTIR DI reservoir). * They can be ignored if not using ReSTIR DI */ HIPRT_DEVICE ColorRGB32F estimate_direct_lighting_no_clamping(HIPRTRenderData& render_data, RayPayload& ray_payload, ColorRGB32F custom_ray_throughput, HitInfo& closest_hit_info, float3 view_direction, int x, int y, Xorshift32Generator& random_number_generator) { return estimate_direct_lighting(render_data, ray_payload, custom_ray_throughput, closest_hit_info, view_direction, x, y, random_number_generator); } /** * The x & y parameters are only used if using ReSTIR DI (they are for fetching the ReSTIR DI reservoir). * They can be ignored if not using ReSTIR DI */ HIPRT_DEVICE ColorRGB32F estimate_direct_lighting(HIPRTRenderData& render_data, RayPayload& ray_payload, HitInfo& closest_hit_info, float3 view_direction, int x, int y, Xorshift32Generator& random_number_generator) { ColorRGB32F unclamped_direct_lighting = estimate_direct_lighting(render_data, ray_payload, ray_payload.throughput, closest_hit_info, view_direction, x, y, random_number_generator); return clamp_direct_lighting_estimation(unclamped_direct_lighting, render_data.render_settings.indirect_contribution_clamp, ray_payload.bounce); } #endif ================================================ FILE: src/Device/includes/LightSampling/PDFConversion.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_PDF_CONVERSION_H #define DEVICE_INCLUDES_PDF_CONVERSION_H #include "HostDeviceCommon/KernelOptions/DirectLightSamplingOptions.h" #include "HostDeviceCommon/Math.h" /** * Returns the cosine term of the given light source normal and the direction to the light source * 'minus_direction_to_light' must be the direction *towards* the light but *negated*, such that * dot(light_source_normal, minus_direction_to_light) > 0.0f (if the light isn't backfacing us) * * This function does the branching that allows backfacing lights or not */ HIPRT_INLINE HIPRT_DEVICE float compute_cosine_term_at_light_source(float3 light_source_normal, float3 minus_direction_to_light) { // The cosine term is the dot product between the light source normal and the direction to the shading point #if DirectLightSamplingAllowBackfacingLights == KERNEL_OPTION_TRUE // abs() to allow backfacing lights return hippt::abs(hippt::dot(light_source_normal, minus_direction_to_light)); #else // clamping to 0 to disallow backfacing lights return hippt::max(0.0f, hippt::dot(light_source_normal, minus_direction_to_light)); #endif } HIPRT_INLINE HIPRT_HOST_DEVICE float area_to_solid_angle_pdf(float area_pdf, float distance, float cos_theta_at_light_source) { if (cos_theta_at_light_source < 1.0e-8f) return 0.0f; return area_pdf * hippt::square(distance) / cos_theta_at_light_source; } HIPRT_INLINE HIPRT_HOST_DEVICE float solid_angle_to_area_pdf(float solid_angle_pdf, float distance, float cos_theta_at_light_source) { if (cos_theta_at_light_source < 1.0e-8f) return 0.0f; return solid_angle_pdf / hippt::square(distance) * cos_theta_at_light_source; } #endif ================================================ FILE: src/Device/includes/Material.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_MATERIAL_H #define DEVICE_MATERIAL_H #include "Device/includes/Texture.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/Material/MaterialUtils.h" #include "HostDeviceCommon/RenderData.h" #ifndef __KERNELCC__ #include "Image/Image.h" #endif template HIPRT_DEVICE HIPRT_INLINE T get_material_property(const HIPRTRenderData& render_data, bool is_srgb, const float2& texcoords, int texture_index); HIPRT_DEVICE HIPRT_INLINE float2 get_metallic_roughness(const HIPRTRenderData& render_data, const float2& texcoords, int metallic_texture_index, int roughness_texture_index, int metallic_roughness_texture_index); HIPRT_DEVICE HIPRT_INLINE ColorRGB32F get_base_color(const HIPRTRenderData& render_data, float& out_alpha, const float2& texcoords, int base_color_texture_index); HIPRT_DEVICE HIPRT_INLINE float get_hit_base_color_alpha(const HIPRTRenderData& render_data, unsigned short int base_color_texture_index, int prim_id, float2 uv) { if (base_color_texture_index == MaterialConstants::NO_TEXTURE) // Quick exit if no texture return 1.0f; float2 texcoords = uv_interpolate(render_data.buffers.triangles_indices, prim_id, render_data.buffers.texcoords, uv); // Getting the alpha for transparency check to see if we need to pass the ray through or not float alpha; get_base_color(render_data, alpha, texcoords, base_color_texture_index); return alpha; } HIPRT_DEVICE HIPRT_INLINE float get_hit_base_color_alpha(const HIPRTRenderData& render_data, const DevicePackedTexturedMaterial& material, hiprtHit hit) { return get_hit_base_color_alpha(render_data, material.get_base_color_texture_index(), hit.primID, hit.uv); } HIPRT_DEVICE HIPRT_INLINE float get_hit_base_color_alpha(const HIPRTRenderData& render_data, int prim_id, float2 uv) { int material_index = render_data.buffers.material_indices[prim_id]; unsigned short int base_color_texture_index = render_data.buffers.materials_buffer.get_base_color_texture_index(material_index); return get_hit_base_color_alpha(render_data, base_color_texture_index, prim_id, uv); } HIPRT_DEVICE HIPRT_INLINE float get_hit_base_color_alpha(const HIPRTRenderData& render_data, hiprtHit hit) { int material_index = render_data.buffers.material_indices[hit.primID]; unsigned short int base_color_texture_index = render_data.buffers.materials_buffer.get_base_color_texture_index(material_index); return get_hit_base_color_alpha(render_data, base_color_texture_index, hit.primID, hit.uv); } HIPRT_DEVICE HIPRT_INLINE DeviceUnpackedEffectiveMaterial get_intersection_material(const HIPRTRenderData& render_data, int material_index, float2 texcoords) { DeviceUnpackedTexturedMaterial material = render_data.buffers.materials_buffer.read_partial_material(material_index).unpack(); float trash_alpha; if (render_data.bsdfs_data.white_furnace_mode) material.base_color = ColorRGB32F(1.0f); else { if (material.base_color_texture_index != MaterialConstants::NO_TEXTURE) material.base_color = get_base_color(render_data, trash_alpha, texcoords, material.base_color_texture_index); } // Reading some parameters from the textures float2 roughness_metallic = get_metallic_roughness(render_data, texcoords, material.metallic_texture_index, material.roughness_texture_index, material.roughness_metallic_texture_index); if (material.roughness_metallic_texture_index != MaterialConstants::NO_TEXTURE) { material.roughness = roughness_metallic.x; material.metallic = roughness_metallic.y; } else { if (material.roughness_texture_index != MaterialConstants::NO_TEXTURE) material.roughness = roughness_metallic.x; if (material.metallic_texture_index != MaterialConstants::NO_TEXTURE) material.metallic = roughness_metallic.y; // If not reading from a texture, setting the roughness into the roughness_metallic // variable because the roughness is going to be used later roughness_metallic.x = material.roughness; } float anisotropy = get_material_property(render_data, false, texcoords, material.anisotropic_texture_index); if (material.anisotropic_texture_index != MaterialConstants::NO_TEXTURE) material.anisotropy = anisotropy; float specular = get_material_property(render_data, false, texcoords, material.specular_texture_index); if (material.specular_texture_index != MaterialConstants::NO_TEXTURE) material.specular = specular; float coat = get_material_property(render_data, false, texcoords, material.coat_texture_index); if (material.coat_texture_index != MaterialConstants::NO_TEXTURE) material.coat = coat; else coat = material.coat; float sheen = get_material_property(render_data, false, texcoords, material.sheen_texture_index); if (material.sheen_texture_index != MaterialConstants::NO_TEXTURE) material.sheen = sheen; float specular_transmission = get_material_property(render_data, false, texcoords, material.specular_transmission_texture_index); if (material.specular_transmission_texture_index != MaterialConstants::NO_TEXTURE) material.specular_transmission = specular_transmission; ColorRGB32F emission = get_material_property(render_data, false, texcoords, material.emission_texture_index); if (material.emission_texture_index == MaterialConstants::NO_TEXTURE || material.emission_texture_index == MaterialConstants::CONSTANT_EMISSIVE_TEXTURE) emission = material.emission; DeviceUnpackedEffectiveMaterial unpacked_material(material); unpacked_material.emissive_texture_used = material.emission_texture_index != MaterialConstants::NO_TEXTURE; unpacked_material.emission = emission; // Roughening of the base roughness and second metallic roughness based // on the coat roughness. This should be precomputed instead of being done here // // Reference: [OpenPBR Surface 2024 Specification] https://academysoftwarefoundation.github.io/OpenPBR/#model/coat/roughening float coat_roughening = unpacked_material.coat_roughening; if (coat > 0.0f && coat_roughening > 0.0f) { float base_roughness = roughness_metallic.x; float coat_roughness = unpacked_material.coat_roughness; // Roughening of the base roughness of the material based on the coat roughness float target_base_roughness = hippt::pow_1_4(hippt::min(1.0f, hippt::pow_4(base_roughness) + 2.0f * hippt::pow_4(coat_roughness))); float roughened_base_roughness = hippt::lerp(base_roughness, target_base_roughness, coat); unpacked_material.roughness = hippt::lerp(base_roughness, roughened_base_roughness, coat_roughening); if (unpacked_material.second_roughness_weight > 0.0f) { // Roughening of the second metallic roughness based on the coat roughness float second_roughness = unpacked_material.second_roughness; float target_second_metal_roughness = hippt::pow_1_4(hippt::min(1.0f, hippt::pow_4(second_roughness) + 2.0f * hippt::pow_4(coat_roughness))); float roughened_second_metal_roughness = hippt::lerp(second_roughness, target_second_metal_roughness, coat); unpacked_material.second_roughness = hippt::lerp(second_roughness, roughened_second_metal_roughness, coat_roughening); } } return unpacked_material; } /** * The float2 returned is (roughness, metallic) */ HIPRT_DEVICE HIPRT_INLINE float2 get_metallic_roughness(const HIPRTRenderData& render_data, const float2& texcoords, int metallic_texture_index, int roughness_texture_index, int metallic_roughness_texture_index) { float2 out; if (metallic_roughness_texture_index != MaterialConstants::NO_TEXTURE) { ColorRGB32F rgb = sample_texture_rgb_8bits(render_data.buffers.material_textures, metallic_roughness_texture_index, false, texcoords); // Not converting to linear here because material properties (roughness and metallic) here are assumed to be linear already out.x = rgb.g; out.y = rgb.b; } else { out.x = get_material_property(render_data, false, texcoords, roughness_texture_index); out.y = get_material_property(render_data, false, texcoords, metallic_texture_index); } return out; } HIPRT_DEVICE HIPRT_INLINE ColorRGB32F get_base_color(const HIPRTRenderData& render_data, float& out_alpha, const float2& texcoords, int base_color_texture_index) { out_alpha = 1.0f; ColorRGBA32F rgba = get_material_property(render_data, true, texcoords, base_color_texture_index); if (base_color_texture_index != MaterialConstants::NO_TEXTURE) { ColorRGB32F base_color = ColorRGB32F(rgba.r, rgba.g, rgba.b); out_alpha = rgba.a; return base_color; } return ColorRGB32F(); } template HIPRT_DEVICE HIPRT_INLINE T read_data(const ColorRGBA32F& rgba) {} template<> HIPRT_DEVICE HIPRT_INLINE ColorRGBA32F read_data(const ColorRGBA32F& rgba) { return rgba; } template<> HIPRT_DEVICE HIPRT_INLINE ColorRGB32F read_data(const ColorRGBA32F& rgba) { return ColorRGB32F(rgba.r, rgba.g, rgba.b); } template<> HIPRT_DEVICE HIPRT_INLINE float read_data(const ColorRGBA32F& rgba) { return rgba.r; } template HIPRT_DEVICE HIPRT_INLINE T get_material_property(const HIPRTRenderData& render_data, bool is_srgb, const float2& texcoords, int texture_index) { if (texture_index == MaterialConstants::NO_TEXTURE || texture_index == MaterialConstants::CONSTANT_EMISSIVE_TEXTURE) return T(); ColorRGBA32F rgba = sample_texture_rgba(render_data.buffers.material_textures, texture_index, is_srgb, texcoords); return read_data(rgba); } #endif ================================================ FILE: src/Device/includes/NEE++/NEE++.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_NEE_PLUS_PLUS #define DEVICE_INCLUDES_NEE_PLUS_PLUS #include "Device/includes/HashGrid.h" #include "Device/includes/HashGridHash.h" #include "HostDeviceCommon/Math.h" /** * Context passed when tracing shadow rays */ struct NEEPlusPlusContext { float3 shaded_point; float3 point_on_light; // After passing this context to a call to 'evaluate_shadow_ray_nee_plus_plus', // this member will be filled with the probability that the points 'shaded_point' // and 'point_on_light' are mutually visible. // // If the call to 'evaluate_shadow_ray_nee_plus_plus' returns 'false' i.e. that the // points are mutually visibile, you will need to account this // 'unoccluded_probability' in the PDF of the light you sampled i.e. multiply // your PDF by this 'unoccluded_probability' to guarantee unbiasedness float unoccluded_probability = 1.0f; // Set this flag to true if this context should be used // for testing visibility probability between 'shaded_point' and the // envmap. // // ----- WARNING: // 'point_on_light' should be the normalized direction towards the envmap if this is set to true bool envmap = false; }; struct NEEPlusPlusEntry { AtomicType* total_unoccluded_rays = nullptr; AtomicType* total_num_rays = nullptr; AtomicType* checksum_buffer = nullptr; }; /** * Structure that contains the data for the implementation of NEE++. * * Reference: * [1] [Next Event Estimation++: Visibility Mapping for Efficient Light Transport Simulation] */ struct NEEPlusPlusDevice { // If true, the next camera rays kernel call will reset the visibility map bool m_reset_visibility_map = false; // If true, the grid visibility will be updated this frame (new visibility values will be accumulated) bool m_update_visibility_map = true; // Whether or not to do russian roulette with NEE++ on emissive lights bool m_enable_nee_plus_plus_RR_for_emissives = true; // Whether or not to do russian roulette with NEE++ on envmap samples bool m_enable_nee_plus_plus_RR_for_envmap = false; unsigned int m_total_number_of_cells = 0; float m_grid_cell_min_size = 0.25f; float m_grid_cell_target_projected_size = 25.0f; // After how many samples to stop updating the visibility map // (because it's probably converged enough) int m_stop_update_samples = 256; enum BufferNames : unsigned int { VISIBILITY_MAP_UNOCCLUDED_COUNT = 0, VISIBILITY_MAP_TOTAL_COUNT = 1, }; // Linear buffer that is a packing of 4 buffers: // // - 1 buffer that stores the number of rays that were // computed as non-occluded from voxel to voxel in the scene. // // For example, if 16 rays were shot from one voxel to another // and 7 of these rays were found to be unoccluded, then the corresponding // entry in the map will contain the value 7 // // Because the visibility map is symmetrical, this is a linear buffer that contains // only half of the visibility matrix // // For the indexing logic, (0, 0) is in the top left corner of the matrix // // - 1 buffer that is the same the same as the previous one but stores how many rays // in total were traced in total from one voxel to another, not just the unoccluded ones. // In the example from above, this would contain the value 16. // // For the indexing logic, (0, 0) is in the top left corner of the matrix // // - 2 buffers used for accumulation during the rendering process // These two buffers are used for accumulation of the visibility information during the rendering // For example, if we trace a shadow ray between voxel A and voxel B and that this shadow ray is // occluded, we're going to have to update the visibility map with information. // // However, we cannot just simply update the visibility map (i.e. the 2 first buffers) // during the rendering because this would lead to concurrency issues where the map is // being updated while also being read by other threads. // // The race condition is fine, what's not fine is that this will vary the estimate of the occlusion probability // from voxel A to voxel B and I found that this resulted in bias / non-determinism because the order in which // the threads update the map now influences how the other threads are going to read the map // // So instead we have some additional buffers here to accumulate separately and then this buffers are copied // every N frames (or N seconds) to the 'true' visibility map used during rendering // // Each one these 4 buffers are of type unsigned chars, packed into 1 unsigned ints. // // The data is stored such that the first unsigned int contains the 4 buffers at index 0 of the matrix // The second unsigned int contains the 4 buffers at index 1 // ... NEEPlusPlusEntry m_entries_buffer; // Counter that keep tracks of how many cells are currently used in the hash grid AtomicType* m_total_cells_alive_count = nullptr; // If a voxel-to-voxel unocclusion probability is higher than that, the voxel will be considered unoccluded // and so a shadow ray will be traced. This is to avoid trusting voxel that have a low probability of // being unoccluded // // 0.0f basically disables NEE++ as any entry of the visibility map will require a shadow ray float m_confidence_threshold = 0.025f; float m_minimum_unoccluded_proba = 0.0f; // Whether or not to count the number of shadow rays actually traced vs. the number of shadow // queries made. This is used in 'evaluate_shadow_ray_nee_plus_plus()' bool do_update_shadow_rays_traced_statistics = true; AtomicType* m_total_shadow_ray_queries = nullptr; AtomicType* m_shadow_rays_actually_traced = nullptr; HIPRT_HOST_DEVICE void accumulate_visibility(bool visible, unsigned int hash_grid_index) { if (hash_grid_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) // One of the two points was outside the scene, cannot cache this return; if (read_buffer(hash_grid_index) >= 255) return; if (visible) increment_buffer(hash_grid_index, 1); unsigned int total_count_before = increment_buffer(hash_grid_index, 1); if (total_count_before == 0) // If we just inserted a new cell in the hash grid, that's one more cell alive hippt::atomic_fetch_add(m_total_cells_alive_count, 1u); } /** * Updates the visibility map with one additional entry: whether or not the two given world points are visible */ HIPRT_HOST_DEVICE void accumulate_visibility(const NEEPlusPlusContext& context, HIPRTCamera& current_camera, bool visible) { return accumulate_visibility(visible, get_visibility_map_index(context, current_camera)); } /** * Returns the estimated probability that a ray between the two given world points * is going to be unoccluded (i.e. the two points are mutually visible) * * Returns the index in the visibility matrix of the voxel-to-voxel correspondance of the * two given points. This value can then be passed as argument to 'accumulate_visibility' * to save a little bit of computations (otherwise, 'accumulate_visibility' would have recomputed * that value on its own even though the world points given may be the same and thus, the matrix * index is the same) */ HIPRT_HOST_DEVICE float estimate_visibility_probability(const NEEPlusPlusContext& context, const HIPRTCamera& current_camera, unsigned int& out_hash_grid_index, unsigned int& out_cell_total_accumulation_count) const { out_hash_grid_index = get_visibility_map_index(context, current_camera); if (out_hash_grid_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) // One of the two points was outside the scene, cannot read the cache for this // // Returning 1.0f indicating that the two points are not occluded such that the caller // tests for a shadow ray return 1.0f; out_cell_total_accumulation_count = read_buffer(out_hash_grid_index); if (out_cell_total_accumulation_count == 0) // No information for these two points // // Returning 1.0f indicating that the two points are not occluded such that the caller // tests for a shadow ray return 1.0f; else { unsigned int unoccluded_count = read_buffer(out_hash_grid_index); float unoccluded_proba = unoccluded_count / static_cast(out_cell_total_accumulation_count); if (unoccluded_proba >= m_confidence_threshold) return 1.0f; else return hippt::max(m_minimum_unoccluded_proba, unoccluded_proba); } } /** * Returns the estimated probability that a ray between the two given world points * is going to be unoccluded (i.e. the two points are mutually visible) */ HIPRT_HOST_DEVICE float estimate_visibility_probability(const NEEPlusPlusContext& context, const HIPRTCamera& current_camera, unsigned int& out_cell_total_accumulation_count) const { unsigned int trash_matrix_index; return estimate_visibility_probability(context, current_camera, trash_matrix_index, out_cell_total_accumulation_count); } HIPRT_HOST_DEVICE float estimate_visibility_probability(const NEEPlusPlusContext& context, const HIPRTCamera& current_camera) const { unsigned int trash_matrix_index; unsigned int trash_accumulation_count; return estimate_visibility_probability(context, current_camera, trash_matrix_index, trash_accumulation_count); } HIPRT_HOST_DEVICE unsigned int hash_context(const NEEPlusPlusContext& context, const HIPRTCamera& current_camera, unsigned int& out_checksum) const { float3 second_point = context.envmap ? (context.shaded_point + context.point_on_light * 1.0e20f) : context.point_on_light; return hash_double_position_camera(m_total_number_of_cells, context.shaded_point, second_point, current_camera, m_grid_cell_target_projected_size, m_grid_cell_min_size, out_checksum); } template HIPRT_HOST_DEVICE unsigned int get_visibility_map_index(const NEEPlusPlusContext& context, const HIPRTCamera& current_camera) const { unsigned int checksum; unsigned int hash_grid_index = hash_context(context, current_camera, checksum); if (!HashGrid::resolve_collision(m_entries_buffer.checksum_buffer, m_total_number_of_cells, hash_grid_index, checksum)) { return HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX; } return hash_grid_index; } // TODO compare with the alpha learning rate and the ground truth to see the behavior of a single float buffer // TODO see if capping at 255 / 65535 is enough private: /** * Returns the value packed in the buffer at the given visibility matrix index and with the given * buffer name from the BufferNames enum */ template HIPRT_HOST_DEVICE unsigned int read_buffer(unsigned int hash_grid_index) const { if constexpr (bufferName == 0) return m_entries_buffer.total_unoccluded_rays[hash_grid_index]; else if constexpr (bufferName == 1) return m_entries_buffer.total_num_rays[hash_grid_index]; } /** * Increments the packed value in the packed buffer 'bufferName' at the given matrix index * * There is no protection against overflows in this function */ template HIPRT_HOST_DEVICE unsigned int increment_buffer(unsigned int hash_grid_index, unsigned int value) { if constexpr (bufferName == 0) return hippt::atomic_fetch_add(&m_entries_buffer.total_unoccluded_rays[hash_grid_index], value); if constexpr (bufferName == 1) return hippt::atomic_fetch_add(&m_entries_buffer.total_num_rays[hash_grid_index], value); } /** * Sets the value in one of the packed buffer * * WARNING: * This function is non-atomic */ template HIPRT_HOST_DEVICE void set_buffer(unsigned int hash_grid_index, unsigned int value) { if constexpr (bufferName == 0) m_entries_buffer.total_unoccluded_rays[hash_grid_index] = value; if constexpr (bufferName == 1) m_entries_buffer.total_num_rays[hash_grid_index] = value; } }; #endif ================================================ FILE: src/Device/includes/NestedDielectrics.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_NESTED_DIELECTRICS_H #define DEVICE_NESTED_DIELECTRICS_H #include "HostDeviceCommon/KernelOptions/KernelOptions.h" #include #ifdef __KERNELCC__ // On the GPU, the nested dielectrics stack is allocated in shared memory. // This means that all the entries of the nested dielectrics stacks are in shared memory. // // For example, for thread blocks of 64 and a NestedDielectricStackSize of 3, this gives us // a shared memory array of 3*64 = 192 entries. // // We then need a mapping that "redirects" each thread to its proper entry in that 192-long array. // // That's what this macro does, it takes an index in the stack as parameter (so 0, 1 or 2 for a NestedDielectricStackSize of 3) // and maps it to the index to use in the shared memory array by using the threadIdx. // // Note that the mapping is written to minimize shared memory bank conflicts #define NESTED_DIELECTRICS_STACK_INDEX_SHIFT(x) (x) #else // This macro is used to offset the index used to index the priority stack. // On the CPU, there is nothing to do, just use the given index, there is really nothing // special. The special case is for the GPU, explained above the GPU macro definition #define NESTED_DIELECTRICS_STACK_INDEX_SHIFT(x) (x) #endif /** * Reference: * * [1] [Simple Nested Dielectrics in Ray Traced Images, Schmidt, 2002] */ struct StackPriorityEntry { // How many bits for encoding the packed priority // and its shift to locate the bits in the packed 32bits integer static constexpr unsigned int PRIORITY_BIT_MASK = 0b1111; static constexpr unsigned int PRIORITY_BIT_SHIFT = 0; static constexpr unsigned int PRIORITY_MAXIMUM = PRIORITY_BIT_MASK; // How many bits for encoding the topmost flag // and its shift to locate the bits in the packed 32bits integer static constexpr unsigned int TOPMOST_BIT_MASK = 0b1; static constexpr unsigned int TOPMOST_BIT_SHIFT = PRIORITY_BIT_SHIFT + 4; // How many bits for encoding the odd_parity flag // and its shift to locate the bits in the packed 32bits integer static constexpr unsigned int ODD_PARTIY_BIT_MASK = 0b1; static constexpr unsigned int ODD_PARTIY_BIT_SHIFT = TOPMOST_BIT_SHIFT + 1; // How many bits for encoding the material_index flag // and its shift to locate the bits in the packed 32bits integer // This is the rest of the bits after we've added the other flags static constexpr unsigned int COMBINED_OTHER_FLAGS = (PRIORITY_BIT_MASK << PRIORITY_BIT_SHIFT) | (TOPMOST_BIT_MASK << TOPMOST_BIT_SHIFT) | (ODD_PARTIY_BIT_MASK << ODD_PARTIY_BIT_SHIFT); static constexpr unsigned int MATERIAL_INDEX_BIT_SHIFT = ODD_PARTIY_BIT_SHIFT + 1; static constexpr unsigned int MATERIAL_INDEX_BIT_MASK = (0xffffffff & (~COMBINED_OTHER_FLAGS)) >> MATERIAL_INDEX_BIT_SHIFT; // This 'MATERIAL_INDEX_MAXIMUM' is just an alias basically static constexpr unsigned int MATERIAL_INDEX_MAXIMUM = MATERIAL_INDEX_BIT_MASK; HIPRT_HOST_DEVICE void set_priority(int priority) { // Clear packed_data &= ~(PRIORITY_BIT_MASK << PRIORITY_BIT_SHIFT); // Set packed_data |= (priority & PRIORITY_BIT_MASK) << PRIORITY_BIT_SHIFT; } HIPRT_HOST_DEVICE void set_topmost(bool topmost) { // Clear packed_data &= ~(TOPMOST_BIT_MASK << TOPMOST_BIT_SHIFT); // Set packed_data |= (topmost == true) << TOPMOST_BIT_SHIFT; } HIPRT_HOST_DEVICE void set_odd_parity(bool odd_parity) { // Clear packed_data &= ~(ODD_PARTIY_BIT_MASK << ODD_PARTIY_BIT_SHIFT); // Set packed_data |= (odd_parity == true) << ODD_PARTIY_BIT_SHIFT; } HIPRT_HOST_DEVICE void set_material_index(int material_index) { // Clear packed_data &= ~(MATERIAL_INDEX_BIT_MASK << MATERIAL_INDEX_BIT_SHIFT); // Set packed_data |= (material_index & MATERIAL_INDEX_BIT_MASK) << MATERIAL_INDEX_BIT_SHIFT; } HIPRT_HOST_DEVICE int get_priority() const { return (packed_data >> PRIORITY_BIT_SHIFT) & PRIORITY_BIT_MASK; } HIPRT_HOST_DEVICE bool get_topmost() const { return (packed_data >> TOPMOST_BIT_SHIFT) & TOPMOST_BIT_MASK; } HIPRT_HOST_DEVICE bool get_odd_parity() const { return (packed_data >> ODD_PARTIY_BIT_SHIFT) & ODD_PARTIY_BIT_MASK; } HIPRT_HOST_DEVICE int get_material_index() const { return (packed_data >> MATERIAL_INDEX_BIT_SHIFT) & MATERIAL_INDEX_BIT_MASK; } // Packed data contains: // - the priority of the stack entry // - whether or not this is the topmost entry for that material in the stack // - An odd_parity flag // - The material index // // We get the bits: // // **** *** material index* **** **OT PRIO // // With : // - O the odd_parity flag // - T the topmost flag // - PRIO the dielectric priority unsigned int packed_data; }; struct NestedDielectricsInteriorStack { /** * Pushes a new material index onto the stack * * Returns true if that intersection should be skipped (because we are currently in a material with * higher priority than the material we just intersected) * * Returns false if that intersection should not be skipped */ HIPRT_HOST_DEVICE bool push(int& out_incident_material_index, int& out_outgoing_material_index, bool& out_inside_material, int material_index, int material_priority) { if (stack_position == NestedDielectricsStackSize - 1) // The stack is already at the maximum return false; // Index of the material we last entered before intersecting the // material we're currently inserting in the stack int last_entered_mat_index = 0; for (last_entered_mat_index = stack_position; last_entered_mat_index >= 0; last_entered_mat_index--) // The three conditions in order are: // - We found a materal in the stack that is not the material that we're currently intersecting // - The entry of that material in the stack is the topmost (the last entry of its material kind) // - The entry of that material in the stack is odd_parity = we've entered that material but haven't left it yet // // = the last entered material if (stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(last_entered_mat_index)].get_material_index() != material_index && stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(last_entered_mat_index)].get_topmost() && stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(last_entered_mat_index)].get_odd_parity()) break; // Parity of the material we're inserting in the stack bool odd_parity = true; // Index in the stack of the previous material that is the same as // the one we're trying to insert in the stack. int previous_same_mat_index; for (previous_same_mat_index = stack_position; previous_same_mat_index >= 0; previous_same_mat_index--) { int stack_index = NESTED_DIELECTRICS_STACK_INDEX_SHIFT(previous_same_mat_index); if (stack_entries[stack_index].get_material_index() == material_index) { // The previous stack entry of the same material is not the topmost anymore stack_entries[stack_index].set_topmost(false); // The current parity is the inverse of the previous one odd_parity = !stack_entries[stack_index].get_odd_parity(); break; } } out_inside_material = !odd_parity; // Inserting the material in the stack if (stack_position < NestedDielectricsStackSize - 1) stack_position++; int new_stack_index = NESTED_DIELECTRICS_STACK_INDEX_SHIFT(stack_position); stack_entries[new_stack_index].set_material_index(material_index); stack_entries[new_stack_index].set_odd_parity(odd_parity); stack_entries[new_stack_index].set_topmost(true); stack_entries[new_stack_index].set_priority(material_priority); if (material_priority < stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(last_entered_mat_index)].get_priority()) { // Skipping the boundary because the intersected material has a // lower priority than the material we're currently in return true; } else { if (odd_parity) { // We are entering the material out_incident_material_index = stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(last_entered_mat_index)].get_material_index(); out_outgoing_material_index = material_index; } else { // Exiting material out_incident_material_index = material_index; out_outgoing_material_index = stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(last_entered_mat_index)].get_material_index(); } // Not skipping the boundary return false; } } HIPRT_HOST_DEVICE void pop(const bool inside_material) { int stack_top_mat_index = stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(stack_position)].get_material_index(); if (stack_position > 0) // Checking that we have room to pop. // For a very small stack (size of 2) that overflown // (we couldn't push all the material we needed to because of // stack size constraint), it can happen that the stack position // at this point is already 0 and we cannot pop. stack_position--; if (inside_material) { int previous_same_mat_index; for (previous_same_mat_index = stack_position; previous_same_mat_index >= 0; previous_same_mat_index--) if (stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(previous_same_mat_index)].get_material_index() == stack_top_mat_index) break; if (previous_same_mat_index >= 0) for (int i = previous_same_mat_index + 1; i <= stack_position; i++) stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(i - 1)] = stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(i)]; // For very small stacks (2 for example), we may not be able to pop twice // at all so we check the position on the stack first if (stack_position > 0) stack_position--; } for (int i = stack_position; i >= 0; i--) { if (stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(i)].get_material_index() == stack_top_mat_index) { stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(i)].set_topmost(true); break; } } } // We only need all of this if the stack size is actually > 0, // otherwise, we're just not going to do the nested dielectrics handling at all StackPriorityEntry stack_entries[NestedDielectricsStackSize]; static constexpr unsigned int MAX_MATERIAL_INDEX = StackPriorityEntry::MATERIAL_INDEX_MAXIMUM; // Stack position is pointing at the last valid entry. // Entry 0 is always present and represent air basically int stack_position = 0; }; #endif ================================================ FILE: src/Device/includes/ONB.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_ONB_H #define DEVICE_ONB_H #include "HostDeviceCommon/Math.h" /* * This uses the technique from "Improved accuracy when building an orthonormal basis" by Nelson Max, * https://jcgt.org/published/0006/01/02. * * Taken from https://github.com/nvpro-samples/nvpro_core/blob/master/nvvkhl/shaders/func.h * and optimised a little bit by @tigrazone */ HIPRT_HOST_DEVICE HIPRT_INLINE void build_ONB(const float3& N, float3& T, float3& B) { if (N.z < -0.99998796f) // Handle the singularity { T = make_float3(0.0f, -1.0f, 0.0f); B = make_float3(-1.0f, 0.0f, 0.0f); return; } float nxa = -N.x / (1.0f + N.z); T = make_float3(1.0f + N.x * nxa, nxa * N.y, -N.x); B = make_float3(T.y, 1.0f - N.y * N.y / (1.0f + N.z), -N.y); } /* * Rotation of the basis around the normal by 'basis_rotation' radians */ HIPRT_HOST_DEVICE HIPRT_INLINE void build_rotated_ONB(const float3& N, float3& T, float3& B, float basis_rotation) { float3 up = hippt::abs(N.z) < 0.9999999f ? make_float3(0.0f, 0.0f, 1.0f) : make_float3(1.0f, 0.0f, 0.0f); T = hippt::normalize(hippt::cross(up, N)); // Rodrigues' rotation T = T * cos(basis_rotation) + hippt::cross(N, T) * sin(basis_rotation) + N * hippt::dot(N, T) * (1.0f - cos(basis_rotation)); B = hippt::cross(N, T); } /* * Transforms V from its local space to the space around the normal */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 local_to_world_frame(const float3& N, const float3& V) { float3 T, B; build_ONB(N, T, B); return hippt::normalize(V.x * T + V.y * B + V.z * N); } HIPRT_HOST_DEVICE HIPRT_INLINE float3 local_to_world_frame(const float3& T, const float3& B, const float3& N, const float3& V) { return hippt::normalize(V.x * T + V.y * B + V.z * N); } /* * Transforms V from its space to the local space around the normal * The given normal is the Z axis of the local frame around the normal */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 world_to_local_frame(const float3& N, const float3& V) { float3 T, B; build_ONB(N, T, B); return hippt::normalize(make_float3(hippt::dot(V, T), hippt::dot(V, B), hippt::dot(V, N))); } HIPRT_HOST_DEVICE HIPRT_INLINE float3 world_to_local_frame(const float3& T, const float3& B, const float3& N, const float3& V) { return hippt::normalize(make_float3(hippt::dot(V, T), hippt::dot(V, B), hippt::dot(V, N))); } #endif ================================================ FILE: src/Device/includes/PathTracing.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_PATH_TRACING_H #define DEVICE_INCLUDES_PATH_TRACING_H #include "Device/includes/LightSampling/Envmap.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/RussianRoulette.h" #include "Device/includes/WarpDirectionReuse.h" #include "HostDeviceCommon/RenderData.h" HIPRT_DEVICE bool path_tracing_find_indirect_bounce_intersection(HIPRTRenderData& render_data, hiprtRay ray, RayPayload& out_ray_payload, HitInfo& out_closest_hit_info, Xorshift32Generator& random_number_generator) { return trace_main_path_ray(render_data, ray, out_ray_payload, out_closest_hit_info, out_closest_hit_info.primitive_index, out_ray_payload.bounce, random_number_generator); } /** * If sampleDirectionOnly is 'true', only the direction for the next bounce will be computed * but without evaluating the contribution of the BSDF or the PDF. */ template HIPRT_DEVICE void path_tracing_sample_next_indirect_bounce(HIPRTRenderData& render_data, RayPayload& ray_payload, HitInfo& closest_hit_info, float3 view_direction, ColorRGB32F& out_bsdf_color, float3& out_bounce_direction, float& out_bsdf_pdf, Xorshift32Generator& random_number_generator, BSDFIncidentLightInfo* out_sampled_light_info = nullptr) { BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, make_float3(0.0f, 0.0f, 0.0f), *out_sampled_light_info, ray_payload.volume_state, true, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness); out_bsdf_color = bsdf_dispatcher_sample(render_data, bsdf_context, out_bounce_direction, out_bsdf_pdf, random_number_generator); ray_payload.accumulate_roughness(*out_sampled_light_info); #if DoFirstBounceWarpDirectionReuse == KERNEL_OPTION_TRUE warp_direction_reuse(render_data, closest_hit_info, ray_payload, -ray.direction, bounce_direction, bsdf_color, bsdf_pdf, bounce, random_number_generator); #endif } /** * Returns the new ray throughput after attenuation of the given 'current_throughput' */ HIPRT_DEVICE ColorRGB32F path_tracing_update_ray_throughput(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo& closest_hit_info, ColorRGB32F current_throughput, float& rr_throughput_scaling, ColorRGB32F bsdf_color, float3 bounce_direction, float bsdf_pdf, Xorshift32Generator& random_number_generator, bool apply_russian_roulette = true) { ColorRGB32F throughput_attenuation = bsdf_color * hippt::abs(hippt::dot(bounce_direction, closest_hit_info.shading_normal)) / bsdf_pdf; // Russian roulette if (apply_russian_roulette && !do_russian_roulette(render_data.render_settings, ray_payload.bounce, current_throughput, rr_throughput_scaling, throughput_attenuation, random_number_generator)) return ColorRGB32F(0.0f); // Dispersion ray throughput filter current_throughput *= get_dispersion_ray_color(ray_payload.volume_state.sampled_wavelength, ray_payload.material.dispersion_scale); current_throughput *= throughput_attenuation; // Clamp every component to a minimum of 1.0e-5f to avoid numerical instabilities that can // happen: with some material, the throughput can get so low that it becomes denormalized and // this can cause issues in some parts of the renderer (most notably the NaN detection) current_throughput.max(ColorRGB32F(1.0e-5f, 1.0e-5f, 1.0e-5f)); ray_payload.next_ray_state = RayState::BOUNCE; return current_throughput; } /** * Returns the new ray throughput after attenuation of the given 'current_throughput' */ HIPRT_DEVICE ColorRGB32F path_tracing_update_ray_throughput(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo& closest_hit_info, ColorRGB32F current_throughput, ColorRGB32F bsdf_color, float3 bounce_direction, float bsdf_pdf, Xorshift32Generator& random_number_generator, bool apply_russian_roulette = true) { float unused_rr_throughput_scaling; return path_tracing_update_ray_throughput(render_data, ray_payload, closest_hit_info, current_throughput, unused_rr_throughput_scaling, bsdf_color, bounce_direction, bsdf_pdf, random_number_generator, apply_russian_roulette); } /** * Returns true if the bounce was sampled successfully, * false otherwise (is the BSDF sample failed, if russian roulette killed the sample, ...) * * If sampleDirectionOnly is 'true', only the direction for the next bounce will be computed * but without evaluating the contribution of the BSDF or the PDF. */ template HIPRT_DEVICE bool path_tracing_compute_next_indirect_bounce(HIPRTRenderData& render_data, RayPayload& ray_payload, HitInfo& closest_hit_info, float3 view_direction, hiprtRay& out_ray, Xorshift32Generator& random_number_generator, BSDFIncidentLightInfo* incident_light_info = nullptr) { ColorRGB32F bsdf_color; float3 bounce_direction; float bsdf_pdf; path_tracing_sample_next_indirect_bounce(render_data, ray_payload, closest_hit_info, view_direction, bsdf_color, bounce_direction, bsdf_pdf, random_number_generator, incident_light_info); // Terminate ray if bad sampling if (bsdf_pdf <= 0.0f && !sampleDirectionOnly) return false; ray_payload.throughput = path_tracing_update_ray_throughput(render_data, ray_payload, closest_hit_info, ray_payload.throughput, bsdf_color, bounce_direction, bsdf_pdf, random_number_generator); if (ray_payload.throughput.is_black() && !sampleDirectionOnly) // Killed by russian roulette return false; out_ray.origin = closest_hit_info.inter_point; out_ray.direction = bounce_direction; return true; } HIPRT_DEVICE void store_denoiser_AOVs(HIPRTRenderData& render_data, uint32_t pixel_index, float3 shading_normal, ColorRGB32F base_color) { if (render_data.render_settings.sample_number == 0) render_data.aux_buffers.denoiser_albedo[pixel_index] = base_color; else render_data.aux_buffers.denoiser_albedo[pixel_index] = (render_data.aux_buffers.denoiser_albedo[pixel_index] * render_data.render_settings.denoiser_AOV_accumulation_counter + base_color) / (render_data.render_settings.denoiser_AOV_accumulation_counter + 1.0f); if (render_data.render_settings.sample_number == 0) render_data.aux_buffers.denoiser_normals[pixel_index] = shading_normal; else { float3 accumulated_normal = (render_data.aux_buffers.denoiser_normals[pixel_index] * render_data.render_settings.denoiser_AOV_accumulation_counter + shading_normal) / (render_data.render_settings.denoiser_AOV_accumulation_counter + 1.0f); float normal_length = hippt::length(accumulated_normal); if (!hippt::is_zero(normal_length)) // Checking that it is non-zero otherwise we would accumulate a persistent NaN in the buffer when normalizing by the 0-length render_data.aux_buffers.denoiser_normals[pixel_index] = accumulated_normal / normal_length; } } HIPRT_DEVICE ColorRGB32F path_tracing_miss_gather_envmap(HIPRTRenderData& render_data, const ColorRGB32F& ray_throughput, float3 ray_direction, int bounce, uint32_t pixel_index) { ColorRGB32F skysphere_color; if (render_data.world_settings.ambient_light_type == AmbientLightType::UNIFORM || render_data.bsdfs_data.white_furnace_mode) skysphere_color = render_data.world_settings.uniform_light_color; else if (render_data.world_settings.ambient_light_type == AmbientLightType::ENVMAP && render_data.world_settings.envmap_intensity == 0.0f) return ColorRGB32F(0.0f); else if (render_data.world_settings.ambient_light_type == AmbientLightType::ENVMAP) { #if EnvmapSamplingStrategy != ESS_NO_SAMPLING // If we have sampling, only taking envmap into account on camera ray miss if (bounce == 0) #endif { // We're only getting the skysphere radiance for the first rays because the // syksphere is importance sampled. skysphere_color = eval_envmap_no_pdf(render_data.world_settings, ray_direction); #if EnvmapSamplingStrategy == ESS_NO_SAMPLING // If we don't have envmap sampling, we're only going to unscale on // bounce 0 (which is when a ray misses directly --> background color). // Otherwise, if not bounce 2, we do want to take the scaling into // account so this if will fail and the envmap color will never be unscaled if (!render_data.world_settings.envmap_scale_background_intensity && bounce == 0) #else if (!render_data.world_settings.envmap_scale_background_intensity) #endif // Un-scaling the envmap if the user doesn't want to scale the background skysphere_color /= render_data.world_settings.envmap_intensity; } } skysphere_color = clamp_light_contribution(skysphere_color, render_data.render_settings.envmap_contribution_clamp, /* clamp condition */ true); ColorRGB32F indirect_lighting_contribution = skysphere_color * ray_throughput; // Only clamping with the indirect lighting clamp value if // this is bounce > 0 (thanks to /* clamp condition */ bounce > 0) ColorRGB32F clamped_indirect_lighting_contribution = clamp_light_contribution( indirect_lighting_contribution, render_data.render_settings.indirect_contribution_clamp, /* clamp condition */ bounce > 0); if (bounce == 0) // The camera ray missed so we don't have the normals but we have the base color store_denoiser_AOVs(render_data, pixel_index, make_float3(0, 0, 0), skysphere_color); return clamped_indirect_lighting_contribution; } HIPRT_DEVICE ColorRGB32F path_tracing_miss_gather_envmap(HIPRTRenderData& render_data, RayPayload& ray_payload, float3 ray_direction, uint32_t pixel_index) { return path_tracing_miss_gather_envmap(render_data, ray_payload.throughput, ray_direction, ray_payload.bounce, pixel_index); } HIPRT_DEVICE void path_tracing_accumulate_color(const HIPRTRenderData& render_data, const ColorRGB32F& ray_color, uint32_t pixel_index) { #if DisplayOnlySampleN == KERNEL_OPTION_TRUE int sampleIndex = render_data.render_settings.output_debug_sample_N; if (render_data.render_settings.sample_number == sampleIndex) { render_data.buffers.accumulated_ray_colors[pixel_index] = ray_color; #if ViewportColorOverriden == 0 render_data.buffers.accumulated_ray_colors[pixel_index] *= (sampleIndex + 1); #endif } return; #endif if (render_data.render_settings.has_access_to_adaptive_sampling_buffers()) { float squared_luminance_of_samples = ray_color.luminance() * ray_color.luminance(); // We can only use these buffers if the adaptive sampling or the stop noise threshold is enabled. // Otherwise, the buffers are destroyed to save some VRAM so they are not accessible render_data.aux_buffers.pixel_squared_luminance[pixel_index] += squared_luminance_of_samples; } if (render_data.render_settings.sample_number == 0) render_data.buffers.accumulated_ray_colors[pixel_index] = ray_color; else // If we are at a sample that is not 0, this means that we are accumulating render_data.buffers.accumulated_ray_colors[pixel_index] += ray_color; if (render_data.buffers.gmon_estimator.sets != nullptr) { // GMoN is in use, accumulating in the GMoN sets unsigned int offset = render_data.render_settings.render_resolution.x * render_data.render_settings.render_resolution.y * render_data.buffers.gmon_estimator.next_set_to_accumulate + pixel_index; if (render_data.render_settings.sample_number == 0) render_data.buffers.gmon_estimator.sets[offset] = ray_color; else render_data.buffers.gmon_estimator.sets[offset] += ray_color; } } HIPRT_DEVICE void path_tracing_accumulate_debug_view_color(const HIPRTRenderData& render_data, RayPayload& ray_payload, int pixel_index, Xorshift32Generator& rng) { #if ViewportColorOverriden == 1 // Modifying the ray color such that we display some debug color to the screen #if DirectLightNEEPlusPlusDisplayShadowRaysDiscarded == KERNEL_OPTION_TRUE // Nothing to do, the debug is already handled in the shadow ray NEE function #elif NEEPlusPlusDebugMode != NEE_PLUS_PLUS_DEBUG_MODE_NO_DEBUG if (render_data.g_buffer.first_hit_prim_index[pixel_index] != -1) { // We have a first hit float3 primary_hit = render_data.g_buffer.primary_hit_position[pixel_index]; float3 shading_normal = render_data.g_buffer.shading_normals[pixel_index].unpack(); float3 view_direction = render_data.g_buffer.get_view_direction(render_data.current_camera.position, pixel_index); unsigned int trash_checksum; NEEPlusPlusContext context; context.envmap = false; context.point_on_light = make_float3(0, 0, 0); context.shaded_point = primary_hit; ray_payload.ray_color = ColorRGB32F::random_color(render_data.nee_plus_plus.hash_context(context, render_data.current_camera, trash_checksum)); ray_payload.ray_color *= (render_data.render_settings.sample_number + 1); ray_payload.ray_color *= hippt::dot(shading_normal, view_direction); } #elif ReGIR_DebugMode != REGIR_DEBUG_MODE_NO_DEBUG #if ReGIR_DebugMode == REGIR_DEBUG_MODE_GRID_CELLS if (render_data.g_buffer.first_hit_prim_index[pixel_index] != -1) { // We have a first hit float3 primary_hit = render_data.g_buffer.primary_hit_position[pixel_index]; float3 normal = render_data.g_buffer.geometric_normals[pixel_index].unpack(); float3 view_direction = render_data.g_buffer.get_view_direction(render_data.current_camera.position, pixel_index); float primary_hit_roughness = render_data.g_buffer.materials[pixel_index].get_roughness(); ray_payload.ray_color = render_data.render_settings.regir_settings.get_random_cell_color(primary_hit, normal, render_data.current_camera, primary_hit_roughness, true); ray_payload.ray_color *= (render_data.render_settings.sample_number + 1); ray_payload.ray_color *= hippt::dot(normal, view_direction); } #elif ReGIR_DebugMode == REGIR_DEBUG_MODE_AVERAGE_CELL_NON_CANONICAL_RESERVOIR_CONTRIBUTION if (render_data.g_buffer.first_hit_prim_index[pixel_index] != -1) { float3 primary_hit = render_data.g_buffer.primary_hit_position[pixel_index]; unsigned int cell_index = render_data.render_settings.regir_settings.get_hash_grid_cell_index_from_world_pos(primary_hit); float average_contribution = 0.0f; for (int i = 0; i < render_data.render_settings.regir_settings.grid_fill.get_non_canonical_reservoir_count_per_cell(); i++) { ReGIRReservoir reservoir = render_data.render_settings.regir_settings.get_cell_non_canonical_reservoir_from_cell_reservoir_index(cell_index, i); average_contribution += reservoir.sample.target_function * reservoir.UCW; } // Averaging average_contribution /= render_data.render_settings.regir_settings.grid_fill.get_non_canonical_reservoir_count_per_cell(); // Scaling by the debug factor for visualization purposes average_contribution *= render_data.render_settings.regir_settings.debug_view_scale_factor; // Scaling by SPP average_contribution *= (render_data.render_settings.sample_number + 1); ray_payload.ray_color = ColorRGB32F(average_contribution); } #elif ReGIR_DebugMode == REGIR_DEBUG_MODE_AVERAGE_CELL_CANONICAL_RESERVOIR_CONTRIBUTION if (render_data.g_buffer.first_hit_prim_index[pixel_index] != -1) { float3 primary_hit = render_data.g_buffer.primary_hit_position[pixel_index]; unsigned int cell_index = render_data.render_settings.regir_settings.get_hash_grid_cell_index_from_world_pos(primary_hit); float average_contribution = 0.0f; for (int i = 0; i < render_data.render_settings.regir_settings.grid_fill.get_canonical_reservoir_count_per_cell(); i++) { ReGIRReservoir reservoir = render_data.render_settings.regir_settings.get_cell_canonical_reservoir_from_cell_reservoir_index(cell_index, i); average_contribution += reservoir.sample.target_function * reservoir.UCW; } // Averaging average_contribution /= render_data.render_settings.regir_settings.grid_fill.get_canonical_reservoir_count_per_cell(); // Scaling by the debug factor for visualization purposes average_contribution *= render_data.render_settings.regir_settings.debug_view_scale_factor; // Scaling by SPP average_contribution *= (render_data.render_settings.sample_number + 1); ray_payload.ray_color = ColorRGB32F(average_contribution); } #elif ReGIR_DebugMode == REGIR_DEBUG_MODE_REPRESENTATIVE_POINTS if (render_data.g_buffer.first_hit_prim_index[pixel_index] != -1) { float3 primary_hit = render_data.g_buffer.primary_hit_position[pixel_index]; float3 normal = render_data.g_buffer.geometric_normals[pixel_index].unpack(); float primary_hit_roughness = render_data.g_buffer.materials[pixel_index].get_roughness(); unsigned int cell_index = render_data.render_settings.regir_settings.get_hash_grid_cell_index_from_world_pos(primary_hit, normal, render_data.current_camera, primary_hit_roughness, true); ColorRGB32F color; float3 rep_point = ReGIR_get_cell_world_point(render_data, cell_index, true); // Interpreting debug_view_scale_factor as a distance if (hippt::length(rep_point - primary_hit) < render_data.render_settings.regir_settings.debug_view_scale_factor) color = ColorRGB32F::random_color(cell_index + 1); // Scaling by SPP so that the visualization doesn't get darker and darker with increasing number of SPP color *= render_data.render_settings.sample_number + 1; ray_payload.ray_color = ColorRGB32F(color); } #elif ReGIR_DebugMode == REGIR_DEBUG_MODE_REPRESENTATIVE_NORMALS if (render_data.g_buffer.first_hit_prim_index[pixel_index] != -1) { float3 primary_hit = render_data.g_buffer.primary_hit_position[pixel_index]; float3 normal = render_data.g_buffer.geometric_normals[pixel_index].unpack(); float primary_hit_roughness = render_data.g_buffer.materials[pixel_index].get_roughness(); unsigned int cell_index = render_data.render_settings.regir_settings.get_hash_grid_cell_index_from_world_pos(primary_hit, normal, render_data.current_camera, primary_hit_roughness, true); ColorRGB32F color = (ColorRGB32F(ReGIR_get_cell_world_normal(render_data, cell_index, true)) + ColorRGB32F(1.0f)) * 0.5f; // Scaling by SPP so that the visualization doesn't get darker and darker with increasing number of SPP color *= render_data.render_settings.sample_number + 1; ray_payload.ray_color = ColorRGB32F(color); } #endif #endif #endif } #endif ================================================ FILE: src/Device/includes/RIS/RIS.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RIS_H #define DEVICE_RIS_H #include "Device/includes/Dispatcher.h" #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/RIS/RIS_Reservoir.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/RenderData.h" HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F evaluate_reservoir_sample(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo& closest_hit_info, const float3& view_direction, const RISReservoir& reservoir, Xorshift32Generator& random_number_generator) { ColorRGB32F final_color; if (reservoir.UCW <= 0.0f) // No valid sample means no light contribution return ColorRGB32F(0.0f); RISSample sample = reservoir.sample; bool in_shadow; float distance_to_light; float3 evaluated_point = closest_hit_info.inter_point; float3 shadow_ray_direction = sample.point_on_light_source - evaluated_point; float3 shadow_ray_direction_normalized = shadow_ray_direction / (distance_to_light = hippt::length(shadow_ray_direction)); NEEPlusPlusContext nee_plus_plus_context; if (sample.is_bsdf_sample) // A BSDF sample that has been picked by RIS cannot be occluded otherwise // it would have a weight of 0 and would never be picked by RIS in_shadow = false; else { hiprtRay shadow_ray; shadow_ray.origin = evaluated_point; shadow_ray.direction = shadow_ray_direction_normalized; nee_plus_plus_context.point_on_light = sample.point_on_light_source; nee_plus_plus_context.shaded_point = shadow_ray.origin; in_shadow = evaluate_shadow_ray_nee_plus_plus(render_data, shadow_ray, distance_to_light, closest_hit_info.primitive_index, nee_plus_plus_context, random_number_generator, ray_payload.bounce); } if (!in_shadow) { float bsdf_pdf; float cosine_at_evaluated_point; ColorRGB32F bsdf_color; if (sample.is_bsdf_sample) { // If we picked a BSDF sample, we're using the already computed cosine term and color // because it's annoying to recompute it (we have to know if the BSDF is a refraction // sample or not) bsdf_color = sample.bsdf_sample_contribution; cosine_at_evaluated_point = sample.bsdf_sample_cosine_term; } else { BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, shadow_ray_direction_normalized, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); cosine_at_evaluated_point = hippt::abs(hippt::dot(closest_hit_info.shading_normal, shadow_ray_direction_normalized)); } final_color = bsdf_color * reservoir.UCW * sample.emission * cosine_at_evaluated_point; if (!sample.is_bsdf_sample) final_color /= nee_plus_plus_context.unoccluded_probability; } return final_color; } HIPRT_HOST_DEVICE HIPRT_INLINE RISReservoir sample_bsdf_and_lights_RIS_reservoir(const HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo& closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { // If we're rendering at low resolution, only doing 1 candidate of each // for better interactive framerates int nb_light_candidates = render_data.render_settings.do_render_low_resolution() ? 1 : render_data.render_settings.ris_settings.number_of_light_candidates; int nb_bsdf_candidates = render_data.render_settings.do_render_low_resolution() ? 1 : render_data.render_settings.ris_settings.number_of_bsdf_candidates; if (!MaterialUtils::can_do_light_sampling(ray_payload.material)) nb_light_candidates = 0; // Sampling candidates with weighted reservoir sampling RISReservoir reservoir; for (int i = 0; i < nb_light_candidates; i++) { float target_function = 0.0f; float candidate_weight = 0.0f; LightSampleInformation light_sample_info = sample_one_emissive_triangle(render_data, closest_hit_info.inter_point, view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, closest_hit_info.primitive_index, ray_payload, random_number_generator); if (light_sample_info.area_measure_pdf > 0.0f) { // It can happen that the light PDF returned by the emissive triangle // sampling function is 0 because of emissive triangles that are so // small that we cannot compute their normal and their area (the cross // product of their edges gives a quasi-null vector --> length of 0.0f --> area of 0) float3 to_light_direction = light_sample_info.point_on_light - closest_hit_info.inter_point; float distance_to_light = hippt::length(to_light_direction); to_light_direction = to_light_direction / distance_to_light; // Normalization float cosine_at_light_source = compute_cosine_term_at_light_source(light_sample_info.light_source_normal, -to_light_direction); // Multiplying by the inside_surface_multiplier here because if we're inside the surface, we want to flip the normal // for the dot product to be "properly" oriented. float cosine_at_evaluated_point = hippt::abs(hippt::dot(closest_hit_info.shading_normal, to_light_direction)); if (cosine_at_evaluated_point > 0.0f && cosine_at_light_source > 1.0e-6f) { float bsdf_pdf = 0.0f; // Early check for minimum light contribution: if the light itself doesn't contribute enough, // adding the BSDF attenuation on top of it will only make it worse so we can already // skip the light and saves ourselves the evaluation of the BSDF bool contributes_enough = check_minimum_light_contribution(render_data.render_settings.minimum_light_contribution, light_sample_info.emission / light_sample_info.area_measure_pdf); if (!contributes_enough) target_function = 0.0f; else { // Only going to evaluate the target function if we passed the preliminary minimum light contribution test BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, to_light_direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); ColorRGB32F light_contribution = bsdf_color * light_sample_info.emission * cosine_at_evaluated_point; // Checking the light contribution and taking the BSDF and light PDFs into account contributes_enough = check_minimum_light_contribution(render_data.render_settings.minimum_light_contribution, light_contribution / bsdf_pdf / light_sample_info.area_measure_pdf); if (!contributes_enough) // The light doesn't contribute enough, setting the target function to 0.0f // so that this light sample is skipped // // Also, if at least one thread is going to evaluate the light anyways, because of the divergence that this would // create, we may as well evaluate the light for all threads and not loose that much performance anyways target_function = 0.0f; else target_function = light_contribution.luminance(); } #if RISUseVisiblityTargetFunction == KERNEL_OPTION_TRUE if (!render_data.render_settings.do_render_low_resolution() && target_function > 0.0f) { // Only doing visiblity if we're not rendering at low resolution // (meaning we're moving the camera) for better interaction framerates hiprtRay shadow_ray; shadow_ray.origin = closest_hit_info.inter_point; shadow_ray.direction = to_light_direction; bool visible = !evaluate_shadow_ray_occluded(render_data, shadow_ray, distance_to_light, closest_hit_info.primitive_index, ray_payload.bounce, random_number_generator); target_function *= visible; } #endif // Converting the PDF from area measure to solid angle measure float solid_angle_light_pdf = area_to_solid_angle_pdf(light_sample_info.area_measure_pdf, distance_to_light, cosine_at_light_source); float mis_weight = balance_heuristic(solid_angle_light_pdf, nb_light_candidates, bsdf_pdf, nb_bsdf_candidates); candidate_weight = mis_weight * target_function / solid_angle_light_pdf; } } RISSample light_RIS_sample; light_RIS_sample.is_bsdf_sample = false; light_RIS_sample.point_on_light_source = light_sample_info.point_on_light; light_RIS_sample.target_function = target_function; light_RIS_sample.emission = light_sample_info.emission; reservoir.add_one_candidate(light_RIS_sample, candidate_weight, random_number_generator); reservoir.sanity_check(); } // Whether or not a BSDF sample has been retained by the reservoir for (int i = 0; i < nb_bsdf_candidates; i++) { float bsdf_sample_pdf = 0.0f; float target_function = 0.0f; float candidate_weight = 0.0f; float3 sampled_bsdf_direction; BSDFIncidentLightInfo incident_light_info; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, make_float3(0.0f, 0.0f, 0.0f), incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); ColorRGB32F bsdf_color = bsdf_dispatcher_sample(render_data, bsdf_context, sampled_bsdf_direction, bsdf_sample_pdf, random_number_generator); RISSample bsdf_RIS_sample; if (bsdf_sample_pdf > 0.0f) { hiprtRay bsdf_ray; bsdf_ray.origin = closest_hit_info.inter_point; bsdf_ray.direction = sampled_bsdf_direction; BSDFLightSampleRayHitInfo shadow_light_ray_hit_info; bool hit_found = evaluate_bsdf_light_sample_ray(render_data, bsdf_ray, 1.0e35f, shadow_light_ray_hit_info, closest_hit_info.primitive_index, ray_payload.bounce, random_number_generator); if (hit_found && !shadow_light_ray_hit_info.hit_emission.is_black() && compute_cosine_term_at_light_source(shadow_light_ray_hit_info.hit_geometric_normal, -sampled_bsdf_direction) > 0.0f) { // If we intersected an emissive material, compute the weight. // Otherwise, the weight is 0 because of the emision being 0 so we just don't compute it // Using abs here because we want the dot product to be positive. // You may be thinking that if we're doing this, then we're not going to discard BSDF // sampled direction that are below the surface (whereas we should discard them). // That would be correct but bsdf_dispatcher_sample return a PDF == 0.0f if a bad // direction was sampled and if the PDF is 0.0f, we never get to this line of code // you're reading. If we are here, this is because we sampled a direction that is // correct for the BSDF. Even if the direction is correct, the dot product may be // negative in the case of refractions / total internal reflections and so in this case, // we'll need to abs() the dot product for it to be positive float cosine_at_evaluated_point = hippt::abs(hippt::dot(closest_hit_info.shading_normal, sampled_bsdf_direction)); // Our target function does not include the geometry term because we're integrating // in solid angle. The geometry term in the target function ( / in the integrand) is only // for surface area direct lighting integration ColorRGB32F light_contribution = bsdf_color * shadow_light_ray_hit_info.hit_emission * cosine_at_evaluated_point; target_function = light_contribution.luminance(); float light_pdf = pdf_of_emissive_triangle_hit_solid_angle(render_data, shadow_light_ray_hit_info, sampled_bsdf_direction); bool contributes_enough = bsdf_sample_pdf <= 0.0f || check_minimum_light_contribution(render_data.render_settings.minimum_light_contribution, light_contribution / light_pdf / bsdf_sample_pdf); if (!contributes_enough) target_function = 0.0f; float mis_weight = balance_heuristic(bsdf_sample_pdf, nb_bsdf_candidates, light_pdf, nb_light_candidates); candidate_weight = mis_weight * target_function / bsdf_sample_pdf; bsdf_RIS_sample.emission = shadow_light_ray_hit_info.hit_emission; bsdf_RIS_sample.point_on_light_source = bsdf_ray.origin + bsdf_ray.direction * shadow_light_ray_hit_info.hit_distance; bsdf_RIS_sample.is_bsdf_sample = true; bsdf_RIS_sample.bsdf_sample_contribution = bsdf_color; bsdf_RIS_sample.bsdf_sample_cosine_term = cosine_at_evaluated_point; bsdf_RIS_sample.target_function = target_function; } } reservoir.add_one_candidate(bsdf_RIS_sample, candidate_weight, random_number_generator); reservoir.sanity_check(); } reservoir.end(); return reservoir; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F sample_lights_RIS(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo& closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { if (render_data.buffers.emissive_triangles_count == 0) return ColorRGB32F(0.0f); RISReservoir reservoir = sample_bsdf_and_lights_RIS_reservoir(render_data, ray_payload, closest_hit_info, view_direction, random_number_generator); return evaluate_reservoir_sample(render_data, ray_payload, closest_hit_info, view_direction, reservoir, random_number_generator); } #endif ================================================ FILE: src/Device/includes/RIS/RIS_Reservoir.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RIS_RESERVOIR_H #define DEVICE_RIS_RESERVOIR_H #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Xorshift.h" #ifndef __KERNELCC__ #include "Utils/Utils.h" // For multithreaded console error logging on the CPU if NaNs are detected #include static std::mutex ris_log_mutex; #endif struct RISSample { ColorRGB32F emission; float3 point_on_light_source = { 0, 0, 0 }; float target_function = 0.0f; // TODO Can this be refactored? Is this needed? bool is_bsdf_sample = false; ColorRGB32F bsdf_sample_contribution; float bsdf_sample_cosine_term = 0.0f; }; struct RISReservoir { HIPRT_HOST_DEVICE void add_one_candidate(RISSample new_sample, float weight, Xorshift32Generator& random_number_generator) { M++; weight_sum += weight; if (random_number_generator() < weight / weight_sum) sample = new_sample; } HIPRT_HOST_DEVICE void end() { if (weight_sum == 0.0f) UCW = 0.0f; else UCW = 1.0f / sample.target_function * weight_sum; } HIPRT_HOST_DEVICE HIPRT_INLINE void sanity_check(int2 pixel_coords = make_int2(-1, -1)) { #ifndef __KERNELCC__ if (M < 0) { std::lock_guard lock(ris_log_mutex); std::cerr << "Negative reservoir M value at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << M << std::endl; Utils::debugbreak(); } else if (std::isnan(weight_sum) || std::isinf(weight_sum)) { std::lock_guard lock(ris_log_mutex); std::cerr << "NaN or inf reservoir weight_sum at pixel (" << pixel_coords.x << ", " << pixel_coords.y << ")" << std::endl; Utils::debugbreak(); } else if (weight_sum < 0) { std::lock_guard lock(ris_log_mutex); std::cerr << "Negative reservoir weight_sum at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << weight_sum << std::endl; Utils::debugbreak(); } else if (std::abs(weight_sum) < std::numeric_limits::min() && weight_sum != 0.0f) { std::lock_guard lock(ris_log_mutex); std::cerr << "Denormalized weight_sum at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << weight_sum << std::endl; Utils::debugbreak(); } else if (std::isnan(UCW) || std::isinf(UCW)) { std::lock_guard lock(ris_log_mutex); std::cerr << "NaN or inf reservoir UCW at pixel (" << pixel_coords.x << ", " << pixel_coords.y << ")" << std::endl; Utils::debugbreak(); } else if (UCW < 0) { std::lock_guard lock(ris_log_mutex); std::cerr << "Negative reservoir UCW at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << UCW << std::endl; Utils::debugbreak(); } else if (std::isnan(sample.target_function) || std::isinf(sample.target_function)) { std::lock_guard lock(ris_log_mutex); std::cerr << "NaN or inf reservoir sample.target_function at pixel (" << pixel_coords.x << ", " << pixel_coords.y << ")" << std::endl; Utils::debugbreak(); } else if (sample.target_function < 0) { std::lock_guard lock(ris_log_mutex); std::cerr << "Negative reservoir sample.target_function at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << sample.target_function << std::endl; Utils::debugbreak(); } #else (void)pixel_coords; #endif } unsigned int M = 0; // TODO weight sum is never used at the same time as UCW so only one variable can be used for both to save space float weight_sum = 0.0f; float UCW = 0.0f; RISSample sample; }; #endif ================================================ FILE: src/Device/includes/RayPayload.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RAY_PAYLOAD_H #define DEVICE_RAY_PAYLOAD_H #include "Device/includes/BSDFs/BSDFIncidentLightInfo.h" #include "Device/includes/RayVolumeState.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/KernelOptions/KernelOptions.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" enum RayState { BOUNCE, MISSED }; struct RayPayload { // Energy left in the ray after it bounces around the scene ColorRGB32F throughput = ColorRGB32F(1.0f); // Final color of the ray ColorRGB32F ray_color = ColorRGB32F(0.0f); // Camera ray is "Bounce" to give it a chance to hit the scene RayState next_ray_state = RayState::BOUNCE; // What bounce we're currently at int bounce = 0; // Roughness accumulated by the bounces of the ray along the path. In [0, 1] // // If a ray bounced on a Lambertian surface along its path for example, the // accumulated roughness is going to be 1.0f. // // If the camera ray bounced on a mirror, the accumulated roughness is going to be 0.0f at bounce == 1. // // If the ray bounced on a specular diffuse surface, the accumulated roughness is going to be that // of which lobe was sampled between the specular or diffuse // // The accumulated roughness is computed as the maximum between the current accumulated roughness // and the roughness of the lobe that was sampled to get the next bounce direction float accumulated_roughness = 0.0f; // Material of the current hit DeviceUnpackedEffectiveMaterial material; RayVolumeState volume_state; HIPRT_HOST_DEVICE void accumulate_roughness(BSDFIncidentLightInfo sampled_lobe) { switch (sampled_lobe) { case LIGHT_DIRECTION_SAMPLED_FROM_DIFFUSE_LOBE: case LIGHT_DIRECTION_SAMPLED_FROM_DIFFUSE_TRANSMISSION_LOBE: accumulated_roughness = 1.0f; break; case LIGHT_DIRECTION_SAMPLED_FROM_COAT_LOBE: accumulated_roughness = hippt::max(material.coat_roughness, accumulated_roughness); break; case LIGHT_DIRECTION_SAMPLED_FROM_FIRST_METAL_LOBE: accumulated_roughness = hippt::max(material.roughness, accumulated_roughness); break; case LIGHT_DIRECTION_SAMPLED_FROM_SECOND_METAL_LOBE: accumulated_roughness = hippt::max(material.second_roughness, accumulated_roughness); break; case LIGHT_DIRECTION_SAMPLED_FROM_SPECULAR_LOBE: // The specular roughness is just material.roughness accumulated_roughness = hippt::max(material.roughness, accumulated_roughness); break; case LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFLECT_LOBE: // The glass roughness is just material.roughness accumulated_roughness = hippt::max(material.roughness, accumulated_roughness); break; case LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFRACT_LOBE: // The glass roughness is just material.roughness accumulated_roughness = hippt::max(material.roughness, accumulated_roughness); break; default: break; } } }; #endif ================================================ FILE: src/Device/includes/RayVolumeState.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RAY_VOLUME_STATE_H #define DEVICE_RAY_VOLUME_STATE_H #include "Device/includes/NestedDielectrics.h" // Including dispersion for sampling a wavelength in the reconstruction of the first hit of RayVolumeState #include "Device/includes/Dispersion.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" struct RayVolumeState { /** * On the GPU, it is necessary that the RayVolumeState is initialized manually as opposed to in a default constructor for example. * That's because the nested dielectrics stack is in shared memory and is thus a "global variable". * * If it were to be initialized in the RayVolumeState constructor, every declaration of a RayVolumeState variable * would call the constructor and reinitialize the whole nested dielectrics stack. */ HIPRT_HOST_DEVICE RayVolumeState() { for (int i = 0; i < NestedDielectricsStackSize; i++) { interior_stack.stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(i)].set_priority(0); interior_stack.stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(i)].set_odd_parity(true); interior_stack.stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(i)].set_topmost(true); // Setting the material index to the maximum interior_stack.stack_entries[NESTED_DIELECTRICS_STACK_INDEX_SHIFT(i)].set_material_index(NestedDielectricsInteriorStack::MAX_MATERIAL_INDEX); } } HIPRT_HOST_DEVICE void reconstruct_first_hit(const DeviceUnpackedEffectiveMaterial& material, int* material_indices_buffer, int primitive_index, Xorshift32Generator& random_number_generator) { if (primitive_index == -1) // No primary hit i.e. straight into the envmap return; int mat_index = material_indices_buffer[primitive_index]; interior_stack.push( incident_mat_index, outgoing_mat_index, inside_material, mat_index, material.get_dielectric_priority()); if (material.dispersion_scale > 0.0f && material.specular_transmission > 0.0f && sampled_wavelength == 0.0f) // If we hit a dispersive material, we sample the wavelength that will be used // for computing the wavelength dependent IORs used for dispersion // // We're also not re-doing the sampling if a wavelength has already been sampled for that path // // Negating the wavelength to indicate that the throughput filter of the wavelength // hasn't been applied yet (applied in principled_glass_eval()) sampled_wavelength = -sample_wavelength_uniformly(random_number_generator); } // How far has the ray traveled in the current volume. float distance_in_volume = 0.0f; // The stack of materials being traversed. Used for nested dielectrics handling NestedDielectricsInteriorStack interior_stack; // Indices of the material we were in before hitting the current dielectric surface int incident_mat_index = -1, outgoing_mat_index = -1; // Whether or not we're exiting a material bool inside_material = false; // For spectral dispersion. A random wavelength is sampled and replaces this value // when a glass object is hit. This wavelength can then be used to determine the IOR // that should be used for refractions/reflections on the dielectric object. // // The wavelength is also used to apply a throughput filter on the ray such that only the // sampled wavelength's color travels around the scene. // // If this value is negative, this is because the ray throughput filter hasn't been applied // yet. If the value is positive, the filter has been applied float sampled_wavelength = 0.0f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/DI/FinalShading.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_FINAL_SHADING_H #define DEVICE_RESTIR_DI_FINAL_SHADING_H #include "Device/includes/LightSampling/Envmap.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/RenderData.h" // TODO make some simplification assuming that ReSTIR DI is never inside a surface (the camera being inside a surface may be an annoying case to handle) HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F evaluate_ReSTIR_DI_reservoir(const HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo& closest_hit_info, const float3& view_direction, const ReSTIRDIReservoir& reservoir, Xorshift32Generator& random_number_generator) { ColorRGB32F final_color; if (reservoir.UCW <= 0.0f) // No valid sample means no light contribution return ColorRGB32F(0.0f); ReSTIRDISample sample = reservoir.sample; float distance_to_light; float3 shadow_ray_direction; if (sample.is_envmap_sample()) { shadow_ray_direction = matrix_X_vec(render_data.world_settings.envmap_to_world_matrix, sample.point_on_light_source); distance_to_light = 1.0e35f; } else { shadow_ray_direction = sample.point_on_light_source - closest_hit_info.inter_point; shadow_ray_direction = shadow_ray_direction / (distance_to_light = hippt::length(shadow_ray_direction)); } bool in_shadow = false; if (sample.flags & ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED) in_shadow = false; else if (render_data.render_settings.restir_di_settings.do_final_shading_visibility) { hiprtRay shadow_ray; shadow_ray.origin = closest_hit_info.inter_point; shadow_ray.direction = shadow_ray_direction; in_shadow = evaluate_shadow_ray_occluded(render_data, shadow_ray, distance_to_light, closest_hit_info.primitive_index, /* bounce. Always 0 for ReSTIR */0, random_number_generator); } if (!in_shadow) { float bsdf_pdf; float cosine_at_evaluated_point; BSDFIncidentLightInfo incident_light_info = sample.flags_to_BSDF_incident_light_info(); BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, shadow_ray_direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, /* bounce. Always 0 for ReSTIR DI */ 0, 0.0f, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); cosine_at_evaluated_point = hippt::dot(closest_hit_info.shading_normal, shadow_ray_direction); if (sample.flags & ReSTIRDISampleFlags::RESTIR_DI_FLAGS_SAMPLED_FROM_GLASS_REFRACT_LOBE) // We're not allowing samples that are below the surface // UNLESS it's a BSDF refraction sample in which case it's valid // so we're restoring the cosine term to be > 0.0f so that it passes // the if() condition below cosine_at_evaluated_point = hippt::abs(cosine_at_evaluated_point); if (cosine_at_evaluated_point > 0.0f) { ColorRGB32F sample_emission; if (sample.is_envmap_sample()) { float envmap_pdf; sample_emission = envmap_eval(render_data, shadow_ray_direction, envmap_pdf); } else { int material_index = render_data.buffers.material_indices[sample.emissive_triangle_index]; sample_emission = render_data.buffers.materials_buffer.get_emission(material_index); } float area_measure_to_solid_angle_conversion; if (sample.is_envmap_sample()) area_measure_to_solid_angle_conversion = 1.0f; else { float3 emissive_triangle_normal = hippt::normalize(get_triangle_normal_not_normalized(render_data, sample.emissive_triangle_index)); area_measure_to_solid_angle_conversion = compute_cosine_term_at_light_source(emissive_triangle_normal, -shadow_ray_direction); area_measure_to_solid_angle_conversion /= hippt::square(distance_to_light); } final_color = bsdf_color * reservoir.UCW * sample_emission * cosine_at_evaluated_point * area_measure_to_solid_angle_conversion; } } return final_color; } HIPRT_HOST_DEVICE HIPRT_INLINE void validate_reservoir(const HIPRTRenderData& render_data, ReSTIRDIReservoir& reservoir) { if (reservoir.sample.is_envmap_sample() && render_data.world_settings.ambient_light_type != AmbientLightType::ENVMAP) // Killing the reservoir if it was an envmap sample but the envmap is not used anymore reservoir.UCW = 0.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F sample_light_ReSTIR_DI(const HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator, int2 pixel_coords) { int pixel_index = pixel_coords.x + pixel_coords.y * render_data.render_settings.render_resolution.x; // Because the spatial reuse pass runs last, the output buffer of the spatial // pass contains the reservoir whose sample we're going to shade ReSTIRDIReservoir& reservoir = render_data.render_settings.restir_di_settings.restir_output_reservoirs[pixel_index]; // Validates the reservoir i.e. kills the reservoir if it isn't valid // anymore i.e. if it refers to a light that doesn't exist anymore validate_reservoir(render_data, reservoir); return evaluate_ReSTIR_DI_reservoir(render_data, ray_payload, closest_hit_info, view_direction, reservoir, random_number_generator); } #endif ================================================ FILE: src/Device/includes/ReSTIR/DI/PresampledLight.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef RESTIR_DI_PRESAMPLED_LIGHT_H #define RESTIR_DI_PRESAMPLED_LIGHT_H #include "Device/includes/ReSTIR/DI/SampleFlags.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Math.h" struct ReSTIRDIPresampledLight { // Global primitive index corresponding to the emissive triangle sampled int emissive_triangle_index = -1; // For envmap samples, this 'point_on_light_source' is the envmap direction in *envmap space* // A sample is an envmap sample if 'flags' contains 'RESTIR_DI_FLAGS_ENVMAP_SAMPLE' float3 point_on_light_source = { 0, 0, 0 }; // Only defined if the sample isn't an envmap sample float3 light_source_normal = { 0, 0, 0 }; ColorRGB32F radiance; float pdf = 0.0f; // Some flags about the sample unsigned char flags = RESTIR_DI_FLAGS_NONE; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/DI/Reservoir.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_RESERVOIR_H #define DEVICE_RESTIR_DI_RESERVOIR_H #include "Device/includes/ReSTIR/DI/SampleFlags.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Xorshift.h" #ifndef __KERNELCC__ #include "Utils/Utils.h" // For multithreaded console error logging on the CPU if NaNs are detected #include static std::mutex restir_di_log_mutex; #endif struct ReSTIRDISample { // For envmap samples, this 'point_on_light_source' is the envmap direction in *envmap space* // A sample is an envmap sample if 'flags' contains 'RESTIR_DI_FLAGS_ENVMAP_SAMPLE' float3 point_on_light_source = { 0, 0, 0 }; // Global primitive index corresponding to the emissive triangle sampled int emissive_triangle_index = -1; float target_function = 0.0f; // Some flags about the sample unsigned char flags = RESTIR_DI_FLAGS_NONE; HIPRT_HOST_DEVICE bool is_envmap_sample() const { return flags & ReSTIRDISampleFlags::RESTIR_DI_FLAGS_ENVMAP_SAMPLE; } HIPRT_HOST_DEVICE static int flags_from_BSDF_incident_light_info(BSDFIncidentLightInfo sampled_lobe_info) { return static_cast(sampled_lobe_info); } HIPRT_HOST_DEVICE BSDFIncidentLightInfo flags_to_BSDF_incident_light_info() const { return static_cast(flags & (0b111111 << (BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_COAT_LOBE - 1))); } }; struct ReSTIRDIReservoir { HIPRT_HOST_DEVICE void add_one_candidate(ReSTIRDISample new_sample, float weight, Xorshift32Generator& random_number_generator) { M++; weight_sum += weight; if (random_number_generator() < weight / weight_sum) sample = new_sample; } /** * Combines 'other_reservoir' into this reservoir * * 'target_function' is the target function evaluated at the pixel that is doing the * resampling with the sample from the reservoir that we're combining (which is 'other_reservoir') * * 'jacobian_determinant' is the determinant of the jacobian. In ReSTIR DI, it is used * for converting the solid angle PDF (or UCW since the UCW is an estimate of the PDF) * with respect to the shading point of the reservoir we're resampling to the solid * angle PDF with respect to the shading point of 'this' reservoir * * 'random_number_generator' for generating the random number that will be used to stochastically * select the sample from 'other_reservoir' or not */ HIPRT_HOST_DEVICE bool combine_with(ReSTIRDIReservoir other_reservoir, float mis_weight, float target_function, float jacobian_determinant, Xorshift32Generator& random_number_generator) { if (other_reservoir.UCW <= 0.0f) { // Not going to be resampled anyways because of invalid UCW so quit exit M += other_reservoir.M; return false; } float reservoir_sample_weight = mis_weight * target_function * other_reservoir.UCW * jacobian_determinant; M += other_reservoir.M; weight_sum += reservoir_sample_weight; if (random_number_generator() < reservoir_sample_weight / weight_sum) { sample = other_reservoir.sample; sample.target_function = target_function; return true; } return false; } HIPRT_HOST_DEVICE void end() { if (weight_sum == 0.0f) UCW = 0.0f; else UCW = 1.0f / sample.target_function * weight_sum; } HIPRT_HOST_DEVICE void end_with_normalization(float normalization_numerator, float normalization_denominator) { // Checking some limit values if (weight_sum == 0.0f || weight_sum < 1.0e-10f || weight_sum > 1.0e10f || normalization_denominator == 0.0f || normalization_numerator == 0.0f) UCW = 0.0f; else UCW = 1.0f / sample.target_function * weight_sum * normalization_numerator / normalization_denominator; // Hard limiting M to avoid explosions if the user decides not to use any M-cap (M-cap == 0) M = hippt::min(M, 1000000); } HIPRT_HOST_DEVICE HIPRT_INLINE void sanity_check(int2 pixel_coords) { #ifndef __KERNELCC__ if (M < 0) { std::lock_guard lock(restir_di_log_mutex); std::cerr << "Negative reservoir M value at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << M << std::endl; Utils::debugbreak(); } else if (std::isnan(weight_sum) || std::isinf(weight_sum)) { std::lock_guard lock(restir_di_log_mutex); std::cerr << "NaN or inf reservoir weight_sum at pixel (" << pixel_coords.x << ", " << pixel_coords.y << ")" << std::endl; Utils::debugbreak(); } else if (weight_sum < 0) { std::lock_guard lock(restir_di_log_mutex); std::cerr << "Negative reservoir weight_sum at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << weight_sum << std::endl; Utils::debugbreak(); } else if (std::abs(weight_sum) < std::numeric_limits::min() && weight_sum != 0.0f) { std::lock_guard lock(restir_di_log_mutex); std::cerr << "Denormalized weight_sum at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << weight_sum << std::endl; Utils::debugbreak(); } else if (std::isnan(UCW) || std::isinf(UCW)) { std::lock_guard lock(restir_di_log_mutex); std::cerr << "NaN or inf reservoir UCW at pixel (" << pixel_coords.x << ", " << pixel_coords.y << ")" << std::endl; Utils::debugbreak(); } else if (UCW < 0) { std::lock_guard lock(restir_di_log_mutex); std::cerr << "Negative reservoir UCW at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << UCW << std::endl; Utils::debugbreak(); } else if (std::isnan(sample.target_function) || std::isinf(sample.target_function)) { std::lock_guard lock(restir_di_log_mutex); std::cerr << "NaN or inf reservoir sample.target_function at pixel (" << pixel_coords.x << ", " << pixel_coords.y << ")" << std::endl; Utils::debugbreak(); } else if (sample.target_function < 0) { std::lock_guard lock(restir_di_log_mutex); std::cerr << "Negative reservoir sample.target_function at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << sample.target_function << std::endl; Utils::debugbreak(); } #else (void)pixel_coords; #endif } int M = 0; // TODO weight sum is never used at the same time as UCW so only one variable can be used for both to save space float weight_sum = 0.0f; // If the UCW is set to -1, this is because the reservoir was killed by visibility reuse float UCW = 0.0f; ReSTIRDISample sample; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/DI/SampleFlags.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_SAMPLE_FLAGS_H #define DEVICE_RESTIR_DI_SAMPLE_FLAGS_H #include "Device/includes/BSDFs/BSDFIncidentLightInfo.h" enum ReSTIRDISampleFlags { RESTIR_DI_FLAGS_NONE = 0, // The sample is an evmap sample and 'point_on_light_source' // should be interpreted as a direction, not a point on a light source RESTIR_DI_FLAGS_ENVMAP_SAMPLE = 1 << 0, // The sample is a BSDF sample and we're indicating which lobe it comes from // so that when evaluating the reservoir in FinalShading, we know what lobe // the sample comes from and we can properly evaluate the BSDF // // We're reusing the values from the BSDFIncidentLightInfo enum here to be able // to convert easily from the ReSTIRDI flags back to BSDFIncidentLightInfo (i.e. // retrieve which lobe we sampled from the ReSTIRDISampleFlags) RESTIR_DI_FLAGS_SAMPLED_FROM_COAT_LOBE = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_COAT_LOBE, RESTIR_DI_FLAGS_SAMPLED_FROM_FIRST_METAL_LOBE = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_FIRST_METAL_LOBE, RESTIR_DI_FLAGS_SAMPLED_FROM_SECOND_METAL_LOBE = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_SECOND_METAL_LOBE, RESTIR_DI_FLAGS_SAMPLED_FROM_SPECULAR_LOBE = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_SPECULAR_LOBE, RESTIR_DI_FLAGS_SAMPLED_FROM_GLASS_REFLECT_LOBE = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFLECT_LOBE, RESTIR_DI_FLAGS_SAMPLED_FROM_GLASS_REFRACT_LOBE = BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFRACT_LOBE, // This sample *AT ITS OWN PIXEL* is unoccluded. This can be used to avoid tracing // rays for visibility since we know it's unoccluded already RESTIR_DI_FLAGS_UNOCCLUDED = 1 << 7 }; #endif ================================================ FILE: src/Device/includes/ReSTIR/DI/TargetFunction.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_TARGET_FUNCTION_H #define DEVICE_RESTIR_DI_TARGET_FUNCTION_H #include "Device/includes/ReSTIR/Utils.h" #include "HostDeviceCommon/RenderData.h" HIPRT_HOST_DEVICE HIPRT_INLINE float3 ReSTIR_DI_get_light_sample_direction(const HIPRTRenderData& render_data, const ReSTIRDISample& sample, float3 surface_shading_point, float& out_distance_to_light) { float3 sample_direction; if (sample.is_envmap_sample()) { sample_direction = matrix_X_vec(render_data.world_settings.envmap_to_world_matrix, sample.point_on_light_source); out_distance_to_light = 1.0e35f; } else { sample_direction = sample.point_on_light_source - surface_shading_point; sample_direction = sample_direction / (out_distance_to_light = hippt::length(sample_direction)); } return sample_direction; } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F ReSTIR_DI_get_light_sample_emission(const HIPRTRenderData& render_data, const ReSTIRDISample& sample, float3 sample_direction) { ColorRGB32F sample_emission; if (sample.is_envmap_sample()) { float envmap_pdf; sample_emission = envmap_eval(render_data, sample_direction, envmap_pdf); } else { int material_index = render_data.buffers.material_indices[sample.emissive_triangle_index]; sample_emission = render_data.buffers.materials_buffer.get_emission(material_index); } return sample_emission; } template HIPRT_HOST_DEVICE HIPRT_INLINE float ReSTIR_DI_evaluate_target_function(const HIPRTRenderData& render_data, const ReSTIRDISample& sample, ReSTIRSurface& surface, Xorshift32Generator& random_number_generator) { if (sample.emissive_triangle_index == -1 && !sample.is_envmap_sample()) // No sample return 0.0f; float bsdf_pdf; float distance_to_light; float3 sample_direction = ReSTIR_DI_get_light_sample_direction(render_data, sample, surface.shading_point, distance_to_light); float cosine_term = hippt::dot(surface.shading_normal, sample_direction); if (cosine_term <= 0.0f) // If the cosine term is 0.0f, the rest is going to be multiplied by that zero-cosine-term // and everything is going to be 0.0f anyway so we can return already return 0.0f; BSDFIncidentLightInfo incident_light_info = sample.flags_to_BSDF_incident_light_info(); BSDFContext bsdf_context(surface.view_direction, surface.shading_normal, surface.geometric_normal, sample_direction, incident_light_info, surface.ray_volume_state, false, surface.material, /* bounce. Always 0 for ReSTIR DI */ 0, 0.0f); ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); ColorRGB32F sample_emission = ReSTIR_DI_get_light_sample_emission(render_data, sample, sample_direction); float geometry_term = 1.0f; if (!sample.is_envmap_sample()) { float3 emissive_triangle_normal = hippt::normalize(get_triangle_normal_not_normalized(render_data, sample.emissive_triangle_index)); geometry_term = compute_cosine_term_at_light_source(emissive_triangle_normal, -sample_direction); geometry_term /= hippt::square(distance_to_light); } float target_function = (bsdf_color * sample_emission * cosine_term * geometry_term).luminance(); if (target_function == 0.0f) // Quick exit because computing the visiblity that follows isn't going // to change anything to the fact that we have 0.0f target function here return 0.0f; if constexpr (withVisibility) { hiprtRay shadow_ray; shadow_ray.origin = surface.shading_point; shadow_ray.direction = sample_direction; bool visible = !evaluate_shadow_ray_occluded(render_data, shadow_ray, distance_to_light, surface.primitive_index, /* bounce. Always 0 for ReSTIR DI*/ 0, random_number_generator); target_function *= visible; } return target_function; } #endif ================================================ FILE: src/Device/includes/ReSTIR/GI/InitialCandidatesUtils.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_RESTIR_GI_INITIAL_CANDIDATES_UTILS_H #define DEVICE_INCLUDES_RESTIR_GI_INITIAL_CANDIDATES_UTILS_H #include "Device/includes/PathTracing.h" HIPRT_HOST_DEVICE bool restir_gi_update_ray_throughputs(HIPRTRenderData& render_data, RayPayload& ray_payload, ColorRGB32F& ray_throughput_to_visible_point, HitInfo& closest_hit_info, ColorRGB32F bsdf_color, const float3& bounce_direction, float bsdf_pdf, Xorshift32Generator& random_number_generator) { ColorRGB32F throughput_attenuation = bsdf_color * hippt::abs(hippt::dot(bounce_direction, closest_hit_info.shading_normal)) / bsdf_pdf; ColorRGB32F dispersion_throughput = get_dispersion_ray_color(ray_payload.volume_state.sampled_wavelength, ray_payload.material.dispersion_scale); if (ray_payload.bounce > 0) { // With ReSTIR GI, we want the outgoing radiance from the second hit to the camera hit // This means that we're basically not taking the first hit into account and so we're not // updating the throughput (or the ray_color either, see the main loop) on the bounce 0 float rr_throughput_scaling = 1.0f; // Doing the russian roulette if (!do_russian_roulette(render_data.render_settings, ray_payload.bounce, ray_payload.throughput, rr_throughput_scaling, throughput_attenuation, random_number_generator)) { // Killed by russian roulette ray_throughput_to_visible_point = ColorRGB32F(0.0f); return false; } else { // Not killed by russian roulette so we're scaling the throughputs ray_throughput_to_visible_point *= rr_throughput_scaling; } // Dispersion ray throughput filter ray_throughput_to_visible_point *= dispersion_throughput; ray_throughput_to_visible_point *= throughput_attenuation; // Clamp every component to a minimum of 1.0e-5f to avoid numerical instabilities that can // happen: with some material, the throughput can get so low that it becomes denormalized and // this can cause issues in some parts of the renderer (most notably the NaN detection) ray_throughput_to_visible_point.max(ColorRGB32F(1.0e-5f, 1.0e-5f, 1.0e-5f)); } else { if (ray_payload.bounce >= render_data.render_settings.russian_roulette_min_depth && render_data.render_settings.do_russian_roulette) // Advancing the random number generation just to match non-ReSTIR GI path tracing in terms of randomness random_number_generator(); } ray_payload.throughput *= dispersion_throughput; ray_payload.throughput *= throughput_attenuation; // Clamp every component to a minimum of 1.0e-5f to avoid numerical instabilities that can // happen: with some material, the throughput can get so low that it becomes denormalized and // this can cause issues in some parts of the renderer (most notably the NaN detection) ray_payload.throughput.max(ColorRGB32F(1.0e-5f, 1.0e-5f, 1.0e-5f)); return true; } /** * Returns true if the bounce was sampled successfully, * false otherwise (is the BSDF sample failed, if russian roulette killed the sample, ...) */ HIPRT_HOST_DEVICE bool restir_gi_compute_next_indirect_bounce(HIPRTRenderData& render_data, RayPayload& ray_payload, ColorRGB32F& ray_throughput_to_visible_point, HitInfo& closest_hit_info, float3 view_direction, hiprtRay& out_ray, Xorshift32Generator& random_number_generator, BSDFIncidentLightInfo* incident_light_info = nullptr, float* out_bsdf_pdf = nullptr) { ColorRGB32F bsdf_color; float3 bounce_direction; float bsdf_pdf; path_tracing_sample_next_indirect_bounce(render_data, ray_payload, closest_hit_info, view_direction, bsdf_color, bounce_direction, bsdf_pdf, random_number_generator, incident_light_info); if (out_bsdf_pdf != nullptr) *out_bsdf_pdf = bsdf_pdf; // Terminate ray if bad sampling if (bsdf_pdf <= 0.0f) return false; if (!restir_gi_update_ray_throughputs(render_data, ray_payload, ray_throughput_to_visible_point, closest_hit_info, bsdf_color, bounce_direction, bsdf_pdf, random_number_generator)) return false; out_ray.origin = closest_hit_info.inter_point; out_ray.direction = bounce_direction; return true; } #endif ================================================ FILE: src/Device/includes/ReSTIR/GI/Reservoir.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_GI_RESERVOIR_H #define DEVICE_RESTIR_GI_RESERVOIR_H #include "Device/includes/BSDFs/BSDFIncidentLightInfo.h" #include "Device/includes/RayVolumeState.h" #include "HostDeviceCommon/Material/MaterialPacked.h" #include "HostDeviceCommon/Xorshift.h" #ifndef __KERNELCC__ #include "Utils/Utils.h" // For multithreaded console error logging on the CPU if NaNs are detected #include static std::mutex restir_gi_log_mutex; #endif struct ReSTIRGISample { float3 sample_point = make_float3(-1.0f, -1.0f, -1.0f); int sample_point_primitive_index = -1; RGBE9995Packed incoming_radiance_to_visible_point; BSDFIncidentLightInfo incident_light_info_at_visible_point = BSDFIncidentLightInfo::NO_INFO; // TODO is this one needed? I guess we're going to get a bunch of wrong shading where a sample was resampled and at shading time it hits an alpha geometry where that alpha geometry let the ray through at initial candidates sampling time. This should be unbiased? Maybe not actually. But is it that bad? unsigned int visible_to_sample_point_alpha_test_random_seed = 42; // TODO can be stored in outoging_radiance_to_first_hit? float target_function = 0.0f; // Whether or not the sample point is on a material that is rough enough to be reconnected // If the sample point is on a mirror for example, reconnecting to that point from our center pixel // is going to change the view direction of the mirror BSDF without changing the incident light // direction of the mirror BSDF and that's not going to work // // Also, because we do not re-evaluate the BSDF at the sample point, this would lead to some brightening // bias because this would be assuming that reconnecting to the mirror has non-zero energy, even with // the new view direction which is incorrect // // Is the bias bad if not using this? bool sample_point_rough_enough = false; Octahedral24BitNormalPadded32b sample_point_geometric_normal; HIPRT_HOST_DEVICE bool is_envmap_path() const { return sample_point_primitive_index == -1; } }; struct ReSTIRGIReservoir { HIPRT_HOST_DEVICE void add_one_candidate(ReSTIRGISample new_sample, float weight, Xorshift32Generator& random_number_generator) { M++; weight_sum += weight; if (random_number_generator() < weight / weight_sum) sample = new_sample; } /** * Combines 'other_reservoir' into this reservoir * * 'target_function' is the target function evaluated at the pixel that is doing the * resampling with the sample from the reservoir that we're combining (which is 'other_reservoir') * * 'jacobian_determinant' is the determinant of the jacobian. In ReSTIR DI, it is used * for converting the solid angle PDF (or UCW since the UCW is an estimate of the PDF) * with respect to the shading point of the reservoir we're resampling to the solid * angle PDF with respect to the shading point of 'this' reservoir * * 'random_number_generator' for generating the random number that will be used to stochastically * select the sample from 'other_reservoir' or not */ HIPRT_HOST_DEVICE bool combine_with(const ReSTIRGIReservoir& other_reservoir, float mis_weight, float target_function, float jacobian_determinant, Xorshift32Generator& random_number_generator) { // Bullet point 4. of the intro of Section 5.2 of [A Gentle Introduction to ReSTIR: Path Reuse in Real-time] https://intro-to-restir.cwyman.org/ float reservoir_resampling_weight = mis_weight * target_function * other_reservoir.UCW * jacobian_determinant; weight_sum += reservoir_resampling_weight; M += other_reservoir.M; if (random_number_generator() < reservoir_resampling_weight / weight_sum) { sample = other_reservoir.sample; sample.target_function = target_function; return true; } return false; } HIPRT_HOST_DEVICE void end() { if (weight_sum == 0.0f) UCW = 0.0f; else UCW = 1.0f / sample.target_function * weight_sum; } HIPRT_HOST_DEVICE void end_with_normalization(float normalization_numerator, float normalization_denominator) { // Checking some limit values if (weight_sum == 0.0f || weight_sum > 1.0e10f || normalization_denominator == 0.0f || normalization_numerator == 0.0f) UCW = 0.0f; else UCW = 1.0f / sample.target_function * weight_sum * normalization_numerator / normalization_denominator; // Hard limiting M to avoid explosions if the user decides not to use any M-cap (M-cap == 0) M = hippt::min(M, 1000000); } HIPRT_HOST_DEVICE HIPRT_INLINE void sanity_check(int2 pixel_coords) { #ifndef __KERNELCC__ if (M < 0) { std::lock_guard lock(restir_gi_log_mutex); std::cerr << "Negative reservoir M value at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << M << std::endl; Utils::debugbreak(); } else if (std::isnan(weight_sum) || std::isinf(weight_sum)) { std::lock_guard lock(restir_gi_log_mutex); std::cerr << "NaN or inf reservoir weight_sum at pixel (" << pixel_coords.x << ", " << pixel_coords.y << ")" << std::endl; Utils::debugbreak(); } else if (weight_sum < 0) { std::lock_guard lock(restir_gi_log_mutex); std::cerr << "Negative reservoir weight_sum at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << weight_sum << std::endl; Utils::debugbreak(); } else if (std::abs(weight_sum) < std::numeric_limits::min() && weight_sum != 0.0f) { std::lock_guard lock(restir_gi_log_mutex); std::cerr << "Denormalized weight_sum at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << weight_sum << std::endl; Utils::debugbreak(); } else if (std::isnan(UCW) || std::isinf(UCW)) { std::lock_guard lock(restir_gi_log_mutex); std::cerr << "NaN or inf reservoir UCW at pixel (" << pixel_coords.x << ", " << pixel_coords.y << ")" << std::endl; Utils::debugbreak(); } else if (UCW < 0) { std::lock_guard lock(restir_gi_log_mutex); std::cerr << "Negative reservoir UCW at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << UCW << std::endl; Utils::debugbreak(); } else if (std::isnan(sample.target_function) || std::isinf(sample.target_function)) { std::lock_guard lock(restir_gi_log_mutex); std::cerr << "NaN or inf reservoir sample.target_function at pixel (" << pixel_coords.x << ", " << pixel_coords.y << ")" << std::endl; Utils::debugbreak(); } else if (sample.target_function < 0) { std::lock_guard lock(restir_gi_log_mutex); std::cerr << "Negative reservoir sample.target_function at pixel (" << pixel_coords.x << ", " << pixel_coords.y << "): " << sample.target_function << std::endl; Utils::debugbreak(); } #else (void)pixel_coords; #endif } ReSTIRGISample sample; int M = 0; // TODO weight sum is never used at the same time as UCW so only one variable can be used for both to save space float weight_sum = 0.0f; // If the UCW is set to -1, this is because the reservoir was killed by visibility reuse float UCW = 0.0f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/GI/TargetFunction.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_GI_TARGET_FUNCTION_H #define DEVICE_RESTIR_GI_TARGET_FUNCTION_H #include "Device/includes/LightSampling/Lights.h" #include "Device/includes/ReSTIR/Jacobian.h" #include "Device/includes/ReSTIR/Surface.h" #include "Device/includes/ReSTIR/GI/Reservoir.h" #include "HostDeviceCommon/RenderData.h" template HIPRT_HOST_DEVICE float ReSTIR_GI_evaluate_target_function(const HIPRTRenderData& render_data, const ReSTIRGISample& sample, ReSTIRSurface& surface, Xorshift32Generator& random_number_generator) { float distance_to_sample_point; float3 incident_light_direction; if (sample.is_envmap_path()) { // For envmap path, the direction is stored in the 'sample_point' value incident_light_direction = sample.sample_point; distance_to_sample_point = 1.0e35f; } else { // Not an envmap path, the direction is the difference between the current shading // point and the reconnection point incident_light_direction = sample.sample_point - surface.shading_point; distance_to_sample_point = hippt::length(incident_light_direction); if (distance_to_sample_point <= 1.0e-6f) // To avoid numerical instabilities return 0.0f; incident_light_direction /= distance_to_sample_point; } float cosine_term = hippt::dot(incident_light_direction, surface.shading_normal); if (cosine_term <= 0.0f && sample.incident_light_info_at_visible_point != BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFRACT_LOBE) return 0.0f; else if constexpr (resamplingNeighbor) { // If resampling a neighbor, the target function is going to evaluate to 0.0f if the sample point of the neighbor // is specular: that is because when resampling a neighbor, i.e. reconnecting to the sample point of the neighbor, // we're changing the view direction of the BSDF at the sample point. // // And changing the view direction of a specular BSDF without changing the incident light direction (which we are not // modifying) isn't going to adhere to the law of perfect reflection and so the contribution of the BSDF at the neighbor's // sample point will be 0.0f. // // So that's why we're returning 0.0f here if (render_data.render_settings.restir_gi_settings.use_neighbor_sample_point_roughness_heuristic && !sample.sample_point_rough_enough) return 0.0f; } if constexpr (withVisiblity) { hiprtRay visibility_ray; visibility_ray.origin = surface.shading_point; visibility_ray.direction = incident_light_direction; Xorshift32Generator random_number_generator_alpha_test(sample.visible_to_sample_point_alpha_test_random_seed); bool sample_point_occluded = evaluate_shadow_ray_occluded(render_data, visibility_ray, distance_to_sample_point, surface.primitive_index, 0, random_number_generator_alpha_test); if (sample_point_occluded) return 0.0f; } float bsdf_pdf; BSDFContext bsdf_context(surface.view_direction, surface.shading_normal, surface.geometric_normal, incident_light_direction, const_cast(sample.incident_light_info_at_visible_point), surface.ray_volume_state, false, surface.material, 0, 0.0f, MicrofacetRegularization::RegularizationMode::NO_REGULARIZATION); ColorRGB32F visible_point_bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); if (bsdf_pdf > 0.0f) visible_point_bsdf_color *= hippt::abs(cosine_term); return (visible_point_bsdf_color * sample.incoming_radiance_to_visible_point.unpack()).luminance(); } #endif ================================================ FILE: src/Device/includes/ReSTIR/Jacobian.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_JACOBIAN_H #define DEVICE_RESTIR_JACOBIAN_H #include "Device/includes/LightSampling/LightUtils.h" #include "HostDeviceCommon/RenderData.h" HIPRT_HOST_DEVICE HIPRT_INLINE float get_jacobian_determinant_reconnection_shift(const float3& reconnection_point, const float3& reconnection_point_surface_normal, const float3& point_being_reconnected, const float3& vertex_before_reconnection_original_path, float jacobian_threshold) { float3 direction_to_reconnection_point_from_center = reconnection_point - point_being_reconnected; float3 direction_to_reconnection_point_from_neighbor = reconnection_point - vertex_before_reconnection_original_path; float distance_to_reconnection_point_from_center = hippt::length(direction_to_reconnection_point_from_center); float distance_to_reconnection_point_from_neighbor = hippt::length(direction_to_reconnection_point_from_neighbor); direction_to_reconnection_point_from_center /= distance_to_reconnection_point_from_center; direction_to_reconnection_point_from_neighbor /= distance_to_reconnection_point_from_neighbor; float cosine_at_reconnection_point_from_center = hippt::abs(hippt::dot(-direction_to_reconnection_point_from_center, reconnection_point_surface_normal)); float cosine_at_reconnection_point_from_neighbor = hippt::abs(hippt::dot(-direction_to_reconnection_point_from_neighbor, reconnection_point_surface_normal)); float cosine_ratio = cosine_at_reconnection_point_from_center / cosine_at_reconnection_point_from_neighbor; float distance_squared_ratio = (distance_to_reconnection_point_from_neighbor * distance_to_reconnection_point_from_neighbor) / (distance_to_reconnection_point_from_center * distance_to_reconnection_point_from_center); float jacobian_determinant = cosine_ratio * distance_squared_ratio; if (jacobian_determinant > jacobian_threshold || jacobian_determinant < 1.0f / jacobian_threshold || hippt::is_nan(jacobian_determinant) || hippt::is_inf(jacobian_determinant)) // Samples are too dissimilar, returning 0 to indicate that we must reject the sample return 0.0f; else return jacobian_determinant; } #endif ================================================ FILE: src/Device/includes/ReSTIR/MISWeightsCommon.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_MIS_WEIGHTS_COMMON_H #define DEVICE_RESTIR_DI_MIS_WEIGHTS_COMMON_H /** * Forward declarations */ struct ReSTIRDISample; struct ReSTIRGISample; struct ReSTIRDIReservoir; struct ReSTIRGIReservoir; /** * The ReSTIRTypeStruct is used to automatically determine what SampleType to use * based on the 'IsReSTIRGI' template parameter * * This allows us to use the ReSTIRDISample type of ReSTIRGISample type automatically * based on whether or not we're instantiating the structures for ReSTIR DI or ReSTIR GI * * This sample type is then used in some of the specialization to pass to the target functions */ template struct ReSTIRTypeStruct {}; template <> struct ReSTIRTypeStruct { using SampleType = ReSTIRDISample; using ReservoirType = ReSTIRDIReservoir; }; template <> struct ReSTIRTypeStruct { using SampleType = ReSTIRGISample; using ReservoirType = ReSTIRGIReservoir; }; template using ReSTIRSampleType = typename ReSTIRTypeStruct::SampleType; template using ReSTIRReservoirType = typename ReSTIRTypeStruct::ReservoirType; HIPRT_DEVICE float symmetric_ratio_MIS_weights_difference_function(float target_function_at_center, float target_function_from_i, float exponent) { if (target_function_at_center == 0.0f || target_function_from_i == 0.0f) return 0.0f; float ratio = hippt::min(target_function_at_center / target_function_from_i, target_function_from_i / target_function_at_center); if (exponent == 2.0f) return hippt::square(ratio); else if (exponent == 3.0f) return hippt::pow_3(ratio); else if (exponent == 4.0f) return hippt::pow_4(ratio); else return powf(ratio, exponent); } #endif ================================================ FILE: src/Device/includes/ReSTIR/NeighborSimilarity.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_NEIGHBOR_SIMILARITY_H #define DEVICE_RESTIR_NEIGHBOR_SIMILARITY_H #include "Device/includes/ReSTIR/Jacobian.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/ReSTIRSettingsHelper.h" /** * Returns true if the two given points pass the plane distance check, false otherwise */ HIPRT_HOST_DEVICE HIPRT_INLINE bool plane_distance_heuristic(const ReSTIRCommonNeighborSimiliaritySettings& neighbor_similarity_settings, const float3& temporal_world_space_point, const float3& current_point, const float3& current_surface_normal, float plane_distance_threshold) { if (!neighbor_similarity_settings.use_plane_distance_heuristic) return true; float3 direction_between_points = temporal_world_space_point - current_point; float distance_to_plane = hippt::abs(hippt::dot(direction_between_points, current_surface_normal)); return distance_to_plane < plane_distance_threshold; } HIPRT_HOST_DEVICE HIPRT_INLINE bool normal_similarity_heuristic(const ReSTIRCommonNeighborSimiliaritySettings& neighbor_similarity_settings, const float3& current_normal, const float3& neighbor_normal, float threshold) { if (!neighbor_similarity_settings.use_normal_similarity_heuristic) return true; return hippt::dot(current_normal, neighbor_normal) > threshold; } HIPRT_HOST_DEVICE HIPRT_INLINE bool roughness_similarity_heuristic(const ReSTIRCommonNeighborSimiliaritySettings& neighbor_similarity_settings, float neighbor_roughness, float center_pixel_roughness, float threshold) { if (!neighbor_similarity_settings.use_roughness_similarity_heuristic) return true; // We don't want to temporally reuse on materials smoother than 0.075f because this // causes near-specular/glossy reflections to darken when camera ray jittering is used. // // This glossy reflections darkening only happens with confidence weights and // ray jittering but I'm not sure why. Probably because samples from one pixel (or sub-pixel location) // cannot efficiently be reused at another pixel (or sub-pixel location through jittering) // but confidence weights overweight these bad neighbor samples --> you end up using these // bad samples --> the shading loses in energy since we're now shading with samples that // don't align well with the glossy reflection direction return hippt::abs(neighbor_roughness - center_pixel_roughness) < threshold; } template HIPRT_HOST_DEVICE HIPRT_INLINE bool check_neighbor_similarity_heuristics(const HIPRTRenderData& render_data, int neighbor_pixel_index, int center_pixel_index, const float3& current_shading_point, const float3& current_normal, bool previous_frame = false) { if (neighbor_pixel_index == center_pixel_index) // A pixel always passes the similarity test with itself return true; if (previous_frame) { if (render_data.g_buffer_prev_frame.first_hit_prim_index[neighbor_pixel_index] == -1) // Cannot reuse from a neighbor that doesn't have a primary hit (direct miss into the envmap) return false; } else { if (render_data.g_buffer.first_hit_prim_index[neighbor_pixel_index] == -1) // Cannot reuse from a neighbor that doesn't have a primary hit (direct miss into the envmap) return false; } const ReSTIRCommonNeighborSimiliaritySettings& neighbor_similarity_settings = ReSTIRSettingsHelper::get_restir_neighbor_similarity_settings(render_data); float3 neighbor_world_space_point; float neighbor_roughness = 0.0f; float current_material_roughness = 0.0f; if (previous_frame) { if (neighbor_similarity_settings.use_plane_distance_heuristic) // Only getting the point plane distance heuristic, otherwise it's never used neighbor_world_space_point = render_data.g_buffer_prev_frame.primary_hit_position[neighbor_pixel_index]; if (neighbor_similarity_settings.use_roughness_similarity_heuristic) // Only getting the roughness for the roughness heuristic otherwise it's not going to be used neighbor_roughness = render_data.g_buffer_prev_frame.materials[neighbor_pixel_index].get_roughness(); } else { neighbor_world_space_point = render_data.g_buffer.primary_hit_position[neighbor_pixel_index]; neighbor_roughness = render_data.g_buffer.materials[neighbor_pixel_index].get_roughness(); } if (neighbor_similarity_settings.use_roughness_similarity_heuristic) // Getting the roughness at the current point current_material_roughness = render_data.g_buffer.materials[center_pixel_index].get_roughness(); float3 neighbor_normal = neighbor_similarity_settings.reject_using_geometric_normals ? render_data.g_buffer.geometric_normals[neighbor_pixel_index].unpack() : render_data.g_buffer.shading_normals[neighbor_pixel_index].unpack(); bool plane_distance_passed = plane_distance_heuristic(neighbor_similarity_settings, neighbor_world_space_point, current_shading_point, current_normal, neighbor_similarity_settings.plane_distance_threshold); bool normal_similarity_passed = normal_similarity_heuristic(neighbor_similarity_settings, current_normal, neighbor_normal, neighbor_similarity_settings.normal_similarity_angle_precomp); bool roughness_similarity_passed = roughness_similarity_heuristic(neighbor_similarity_settings, neighbor_roughness, current_material_roughness, neighbor_similarity_settings.roughness_similarity_threshold); return plane_distance_passed && normal_similarity_passed && roughness_similarity_passed; } #endif ================================================ FILE: src/Device/includes/ReSTIR/OptimalVisibilitySampling.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_OPTIMAL_VISIBILITY_SAMPLING_H #define DEVICE_RESTIR_OPTIMAL_VISIBILITY_SAMPLING_H #include "Device/includes/ReSTIR/MISWeightsCommon.h" // For the ReSTIRReservoirType #include "Device/includes/ReSTIR/Utils.h" // For the ReSTIRReservoirType #include "HostDeviceCommon/KernelOptions/ReSTIRDIOptions.h" #include "HostDeviceCommon/KernelOptions/ReSTIRGIOptions.h" #include "HostDeviceCommon/RenderData.h" template HIPRT_DEVICE bool ReSTIR_optimal_visibility_sampling(HIPRTRenderData& render_data, ReSTIRReservoirType& spatial_reuse_output_reservoir, const ReSTIRReservoirType& center_pixel_reservoir, ReSTIRSurface& center_pixel_surface, int neighbor_index, int reused_neighbors_count, Xorshift32Generator& random_number_generator) { #if ReSTIR_DI_DoOptimalVisibilitySampling == KERNEL_OPTION_TRUE || ReSTIR_GI_DoOptimalVisibilitySampling == KERNEL_OPTION_TRUE bool at_least_one_neighbor_resampled = spatial_reuse_output_reservoir.weight_sum > 0.0f; bool last_neighbor_before_canonical = neighbor_index == reused_neighbors_count - 1; constexpr bool ovs_enabled = (!IsReSTIRGI && ReSTIR_DI_DoOptimalVisibilitySampling == KERNEL_OPTION_TRUE) || (IsReSTIRGI && ReSTIR_GI_DoOptimalVisibilitySampling == KERNEL_OPTION_TRUE); if (at_least_one_neighbor_resampled && last_neighbor_before_canonical && ovs_enabled) { // If the spatial neighbors resampled up until now are occluded, they will be discarded by this // visiblity test and so the canonical sample will be the resulting reservoir bool reservoir_killed; if constexpr (IsReSTIRGI) reservoir_killed = ReSTIR_GI_visibility_validation(render_data, spatial_reuse_output_reservoir, center_pixel_surface.shading_point, center_pixel_surface.last_hit_primitive_index, random_number_generator); else reservoir_killed = ReSTIR_DI_visibility_test_kill_reservoir(render_data, spatial_reuse_output_reservoir, center_pixel_surface.shading_point, center_pixel_surface.last_hit_primitive_index, random_number_generator); if (reservoir_killed) spatial_reuse_output_reservoir.weight_sum = 0.0f; return reservoir_killed; } #endif return false; } #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/FinalShading.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDE_REGIR_FINAL_SHADING_H #define DEVICE_INCLUDE_REGIR_FINAL_SHADING_H #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/LightUtils.h" #include "HostDeviceCommon/RenderData.h" HIPRT_DEVICE HIPRT_INLINE ColorRGB32F sample_one_light_ReGIR(HIPRTRenderData& render_data, RayPayload& ray_payload, const HitInfo closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { if (!MaterialUtils::can_do_light_sampling(ray_payload.material)) return ColorRGB32F(0.0f); bool point_outside_grid = false; ReGIRShadingAdditionalInfo additional_infos; LightSampleInformation light_sample = sample_one_emissive_triangle_regir_with_info(render_data, closest_hit_info.inter_point, view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, closest_hit_info.primitive_index, ray_payload, point_outside_grid, random_number_generator, additional_infos); if (!point_outside_grid) { if (light_sample.area_measure_pdf <= 0.0f) // Can happen for very small triangles return ColorRGB32F(0.0f); #if ReGIR_ShadingResamplingShadeAllSamples == KERNEL_OPTION_TRUE // If we're shading all samples, we already have the perfectly computed // radiance in additional_infos so we can just return that return additional_infos.sample_radiance; #endif // ReGIR succeeded with sampling, just shooting a shadow ray to validate visibility float3 shadow_ray_origin = closest_hit_info.inter_point; float3 shadow_ray_direction = light_sample.point_on_light - shadow_ray_origin; float distance_to_light = hippt::length(shadow_ray_direction); float3 shadow_ray_direction_normalized = shadow_ray_direction / distance_to_light; hiprtRay shadow_ray; shadow_ray.origin = shadow_ray_origin; shadow_ray.direction = shadow_ray_direction_normalized; // NEE++ context for the shadow ray NEEPlusPlusContext nee_plus_plus_context; nee_plus_plus_context.point_on_light = light_sample.point_on_light; nee_plus_plus_context.shaded_point = shadow_ray_origin; bool in_shadow = evaluate_shadow_ray_nee_plus_plus(render_data, shadow_ray, distance_to_light, closest_hit_info.primitive_index, nee_plus_plus_context, random_number_generator, ray_payload.bounce); if (!in_shadow) return additional_infos.sample_radiance / light_sample.area_measure_pdf / nee_plus_plus_context.unoccluded_probability; else return ColorRGB32F(0.0f); } else { #if ReGIR_DebugMode == REGIR_DEBUG_MODE_SAMPLING_FALLBACK return ColorRGB32F(1.0e10f, 0.0f, 1.0e10f); #endif #if ReGIR_FallbackLightSamplingStrategy == LSS_BASE_REGIR // Invalid fallback strategy invalid ReGIR light sampling fallback strategy #endif // Fallback method as the point was outside of the ReGIR grid light_sample = sample_one_emissive_triangle(render_data, closest_hit_info.inter_point, view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, closest_hit_info.primitive_index, ray_payload, random_number_generator); float3 shadow_ray_origin = closest_hit_info.inter_point; float3 shadow_ray_direction = light_sample.point_on_light - shadow_ray_origin; float distance_to_light = hippt::length(shadow_ray_direction); float3 shadow_ray_direction_normalized = shadow_ray_direction / distance_to_light; hiprtRay shadow_ray; shadow_ray.origin = shadow_ray_origin; shadow_ray.direction = shadow_ray_direction_normalized; // NEE++ context for the shadow ray NEEPlusPlusContext nee_plus_plus_context; nee_plus_plus_context.point_on_light = light_sample.point_on_light; nee_plus_plus_context.shaded_point = shadow_ray_origin; ColorRGB32F light_source_radiance; // abs() here to allow backfacing light sources float dot_light_source = compute_cosine_term_at_light_source(light_sample.light_source_normal, -shadow_ray.direction); if (dot_light_source > 0.0f) { bool in_shadow = evaluate_shadow_ray_nee_plus_plus(render_data, shadow_ray, distance_to_light, closest_hit_info.primitive_index, nee_plus_plus_context, random_number_generator, ray_payload.bounce); if (!in_shadow) { float bsdf_pdf; BSDFIncidentLightInfo incident_light_info = light_sample.incident_light_info; #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE && DirectLightSamplingBaseStrategy == LSS_BASE_REGIR BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, shadow_ray.direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); #else BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, shadow_ray.direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC); #endif ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, random_number_generator); if (bsdf_pdf != 0.0f) { // Conversion to solid angle from surface area measure float light_sample_solid_angle_pdf = area_to_solid_angle_pdf(light_sample.area_measure_pdf, distance_to_light, dot_light_source); if (light_sample_solid_angle_pdf > 0.0f) { float cosine_term = hippt::abs(hippt::dot(closest_hit_info.shading_normal, shadow_ray.direction)); light_source_radiance = light_sample.emission * cosine_term * bsdf_color / light_sample_solid_angle_pdf / nee_plus_plus_context.unoccluded_probability; // Just a CPU-only sanity check sanity_check(render_data, light_source_radiance, 0, 0); return light_source_radiance; } } } } } return ColorRGB32F(0.0f); } #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/GridFillSurface.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_REGIR_GRID_FILL_SURFACE_H #define DEVICE_INCLUDES_REGIR_GRID_FILL_SURFACE_H struct ReGIRGridFillSurface { int cell_primitive_index = -1; float3 cell_point = make_float3(0.0f, 0.0f, 0.0f); float3 cell_normal = make_float3(0.0f, 0.0f, 0.0f); float cell_roughness = -1.0f; float cell_metallic = -1.0f; float cell_specular = -1.0f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/HashGridCellData.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_HASH_GRID_CELL_DATA_H #define DEVICE_KERNELS_REGIR_HASH_GRID_CELL_DATA_H #include "HostDeviceCommon/Packing.h" struct ReGIRHashCellDataSoADevice { static constexpr float UNDEFINED_DISTANCE = -42.0f; static constexpr unsigned int UNDEFINED_POINT = 0xFFFFFFFF; static constexpr float3 UNDEFINED_NORMAL = { 0.0f, 0.0f, 0.0f }; static constexpr int UNDEFINED_PRIMITIVE = -1; // These three buffers are only allocated per each cell, not per each reservoir so they are // 'number_cells' in size // Buffer that holds the index of the thread that inserted into that grid cell AtomicType* hit_primitive = nullptr; float3* world_points = nullptr; Octahedral24BitNormalPadded32b* world_normals = nullptr; // TODO these guys in a single buffer to have only one memory access unsigned char* roughness = nullptr; unsigned char* specular = nullptr; unsigned char* metallic = nullptr; // The checksum for each entry of the table to check for collisions AtomicType* checksums = nullptr; // The staging buffer is used to store the grid cells that are alive during shading: for each grid cell that a ray falls into during shading, // we position the unsigned char to 1 // // We need a staging buffer to do that because modifying the 'grid_cell_alive' buffer directly would be a race condition since other threads // may be reading from that buffer at the same time to see if a cell is alive or not // // That staging buffer is then copied to the 'grid_cell_alive' buffer at the end of the frame AtomicType* grid_cell_alive = nullptr; unsigned int* grid_cells_alive_list = nullptr; AtomicType* grid_cells_alive_count = nullptr; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/HashGridSoADevice.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_REGIR_HASH_GRID_SOA_DEVICE_H #define DEVICE_INCLUDES_REGIR_HASH_GRID_SOA_DEVICE_H #include "Device/includes/ReSTIR/ReGIR/ReservoirSoA.h" struct ReGIRHashGridSoADevice { // These two SoAs are allocated to hold 'number_cells * number_reservoirs_per_cell' // So for a given 'hash_grid_cell_index', the cell contains reservoirs and samples going from // reservoirs[hash_grid_cell_index * number_reservoirs_per_cell] to reservoirs[cell_index * number_reservoirs_per_cell + number_reservoirs_per_cell[ ReGIRReservoirSoADevice reservoirs; ReGIRSampleSoADevice samples; unsigned int m_total_number_of_cells = 0; }; #endif // DEVICE_INCLUDES_REGIR_HASH_GRID_SOA_DEVICE_H ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/PresampledLight.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_PRESAMPLED_LIGHT_H #define DEVICE_KERNELS_REGIR_PRESAMPLED_LIGHT_H #include "HostDeviceCommon/Packing.h" struct ReGIRPresampledLight { // Index in the whole scene of the triangle sampled int emissive_triangle_index = -1; // Area of the sampled triangle float triangle_area = 0.0f; // Point sampled on the light float3 point_on_light = make_float3(0.0f, 0.0f, 0.0f); // Packed normal of the sampled emissive triangle Octahedral24BitNormalPadded32b normal; // Emission strength of the triangle ColorRGB32F emission; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/ReGIRHashGrid.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_REGIR_HASH_GRID_H #define DEVICE_INCLUDES_REGIR_HASH_GRID_H #include "Device/includes/HashGrid.h" #include "Device/includes/HashGridHash.h" #include "Device/includes/ReSTIR/ReGIR/HashGridCellData.h" #include "Device/includes/ReSTIR/ReGIR/HashGridSoADevice.h" #include "Device/includes/ReSTIR/ReGIR/ShadingSettings.h" #include "Device/includes/ReSTIR/ReGIR/ReservoirSoA.h" #include "HostDeviceCommon/HIPRTCamera.h" #include "HostDeviceCommon/KernelOptions/KernelOptions.h" #include "HostDeviceCommon/KernelOptions/ReGIROptions.h" struct ReGIRHashGrid { HIPRT_DEVICE static float compute_adaptive_cell_size_roughness(float3 world_position, const HIPRTCamera& current_camera, float roughness, bool primary_hit, float target_projected_size, float grid_cell_min_size) { int width = current_camera.sensor_width; int height = current_camera.sensor_height; #if ReGIR_HashGridAdaptiveRoughnessGridPrecision == KERNEL_OPTION_TRUE && (BSDFOverride != BSDF_LAMBERTIAN && BSDFOverride != BSDF_OREN_NAYAR) if (primary_hit) { // Only increasing the resolution for the primary hit cells where // we can actually use that resolution for resampling according to the BSDF // // For secondary grid cells, we cannot resample according to the BSDF because // we do not have the view direction so there's no point increasing the resolution. if (roughness >= 0.08f && roughness < 0.2f) { float t = hippt::inverse_lerp(roughness, 0.08f, 0.2f); float res_increase_factor = hippt::lerp(2.0f, 5.0f, 1.0f - t); target_projected_size /= res_increase_factor; grid_cell_min_size /= res_increase_factor; } else if (roughness >= 0.2f && roughness < 0.35f) { float t = hippt::inverse_lerp(roughness, 0.2f, 0.35f); float res_increase_factor = hippt::lerp(1.0f, 2.0f, 1.0f - t); target_projected_size /= res_increase_factor; grid_cell_min_size /= res_increase_factor; } } #endif #if ReGIR_HashGridConstantGridCellSize == KERNEL_OPTION_TRUE return grid_cell_min_size; #else float cell_size_step = hippt::length(world_position - current_camera.position) * tanf(target_projected_size * current_camera.vertical_fov * hippt::max(1.0f / height, (float)height / hippt::square(width))); float log_step = floorf(log2f(cell_size_step / grid_cell_min_size)); return hippt::max(grid_cell_min_size, grid_cell_min_size * exp2f(log_step)); #endif } HIPRT_DEVICE unsigned int custom_regir_hash(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, unsigned int total_number_of_cells, unsigned int& out_checksum) const { float cell_size = ReGIRHashGrid::compute_adaptive_cell_size_roughness(world_position, current_camera, roughness, primary_hit, m_grid_cell_target_projected_size, m_grid_cell_min_size); // Reference: SIGGRAPH 2022 - Advances in Spatial Hashing world_position = hash_periodic_shifting(world_position, cell_size); unsigned int grid_coord_x = static_cast(floorf(world_position.x / cell_size)); unsigned int grid_coord_y = static_cast(floorf(world_position.y / cell_size)); unsigned int grid_coord_z = static_cast(floorf(world_position.z / cell_size)); // Using two hash functions as proposed in [WORLD-SPACE SPATIOTEMPORAL RESERVOIR REUSE FOR RAY-TRACED GLOBAL ILLUMINATION, Boisse, 2021] #if ReGIR_HashGridHashSurfaceNormal == KERNEL_OPTION_TRUE // And adding normal hasing from [World-Space Spatiotemporal Path Resampling for Path Tracing, 2023] unsigned int quantized_normal = hash_quantize_normal(surface_normal, primary_hit ? ReGIR_HashGridHashSurfaceNormalResolutionPrimaryHits : ReGIR_HashGridHashSurfaceNormalResolutionSecondaryHits); unsigned int checksum = h2_xxhash32(quantized_normal + h2_xxhash32(cell_size + h2_xxhash32(grid_coord_z + h2_xxhash32(grid_coord_y + h2_xxhash32(grid_coord_x))))); unsigned int cell_hash = h1_pcg(quantized_normal + h1_pcg(cell_size + h1_pcg(grid_coord_z + h1_pcg(grid_coord_y + h1_pcg(grid_coord_x))))) % total_number_of_cells; #else unsigned int checksum = h2_xxhash32(cell_size + h2_xxhash32(grid_coord_z + h2_xxhash32(grid_coord_y + h2_xxhash32(grid_coord_x)))); unsigned int cell_hash = h1_pcg(cell_size + h1_pcg(grid_coord_z + h1_pcg(grid_coord_y + h1_pcg(grid_coord_x)))) % total_number_of_cells; #endif out_checksum = checksum; return cell_hash; } HIPRT_DEVICE void reset_reservoir(ReGIRHashGridSoADevice& soa, unsigned int hash_grid_cell_index, unsigned int reservoir_index_in_cell) { int reservoir_index_in_grid = hash_grid_cell_index * soa.reservoirs.number_of_reservoirs_per_cell + reservoir_index_in_cell; soa.reservoirs.store_reservoir_opt(reservoir_index_in_grid, ReGIRReservoir()); soa.samples.store_sample(reservoir_index_in_grid, ReGIRReservoir().sample); } /** * Overload if you already the hash grid cell index */ HIPRT_DEVICE void store_reservoir_and_sample_opt(const ReGIRReservoir& reservoir, ReGIRHashGridSoADevice& soa, unsigned int hash_grid_cell_index, int reservoir_index_in_cell) { int reservoir_index_in_grid = hash_grid_cell_index * soa.reservoirs.number_of_reservoirs_per_cell + reservoir_index_in_cell; store_full_reservoir(soa, reservoir, reservoir_index_in_grid); } HIPRT_DEVICE void store_reservoir_and_sample_opt(const ReGIRReservoir& reservoir, ReGIRHashGridSoADevice& soa, ReGIRHashCellDataSoADevice& hash_cell_data, float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, int reservoir_index_in_cell) { unsigned int hash_key; unsigned int hash_grid_cell_index = custom_regir_hash(world_position, surface_normal, current_camera, roughness, primary_hit, soa.m_total_number_of_cells, hash_key); if (!HashGrid::resolve_collision(hash_cell_data.checksums, soa.m_total_number_of_cells, hash_grid_cell_index, hash_key)) return; store_reservoir_and_sample_opt(reservoir, soa, hash_grid_cell_index, reservoir_index_in_cell); } HIPRT_DEVICE unsigned int get_hash_grid_cell_index(const ReGIRHashGridSoADevice& soa, const ReGIRHashCellDataSoADevice& hash_cell_data, float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit) const { unsigned int hash_key; unsigned int hash_grid_cell_index = custom_regir_hash(world_position, surface_normal, current_camera, roughness, primary_hit, soa.m_total_number_of_cells, hash_key); unsigned int original = hash_grid_cell_index; if (!HashGrid::resolve_collision(hash_cell_data.checksums, soa.m_total_number_of_cells, hash_grid_cell_index, hash_key) || hash_cell_data.grid_cell_alive[hash_grid_cell_index] == 0u) return HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX; return hash_grid_cell_index; } /** * Overload if you already the hash grid cell index */ HIPRT_DEVICE unsigned int get_reservoir_index_in_grid(const ReGIRHashGridSoADevice& soa, unsigned int hash_grid_cell_index, int reservoir_index_in_cell) const { return hash_grid_cell_index * soa.reservoirs.number_of_reservoirs_per_cell + reservoir_index_in_cell; } HIPRT_DEVICE unsigned int get_reservoir_index_in_grid(const ReGIRHashGridSoADevice& soa, const ReGIRHashCellDataSoADevice& hash_cell_data, float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, int reservoir_index_in_cell) const { unsigned int hash_grid_cell_index = get_hash_grid_cell_index(soa, hash_cell_data, world_position, surface_normal, current_camera, roughness, primary_hit); if (hash_grid_cell_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) return HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX; return get_reservoir_index_in_grid(soa, hash_grid_cell_index, reservoir_index_in_cell); } HIPRT_DEVICE void store_full_reservoir(ReGIRHashGridSoADevice& soa, const ReGIRReservoir& reservoir, int reservoir_index_in_grid) { if (reservoir.UCW <= 0.0f) { soa.reservoirs.UCW[reservoir_index_in_grid] = reservoir.UCW; // No need to store the rest if the UCW is invalid, we can already return return; } soa.reservoirs.store_reservoir_opt(reservoir_index_in_grid, reservoir); soa.samples.store_sample(reservoir_index_in_grid, reservoir.sample); } HIPRT_DEVICE ReGIRReservoir read_full_reservoir(const ReGIRHashGridSoADevice& soa, unsigned int reservoir_index_in_grid) const { if (reservoir_index_in_grid == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) return ReGIRReservoir(); ReGIRReservoir reservoir; float UCW = soa.reservoirs.UCW[reservoir_index_in_grid]; if (UCW <= 0.0f) { // If the reservoir doesn't have a valid sample, not even reading the rest of it ReGIRReservoir out; out.UCW = UCW; return out; } reservoir = soa.reservoirs.read_reservoir(reservoir_index_in_grid); reservoir.UCW = UCW; reservoir.sample = soa.samples.read_sample(reservoir_index_in_grid); return reservoir; } /** * Override if you already have the hash grid cell index */ HIPRT_DEVICE ReGIRReservoir read_full_reservoir(const ReGIRHashGridSoADevice& soa, unsigned int hash_grid_cell_index, int reservoir_index_in_cell, bool* out_invalid_sample = nullptr) const { unsigned int reservoir_index_in_grid = get_reservoir_index_in_grid(soa, hash_grid_cell_index, reservoir_index_in_cell); if (out_invalid_sample) { if (reservoir_index_in_grid == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) *out_invalid_sample = true; else *out_invalid_sample = false; } return read_full_reservoir(soa, reservoir_index_in_grid); } HIPRT_DEVICE ReGIRReservoir read_full_reservoir(const ReGIRHashGridSoADevice& soa, const ReGIRHashCellDataSoADevice& hash_cell_data, float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, int reservoir_index_in_cell, bool* out_invalid_sample = nullptr) const { unsigned int reservoir_index_in_grid = get_reservoir_index_in_grid(soa, hash_cell_data, world_position, surface_normal, current_camera, roughness, primary_hit, reservoir_index_in_cell); if (out_invalid_sample) { if (reservoir_index_in_grid == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) *out_invalid_sample = true; else *out_invalid_sample = false; } return read_full_reservoir(soa, reservoir_index_in_grid); } HIPRT_DEVICE unsigned int get_hash_grid_cell_index_from_world_pos(const ReGIRHashGridSoADevice& soa, const ReGIRHashCellDataSoADevice& hash_cell_data, float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit) const { unsigned int hash_key; unsigned int hash_grid_cell_index = custom_regir_hash(world_position, surface_normal, current_camera, roughness, primary_hit, soa.m_total_number_of_cells, hash_key); if (!HashGrid::resolve_collision(hash_cell_data.checksums, soa.m_total_number_of_cells, hash_grid_cell_index, hash_key)) return HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX; else return hash_grid_cell_index; } HIPRT_DEVICE float3 jitter_world_position(float3 original_world_position, const HIPRTCamera& current_camera, float roughness, bool primary_hit, Xorshift32Generator& rng, float jittering_radius = 0.5f) const { float3 random_offset = make_float3(rng(), rng(), rng()) * 2.0f - make_float3(1.0f, 1.0f, 1.0f); return original_world_position + random_offset * ReGIRHashGrid::compute_adaptive_cell_size_roughness(original_world_position, current_camera, roughness, primary_hit, m_grid_cell_target_projected_size, m_grid_cell_min_size) * jittering_radius; } HashGrid m_hash_grid; float m_grid_cell_min_size = ReGIR_HashGridConstantGridCellSize ? 0.75f : 0.25f; float m_grid_cell_target_projected_size = 10.0f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/Representative.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_REPRESENTATIVE_H #define DEVICE_KERNELS_REGIR_REPRESENTATIVE_H #include "Device/includes/ReSTIR/ReGIR/GridFillSurface.h" #include "Device/includes/ReSTIR/ReGIR/HashGridCellData.h" #include "HostDeviceCommon/RenderData.h" HIPRT_HOST_DEVICE HIPRT_INLINE float3 ReGIR_get_cell_world_normal(const HIPRTRenderData& render_data, int hash_grid_cell_index, bool primary_hit) { return render_data.render_settings.regir_settings.get_hash_cell_data_soa(primary_hit).world_normals[hash_grid_cell_index].unpack(); } HIPRT_HOST_DEVICE HIPRT_INLINE float3 ReGIR_get_cell_world_point(const HIPRTRenderData& render_data, int hash_grid_cell_index, bool primary_hit) { return render_data.render_settings.regir_settings.get_hash_cell_data_soa(primary_hit).world_points[hash_grid_cell_index]; } HIPRT_HOST_DEVICE HIPRT_INLINE int ReGIR_get_cell_primitive_index(const HIPRTRenderData& render_data, int hash_grid_cell_index, bool primary_hit) { return render_data.render_settings.regir_settings.get_hash_cell_data_soa(primary_hit).hit_primitive[hash_grid_cell_index]; } HIPRT_HOST_DEVICE HIPRT_INLINE float ReGIR_get_cell_roughness(const HIPRTRenderData& render_data, int hash_grid_cell_index, bool primary_hit) { // / 255.0f to convert from uchar [0, 255] to float [0, 1] return render_data.render_settings.regir_settings.get_hash_cell_data_soa(primary_hit).roughness[hash_grid_cell_index] / 255.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE float ReGIR_get_cell_metallic(const HIPRTRenderData& render_data, int hash_grid_cell_index, bool primary_hit) { // / 255.0f to convert from uchar [0, 255] to float [0, 1] return render_data.render_settings.regir_settings.get_hash_cell_data_soa(primary_hit).metallic[hash_grid_cell_index] / 255.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE float ReGIR_get_cell_specular(const HIPRTRenderData& render_data, int hash_grid_cell_index, bool primary_hit) { // / 255.0f to convert from uchar [0, 255] to float [0, 1] return render_data.render_settings.regir_settings.get_hash_cell_data_soa(primary_hit).specular[hash_grid_cell_index] / 255.0f; } HIPRT_HOST_DEVICE HIPRT_INLINE ReGIRGridFillSurface ReGIR_get_cell_surface(const HIPRTRenderData& render_data, int hash_grid_cell_index, bool primary_hit) { int cell_primitive_index = ReGIR_get_cell_primitive_index(render_data, hash_grid_cell_index, primary_hit); float3 cell_point = ReGIR_get_cell_world_point(render_data, hash_grid_cell_index, primary_hit); float3 cell_normal = ReGIR_get_cell_world_normal(render_data, hash_grid_cell_index, primary_hit); float cell_roughness = ReGIR_get_cell_roughness(render_data, hash_grid_cell_index, primary_hit); float cell_metallic = ReGIR_get_cell_metallic(render_data, hash_grid_cell_index, primary_hit); float cell_specular = ReGIR_get_cell_specular(render_data, hash_grid_cell_index, primary_hit); ReGIRGridFillSurface surface; surface.cell_primitive_index = cell_primitive_index; surface.cell_point = cell_point; surface.cell_normal = cell_normal; surface.cell_roughness = cell_roughness; surface.cell_metallic = cell_metallic; surface.cell_specular = cell_specular; return surface; } /** * Updates the representative point and normal (and other data) of the cell at the given shading point */ HIPRT_HOST_DEVICE HIPRT_INLINE void ReGIR_update_representative_data(HIPRTRenderData& render_data, float3 shading_point, float3 surface_normal, const HIPRTCamera& current_camera, int primitive_index, bool primary_hit, const DeviceUnpackedEffectiveMaterial& material) { if (DirectLightSamplingBaseStrategy != LSS_BASE_REGIR) return; else if (primitive_index == -1) return; // We're using the packed-unpacked surface normal here because // packing/unpacking (as used in the G-Buffer) normals introduces // small differences that are enough to shift us from one cell to // another. // // In practice this leads to the cell being inserted into the hash grid // with non-packed normals but then when the cell is queried at the first // during the path tracing kernels (and thus with the packed + unpacked normal // from the G-Buffer we get different hashing results and we can't find our cells // back) surface_normal = Octahedral24BitNormalPadded32b(surface_normal).unpack(); render_data.render_settings.regir_settings.insert_hash_cell_data(shading_point, surface_normal, current_camera, primary_hit, primitive_index, material); } #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/Reservoir.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_RESERVOIR_H #define DEVICE_KERNELS_REGIR_RESERVOIR_H #include "HostDeviceCommon/Xorshift.h" #include "HostDeviceCommon/LightSampleInformation.h" #include "HostDeviceCommon/Packing.h" struct ReGIRSample { int emissive_triangle_index = -1; // Only needed for ReSTIR DI float3 point_on_light; // Note: the target function isn't stored in the sample SoA, it's just there during the sampling process float target_function = 0.0f; }; struct ReGIRReservoir { static constexpr float VISIBILITY_REUSE_KILLED_UCW = -42.0f; static constexpr float UNDEFINED_UCW = -4242.0f; HIPRT_DEVICE bool stream_sample_raw(float mis_weight, float target_function, float source_pdf, int emissive_triangle_index, float3 point_on_light, Xorshift32Generator& rng) { float resampling_weight = mis_weight * target_function / source_pdf; weight_sum += resampling_weight; if (rng() < resampling_weight / weight_sum) { sample.emissive_triangle_index = emissive_triangle_index; sample.point_on_light = point_on_light; sample.target_function = target_function; return true; } return false; } HIPRT_DEVICE bool stream_sample(float mis_weight, float target_function, float source_pdf, const LightSampleInformation& light_sample, Xorshift32Generator& rng) { return stream_sample_raw(mis_weight, target_function, source_pdf, light_sample.emissive_triangle_index, light_sample.point_on_light, rng); } HIPRT_DEVICE bool stream_reservoir(float mis_weight, float target_function, const ReGIRReservoir& other_reservoir, Xorshift32Generator& rng) { float resampling_weight = mis_weight * target_function * other_reservoir.UCW; if (resampling_weight <= 0.0f) return false; weight_sum += resampling_weight; if (rng() < resampling_weight / weight_sum) { sample = other_reservoir.sample; sample.target_function = target_function; return true; } return false; } HIPRT_DEVICE void finalize_resampling(float normalization_numerator, float normalization_denominator) { if (weight_sum <= 0.0f || normalization_denominator == 0.0f) UCW = 0.0f; else UCW = 1.0f / sample.target_function * weight_sum * normalization_numerator / normalization_denominator; } ReGIRSample sample; float weight_sum = 0.0f; // If the UCW is set to -1, this is because the reservoir was killed by visibility reuse float UCW = 0.0f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/ReservoirSoA.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_RESERVOIR_SOA_H #define DEVICE_KERNELS_REGIR_RESERVOIR_SOA_H #include "Device/includes/ReSTIR/ReGIR/Reservoir.h" struct ReGIRSampleSoADevice { HIPRT_HOST_DEVICE void store_sample(int linear_reservoir_index, const ReGIRSample& sample) { emissive_triangle_index[linear_reservoir_index] = sample.emissive_triangle_index; // random_seed[linear_reservoir_index] = sample.random_seed; point_on_light[linear_reservoir_index] = sample.point_on_light; } HIPRT_HOST_DEVICE ReGIRSample read_sample(int linear_reservoir_index) const { ReGIRSample sample; sample.emissive_triangle_index = emissive_triangle_index[linear_reservoir_index]; // sample.random_seed = random_seed[linear_reservoir_index]; sample.point_on_light = point_on_light[linear_reservoir_index]; return sample; } int* emissive_triangle_index = nullptr; // Random seed for generating the point on the light // unsigned int* random_seed = nullptr; float3* point_on_light = nullptr; }; struct ReGIRReservoirSoADevice { HIPRT_HOST_DEVICE void store_reservoir_opt(int linear_reservoir_index, const ReGIRReservoir& reservoir) { UCW[linear_reservoir_index] = reservoir.UCW; } /** * The template parameter can be used to indicate whether or not to read the UCW. * * This makes sense to pass this parameter as false if you've already read the UCW * of the reservoir by some other means */ template HIPRT_HOST_DEVICE ReGIRReservoir read_reservoir(int linear_reservoir_index) const { ReGIRReservoir reservoir; if constexpr (readUCW) reservoir.UCW = UCW[linear_reservoir_index]; return reservoir; } float* UCW = nullptr; unsigned int number_of_reservoirs_per_cell = 0; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/Settings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_REGIR_SETTINGS_H #define DEVICE_INCLUDES_REGIR_SETTINGS_H #include "Device/includes/Hash.h" #include "Device/includes/RayPayload.h" #include "Device/includes/ReSTIR/ReGIR/PresampledLight.h" #include "Device/includes/ReSTIR/ReGIR/ReGIRHashGrid.h" #include "Device/includes/ReSTIR/ReGIR/HashGridSoADevice.h" #include "Device/includes/ReSTIR/ReGIR/ReservoirSoA.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" #include "HostDeviceCommon/HIPRTCamera.h" #include "HostDeviceCommon/Xorshift.h" struct ReGIRPresampledLightsSoADevice { int* emissive_triangle_index = nullptr; float* light_area = nullptr; float3* point_on_light = nullptr; Octahedral24BitNormalPadded32b* light_normal = nullptr; Float3xLengthUint10bPacked* emission = nullptr; }; struct ReGIRGridFillPresampledLights { HIPRT_DEVICE ReGIRPresampledLight sample_one_presampled_light(unsigned int hash_grid_cell_index, unsigned int reservoir_index_in_cell, unsigned int reservoir_count_per_grid_cell, float& out_pdf, Xorshift32Generator& rng) const { // Computing a subset index in [0, subset_count - 1] unsigned int subset_index_seed = (hash_grid_cell_index * reservoir_count_per_grid_cell + reservoir_index_in_cell) / stratification_size; unsigned int subset_index_random_seed = wang_hash(subset_index_seed) ^ rng.xorshift32(); unsigned int random_subset = Xorshift32Generator(subset_index_random_seed).xorshift32() % subset_count; unsigned int index_in_subset = (hash_grid_cell_index * reservoir_count_per_grid_cell + reservoir_index_in_cell) % subset_size; ReGIRPresampledLight sample; sample.emissive_triangle_index = presampled_lights_soa.emissive_triangle_index[random_subset * subset_size + index_in_subset]; sample.triangle_area = presampled_lights_soa.light_area[random_subset * subset_size + index_in_subset]; sample.point_on_light = presampled_lights_soa.point_on_light[random_subset * subset_size + index_in_subset]; sample.normal = presampled_lights_soa.light_normal[random_subset * subset_size + index_in_subset]; // sample.emission = presampled_lights_soa.emission[random_subset * subset_size + index_in_subset].unpack_color3x32f(); out_pdf = 1.0f / subset_count; return sample; } HIPRT_DEVICE void store_one_presampled_light(const ReGIRPresampledLight& presampled_light, unsigned int presampled_light_index) { presampled_lights_soa.emissive_triangle_index[presampled_light_index] = presampled_light.emissive_triangle_index; presampled_lights_soa.light_area[presampled_light_index] = presampled_light.triangle_area; presampled_lights_soa.point_on_light[presampled_light_index] = presampled_light.point_on_light; presampled_lights_soa.light_normal[presampled_light_index] = presampled_light.normal; presampled_lights_soa.emission[presampled_light_index].pack(presampled_light.emission); } HIPRT_DEVICE unsigned int get_presampled_light_count() const { return subset_count * subset_size; } ReGIRPresampledLightsSoADevice presampled_lights_soa; // How many consecutive reservoirs in the ReGIR grid // will sample from the same subset of presampled lights? int stratification_size = 64; // How many presampled lights per subset int subset_size = 256; // How many subsets in total int subset_count = 128; }; struct ReGIRGridFillSettings { HIPRT_DEVICE ReGIRGridFillSettings() : ReGIRGridFillSettings(true) {} HIPRT_DEVICE ReGIRGridFillSettings(bool primary_hit) { light_sample_count_per_cell_reservoir = 32; reservoirs_count_per_grid_cell_non_canonical = primary_hit ? 64 : 8; reservoirs_count_per_grid_cell_canonical = primary_hit ? 12 : 4; } // How many light samples are resampled into each reservoir of the grid cell int light_sample_count_per_cell_reservoir; HIPRT_DEVICE int get_non_canonical_reservoir_count_per_cell() const { return reservoirs_count_per_grid_cell_non_canonical; } HIPRT_DEVICE int get_canonical_reservoir_count_per_cell() const { return reservoirs_count_per_grid_cell_canonical; } HIPRT_DEVICE int get_total_reservoir_count_per_cell() const { return reservoirs_count_per_grid_cell_canonical + reservoirs_count_per_grid_cell_non_canonical; } HIPRT_DEVICE int* get_non_canonical_reservoir_count_per_cell_ptr() { return &reservoirs_count_per_grid_cell_non_canonical; } HIPRT_DEVICE int* get_canonical_reservoir_count_per_cell_ptr() { return &reservoirs_count_per_grid_cell_canonical; } HIPRT_DEVICE bool reservoir_index_in_cell_is_canonical(int reservoir_index_in_cell) const { return reservoir_index_in_cell >= get_non_canonical_reservoir_count_per_cell(); } private: // How many reservoirs are going to be produced per each cell of the grid. // // These reservoirs are "non-canonical" as they can include visibility/cosine terms // if visibility reuse is used // // Because these visibility/cosine terms are approximate, using these reservoirs alone // is going to be biased and so we need to combine them with "canonical" reservoirs during // shading for unbiasedness // // In the grid buffers, these reservoirs are stored first, i.e., for a grid cell with 3 non-canonical reservoirs // and 1 canonical reservoir: // // [non-canon, non-canon, non-canon, canonical] int reservoirs_count_per_grid_cell_non_canonical; // Number of canonical reservoirs per cell // // In the grid buffers, these reservoirs are stored last, i.e., for a grid cell with 3 non-canonical reservoirs // and 1 canonical reservoir: // // [non-canon, non-canon, non-canon, canonical] int reservoirs_count_per_grid_cell_canonical; }; struct ReGIRSpatialReuseSettings { bool do_spatial_reuse = true; // If true, the same random seed will be used by all grid cells during the spatial reuse for a given frame // This has the effect of coalescing neighbors memory accesses which improves performance bool do_coalesced_spatial_reuse = true; int spatial_reuse_pass_count = 2; int spatial_reuse_pass_index = 0; int spatial_neighbor_count = 3; int reuse_per_neighbor_count = 3; // When picking a random cell in the neighborhood for reuse, if that // cell is out of the grid or if that cell is not alive etc..., we're // going to retry another cell this many times // // This improves the chances that we're actually going to have a good // neighbor to reuse from --> more reuse --> less variance int retries_per_neighbor = 4; int spatial_reuse_radius = 1; }; struct ReGIRCorrelationReductionSettings { bool do_correlation_reduction = true; int correlation_reduction_factor = 2; int correl_frames_available = 0; unsigned int correl_reduction_current_grid = 0; ReGIRHashGridSoADevice correlation_reduction_grid; }; struct ReGIRSettings { HIPRT_DEVICE bool compute_is_primary_hit(const RayPayload& ray_payload) const { // We're going to assume that this is still a primary hit grid cell if the path spread is low enough. // This is because a low number of reservoirs are usually used for secondary hit grid cells to lower the cost // of the grid fill while still maintaining good quality because a low number of reservoirs is usually enough // for diffuse bounces. Issues happen when we're looking through a mirror and that mirror is going // to reflect directly the scene lit by a low number of reservoirs and that will show as grid artifacts and correlations. // // So what we need to do here is to still use a high number of reservoirs when looking through mirrors (or // low path spread in general) to avoid those artifacts. We can do that very easily by just assuming that this grid // cell (hit by the mirror bounce) is a first hit grid cell and by assuming that it is a primary hit grid cell, // a higher number of reservoirs will be used for the grid fill and we'll avoid the artifacts. return ray_payload.bounce == 0 || ray_payload.accumulated_roughness < 0.1f; } HIPRT_DEVICE ReGIRPresampledLight sample_one_presampled_light(unsigned int hash_grid_cell_index, unsigned int reservoir_index_in_cell, bool primary_hit, float& out_pdf, Xorshift32Generator& rng) const { return presampled_lights.sample_one_presampled_light(hash_grid_cell_index, reservoir_index_in_cell, get_number_of_reservoirs_per_cell(primary_hit), out_pdf, rng); } HIPRT_DEVICE const ReGIRHashGridSoADevice& get_initial_reservoirs_grid(bool primary_hit) const { return primary_hit ? initial_reservoirs_primary_hits_grid : initial_reservoirs_secondary_hits_grid; } HIPRT_DEVICE ReGIRHashGridSoADevice& get_initial_reservoirs_grid(bool primary_hit) { return primary_hit ? initial_reservoirs_primary_hits_grid : initial_reservoirs_secondary_hits_grid; } HIPRT_DEVICE const ReGIRHashGridSoADevice& get_raw_spatial_output_reservoirs_grid(bool primary_hit) const { return primary_hit ? spatial_output_primary_hits_grid : spatial_output_secondary_hits_grid; } HIPRT_DEVICE ReGIRHashGridSoADevice& get_raw_spatial_output_reservoirs_grid(bool primary_hit) { return primary_hit ? spatial_output_primary_hits_grid : spatial_output_secondary_hits_grid; } HIPRT_DEVICE const ReGIRHashGridSoADevice& get_actual_spatial_output_reservoirs_grid(bool primary_hit) const { return primary_hit ? actual_spatial_output_buffers_primary_hits : actual_spatial_output_buffers_secondary_hits; } HIPRT_DEVICE ReGIRHashGridSoADevice& get_actual_spatial_output_reservoirs_grid(bool primary_hit) { return primary_hit ? actual_spatial_output_buffers_primary_hits : actual_spatial_output_buffers_secondary_hits; } HIPRT_DEVICE const ReGIRHashCellDataSoADevice& get_hash_cell_data_soa(bool primary_hit) const { return primary_hit ? hash_cell_data_primary_hits : hash_cell_data_secondary_hits; } HIPRT_DEVICE ReGIRHashCellDataSoADevice& get_hash_cell_data_soa(bool primary_hit) { return primary_hit ? hash_cell_data_primary_hits : hash_cell_data_secondary_hits; } HIPRT_DEVICE const ReGIRGridFillSettings& get_grid_fill_settings(bool primary_hit) const { return primary_hit ? grid_fill_settings_primary_hits : grid_fill_settings_secondary_hits; } HIPRT_DEVICE const AtomicType* get_non_canonical_pre_integration_factor_buffer(bool primary_hit) const { return primary_hit ? non_canonical_pre_integration_factors_primary_hits : non_canonical_pre_integration_factors_secondary_hits; } HIPRT_DEVICE AtomicType* get_non_canonical_pre_integration_factor_buffer(bool primary_hit) { return primary_hit ? non_canonical_pre_integration_factors_primary_hits : non_canonical_pre_integration_factors_secondary_hits; } HIPRT_DEVICE const AtomicType* get_canonical_pre_integration_factor_buffer(bool primary_hit) const { return primary_hit ? canonical_pre_integration_factors_primary_hits : canonical_pre_integration_factors_secondary_hits; } HIPRT_DEVICE AtomicType* get_canonical_pre_integration_factor_buffer(bool primary_hit) { return primary_hit ? canonical_pre_integration_factors_primary_hits : canonical_pre_integration_factors_secondary_hits; } HIPRT_DEVICE float get_non_canonical_pre_integration_factor(unsigned hash_grid_cell_index, bool primary_hit) const { return get_non_canonical_pre_integration_factor_buffer(primary_hit)[hash_grid_cell_index]; } HIPRT_DEVICE float get_canonical_pre_integration_factor(unsigned hash_grid_cell_index, bool primary_hit) const { return get_canonical_pre_integration_factor_buffer(primary_hit)[hash_grid_cell_index]; } ///////////////////// Delegating to the grid for these functions ///////////////////// HIPRT_DEVICE float3 get_cell_size(float3 world_position, const HIPRTCamera& current_camera, float roughness, bool primary_hit) const { float cell_size = ReGIRHashGrid::compute_adaptive_cell_size_roughness(world_position, current_camera, roughness, primary_hit, hash_grid.m_grid_cell_target_projected_size, hash_grid.m_grid_cell_min_size); return make_float3(cell_size, cell_size, cell_size); } HIPRT_DEVICE unsigned int get_hash_grid_cell_index_from_world_pos(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit) const { return hash_grid.get_hash_grid_cell_index_from_world_pos(get_initial_reservoirs_grid(primary_hit), get_hash_cell_data_soa(primary_hit), world_position, surface_normal, current_camera, roughness, primary_hit); } ///////////////////// Delegating to the grid for these functions ///////////////////// /** * Returns the given reservoir index in the given grid cell index in the given grid of reservoirs */ HIPRT_DEVICE ReGIRReservoir get_reservoir_from_grid_cell_index(ReGIRHashGridSoADevice reservoir_grid, unsigned int hash_grid_cell_index, unsigned int reservoir_index_in_cell) { return hash_grid.read_full_reservoir(reservoir_grid, hash_grid.get_reservoir_index_in_grid(reservoir_grid, hash_grid_cell_index, reservoir_index_in_cell)); } /** * Returns a reservoir from the grid cell that corresponds to the given world position, surface normal. * The returned reservoir is a non-canonical reservoir given by the non_canonical_reservoir_number. * * That number must be in the range [0, get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell()[. */ HIPRT_DEVICE ReGIRReservoir get_cell_non_canonical_reservoir_from_index(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, unsigned int non_canonical_reservoir_number, bool* out_invalid_sample = nullptr) const { return get_reservoir_for_shading_from_cell_indices(world_position, surface_normal, current_camera, roughness, primary_hit, non_canonical_reservoir_number, out_invalid_sample); } /** * Overlaod if you already the hash grid cell index */ HIPRT_DEVICE ReGIRReservoir get_cell_non_canonical_reservoir_from_index(unsigned int hash_grid_cell_index, bool primary_hit, unsigned int non_canonical_reservoir_number, bool* out_invalid_sample = nullptr) const { return get_reservoir_for_shading_from_cell_indices(hash_grid_cell_index, primary_hit, non_canonical_reservoir_number, out_invalid_sample); } /** * Same as get_cell_non_canonical_reservoir_from_index() but for canonical reservoirs. */ HIPRT_DEVICE ReGIRReservoir get_cell_canonical_reservoir_from_index(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, unsigned int canonical_reservoir_number, bool* out_invalid_sample = nullptr) const { unsigned int non_canonical_reservoir_count = get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell(); return get_reservoir_for_shading_from_cell_indices(world_position, surface_normal, current_camera, roughness, primary_hit, non_canonical_reservoir_count + canonical_reservoir_number, out_invalid_sample); } /** * Overlaod if you already the hash grid cell index */ HIPRT_DEVICE ReGIRReservoir get_cell_canonical_reservoir_from_index(unsigned int hash_grid_cell_index, bool primary_hit, unsigned int canonical_reservoir_number, bool* out_invalid_sample = nullptr) const { unsigned int non_canonical_reservoir_count = get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell(); return get_reservoir_for_shading_from_cell_indices(hash_grid_cell_index, primary_hit, non_canonical_reservoir_count + canonical_reservoir_number, out_invalid_sample); } HIPRT_DEVICE ReGIRReservoir get_random_cell_non_canonical_reservoir(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, Xorshift32Generator& rng, bool* out_invalid_sample = nullptr) const { int random_non_canonical_reservoir_index_in_cell = rng.random_index(get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell()); return get_reservoir_for_shading_from_cell_indices(world_position, surface_normal, current_camera, roughness, primary_hit, random_non_canonical_reservoir_index_in_cell, out_invalid_sample); } HIPRT_DEVICE ReGIRReservoir get_random_cell_canonical_reservoir(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, Xorshift32Generator& rng, bool* out_invalid_sample = nullptr) const { int random_canonical_reservoir_index_in_cell = rng.random_index(get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell()); unsigned int non_canonical_reservoir_count = get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell(); return get_reservoir_for_shading_from_cell_indices(world_position, surface_normal, current_camera, roughness, primary_hit, non_canonical_reservoir_count + random_canonical_reservoir_index_in_cell, out_invalid_sample); } /** * Overload if you already have the hash grid cell index */ HIPRT_DEVICE ReGIRReservoir get_reservoir_for_shading_from_cell_indices(unsigned int hash_grid_cell_index, bool primary_hit, int reservoir_index_in_cell, bool* out_invalid_sample = nullptr) const { if (spatial_reuse.do_spatial_reuse) // If spatial reuse is enabled, we're shading with the reservoirs from the output of the spatial reuse return hash_grid.read_full_reservoir(get_actual_spatial_output_reservoirs_grid(primary_hit), hash_grid_cell_index, reservoir_index_in_cell, out_invalid_sample); else // No temporal reuse and no spatial reuse, reading from the output of the grid fill pass return hash_grid.read_full_reservoir(get_initial_reservoirs_grid(primary_hit), hash_grid_cell_index, reservoir_index_in_cell, out_invalid_sample); } /** * If 'out_invalid_sample' is set to true, then the given shading point (+ the jittering) was outside of the grid * and no reservoir has been gathered */ HIPRT_DEVICE ReGIRReservoir get_reservoir_for_shading_from_cell_indices(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, int reservoir_index_in_cell, bool* out_invalid_sample = nullptr) const { unsigned int hash_grid_cell_index = hash_grid.get_hash_grid_cell_index(get_initial_reservoirs_grid(primary_hit), get_hash_cell_data_soa(primary_hit), world_position, surface_normal, current_camera, roughness, primary_hit); return get_reservoir_for_shading_from_cell_indices(hash_grid_cell_index, primary_hit, reservoir_index_in_cell, out_invalid_sample); } HIPRT_DEVICE unsigned int get_neighbor_replay_hash_grid_cell_index_for_shading(float3 shading_point, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, bool replay_canonical, bool do_jittering, float jittering_radius, Xorshift32Generator& rng) const { unsigned int neighbor_cell_index; if (replay_canonical) neighbor_cell_index = find_valid_jittered_neighbor_cell_index(shading_point, surface_normal, current_camera, roughness, primary_hit, do_jittering, jittering_radius, rng); else neighbor_cell_index = find_valid_jittered_neighbor_cell_index(shading_point, surface_normal, current_camera, roughness, primary_hit, do_jittering, jittering_radius, rng); if (neighbor_cell_index != HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { // Advancing the RNG simulating the random reservoir pick within the grid cell if (replay_canonical) rng.random_index(get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell()); else rng.random_index(get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell()); } return neighbor_cell_index; } template HIPRT_DEVICE unsigned int find_valid_jittered_neighbor_cell_index(float3 world_position, float3 shading_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, bool do_jittering, float jittering_radius, Xorshift32Generator& rng) const { unsigned int retry = 0; unsigned int neighbor_grid_cell_index; do { float3 jittered; if (do_jittering) jittered = hash_grid.jitter_world_position(world_position, current_camera, roughness, primary_hit, rng, jittering_radius); else jittered = world_position; neighbor_grid_cell_index = hash_grid.get_hash_grid_cell_index(get_initial_reservoirs_grid(primary_hit), get_hash_cell_data_soa(primary_hit), jittered, shading_normal, current_camera, roughness, primary_hit); if (neighbor_grid_cell_index != HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { // This part here is to avoid race concurrency issues from the Megakernel shader: // // In the megakernel, rays that bounce around the scene may hit cells that have never been hit // before. This will cause these cells to become alive. // // When a cell is alive, it may be picked during the megakernel shading with ReGIR. // However, the cells are only filled during the grid fill pass/spatial reuse pass of ReGIR // // What can happen is that the Megakernel sets some grid cells alive and some other threads of the Megakernel then // tries to use that grid cell for shading (since that grid cell is now alive). This though is that the grid fill pass // hasn't been launched yet (it will be launched at the next frame) and so the grid cell, even though it's alive, doesn't // contain valid data --> reading invalid reservoir data for shading // // So we're checking here if the cell contains valid data and if it doesn't, we're going to position the cell // as being invalid with UNDEFINED_HASH_KEY float UCW; if (spatial_reuse.do_spatial_reuse) UCW = get_actual_spatial_output_reservoirs_grid(primary_hit).reservoirs.UCW[neighbor_grid_cell_index * get_number_of_reservoirs_per_cell(primary_hit)]; else UCW = get_initial_reservoirs_grid(primary_hit).reservoirs.UCW[neighbor_grid_cell_index * get_number_of_reservoirs_per_cell(primary_hit)]; if (UCW == ReGIRReservoir::UNDEFINED_UCW) neighbor_grid_cell_index = HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX; } retry++; } while (neighbor_grid_cell_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX && retry < ReGIR_ShadingJitterTries); if (fallbackOnCenterCell && neighbor_grid_cell_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX && retry == ReGIR_ShadingJitterTries) // We couldn't find a valid neighbor and the fallback on center cell is enabled: we're going to return the index of the center cell neighbor_grid_cell_index = hash_grid.get_hash_grid_cell_index(get_initial_reservoirs_grid(primary_hit), get_hash_cell_data_soa(primary_hit), world_position, shading_normal, current_camera, roughness, primary_hit); return neighbor_grid_cell_index; } template HIPRT_DEVICE ReGIRReservoir get_random_reservoir_in_grid_cell_for_shading(unsigned int grid_cell_index, bool primary_hit, Xorshift32Generator& rng) const { unsigned int reservoir_index_in_cell; // If this stays to 0, this means that we're going to read the reservoirs from // either the regular initial candidates grid or spatial reuse grid // // If this is > 0, then we're going to read the reservoirs from the supersampling grid unsigned int grid_index = 0; if constexpr (getCanonicalReservoir) { if (supersampling.do_correlation_reduction) { // If correlation reduction is enabled, we want to pick a reservoir from the whole pool of (regular reservoirs + correlation reduction reservoirs) reservoir_index_in_cell = rng.random_index(get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell() * (supersampling.correl_frames_available + 1)); } else reservoir_index_in_cell = rng.random_index(get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell()); } else { if (supersampling.do_correlation_reduction) // If correlation reduction is enabled, we want to pick a reservoir from the whole pool of (regular reservoirs + correlation reduction reservoirs) reservoir_index_in_cell = rng.random_index(get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell() * (supersampling.correl_frames_available + 1)); else reservoir_index_in_cell = rng.random_index(get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell()); } if constexpr (getCanonicalReservoir) { grid_index = reservoir_index_in_cell / get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell(); reservoir_index_in_cell %= get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell(); } else { grid_index = reservoir_index_in_cell / get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell(); reservoir_index_in_cell %= get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell(); } unsigned int canonical_offset = getCanonicalReservoir ? get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell() : 0; unsigned int reservoir_index_in_grid = grid_cell_index * get_number_of_reservoirs_per_cell(primary_hit) + canonical_offset + reservoir_index_in_cell; if (grid_index == 0 || !primary_hit) { // Reading from the regular grids because the grid index is 0 or we're reading // secondary hits because we're not doing correlation reduction for secondary hits if (spatial_reuse.do_spatial_reuse) // If spatial reuse is enabled, we're shading with the reservoirs from the output of the spatial reuse return hash_grid.read_full_reservoir(get_actual_spatial_output_reservoirs_grid(primary_hit), reservoir_index_in_grid); else // No temporal reuse and no spatial reuse, reading from the output of the grid fill pass return hash_grid.read_full_reservoir(get_initial_reservoirs_grid(primary_hit), reservoir_index_in_grid); } else { // If we have grid_index == 1 here for example, this is going to be grid index 0 of the supersampling grid // so we have grid_index - 1 unsigned int reservoir_index_in_supersample_grid = reservoir_index_in_grid + (grid_index - 1) * get_number_of_reservoirs_per_grid(primary_hit); return hash_grid.read_full_reservoir(supersampling.correlation_reduction_grid, reservoir_index_in_supersample_grid); } } /** * Returns the reservoir indicated by lienar_reservoir_index_in_grid but in the grid_index given * * This function only makes sense with temporal reuse where we have more than 1 grid and so a single reservoir index * isn't enough to fetch the reservoir in the reservoir buffer * * The 'grid_index' parameter allows reading from a specific grid of past frames. * This is index should be in [0, temporal_reuse.temporal_history_length - 1]. * * If not specified, this function reads from the grid of the current frame * * The 'opt' suffix of the function means that the UCW of the reservoir will be read first and the rest of the reservoir * will only be read if the UCW is > 0.0f. * If the UCW is <= 0.0f, the returned reservoir will have uninitialized values in all of its fields */ HIPRT_DEVICE ReGIRReservoir get_temporal_reservoir_opt(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, int reservoir_index_in_cell, bool* out_invalid_sample = nullptr) const { return hash_grid.read_full_reservoir(get_initial_reservoirs_grid(primary_hit), get_hash_cell_data_soa(primary_hit), world_position, surface_normal, current_camera, roughness, primary_hit, reservoir_index_in_cell, out_invalid_sample); } HIPRT_DEVICE ReGIRReservoir get_grid_fill_output_reservoir_opt(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, int reservoir_index_in_cell, bool* out_invalid_sample = nullptr) const { // The output of the grid fill pass is in the current frame grid so we can call the temporal method with // index -1 return get_temporal_reservoir_opt(world_position, surface_normal, current_camera, roughness, primary_hit, reservoir_index_in_cell, out_invalid_sample); } HIPRT_DEVICE void store_reservoir_custom_buffer_opt(ReGIRHashGridSoADevice& output_reservoirs_grid, const ReGIRReservoir& reservoir, unsigned int hash_grid_cell_index, int reservoir_index_in_cell) { hash_grid.store_reservoir_and_sample_opt(reservoir, output_reservoirs_grid, hash_grid_cell_index, reservoir_index_in_cell); } HIPRT_DEVICE void store_reservoir_custom_buffer_opt(ReGIRHashGridSoADevice& output_reservoirs_grid, ReGIRHashCellDataSoADevice& output_reservoirs_cell_data, const ReGIRReservoir& reservoir, float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, int reservoir_index_in_cell) { hash_grid.store_reservoir_and_sample_opt(reservoir, output_reservoirs_grid, output_reservoirs_cell_data, world_position, surface_normal, current_camera, roughness, primary_hit, reservoir_index_in_cell); } /** * Overload if you already have the hash grid cell index */ HIPRT_DEVICE void store_initial_reservoir_opt(ReGIRReservoir reservoir, unsigned int hash_grid_cell_index, bool primary_hit, int reservoir_index_in_cell) { hash_grid.store_reservoir_and_sample_opt(reservoir, get_initial_reservoirs_grid(primary_hit), hash_grid_cell_index, reservoir_index_in_cell); } HIPRT_DEVICE void store_initial_reservoir_opt(ReGIRReservoir reservoir, float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit, int reservoir_index_in_cell) { hash_grid.store_reservoir_and_sample_opt(reservoir, get_initial_reservoirs_grid(primary_hit), get_hash_cell_data_soa(primary_hit), world_position, surface_normal, current_camera, roughness, primary_hit, reservoir_index_in_cell); } HIPRT_DEVICE ColorRGB32F get_random_cell_color(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, float roughness, bool primary_hit) const { unsigned int cell_index = hash_grid.get_hash_grid_cell_index_from_world_pos(get_initial_reservoirs_grid(primary_hit), get_hash_cell_data_soa(primary_hit), world_position, surface_normal, current_camera, roughness, primary_hit); if (cell_index == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) return ColorRGB32F(0.0f); return ColorRGB32F::random_color(cell_index); } HIPRT_DEVICE unsigned int get_total_number_of_cells_per_grid(bool primary_hit_cells) const { return get_initial_reservoirs_grid(primary_hit_cells).m_total_number_of_cells; } HIPRT_DEVICE unsigned int get_number_of_reservoirs_per_grid(bool primary_hit_cells) const { // We need to keep this dynamic on the CPU so not using the precomputed variable return get_total_number_of_cells_per_grid(primary_hit_cells) * get_grid_fill_settings(primary_hit_cells).get_total_reservoir_count_per_cell(); } HIPRT_DEVICE unsigned int get_number_of_reservoirs_per_cell(bool primary_hit_cells) const { // We need to keep this dynamic on the CPU so not using the precomputed variable return get_grid_fill_settings(primary_hit_cells).get_total_reservoir_count_per_cell(); } HIPRT_DEVICE static void insert_hash_cell_data(ReGIRHashCellDataSoADevice& hash_cell_data_to_update, unsigned int hash_grid_cell_index, float3 world_position, float3 shading_normal, int primitive_index, const DeviceUnpackedEffectiveMaterial& material) { if (hippt::atomic_compare_exchange(&hash_cell_data_to_update.hit_primitive[hash_grid_cell_index], ReGIRHashCellDataSoADevice::UNDEFINED_PRIMITIVE, primitive_index) == ReGIRHashCellDataSoADevice::UNDEFINED_PRIMITIVE) { hash_cell_data_to_update.world_points[hash_grid_cell_index] = world_position; hash_cell_data_to_update.world_normals[hash_grid_cell_index].pack(shading_normal); hash_cell_data_to_update.roughness[hash_grid_cell_index] = material.roughness * 255.0f; hash_cell_data_to_update.metallic[hash_grid_cell_index] = material.metallic * 255.0f; hash_cell_data_to_update.specular[hash_grid_cell_index] = material.specular * 255.0f; } // Because we just inserted into that grid cell, it is now alive // Only go through all that atomic stuff if the cell isn't alive if (hash_cell_data_to_update.grid_cell_alive[hash_grid_cell_index] == 0) { // TODO is this atomic needed since we can only be here if the cell was unoccoupied? if (hippt::atomic_compare_exchange(&hash_cell_data_to_update.grid_cell_alive[hash_grid_cell_index], 0u, 1u) == 0u) { unsigned int cell_alive_index = hippt::atomic_fetch_add(hash_cell_data_to_update.grid_cells_alive_count, 1u); hash_cell_data_to_update.grid_cells_alive_list[cell_alive_index] = hash_grid_cell_index; } } } HIPRT_DEVICE static void insert_hash_cell_data_static( const ReGIRHashGrid& hash_grid, ReGIRHashGridSoADevice& hash_grid_to_update, ReGIRHashCellDataSoADevice& hash_cell_data_to_update, float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, int primitive_index, bool primary_hit, const DeviceUnpackedEffectiveMaterial& material) { unsigned int checksum; unsigned int hash_grid_cell_index = hash_grid.custom_regir_hash(world_position, surface_normal, current_camera, material.roughness, primary_hit, hash_grid_to_update.m_total_number_of_cells, checksum); // TODO we can have a if (current_hash_key != undefined_key) here to skip some atomic operations // Trying to insert the new key atomically unsigned int existing_checksum = hippt::atomic_compare_exchange(&hash_cell_data_to_update.checksums[hash_grid_cell_index], HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX, checksum); if (existing_checksum != HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) { // We tried inserting in our cell but there is something else there already if (existing_checksum != checksum) { // And it's not our hash so this is a collision unsigned int new_hash_cell_index = hash_grid_cell_index; if (!HashGrid::resolve_collision(hash_cell_data_to_update.checksums, hash_grid_to_update.m_total_number_of_cells, new_hash_cell_index, checksum, existing_checksum)) { // Could not resolve the collision return; } else { // We resolved the collision by finding an empty cell hash_grid_cell_index = new_hash_cell_index; insert_hash_cell_data(hash_cell_data_to_update, hash_grid_cell_index, world_position, surface_normal, primitive_index, material); } } } else { // We just succeeded the insertion of our key in an empty cell insert_hash_cell_data(hash_cell_data_to_update, hash_grid_cell_index, world_position, surface_normal, primitive_index, material); } } HIPRT_DEVICE void insert_hash_cell_data(float3 world_position, float3 surface_normal, const HIPRTCamera& current_camera, bool primary_hit, int primitive_index, const DeviceUnpackedEffectiveMaterial& material) { ReGIRSettings::insert_hash_cell_data_static(hash_grid, get_initial_reservoirs_grid(primary_hit), get_hash_cell_data_soa(primary_hit), world_position, surface_normal, current_camera, primitive_index, primary_hit, material); } // If true, the ReGIR grid fill and spatial reuse will run in parallel of the // path tracing kernels. This helps with performance a bit and helps amortize // the grid fill/spatial reuse cost of ReGIR. // // Async compute is only supported with spatial reuse enabled though. bool do_asynchronous_compute = true; bool do_light_presampling = ReGIR_GridFillDoLightPresampling; bool DEBUG_CORRELATE_rEGIR = true; bool DEBUG_DO_RIS_INTEGRAL_NORMALIZATION = true; // How many frames to skip before running the grid fill and spatial reuse passes again // // A value of 1 for example means that the grid fill and spatial reuse will be ran at frame 0 // but not at frame 1. And ran at frame 2 but not at frame 3. ... // // This amortizes the overhead of ReGIR grid fill / spatial reuse by using the fact that each cell // contains many reservoirs so the same cell can be used multiple times before all reservoirs have been used // and new samples are necessary int frame_skip_primary_hit_grid = 0; int frame_skip_secondary_hit_grid = 2; ReGIRHashGrid hash_grid; // Grid that contains the output reservoirs of the grid fill pass for the primary hits grid cells ReGIRHashGridSoADevice initial_reservoirs_primary_hits_grid; ReGIRHashGridSoADevice initial_reservoirs_secondary_hits_grid; // Grid that contains the output reservoirs of the spatial reuse pass for the primary hits grid cells ReGIRHashGridSoADevice spatial_output_primary_hits_grid; ReGIRHashGridSoADevice spatial_output_secondary_hits_grid; // If we have multiple spatial reuse passes or async compute, the output of the spatial reuse passes // may not simply be in 'spatial_output_primary_hits_grid' (for primary hits) because of buffer ping-ponging // so instead the actual buffers are in there ReGIRHashGridSoADevice actual_spatial_output_buffers_primary_hits; ReGIRHashGridSoADevice actual_spatial_output_buffers_secondary_hits; // Contains data associated with the primary hits grid cells ReGIRHashCellDataSoADevice hash_cell_data_primary_hits; ReGIRHashCellDataSoADevice hash_cell_data_secondary_hits; ReGIRGridFillPresampledLights presampled_lights; ReGIRGridFillSettings grid_fill_settings_primary_hits = ReGIRGridFillSettings(true); ReGIRGridFillSettings grid_fill_settings_secondary_hits = ReGIRGridFillSettings(false); ReGIRSpatialReuseSettings spatial_reuse; ReGIRShadingSettings shading; ReGIRCorrelationReductionSettings supersampling; AtomicType* non_canonical_pre_integration_factors_primary_hits = nullptr; AtomicType* canonical_pre_integration_factors_primary_hits = nullptr; AtomicType* non_canonical_pre_integration_factors_secondary_hits = nullptr; AtomicType* canonical_pre_integration_factors_secondary_hits = nullptr; // Multiplicative factor to multiply the output of some debug views float debug_view_scale_factor = 0.05f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/ShadingAdditionalInfo.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_REGIR_SHADING_ADDITIONAL_INFO_H #define DEVICE_INCLUDES_REGIR_SHADING_ADDITIONAL_INFO_H struct ReGIRShadingAdditionalInfo { ColorRGB32F sample_radiance = ColorRGB32F(0.0f); }; #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/ShadingSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_REGIR_SHADING_SETTINGS_H #define DEVICE_INCLUDES_REGIR_SHADING_SETTINGS_H struct ReGIRShadingSettings { HIPRT_DEVICE bool get_do_cell_jittering(bool primary_hit) const { return primary_hit ? do_cell_jittering_first_hits : do_cell_jittering_secondary_hits; } int number_of_neighbors = 4; // At path tracing time, how many reservoirs of the grid cell of the point we're trying to shade // are going to be resampled (with the BRDF term) to produce the final light sample used for NEE int reservoir_tap_count_per_neighbor = 1; // Whether or not to jitter the world space position used when looking up the ReGIR grid // This helps eliminate grid discretization artifacts // // First hits are for the camera ray hits (i.e. the grid cells visible by the camera) // Secondary hits are grid cells only found by bouncing around in the scene bool do_cell_jittering_first_hits = true; bool do_cell_jittering_secondary_hits = false; // Radius of jittering when picking reservoirs from neighboring grid cells for shading float jittering_radius = 0.75f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/TargetFunction.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_REGIR_TARGET_FUNCTION_H #define DEVICE_INCLUDES_REGIR_TARGET_FUNCTION_H #include "Device/includes/BSDFs/BSDFContext.h" #include "Device/includes/Dispatcher.h" #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/PDFConversion.h" #include "Device/includes/ReSTIR/ReGIR/GridFillSurface.h" #include "Device/includes/ReSTIR/ReGIR/VisibilityTest.h" #include "HostDeviceCommon/RenderData.h" template HIPRT_DEVICE float ReGIR_grid_fill_evaluate_target_function(const HIPRTRenderData& render_data, ReGIRGridFillSurface surface, bool primary_hit, ColorRGB32F sample_emission, float3 sample_normal, float3 sample_position, Xorshift32Generator& rng) { float3 to_light_direction = sample_position - surface.cell_point; float distance_to_light = hippt::length(to_light_direction); to_light_direction /= distance_to_light; float target_function = sample_emission.luminance() / hippt::square(distance_to_light); if (surface.cell_primitive_index != -1 && withCosineTerm) // We do have a representative normal, taking the cosine term into account target_function *= hippt::max(0.0f, hippt::dot(surface.cell_normal, to_light_direction)); if constexpr (withCosineTermLightSource) target_function *= compute_cosine_term_at_light_source(sample_normal, -to_light_direction); if (target_function <= 0.0f) return 0.0f; if ((primary_hit && includeBSDFPrimaryHit) || (!primary_hit && includeBSDFSecondaryHit)) { float out_pdf; RayVolumeState empty_volume_state; BSDFIncidentLightInfo out_incident_light_info; DeviceUnpackedEffectiveMaterial approximate_material; approximate_material.roughness = surface.cell_roughness; approximate_material.metallic = surface.cell_metallic; approximate_material.specular = surface.cell_specular; #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE && DirectLightSamplingBaseStrategy == LSS_BASE_REGIR BSDFContext bsdf_context = BSDFContext(hippt::normalize(render_data.current_camera.position - surface.cell_point), surface.cell_normal, surface.cell_normal, to_light_direction, out_incident_light_info, empty_volume_state, false, approximate_material, 0, 0, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); #else BSDFContext bsdf_context = BSDFContext(hippt::normalize(render_data.current_camera.position - surface.cell_point), surface.cell_normal, surface.cell_normal, to_light_direction, out_incident_light_info, empty_volume_state, false, approximate_material, 0, 0, MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC); #endif ColorRGB32F bsdf_radiance = bsdf_dispatcher_eval(render_data, bsdf_context, out_pdf, rng); target_function *= bsdf_radiance.luminance(); } if constexpr (includeVisibility) { if (target_function > 0.0f) // No need to visibility test if the target function is already 0 target_function *= ReGIR_grid_cell_visibility_test(render_data, surface.cell_point, surface.cell_primitive_index, sample_position, rng); } else if constexpr (withNeePlusPlusVisibilityEstimation && DirectLightUseNEEPlusPlus == KERNEL_OPTION_TRUE) { NEEPlusPlusContext context; context.envmap = false; context.point_on_light = sample_position; context.shaded_point = surface.cell_point; target_function *= render_data.nee_plus_plus.estimate_visibility_probability(context, render_data.current_camera); } return target_function; } HIPRT_DEVICE float ReGIR_grid_fill_evaluate_non_canonical_target_function(const HIPRTRenderData& render_data, unsigned int hash_grid_cell_index, bool primary_hit, ColorRGB32F sample_emission, float3 sample_normal, float3 sample_position, Xorshift32Generator& rng) { ReGIRGridFillSurface surface = ReGIR_get_cell_surface(render_data, hash_grid_cell_index, primary_hit); return ReGIR_grid_fill_evaluate_target_function< ReGIR_GridFillTargetFunctionVisibility, ReGIR_GridFillTargetFunctionCosineTerm, ReGIR_GridFillTargetFunctionCosineTermLightSource, ReGIR_GridFillPrimaryHitsTargetFunctionBSDF, ReGIR_GridFillSecondaryHitsTargetFunctionBSDF, ReGIR_GridFillTargetFunctionNeePlusPlusVisibilityEstimation>( render_data, surface, primary_hit, sample_emission, sample_normal, sample_position, rng); } HIPRT_DEVICE float ReGIR_grid_fill_evaluate_non_canonical_target_function(const HIPRTRenderData& render_data, const ReGIRGridFillSurface& surface, bool primary_hit, ColorRGB32F sample_emission, float3 sample_normal, float3 sample_position, Xorshift32Generator& rng) { return ReGIR_grid_fill_evaluate_target_function< ReGIR_GridFillTargetFunctionVisibility, ReGIR_GridFillTargetFunctionCosineTerm, ReGIR_GridFillTargetFunctionCosineTermLightSource, ReGIR_GridFillPrimaryHitsTargetFunctionBSDF, ReGIR_GridFillSecondaryHitsTargetFunctionBSDF, ReGIR_GridFillTargetFunctionNeePlusPlusVisibilityEstimation>( render_data, surface, primary_hit, sample_emission, sample_normal, sample_position, rng); } HIPRT_DEVICE float ReGIR_grid_fill_evaluate_canonical_target_function(const HIPRTRenderData& render_data, unsigned int hash_grid_cell_index, bool primary_hit, ColorRGB32F sample_emission, float3 sample_normal, float3 sample_position, Xorshift32Generator& rng) { ReGIRGridFillSurface surface = ReGIR_get_cell_surface(render_data, hash_grid_cell_index, primary_hit); return ReGIR_grid_fill_evaluate_target_function( render_data, surface, primary_hit, sample_emission, sample_normal, sample_position, rng); } HIPRT_DEVICE float ReGIR_grid_fill_evaluate_canonical_target_function(const HIPRTRenderData& render_data, const ReGIRGridFillSurface& surface, bool primary_hit, ColorRGB32F sample_emission, float3 sample_normal, float3 sample_position, Xorshift32Generator& rng) { return ReGIR_grid_fill_evaluate_target_function( render_data, surface, primary_hit, sample_emission, sample_normal, sample_position, rng); } template HIPRT_DEVICE float ReGIR_shading_evaluate_target_function(const HIPRTRenderData& render_data, const float3& shading_point, const float3& view_direction, const float3& shading_normal, const float3& geometric_normal, int last_hit_primitive_index, RayPayload& ray_payload, const float3& point_on_light, const float3& light_source_normal, const ColorRGB32F& light_emission, Xorshift32Generator& rng, BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO) { ColorRGB32F trash_radiance; return ReGIR_shading_evaluate_target_function(render_data, shading_point, view_direction, shading_normal, geometric_normal, last_hit_primitive_index, ray_payload, point_on_light, light_source_normal, light_emission, rng, trash_radiance, incident_light_info); } template HIPRT_DEVICE float ReGIR_shading_evaluate_target_function(const HIPRTRenderData& render_data, const float3& shading_point, const float3& view_direction, const float3& shading_normal, const float3& geometric_normal, int last_hit_primitive_index, RayPayload& ray_payload, const float3& point_on_light, const float3& light_source_normal, const ColorRGB32F& light_emission, Xorshift32Generator& rng, ColorRGB32F& sample_radiance, BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO) { float3 to_light_direction = point_on_light - shading_point; float distance_to_light = hippt::length(to_light_direction); to_light_direction /= distance_to_light; // Normalization float bsdf_pdf; #if ReGIR_ShadingResamplingDoBSDFMIS == KERNEL_OPTION_TRUE && DirectLightSamplingBaseStrategy == LSS_BASE_REGIR BSDFContext bsdf_context(view_direction, shading_normal, geometric_normal, to_light_direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_MIS); #else BSDFContext bsdf_context(view_direction, shading_normal, geometric_normal, to_light_direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness, MicrofacetRegularization::RegularizationMode::REGULARIZATION_CLASSIC); #endif ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf, rng); float cosine_term = hippt::max(0.0f, hippt::dot(shading_normal, to_light_direction)); float geometry_term = compute_cosine_term_at_light_source(light_source_normal, -to_light_direction) / hippt::square(distance_to_light); sample_radiance = bsdf_color * light_emission * cosine_term * geometry_term; float target_function = sample_radiance.luminance(); if (target_function <= 0.0f) return 0.0f; if constexpr (withVisibility) { if (target_function > 0.0f) { hiprtRay shadow_ray; shadow_ray.origin = shading_point; shadow_ray.direction = to_light_direction; bool in_shadow = evaluate_shadow_ray_occluded(render_data, shadow_ray, distance_to_light, last_hit_primitive_index, ray_payload.bounce, rng); target_function *= !in_shadow; sample_radiance *= !in_shadow; } } else if constexpr (withNeePlusPlusVisibilityEstimation && DirectLightUseNEEPlusPlus == KERNEL_OPTION_TRUE) { NEEPlusPlusContext context; context.envmap = false; context.point_on_light = point_on_light; context.shaded_point = shading_point; float visibility_proba = render_data.nee_plus_plus.estimate_visibility_probability(context, render_data.current_camera); if (visibility_proba > 0.005f) visibility_proba = 1.0f; visibility_proba = hippt::max(0.1f, visibility_proba); target_function *= visibility_proba; } return target_function; } #endif ================================================ FILE: src/Device/includes/ReSTIR/ReGIR/VisibilityTest.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_VISIBILITY_TEST_H #define DEVICE_KERNELS_REGIR_VISIBILITY_TEST_H #include "Device/includes/Intersect.h" #include "Device/includes/ReSTIR/ReGIR/Representative.h" #include "HostDeviceCommon/RenderData.h" /** * Returns false if the given 'point_on_light' is occluded from the point of view of the * representative point of the grid cell given by 'hash_grid_cell_index' * * Returns true if unoccluded */ HIPRT_DEVICE bool ReGIR_grid_cell_visibility_test(const HIPRTRenderData& render_data, float3 representative_point, int representative_primitive_index, float3 point_on_light, Xorshift32Generator& rng) { float3 to_light_direction = point_on_light - representative_point; float distance_to_light = hippt::length(to_light_direction); to_light_direction /= distance_to_light; hiprtRay shadow_ray; shadow_ray.origin = representative_point; shadow_ray.direction = to_light_direction; return !evaluate_shadow_ray_occluded(render_data, shadow_ray, distance_to_light, representative_primitive_index, 0, rng); } HIPRT_DEVICE bool ReGIR_grid_cell_visibility_test(const HIPRTRenderData& render_data, int hash_grid_cell_index, bool primary_hit, float3 point_on_light, Xorshift32Generator& rng) { int representative_primitive_index = ReGIR_get_cell_primitive_index(render_data, hash_grid_cell_index, primary_hit); float3 representative_point = ReGIR_get_cell_world_point(render_data, hash_grid_cell_index, primary_hit); return ReGIR_grid_cell_visibility_test(render_data, representative_point, representative_primitive_index, point_on_light, rng); } #endif ================================================ FILE: src/Device/includes/ReSTIR/SpatialMISWeight.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_SPATIAL_MIS_WEIGHT_H #define DEVICE_RESTIR_DI_SPATIAL_MIS_WEIGHT_H #include "Device/includes/ReSTIR/MISWeightsCommon.h" #include "Device/includes/ReSTIR/DI/TargetFunction.h" #include "Device/includes/ReSTIR/Utils.h" #include "Device/includes/ReSTIR/GI/TargetFunction.h" #include "HostDeviceCommon/ReSTIRSettingsHelper.h" template struct ReSTIRSpatialResamplingMISWeight {}; template struct ReSTIRSpatialResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(int reservoir_being_resampled_M) { return reservoir_being_resampled_M; } }; template struct ReSTIRSpatialResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(int reservoir_being_resampled_M) { return reservoir_being_resampled_M; } }; template struct ReSTIRSpatialResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int reservoir_being_resampled_M) { return ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights ? reservoir_being_resampled_M : 1; } }; template struct ReSTIRSpatialResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, float reservoir_being_resampled_UCW, const ReSTIRSampleType& reservoir_being_resampled_sample, const ReSTIRSurface& center_pixel_surface, int current_neighbor_index, int2 center_pixel_coords, Xorshift32Generator& random_number_generator) { if (reservoir_being_resampled_UCW <= 0.0f) // Reservoir that doesn't contain any sample, returning // 1.0f MIS weight so that multiplying by that doesn't do anything return 1.0f; float nume = 0.0f; float denom = 0.0f; unsigned int backup_seed = random_number_generator.m_state.seed; random_number_generator.m_state.seed = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data).spatial_neighbors_rng_seed; for (int j = 0; j < ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data).reuse_neighbor_count + 1; j++) { int neighbor_index_j = get_spatial_neighbor_pixel_index(render_data, j, center_pixel_coords, random_number_generator); if (neighbor_index_j == -1) // Invalid neighbor, skipping continue; int center_pixel_index = center_pixel_coords.x + center_pixel_coords.y * render_data.render_settings.render_resolution.x; if (!check_neighbor_similarity_heuristics(render_data, neighbor_index_j, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface))) // Neighbor too dissimilar according to heuristics, skipping continue; ReSTIRSurface neighbor_surface = get_pixel_surface(render_data, neighbor_index_j, random_number_generator); float target_function_at_j; if constexpr (IsReSTIRGI) { // ReSTIR GI target function if (j == current_neighbor_index) target_function_at_j = ReSTIR_GI_evaluate_target_function(render_data, reservoir_being_resampled_sample, neighbor_surface, random_number_generator); else target_function_at_j = ReSTIR_GI_evaluate_target_function(render_data, reservoir_being_resampled_sample, neighbor_surface, random_number_generator); if (!reservoir_being_resampled_sample.is_envmap_path()) // Applying the jacobian to get "p_hat_from_i" target_function_at_j *= hippt::max(0.0f, get_jacobian_determinant_reconnection_shift(reservoir_being_resampled_sample.sample_point, reservoir_being_resampled_sample.sample_point_geometric_normal.unpack(), center_pixel_surface.shading_point, neighbor_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold())); } else // ReSTIR DI target function target_function_at_j = ReSTIR_DI_evaluate_target_function(render_data, reservoir_being_resampled_sample, neighbor_surface, random_number_generator); int M = 1; if (ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights) M = ReSTIRSettingsHelper::get_restir_spatial_pass_input_reservoir_M(render_data, neighbor_index_j); denom += target_function_at_j * M; if (j == current_neighbor_index) nume = target_function_at_j * M; } if (denom == 0.0f) return 0.0f; else return nume / denom; random_number_generator.m_state.seed = backup_seed; } }; template struct ReSTIRSpatialResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int reservoir_being_resampled_M, float reservoir_being_resampled_target_function, ReSTIRSampleType& center_pixel_reservoir_sample, int center_pixel_reservoir_M, float center_pixel_reservoir_target_function, ReSTIRReservoirType& neighbor_pixel_reservoir, ReSTIRSurface& center_pixel_surface, float target_function_at_center, int neighbor_pixel_index, int valid_neighbors_count, int valid_neighbors_M_sum, bool update_mc, bool resampling_canonical, Xorshift32Generator& random_number_generator) { if (!resampling_canonical) { // Resampling a neighbor // The target function of the neighbor reservoir's sample at the neighbor surface is just // the target function stored in the neighbor's reservoir. // // Care must be taken however because this is not necessarily true anymore after multiple spatial // reuse passes: a given pixel may now hold a sample from another pixel and that means that the visibility // doesn't match anymore. // // However, this ReSTIR implementation does a visibility reuse pass at the end of each spatial reuse pass // so that we know that the visibility is correct and thus we do not run into any issues and we can just // reuse the target function stored in the neighbor's reservoir float target_function_at_neighbor = reservoir_being_resampled_target_function; float target_function_center_sample_at_center = center_pixel_reservoir_target_function; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float reservoir_resampled_M = use_confidence_weights ? reservoir_being_resampled_M : 1; float center_reservoir_M = use_confidence_weights ? center_pixel_reservoir_M : 1; float neighbors_confidence_sum = use_confidence_weights ? valid_neighbors_M_sum : 1; // We only want to divide by M-1 if we're not using confidence weights. // (Eq. 7.6 and 7.7 of "A Gentle Introduction to ReSTIR") float valid_neighbor_division_term = use_confidence_weights ? 1 : valid_neighbors_count; float nume = target_function_at_neighbor * reservoir_resampled_M; float denom = target_function_at_neighbor * neighbors_confidence_sum + target_function_at_center / valid_neighbor_division_term * center_reservoir_M; float mi = denom == 0.0f ? 0.0f : (nume / denom); if (update_mc) { ReSTIRSurface neighbor_pixel_surface = get_pixel_surface(render_data, neighbor_pixel_index, random_number_generator); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { // If this is an envmap path the jacobian is just 1 so this is not needed if (!center_pixel_reservoir_sample.is_envmap_path()) { float jacobian = get_jacobian_determinant_reconnection_shift(center_pixel_reservoir_sample.sample_point, center_pixel_reservoir_sample.sample_point_geometric_normal.unpack(), neighbor_pixel_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); float nume_mc = target_function_center_sample_at_center / valid_neighbor_division_term * center_reservoir_M; float denom_mc = target_function_center_sample_at_neighbor * neighbors_confidence_sum + target_function_center_sample_at_center / valid_neighbor_division_term * center_reservoir_M; float confidence_weights_multiplier; if (use_confidence_weights) { if (neighbors_confidence_sum == 0.0f) confidence_weights_multiplier = 0.0f; else confidence_weights_multiplier = reservoir_resampled_M / neighbors_confidence_sum; } else confidence_weights_multiplier = 1.0f; // (Eq. 7.7 of "A Gentle Introduction to ReSTIR"), c_j / (Sum_{k!=c}^M c_k) if (denom_mc != 0.0f) mc += nume_mc / denom_mc / valid_neighbor_division_term * confidence_weights_multiplier; } return mi / valid_neighbor_division_term; } else { // Resampling the center pixel if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! return mc; } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; template struct ReSTIRSpatialResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int reservoir_being_resampled_M, float reservoir_being_resampled_target_function, ReSTIRSampleType& center_pixel_reservoir_sample, int center_pixel_reservoir_M, float center_pixel_reservoir_target_function, ReSTIRReservoirType& neighbor_pixel_reservoir, ReSTIRSurface& center_pixel_surface, float target_function_at_center, int neighbor_pixel_index, int valid_neighbors_count, int valid_neighbors_M_sum, bool update_mc, bool resampling_canonical, Xorshift32Generator& random_number_generator) { if (!resampling_canonical) { // Resampling a neighbor // The target function of the neighbor reservoir's sample at the neighbor surface is just // the target function stored in the neighbor's reservoir. // // Care must be taken however because this is not necessarily true anymore after multiple spatial // reuse passes: a given pixel may now hold a sample from another pixel and that means that the visibility // doesn't match anymore. // // However, this ReSTIR DI implementation does a visibility reuse pass at the end of each spatial reuse pass // so that we know that the visibility is correct and thus we do not run into any issues and we can just // reuse the target function stored in the neighbor's reservoir float target_function_at_neighbor = reservoir_being_resampled_target_function; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float reservoir_resampled_M = use_confidence_weights ? reservoir_being_resampled_M : 1; float center_reservoir_M = use_confidence_weights ? center_pixel_reservoir_M : 1; float neighbors_confidence_sum = use_confidence_weights ? valid_neighbors_M_sum : 1; // We only want to divide by M-1 if we're not using confidence weights. // (Eq. 7.6 and 7.7 of "A Gentle Introduction to ReSTIR") float valid_neighbor_division_term = use_confidence_weights ? 1 : valid_neighbors_count; float nume = target_function_at_neighbor * reservoir_resampled_M; float denom = target_function_at_neighbor * neighbors_confidence_sum + target_function_at_center / valid_neighbor_division_term * center_reservoir_M; float mi = denom == 0.0f ? 0.0f : (nume / denom); if (use_confidence_weights) mi *= neighbors_confidence_sum / (neighbors_confidence_sum + center_reservoir_M); if (update_mc) { // There's one case where we do not need to update 'mc': when the center pixel (that we're currently resampling) is empty: M = 0 / UCW = 0 // That's because in such cases, the empty reservoir will not be resampled into the final reservoir anyways since it has no contribution // Because 'mc' is only used as the MIS weight of the center reservoir, we don't care about 'mc' since the center reservoir is not going // to be chosen anyways // // So we can avoid computing all that stuff ReSTIRSurface neighbor_pixel_surface = get_pixel_surface(render_data, neighbor_pixel_index, random_number_generator); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { if (!center_pixel_reservoir_sample.is_envmap_path()) { // If this is an envmap path the jacobian is just 1 so this is not needed float jacobian = get_jacobian_determinant_reconnection_shift(center_pixel_reservoir_sample.sample_point, center_pixel_reservoir_sample.sample_point_geometric_normal.unpack(), neighbor_pixel_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); float target_function_center_sample_at_center = center_pixel_reservoir_target_function; float nume_mc = target_function_center_sample_at_center / valid_neighbor_division_term * center_reservoir_M; float denom_mc = target_function_center_sample_at_neighbor * neighbors_confidence_sum + target_function_center_sample_at_center / valid_neighbor_division_term * center_reservoir_M; float confidence_multiplier = 1.0f; if (use_confidence_weights) confidence_multiplier = reservoir_resampled_M / (center_reservoir_M + neighbors_confidence_sum); if (denom_mc != 0.0f) mc += nume_mc / denom_mc * confidence_multiplier; } if (use_confidence_weights) return mi; else // In the defensive formulation, we want to divide by M, not M-1. // (Eq. 7.6 of "A Gentle Introduction to ReSTIR") // // We also only want that division when not using confidence weights return mi / (valid_neighbors_count + 1.0f); } else { // Resampling the center pixel if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else { // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! if (ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights) return mc + static_cast(center_pixel_reservoir_M) / static_cast(center_pixel_reservoir_M + valid_neighbors_M_sum); else // In the defensive formulation, we want to divide by M, not M-1. // (Eq. 7.6 of "A Gentle Introduction to ReSTIR") so 'valid_neighbors_count + 1' return (1 + mc) / (valid_neighbors_count + 1.0f); } } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; template struct ReSTIRSpatialResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int reservoir_being_resampled_M, float reservoir_being_resampled_target_function, ReSTIRSampleType& center_pixel_reservoir_sample, int center_pixel_reservoir_M, float center_pixel_reservoir_target_function, ReSTIRReservoirType& neighbor_pixel_reservoir, ReSTIRSurface& center_pixel_surface, float target_function_neighbor_sample_at_center, int neighbor_pixel_index, int valid_neighbors_count, int valid_neighbors_M_sum, bool update_mc, bool resampling_canonical, Xorshift32Generator& random_number_generator) { if (!resampling_canonical) { // Resampling a neighbor float target_function_neighbor_sample_at_neighbor = reservoir_being_resampled_target_function; float target_function_center_sample_at_center = center_pixel_reservoir_target_function; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float reservoir_resampled_M = use_confidence_weights ? reservoir_being_resampled_M : 1; float center_reservoir_M = use_confidence_weights ? center_pixel_reservoir_M : 1; float neighbors_confidence_sum = use_confidence_weights ? valid_neighbors_M_sum : valid_neighbors_count; // Eq. 15 of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, 2024] generalized // with confidence weights float difference_function = symmetric_ratio_MIS_weights_difference_function(target_function_neighbor_sample_at_center, target_function_neighbor_sample_at_neighbor, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float nume_mi = difference_function * reservoir_resampled_M; float denom_mi = center_reservoir_M + neighbors_confidence_sum * difference_function; float mi = nume_mi / denom_mi; if (update_mc) { ReSTIRSurface neighbor_pixel_surface = get_pixel_surface(render_data, neighbor_pixel_index, random_number_generator); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { // If this is an envmap path the jacobian is just 1 so this is not needed if (!center_pixel_reservoir_sample.is_envmap_path()) { float jacobian = get_jacobian_determinant_reconnection_shift(center_pixel_reservoir_sample.sample_point, center_pixel_reservoir_sample.sample_point_geometric_normal.unpack(), neighbor_pixel_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); float nume_mc = center_reservoir_M; float denom_mc = center_reservoir_M + neighbors_confidence_sum * symmetric_ratio_MIS_weights_difference_function(target_function_center_sample_at_neighbor, target_function_center_sample_at_center, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float confidence_weights_multiplier; if (use_confidence_weights) { if (neighbors_confidence_sum == 0.0f) confidence_weights_multiplier = 0.0f; else confidence_weights_multiplier = reservoir_resampled_M / neighbors_confidence_sum; } else confidence_weights_multiplier = 1.0f / valid_neighbors_count; mc += confidence_weights_multiplier * nume_mc / denom_mc; } return mi; } else { // Resampling the center pixel if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! return mc; } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; template struct ReSTIRSpatialResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int reservoir_being_resampled_M, float reservoir_being_resampled_target_function, ReSTIRSampleType& center_pixel_reservoir_sample, int center_pixel_reservoir_M, float center_pixel_reservoir_target_function, ReSTIRReservoirType& neighbor_pixel_reservoir, ReSTIRSurface& center_pixel_surface, float target_function_neighbor_sample_at_center, int neighbor_pixel_index, int valid_neighbors_count, int valid_neighbors_M_sum, bool update_mc, bool resampling_canonical, Xorshift32Generator& random_number_generator) { if (!resampling_canonical) { // Resampling a neighbor float target_function_neighbor_sample_at_neighbor = reservoir_being_resampled_target_function; float target_function_center_sample_at_center = center_pixel_reservoir_target_function; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float reservoir_resampled_M = use_confidence_weights ? reservoir_being_resampled_M : 1; float center_reservoir_M = use_confidence_weights ? center_pixel_reservoir_M : 1; float neighbors_confidence_sum = use_confidence_weights ? valid_neighbors_M_sum : valid_neighbors_count; // Eq. 16 of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, 2024] generalized // with confidence weights float difference_function = symmetric_ratio_MIS_weights_difference_function(target_function_neighbor_sample_at_center, target_function_neighbor_sample_at_neighbor, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float nume_mi, denom_mi; // Eq. 16 of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, 2024] generalized // with confidence weights if (target_function_neighbor_sample_at_center <= target_function_neighbor_sample_at_neighbor) { nume_mi = difference_function * reservoir_resampled_M; denom_mi = center_reservoir_M + neighbors_confidence_sum * difference_function; } else { nume_mi = difference_function * reservoir_resampled_M; denom_mi = center_reservoir_M + neighbors_confidence_sum; } float mi = nume_mi / denom_mi; if (update_mc) { ReSTIRSurface neighbor_pixel_surface = get_pixel_surface(render_data, neighbor_pixel_index, random_number_generator); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { // If this is an envmap path the jacobian is just 1 so this is not needed if (!center_pixel_reservoir_sample.is_envmap_path()) { float jacobian = get_jacobian_determinant_reconnection_shift(center_pixel_reservoir_sample.sample_point, center_pixel_reservoir_sample.sample_point_geometric_normal.unpack(), neighbor_pixel_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); float nume_mc, denom_mc; float difference_function_mc = symmetric_ratio_MIS_weights_difference_function(target_function_center_sample_at_neighbor, target_function_center_sample_at_center, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); if (target_function_center_sample_at_center <= target_function_center_sample_at_neighbor) { nume_mc = difference_function_mc * reservoir_resampled_M; denom_mc = center_reservoir_M + neighbors_confidence_sum * difference_function_mc; } else { nume_mc = difference_function_mc * reservoir_resampled_M; denom_mc = center_reservoir_M + neighbors_confidence_sum; } mc += nume_mc / denom_mc; } return mi; } else { // Resampling the center pixel if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! // // This is Eq. 16 of the paper: y not in R: m_i(y) = 1 - Sum(...) / |R| // mc here is the sum // and |R| is 1 return 1.0f - mc; } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/SpatialNormalizationWeight.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_SPATIAL_NORMALIZATION_WEIGHT_H #define DEVICE_RESTIR_DI_SPATIAL_NORMALIZATION_WEIGHT_H #include "Device/includes/ReSTIR/MISWeightsCommon.h" #include "Device/includes/ReSTIR/Utils.h" #include "Device/includes/ReSTIR/UtilsSpatial.h" #include "HostDeviceCommon/ReSTIRSettingsHelper.h" template struct ReSTIRSpatialNormalizationWeight {}; template struct ReSTIRSpatialNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(const HIPRTRenderData& render_data, float final_reservoir_weight_sum, const ReSTIRSurface& center_pixel_surface, int2 center_pixel_coords, float& out_normalization_nume, float& out_normalization_denom, Xorshift32Generator& random_number_generator) { if (final_reservoir_weight_sum <= 0.0f) { // Invalid reservoir, returning directly out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; return; } // 1/M MIS weights are basically confidence weights only i.e. c_i / sum(c_j) with // c_i = r_i.M out_normalization_nume = 1.0f; // We're simply going to divide by the sum of all the M values of all the neighbors we resampled (including the center pixel) // so we're only going to set the denominator to that and the numerator isn't going to change out_normalization_denom = 0.0f; for (int neighbor = 0; neighbor < ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data).reuse_neighbor_count + 1; neighbor++) { int neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor, center_pixel_coords, random_number_generator); if (neighbor_pixel_index == -1) // Neighbor out of the viewport continue; int center_pixel_index = center_pixel_coords.x + center_pixel_coords.y * render_data.render_settings.render_resolution.x; if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface))) continue; out_normalization_denom += ReSTIRSettingsHelper::get_restir_spatial_pass_input_reservoir_M(render_data, neighbor_pixel_index); } } }; template struct ReSTIRSpatialNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(const HIPRTRenderData& render_data, const ReSTIRSampleType& final_reservoir_sample, float final_reservoir_weight_sum, const ReSTIRSurface& center_pixel_surface, int2 center_pixel_coords, float& out_normalization_nume, float& out_normalization_denom, Xorshift32Generator& random_number_generator) { if (final_reservoir_weight_sum <= 0) { // Invalid reservoir, returning directly out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; return; } // Checking how many of our neighbors could have produced the sample that we just picked // and we're going to divide by the sum of M values of those neighbors out_normalization_denom = 0.0f; out_normalization_nume = 1.0f; int center_pixel_index = center_pixel_coords.x + center_pixel_coords.y * render_data.render_settings.render_resolution.x; const ReSTIRCommonSpatialPassSettings& spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); random_number_generator.m_state.seed = spatial_pass_settings.spatial_neighbors_rng_seed; for (int neighbor = 0; neighbor < spatial_pass_settings.reuse_neighbor_count + 1; neighbor++) { int neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor, center_pixel_coords, random_number_generator); if (neighbor_pixel_index == -1) // Invalid neighbor continue; if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface))) continue; // Getting the surface data at the neighbor ReSTIRSurface neighbor_surface = get_pixel_surface(render_data, neighbor_pixel_index, random_number_generator); float target_function_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function float jacobian = 1.0f; if (!final_reservoir_sample.is_envmap_path()) jacobian = get_jacobian_determinant_reconnection_shift(final_reservoir_sample.sample_point, final_reservoir_sample.sample_point_geometric_normal, center_pixel_surface.shading_point, neighbor_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); target_function_at_neighbor = jacobian * ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, neighbor_surface, random_number_generator); } else // ReSTIR DI target function target_function_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, neighbor_surface, random_number_generator); if (target_function_at_neighbor > 0.0f) // If the neighbor could have produced this sample... out_normalization_denom += ReSTIRSettingsHelper::get_restir_spatial_pass_input_reservoir_M(render_data, neighbor_pixel_index); } } }; template struct ReSTIRSpatialNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(const HIPRTRenderData& render_data, const ReSTIRSampleType& final_reservoir_sample, float final_reservoir_weight_sum, const ReSTIRSurface& center_pixel_surface, int selected_neighbor, int2 center_pixel_coords, float& out_normalization_nume, float& out_normalization_denom, Xorshift32Generator& random_number_generator) { if (final_reservoir_weight_sum <= 0) { // Invalid reservoir, returning directly out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; return; } out_normalization_denom = 0.0f; out_normalization_nume = 0.0f; random_number_generator.m_state.seed = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data).spatial_neighbors_rng_seed; for (int neighbor = 0; neighbor < ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data).reuse_neighbor_count + 1; neighbor++) { int neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor, center_pixel_coords, random_number_generator); if (neighbor_pixel_index == -1) // Invalid neighbor continue; int center_pixel_index = center_pixel_coords.x + center_pixel_coords.y * render_data.render_settings.render_resolution.x; if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface))) continue; // Getting the surface data at the neighbor ReSTIRSurface neighbor_surface = get_pixel_surface(render_data, neighbor_pixel_index, random_number_generator); float target_function_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, neighbor_surface, random_number_generator); if (!final_reservoir_sample.is_envmap_path()) // Applying the jacobian to get "p_hat_from_i" target_function_at_neighbor *= hippt::max(0.0f, get_jacobian_determinant_reconnection_shift(final_reservoir_sample.sample_point, final_reservoir_sample.sample_point_geometric_normal, center_pixel_surface.shading_point, neighbor_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold())); } else // ReSTIR DI target function target_function_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, neighbor_surface, random_number_generator); if (target_function_at_neighbor > 0.0f) { int M = 1; if (ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights) M = ReSTIRSettingsHelper::get_restir_spatial_pass_input_reservoir_M(render_data, neighbor_pixel_index); if (neighbor == selected_neighbor) // Not multiplying by M here, this was done already when resampling the sample if we // we're using confidence weights out_normalization_nume = target_function_at_neighbor; out_normalization_denom += target_function_at_neighbor * M; }; } } }; template struct ReSTIRSpatialNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the neighbors with balance heuristic MIS weights in the m_i terms out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRSpatialNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled by the MIS weights when resampling the neighbors out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRSpatialNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled by the MIS weights when resampling the neighbors out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRSpatialNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled by the MIS weights when resampling the neighbors out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRSpatialNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled by the MIS weights when resampling the neighbors out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; #endif ================================================ FILE: src/Device/includes/ReSTIR/SpatiotemporalMISWeight.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_SPATIOTEMPORAL_MIS_WEIGHT_H #define DEVICE_RESTIR_DI_SPATIOTEMPORAL_MIS_WEIGHT_H #include "Device/includes/ReSTIR/Utils.h" #include "Device/includes/ReSTIR/MISWeightsCommon.h" #include "Device/includes/ReSTIR/DI/TargetFunction.h" #include "Device/includes/ReSTIR/GI/Reservoir.h" #include "Device/includes/ReSTIR/GI/TargetFunction.h" #define TEMPORAL_NEIGHBOR_ID 0 /** * There are going to be many specialization of this structure, one for each bias correction mode * (RESTIR_DI_BIAS_CORRECTION_1_OVER_M, RESTIR_DI_BIAS_CORRECTION_1_OVER_Z, ...) * * IsReSTIRGI is used to indicate whether the structure should be used to compute * for ReSTIR GI or for ReSTIR DI */ template struct ReSTIRSpatiotemporalResamplingMISWeight {}; template <> struct ReSTIRSpatiotemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(int reservoir_being_resampled_M) { return reservoir_being_resampled_M; } }; template <> struct ReSTIRSpatiotemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(int reservoir_being_resampled_M) { return reservoir_being_resampled_M; } }; template struct ReSTIRSpatiotemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int reservoir_being_resampled_M) { return ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights ? reservoir_being_resampled_M : 1; } }; template struct ReSTIRSpatiotemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, float reservoir_being_resampled_UCW, const ReSTIRSampleType& reservoir_being_resampled_sample, ReSTIRSurface& center_pixel_surface, ReSTIRSurface& temporal_neighbor_surface, int current_neighbor, int initial_candidates_M, int temporal_neighbor_M, int center_pixel_index, int2 temporal_neighbor_coords, Xorshift32Generator& random_number_generator) { if (reservoir_being_resampled_UCW <= 0.0f) // Reservoir that doesn't contain any sample, returning // 1.0f MIS weight so that multiplying by that doesn't do anything return 1.0f; float nume = 0.0f; float denom = 0.0f; const ReSTIRCommonSpatialPassSettings& spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); for (int j = 0; j < spatial_pass_settings.reuse_neighbor_count + 1; j++) { // The last iteration of the loop is a special case that resamples the initial candidates reservoir // and so neighbor_pixel_index is never going to be used so we don't need to set it int neighbor_index_j; if (j != spatial_pass_settings.reuse_neighbor_count) { neighbor_index_j = get_spatial_neighbor_pixel_index(render_data, j, temporal_neighbor_coords, random_number_generator); if (neighbor_index_j == -1) // Invalid neighbor, skipping continue; if (!check_neighbor_similarity_heuristics(render_data, neighbor_index_j, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface), render_data.render_settings.use_prev_frame_g_buffer())) // Neighbor too dissimilar according to heuristics, skipping continue; } ReSTIRSurface neighbor_surface; if (j == spatial_pass_settings.reuse_neighbor_count) neighbor_surface = center_pixel_surface; else neighbor_surface = get_pixel_surface(render_data, neighbor_index_j, render_data.render_settings.use_prev_frame_g_buffer(), random_number_generator); float target_function_at_j; if constexpr (IsReSTIRGI) // ReSTIR GI target function target_function_at_j = ReSTIR_GI_evaluate_target_function(render_data, reservoir_being_resampled_sample, neighbor_surface, random_number_generator); else // ReSTIR DI target function target_function_at_j = ReSTIR_DI_evaluate_target_function(render_data, reservoir_being_resampled_sample, neighbor_surface, random_number_generator); int M = 1; if (ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights) { if (j == spatial_pass_settings.reuse_neighbor_count) M = initial_candidates_M; else M = ReSTIRSettingsHelper::get_restir_spatial_pass_input_reservoir_M(render_data, neighbor_index_j); } denom += target_function_at_j * M; // Using + 1 here because for the spatial neighbors, we want to start at index 1, // not 0 because it is the temporal neighbor that has index 0 if (j + 1 == current_neighbor) nume = target_function_at_j * M; } // Taking the temporal neighbor into account float target_function_at_temporal_neighbor = ReSTIR_DI_evaluate_target_function(render_data, reservoir_being_resampled_sample, temporal_neighbor_surface, random_number_generator); int M = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights ? temporal_neighbor_M : 1; denom += target_function_at_temporal_neighbor * M; if (current_neighbor == TEMPORAL_NEIGHBOR_ID) nume = target_function_at_temporal_neighbor * M; if (denom == 0.0f) return 0.0f; else return nume / denom; } }; template struct ReSTIRSpatiotemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int reservoir_being_resampled_M, float reservoir_being_resampled_target_function, int center_pixel_reservoir_M, float center_pixel_reservoir_target_function, const ReSTIRSampleType& center_pixel_reservoir_sample, float target_function_at_center, int neighbor_pixel_index, int valid_neighbors_count, int valid_neighbors_M_sum, bool update_mc, bool resample_canonical, Xorshift32Generator& random_number_generator) { if (!resample_canonical) { // Resampling a neighbor // The target function of the neighbor reservoir's sample at the neighbor surface is just // the target function stored in the neighbor's reservoir. // // Care must be taken however because this is not necessarily true anymore after multiple spatial // reuse passes: a given pixel may now hold a sample from another pixel and that means that the visibility // doesn't match anymore. // // However, this ReSTIR DI implementation does a visibility reuse pass at the end of each spatial reuse pass // so that we know that the visibility is correct and thus we do not run into any issues and we can just$ // reuse the target function stored in the neighbor's reservoir float target_function_at_neighbor = reservoir_being_resampled_target_function; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float reservoir_resampled_M = use_confidence_weights ? reservoir_being_resampled_M : 1; float center_reservoir_M = use_confidence_weights ? center_pixel_reservoir_M : 1; float neighbors_confidence_sum = use_confidence_weights ? valid_neighbors_M_sum : 1; // We only want to divide by M-1 if we're not using confidence weights. // (Eq. 7.6 and 7.7 of "A Gentle Introduction to ReSTIR") float valid_neighbor_division_term = use_confidence_weights ? 1 : valid_neighbors_count; float nume = target_function_at_neighbor * reservoir_resampled_M; float denom = target_function_at_neighbor * neighbors_confidence_sum + target_function_at_center / valid_neighbor_division_term * center_reservoir_M; float mi = denom == 0.0f ? 0.0f : (nume / denom); if (update_mc) { ReSTIRSurface neighbor_pixel_surface = get_pixel_surface(render_data, neighbor_pixel_index, render_data.render_settings.use_prev_frame_g_buffer(), random_number_generator); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); float target_function_center_sample_at_center = center_pixel_reservoir_target_function; float nume_mc = target_function_center_sample_at_center / valid_neighbor_division_term * center_reservoir_M; float denom_mc = target_function_center_sample_at_neighbor * neighbors_confidence_sum + target_function_center_sample_at_center / valid_neighbor_division_term * center_reservoir_M; // (Eq. 7.7 of "A Gentle Introduction to ReSTIR"), c_j / (Sum_{k!=c}^M c_k) float confidence_weights_multiplier = use_confidence_weights ? reservoir_resampled_M / neighbors_confidence_sum : 1; if (denom_mc != 0.0f) mc += nume_mc / denom_mc / valid_neighbor_division_term * confidence_weights_multiplier; } return mi / valid_neighbor_division_term; } else { // Resampling the center pixel if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! return mc; } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; template struct ReSTIRSpatiotemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int resampled_reservoir_M, float resampled_reservoir_target_function, int center_pixel_reservoir_M, float center_pixel_reservoir_target_function, const ReSTIRSampleType& center_pixel_reservoir_sample, float target_function_at_center, int neighbor_pixel_index, int valid_neighbors_count, int valid_neighbors_M_sum, bool update_mc, bool resample_canonical, Xorshift32Generator& random_number_generator) { if (!resample_canonical) { // Resampling a neighbor // The target function of the neighbor reservoir's sample at the neighbor surface is just // the target function stored in the neighbor's reservoir. // // Care must be taken however because this is not necessarily true anymore after multiple spatial // reuse passes: a given pixel may now hold a sample from another pixel and that means that the visibility // doesn't match anymore. // // However, this ReSTIR DI implementation does a visibility reuse pass at the end of each spatial reuse pass // so that we know that the visibility is correct and thus we do not run into any issues and we can just // reuse the target function stored in the neighbor's reservoir float target_function_at_neighbor = resampled_reservoir_target_function; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float reservoir_resampled_M = use_confidence_weights ? resampled_reservoir_M : 1; float center_reservoir_M = use_confidence_weights ? center_pixel_reservoir_M : 1; float neighbors_confidence_sum = use_confidence_weights ? valid_neighbors_M_sum : 1; // We only want to divide by M-1 if we're not using confidence weights. // (Eq. 7.6 and 7.7 of "A Gentle Introduction to ReSTIR") float valid_neighbor_division_term = use_confidence_weights ? 1 : valid_neighbors_count; float nume = target_function_at_neighbor * reservoir_resampled_M; float denom = target_function_at_neighbor * neighbors_confidence_sum + target_function_at_center / valid_neighbor_division_term * center_reservoir_M; float mi = 0.0f; if (denom != 0.0f) mi = nume / denom; if (use_confidence_weights) mi *= neighbors_confidence_sum / (neighbors_confidence_sum + center_reservoir_M); if (update_mc) { // There's one case where we do not need to update 'mc': when the center pixel (that we're currently resampling) is empty: M = 0 / UCW = 0 // That's because is such cases, the empty reservoir will not be resampled into the final reservoir anyways since it has no contribution // Because 'mc' is only used as the MIS weight of the center reservoir, we don't care about 'mc' since the center reservoir is not going // to be chosen anyways // // So we can avoid computing all that stuff float target_function_center_sample_at_center = center_pixel_reservoir_target_function; // TODO are we loading this surface again where the caller had it already? ReSTIRSurface neighbor_pixel_surface = get_pixel_surface(render_data, neighbor_pixel_index, render_data.render_settings.use_prev_frame_g_buffer(), random_number_generator); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); float nume_mc = target_function_center_sample_at_center / valid_neighbor_division_term * center_reservoir_M; float denom_mc = target_function_center_sample_at_neighbor * neighbors_confidence_sum + target_function_center_sample_at_center / valid_neighbor_division_term * center_reservoir_M; float confidence_multiplier = 1.0f; if (use_confidence_weights) confidence_multiplier = reservoir_resampled_M / (center_reservoir_M + neighbors_confidence_sum); if (denom_mc != 0.0f) mc += nume_mc / denom_mc * confidence_multiplier; } if (use_confidence_weights) return mi; else // In the defensive formulation, we want to divide by M, not M-1. // (Eq. 7.6 of "A Gentle Introduction to ReSTIR") // // We also only want that division when not using confidence weights return mi / (valid_neighbors_count + 1.0f); } else { // Resampling the center pixel if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else { // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! if (ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights) return mc + static_cast(center_pixel_reservoir_M) / static_cast(center_pixel_reservoir_M + valid_neighbors_M_sum); else // In the defensive formulation, we want to divide by M, not M-1. // (Eq. 7.6 of "A Gentle Introduction to ReSTIR") so 'valid_neighbors_count + 1' return (1 + mc) / (valid_neighbors_count + 1.0f); } } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; template struct ReSTIRSpatiotemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int resampled_reservoir_M, float resampled_reservoir_target_function, int center_pixel_reservoir_M, float center_pixel_reservoir_target_function, const ReSTIRSampleType& center_pixel_reservoir_sample, const ReSTIRSurface& center_pixel_surface, float target_function_at_center, int neighbor_pixel_index, int valid_neighbors_count, int valid_neighbors_M_sum, bool update_mc, bool resample_canonical, Xorshift32Generator& random_number_generator) { if (!resample_canonical) { // Resampling a neighbor float target_function_neighbor_sample_at_neighbor = resampled_reservoir_target_function; float target_function_center_sample_at_center = center_pixel_reservoir_target_function; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float reservoir_resampled_M = use_confidence_weights ? resampled_reservoir_M : 1; float center_reservoir_M = use_confidence_weights ? center_pixel_reservoir_M : 1; float neighbors_confidence_sum = use_confidence_weights ? valid_neighbors_M_sum : valid_neighbors_count; // Eq. 15 of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, 2024] generalized // with confidence weights float difference_function = symmetric_ratio_MIS_weights_difference_function(target_function_at_center, target_function_neighbor_sample_at_neighbor, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float nume_mi = difference_function * reservoir_resampled_M; float denom_mi = center_reservoir_M + neighbors_confidence_sum * difference_function; float mi = nume_mi / denom_mi; if (update_mc) { ReSTIRSurface neighbor_pixel_surface = get_pixel_surface(render_data, neighbor_pixel_index, random_number_generator); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { // If this is an envmap path the jacobian is just 1 so this is not needed if (!center_pixel_reservoir_sample.is_envmap_path()) { float jacobian = get_jacobian_determinant_reconnection_shift(center_pixel_reservoir_sample.sample_point, center_pixel_reservoir_sample.sample_point_geometric_normal, neighbor_pixel_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); float nume_mc = center_reservoir_M; float denom_mc = center_reservoir_M + neighbors_confidence_sum * symmetric_ratio_MIS_weights_difference_function(target_function_center_sample_at_neighbor, target_function_center_sample_at_center, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float confidence_weights_multiplier; if (use_confidence_weights) { if (neighbors_confidence_sum == 0.0f) confidence_weights_multiplier = 0.0f; else confidence_weights_multiplier = reservoir_resampled_M / neighbors_confidence_sum; } else confidence_weights_multiplier = 1.0f / valid_neighbors_count; mc += confidence_weights_multiplier * nume_mc / denom_mc; } return mi; } else { // Resampling the center pixel if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! return mc; } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; template struct ReSTIRSpatiotemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, int resampled_reservoir_M, float resampled_reservoir_target_function, int center_pixel_reservoir_M, float center_pixel_reservoir_target_function, const ReSTIRSampleType& center_pixel_reservoir_sample, const ReSTIRSurface& center_pixel_surface, float target_function_at_center, int neighbor_pixel_index, int valid_neighbors_count, int valid_neighbors_M_sum, bool update_mc, bool resample_canonical, Xorshift32Generator& random_number_generator) { if (!resample_canonical) { // Resampling a neighbor float target_function_neighbor_sample_at_neighbor = resampled_reservoir_target_function; float target_function_center_sample_at_center = center_pixel_reservoir_target_function; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float reservoir_resampled_M = use_confidence_weights ? resampled_reservoir_M : 1; float center_reservoir_M = use_confidence_weights ? center_pixel_reservoir_M : 1; float neighbors_confidence_sum = use_confidence_weights ? valid_neighbors_M_sum : valid_neighbors_count; // Eq. 15 of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, 2024] generalized // with confidence weights float difference_function = symmetric_ratio_MIS_weights_difference_function(target_function_at_center, target_function_neighbor_sample_at_neighbor, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float nume_mi = difference_function * reservoir_resampled_M; float denom_mi = center_reservoir_M + neighbors_confidence_sum * difference_function; float mi = nume_mi / denom_mi; if (update_mc) { ReSTIRSurface neighbor_pixel_surface = get_pixel_surface(render_data, neighbor_pixel_index, random_number_generator); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { // If this is an envmap path the jacobian is just 1 so this is not needed if (!center_pixel_reservoir_sample.is_envmap_path()) { float jacobian = get_jacobian_determinant_reconnection_shift(center_pixel_reservoir_sample.sample_point, center_pixel_reservoir_sample.sample_point_geometric_normal, neighbor_pixel_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, center_pixel_reservoir_sample, neighbor_pixel_surface, random_number_generator); float nume_mc = center_reservoir_M; float denom_mc = center_reservoir_M + neighbors_confidence_sum * symmetric_ratio_MIS_weights_difference_function(target_function_center_sample_at_neighbor, target_function_center_sample_at_center, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float confidence_weights_multiplier; if (use_confidence_weights) { if (neighbors_confidence_sum == 0.0f) confidence_weights_multiplier = 0.0f; else confidence_weights_multiplier = reservoir_resampled_M / neighbors_confidence_sum; } else confidence_weights_multiplier = 1.0f / valid_neighbors_count; mc += confidence_weights_multiplier * nume_mc / denom_mc; } return mi; } else { // Resampling the center pixel if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! return mc; } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/SpatiotemporalNormalizationWeight.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_SPATIOTEMPORAL_NORMALIZATION_WEIGHT_H #define DEVICE_RESTIR_DI_SPATIOTEMPORAL_NORMALIZATION_WEIGHT_H #include "Device/includes/ReSTIR/MISWeightsCommon.h" #include "Device/includes/ReSTIR/NeighborSimilarity.h" #include "Device/includes/ReSTIR/Utils.h" #define TEMPORAL_NEIGHBOR_ID 0 template struct ReSTIRSpatiotemporalNormalizationWeight {}; template struct ReSTIRSpatiotemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(const HIPRTRenderData& render_data, float final_reservoir_weight_sum, int initial_candidates_reservoir_M, const ReSTIRSurface& center_pixel_surface, int temporal_neighbor_M, int center_pixel_index, int2 temporal_neighbor_coords, float& out_normalization_nume, float& out_normalization_denom, Xorshift32Generator& random_number_generator) { if (final_reservoir_weight_sum <= 0.0f) { // Invalid reservoir, returning directly out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; return; } // 1/M MIS weights are basically confidence weights only i.e. c_i / sum(c_j) with // c_i = r_i.M out_normalization_nume = 1.0f; // We're simply going to divide by the sum of all the M values of all the neighbors we resampled (including the center pixel) // so we're only going to set the denominator to that and the numerator isn't going to change out_normalization_denom = 0.0f; const ReSTIRCommonSpatialPassSettings& spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); for (int neighbor = 0; neighbor < spatial_pass_settings.reuse_neighbor_count + 1; neighbor++) { // The last iteration of the loop is a special case that resamples the initial candidates reservoir // and so neighbor_pixel_index is never going to be used so we don't need to set it int neighbor_pixel_index; if (neighbor != spatial_pass_settings.reuse_neighbor_count) { neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor, temporal_neighbor_coords, random_number_generator); if (neighbor_pixel_index == -1) // Neighbor out of the viewport continue; if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface), render_data.render_settings.use_prev_frame_g_buffer())) continue; } // Getting the surface data at the neighbor // // The surface at the center pixel passed in parameters is // the surface in the current frame, that's what we want // since we're resampling initial candidates of the current // frame in the center pixel. We're not resampling the center // pixel from the previous frame so we need the current surface ReSTIRSurface neighbor_surface; if (neighbor == spatial_pass_settings.reuse_neighbor_count) neighbor_surface = center_pixel_surface; else neighbor_surface = get_pixel_surface(render_data, neighbor_pixel_index, render_data.render_settings.use_prev_frame_g_buffer(), random_number_generator); if (neighbor == spatial_pass_settings.reuse_neighbor_count) out_normalization_denom += initial_candidates_reservoir_M; else out_normalization_denom += ReSTIRSettingsHelper::get_restir_spatial_pass_input_reservoir_M(render_data, neighbor_pixel_index); } // The fused spatiotemporal pass also resamples a temporal neighbor so we add the M of that neighbor too out_normalization_denom += temporal_neighbor_M; } }; template struct ReSTIRSpatiotemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(const HIPRTRenderData& render_data, const ReSTIRSampleType& final_reservoir_sample, float final_reservoir_weight_sum, ReSTIRSurface& center_pixel_surface, ReSTIRSurface& temporal_neighbor_surface, int center_pixel_M, int temporal_neighbor_M, int center_pixel_index, int2 temporal_neighbor_position, float& out_normalization_nume, float& out_normalization_denom, Xorshift32Generator& random_number_generator) { if (final_reservoir_weight_sum <= 0) { // Invalid reservoir, returning directly out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; return; } // Checking how many of our neighbors could have produced the sample that we just picked // and we're going to divide by the sum of M values of those neighbors out_normalization_denom = 0.0f; out_normalization_nume = 1.0f; const ReSTIRCommonSpatialPassSettings& spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); for (int neighbor = 0; neighbor < spatial_pass_settings.reuse_neighbor_count + 1; neighbor++) { // The last iteration of the loop is a special case that resamples the initial candidates reservoir // and so neighbor_pixel_index is never going to be used so we don't need to set it int neighbor_pixel_index; if (neighbor != spatial_pass_settings.reuse_neighbor_count) { neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor, temporal_neighbor_position, random_number_generator); if (neighbor_pixel_index == -1) // Invalid neighbor continue; if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface), render_data.render_settings.use_prev_frame_g_buffer())) continue; } // Getting the surface data at the neighbor // // The surface at the center pixel passed in parameters is // the surface in the current frame, that's what we want // since we're resampling initial candidates of the current // frame in the center pixel. We're not resampling the center // pixel from the previous frame so we need the current surface ReSTIRSurface neighbor_surface; if (neighbor == spatial_pass_settings.reuse_neighbor_count) neighbor_surface = center_pixel_surface; else neighbor_surface = get_pixel_surface(render_data, neighbor_pixel_index, render_data.render_settings.use_prev_frame_g_buffer(), random_number_generator); float target_function_at_neighbor; if constexpr (IsReSTIRGI) // ReSTIR GI target function target_function_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, neighbor_surface, random_number_generator); else // ReSTIR DI target function target_function_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, neighbor_surface, random_number_generator); if (target_function_at_neighbor > 0.0f) { // If the neighbor could have produced this sample... if (neighbor == spatial_pass_settings.reuse_neighbor_count) out_normalization_denom += center_pixel_M; else out_normalization_denom += ReSTIRSettingsHelper::get_restir_spatial_pass_input_reservoir_M(render_data, neighbor_pixel_index); } } // Also taking the temporal neighbor into account which bool target_function_at_neighbor_gt_0; if constexpr (IsReSTIRGI) // ReSTIR GI target function target_function_at_neighbor_gt_0 = ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, temporal_neighbor_surface, random_number_generator) > 0.0f; else // ReSTIR DI target function target_function_at_neighbor_gt_0 = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, temporal_neighbor_surface, random_number_generator) > 0.0f; if (target_function_at_neighbor_gt_0) out_normalization_denom += temporal_neighbor_M; } }; template struct ReSTIRSpatiotemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(const HIPRTRenderData& render_data, const ReSTIRSampleType& final_reservoir_sample, float final_reservoir_weight_sum, ReSTIRSurface& center_pixel_surface, ReSTIRSurface& temporal_neighbor_surface, int selected_neighbor, int center_pixel_M, int temporal_neighbor_M, int center_pixel_index, int2 temporal_neighbor_coords, float& out_normalization_nume, float& out_normalization_denom, Xorshift32Generator& random_number_generator) { if (final_reservoir_weight_sum <= 0) { // Invalid reservoir, returning directly out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; return; } out_normalization_denom = 0.0f; out_normalization_nume = 0.0f; const ReSTIRCommonSpatialPassSettings& spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); for (int neighbor = 0; neighbor < spatial_pass_settings.reuse_neighbor_count + 1; neighbor++) { // The last iteration of the loop is a special case that resamples the initial candidates reservoir // and so neighbor_pixel_index is never going to be used so we don't need to set it int neighbor_pixel_index; if (neighbor != spatial_pass_settings.reuse_neighbor_count) { neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor, temporal_neighbor_coords, random_number_generator); if (neighbor_pixel_index == -1) // Invalid neighbor continue; if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface), render_data.render_settings.use_prev_frame_g_buffer())) continue; } // Getting the surface data at the neighbor // // The surface at the center pixel passed in parameters is // the surface in the current frame, that's what we want // since we're resampling initial candidates of the current // frame in the center pixel. We're not resampling the center // pixel from the previous frame so we need the current surface ReSTIRSurface neighbor_surface; if (neighbor == spatial_pass_settings.reuse_neighbor_count) neighbor_surface = center_pixel_surface; else neighbor_surface = get_pixel_surface(render_data, neighbor_pixel_index, random_number_generator); float target_function_at_neighbor; if constexpr (IsReSTIRGI) // ReSTIR GI target function target_function_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, neighbor_surface, random_number_generator); else // ReSTIR DI target function target_function_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, neighbor_surface, random_number_generator); if (target_function_at_neighbor > 0.0f) { // If the neighbor could have produced this sample... int M = 1; if (ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights) { if (neighbor == spatial_pass_settings.reuse_neighbor_count) M = center_pixel_M; else M = ReSTIRSettingsHelper::get_restir_spatial_pass_input_reservoir_M(render_data, neighbor_pixel_index); } // neighbor + 1 here because 0 is the temporal neighbor, not the first spatial neighbor if (neighbor + 1 == selected_neighbor) // Not multiplying by M here, this was done already when resampling the sample if we // were using confidence weights out_normalization_nume += target_function_at_neighbor; out_normalization_denom += target_function_at_neighbor * M; }; } // Now handling the temporal neighbor float target_function_at_temporal_neighbor; if constexpr (IsReSTIRGI) // ReSTIR GI target function target_function_at_temporal_neighbor = ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, temporal_neighbor_surface, random_number_generator); else // ReSTIR DI target function target_function_at_temporal_neighbor = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, temporal_neighbor_surface, random_number_generator); if (selected_neighbor == TEMPORAL_NEIGHBOR_ID) out_normalization_nume += target_function_at_temporal_neighbor; int temporal_M = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights ? temporal_neighbor_M : 1; out_normalization_denom += target_function_at_temporal_neighbor * temporal_M; } }; template struct ReSTIRSpatiotemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the neighbors with balance heuristic MIS weights in the m_i terms out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRSpatiotemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the neighbors out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRSpatiotemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the neighbors out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRSpatiotemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the neighbors out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRSpatiotemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the neighbors out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; #endif ================================================ FILE: src/Device/includes/ReSTIR/Surface.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_SURFACE_H #define DEVICE_RESTIR_DI_SURFACE_H #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" struct ReSTIRSurface { DeviceUnpackedEffectiveMaterial material; RayVolumeState ray_volume_state; int primitive_index; // Do we need the view direction here? We can probably reconstruct it float3 view_direction = { 0.0f, 0.0f, 0.0f}; float3 shading_normal = { 0.0f, 0.0f, 0.0f}; float3 geometric_normal = { 0.0f, 0.0f, 0.0f}; float3 shading_point = { 0.0f, 0.0f, 0.0f }; }; HIPRT_HOST_DEVICE HIPRT_INLINE ReSTIRSurface get_pixel_surface(const HIPRTRenderData& render_data, int pixel_index, Xorshift32Generator& random_number_generator) { ReSTIRSurface surface; surface.material = render_data.g_buffer.materials[pixel_index].unpack(); surface.primitive_index = render_data.g_buffer.first_hit_prim_index[pixel_index]; surface.ray_volume_state.reconstruct_first_hit( surface.material, render_data.buffers.material_indices, surface.primitive_index, random_number_generator); surface.view_direction = render_data.g_buffer.get_view_direction(render_data.current_camera.position, pixel_index); surface.shading_normal = render_data.g_buffer.shading_normals[pixel_index].unpack(); surface.geometric_normal = render_data.g_buffer.geometric_normals[pixel_index].unpack(); surface.shading_point = render_data.g_buffer.primary_hit_position[pixel_index]; return surface; } /** * Returns the surface at a pixel in the previous frame (so before the camera moved if it is in motion) * This is needed for unbiasedness in motion in the temporal reuse pass because when we count the neighbors * that could have produced the sample that we picked, we need to consider the neighbors at their previous positions, * not the current so we need to read in the last frame's g-buffer. */ HIPRT_HOST_DEVICE HIPRT_INLINE ReSTIRSurface get_pixel_surface_previous_frame(const HIPRTRenderData& render_data, int pixel_index, Xorshift32Generator& random_number_generator) { ReSTIRSurface surface; surface.material = render_data.g_buffer_prev_frame.materials[pixel_index].unpack(); surface.primitive_index = render_data.g_buffer_prev_frame.first_hit_prim_index[pixel_index]; surface.ray_volume_state.reconstruct_first_hit( surface.material, render_data.buffers.material_indices, surface.primitive_index, random_number_generator); surface.view_direction = render_data.g_buffer.get_view_direction(render_data.prev_camera.position, pixel_index); surface.shading_normal = render_data.g_buffer_prev_frame.shading_normals[pixel_index].unpack(); surface.geometric_normal = render_data.g_buffer_prev_frame.geometric_normals[pixel_index].unpack(); surface.shading_point = render_data.g_buffer_prev_frame.primary_hit_position[pixel_index]; return surface; } /** * Simple overload of the function to base the 'previous_frame' decision on a boolean instead of on the name of the function */ HIPRT_HOST_DEVICE HIPRT_INLINE ReSTIRSurface get_pixel_surface(const HIPRTRenderData& render_data, int pixel_index, bool previous_frame, Xorshift32Generator& random_number_generator) { if (previous_frame) return get_pixel_surface_previous_frame(render_data, pixel_index, random_number_generator); else return get_pixel_surface(render_data, pixel_index, random_number_generator); } #endif ================================================ FILE: src/Device/includes/ReSTIR/TemporalMISWeight.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_MIS_WEIGHT_H #define DEVICE_RESTIR_DI_MIS_WEIGHT_H #include "Device/includes/ReSTIR/DI/TargetFunction.h" #include "Device/includes/ReSTIR/GI/TargetFunction.h" #include "Device/includes/ReSTIR/MISWeightsCommon.h" #include "Device/includes/ReSTIR/Utils.h" #include "HostDeviceCommon/KernelOptions/KernelOptions.h" // By convention, the temporal neighbor is the first one to be resampled in for loops // (for looping over the neighbors when resampling / computing MIS weights) // So instead of hardcoding 0 everywhere in the code, we just basically give it a name // with a #define #define TEMPORAL_NEIGHBOR_ID 0 // Same when resampling the initial candidates #define INITIAL_CANDIDATES_ID 1 /** * This structure here is only meant to encapsulate one method that * returns the resampling MIS weight used by the temporal resampling pass. * * This whole file basically defines the functions to compute the different resampling * MIS weights that the renderer supports. * * This is cleaner that having a single function with a ton of * * #if BiasCorrectionmode == 1_OVER_M * #elif BiasCorrectionmode == 1_OVER_Z * #elif BiasCorrectionmode == MIS_LIKE * .... * * We now have one structure per MIS weight computation mode instead of one #if / #elif */ template struct ReSTIRTemporalResamplingMISWeight {}; template struct ReSTIRTemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const ReSTIRDIReservoir& reservoir_being_resampled) { // 1/M MIS Weights are basically confidence weights only so we only need to return // the confidence of the reservoir return reservoir_being_resampled.M; } }; template struct ReSTIRTemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const ReSTIRDIReservoir& reservoir_being_resampled) { // 1/Z MIS Weights are basically confidence weights only so we only need to return // the confidence of the reservoir. The difference with 1/M weights is how we're going // to normalize the reservoir at the end of the temporal/spatial resampling pass return reservoir_being_resampled.M; } }; template struct ReSTIRTemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, const ReSTIRDIReservoir& reservoir_being_resampled) { if (ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights) { // MIS-like MIS weights with confidence weights are basically a mix of 1/Z // and MIS like for the normalization so we're just returning the confidence here // so that a reservoir that is being resampled gets a bigger weight depending on its // confidence weight (M). return reservoir_being_resampled.M; } else { // MIS-like MIS weights without confidence weights do not weight the neighbor reservoirs // during resampling. We're thus returning 1.0f. // // The bulk of the work of the MIS-like weights is done in during the normalization of the reservoir return 1.0f; } } }; template struct ReSTIRTemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, const ReSTIRSampleType& reservoir_being_resampled_sample, float initial_candidates_reservoir_M, ReSTIRSurface& temporal_neighbor_surface, ReSTIRSurface& center_pixel_surface, int temporal_neighbor_reservoir_M, int current_neighbor_index, Xorshift32Generator& random_number_generator) { float nume = 0.0f; // We already have the target function at the center pixel, adding it to the denom float denom = 0.0f; // Evaluating the sample that we're resampling at the neighor locations (using the neighbors surfaces) float target_function_at_temporal_neighbor = 0.0f; if (temporal_neighbor_reservoir_M != 0) { // Only computing the target function if we do have a temporal neighbor if constexpr (IsReSTIRGI) // ReSTIR GI target function target_function_at_temporal_neighbor = ReSTIR_GI_evaluate_target_function(render_data, reservoir_being_resampled_sample, temporal_neighbor_surface, random_number_generator); else // ReSTIR DI target function target_function_at_temporal_neighbor = ReSTIR_DI_evaluate_target_function(render_data, reservoir_being_resampled_sample, temporal_neighbor_surface, random_number_generator); } if (current_neighbor_index == TEMPORAL_NEIGHBOR_ID && target_function_at_temporal_neighbor == 0.0f) // If we're currently computing the MIS weight for the temporal neighbor, // this means that we're going to have the temporal neighbor weight // (target function) in the numerator. But if the target function // at the temporal neighbor is 0.0f, then we're going to have 0.0f // in the numerator --> 0.0f MIS weight anyways --> no need to // compute anything else, we can already return 0.0f for the MIS weight. return 0.0f; float target_function_at_center; if constexpr (IsReSTIRGI) // ReSTIR GI target function target_function_at_center = ReSTIR_GI_evaluate_target_function(render_data, reservoir_being_resampled_sample, temporal_neighbor_surface, random_number_generator); else // ReSTIR DI target function target_function_at_center = ReSTIR_DI_evaluate_target_function(render_data, reservoir_being_resampled_sample, temporal_neighbor_surface, random_number_generator); int temporal_M = temporal_neighbor_reservoir_M; int center_reservoir_M = initial_candidates_reservoir_M; if (!ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights) { temporal_M = 1; center_reservoir_M = 1; } if (current_neighbor_index == TEMPORAL_NEIGHBOR_ID) nume = target_function_at_temporal_neighbor * temporal_M; else nume = target_function_at_center * center_reservoir_M; denom = target_function_at_temporal_neighbor * temporal_M + target_function_at_center * center_reservoir_M; if (denom == 0.0f) return 0.0f; else return nume / denom; } }; template struct ReSTIRTemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, ReSTIRReservoirType& temporal_neighbor_reservoir, ReSTIRReservoirType& initial_candidates_reservoir, ReSTIRSurface& center_pixel_surface, ReSTIRSurface& temporal_neighbor_surface, float neighbor_sample_target_function_at_center, int current_neighbor_index, Xorshift32Generator& random_number_generator) { if (current_neighbor_index == TEMPORAL_NEIGHBOR_ID) { // Resampling the temporal neighbor float target_function_at_neighbor = temporal_neighbor_reservoir.sample.target_function; float target_function_at_center = neighbor_sample_target_function_at_center; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float temporal_neighbor_M = use_confidence_weights ? temporal_neighbor_reservoir.M : 1; float center_reservoir_M = use_confidence_weights ? initial_candidates_reservoir.M : 1; float neighbors_confidence_sum = use_confidence_weights ? temporal_neighbor_reservoir.M : 1; float nume = target_function_at_neighbor * temporal_neighbor_M; float denom = target_function_at_neighbor * neighbors_confidence_sum + target_function_at_center * center_reservoir_M; float mi = denom == 0.0f ? 0.0f : (nume / denom); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, initial_candidates_reservoir.sample, temporal_neighbor_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { // If this is an envmap path the jacobian is just 1 so this is not needed if (!initial_candidates_reservoir.sample.is_envmap_path()) { float jacobian = get_jacobian_determinant_reconnection_shift(initial_candidates_reservoir.sample.sample_point, initial_candidates_reservoir.sample.sample_point_geometric_normal.unpack(), temporal_neighbor_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, initial_candidates_reservoir.sample, temporal_neighbor_surface, random_number_generator); float target_function_center_sample_at_center = initial_candidates_reservoir.sample.target_function; float nume_mc = target_function_center_sample_at_center * center_reservoir_M; float denom_mc = target_function_center_sample_at_neighbor * neighbors_confidence_sum + target_function_center_sample_at_center * center_reservoir_M; float confidence_multiplier = 1.0f; if (use_confidence_weights) confidence_multiplier = temporal_neighbor_M / neighbors_confidence_sum; if (denom_mc != 0.0f) mc += nume_mc / denom_mc * confidence_multiplier; return mi; } else { // Resampling the center pixel (initial candidates) if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! return mc; } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; template struct ReSTIRTemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, ReSTIRReservoirType& temporal_neighbor_reservoir, ReSTIRReservoirType& initial_candidates_reservoir, ReSTIRSurface& center_pixel_surface, ReSTIRSurface& temporal_neighbor_surface, float neighbor_sample_target_function_at_center, int current_neighbor_index, Xorshift32Generator& random_number_generator) { if (current_neighbor_index == TEMPORAL_NEIGHBOR_ID) { // Resampling the temporal neighbor float target_function_at_neighbor = temporal_neighbor_reservoir.sample.target_function; float target_function_at_center = neighbor_sample_target_function_at_center; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float temporal_neighbor_M = use_confidence_weights ? temporal_neighbor_reservoir.M : 1; float center_reservoir_M = use_confidence_weights ? initial_candidates_reservoir.M : 1; float neighbors_confidence_sum = use_confidence_weights ? temporal_neighbor_reservoir.M : 1; float nume = target_function_at_neighbor * temporal_neighbor_M; float denom = target_function_at_neighbor * neighbors_confidence_sum + target_function_at_center * center_reservoir_M; float mi = denom == 0.0f ? 0.0f : (nume / denom); if (use_confidence_weights) // Eq 7.8 mi *= neighbors_confidence_sum / (neighbors_confidence_sum + center_reservoir_M); float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, initial_candidates_reservoir.sample, temporal_neighbor_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { // If this is an envmap path the jacobian is just 1 so this is not needed if (!initial_candidates_reservoir.sample.is_envmap_path()) { float jacobian = get_jacobian_determinant_reconnection_shift(initial_candidates_reservoir.sample.sample_point, initial_candidates_reservoir.sample.sample_point_geometric_normal.unpack(), temporal_neighbor_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, initial_candidates_reservoir.sample, temporal_neighbor_surface, random_number_generator); float target_function_center_sample_at_center = initial_candidates_reservoir.sample.target_function; float nume_mc = target_function_center_sample_at_center * center_reservoir_M; float denom_mc = target_function_center_sample_at_neighbor * neighbors_confidence_sum + target_function_center_sample_at_center * center_reservoir_M; float confidence_multiplier = 1.0f; if (use_confidence_weights) confidence_multiplier = neighbors_confidence_sum / (neighbors_confidence_sum + center_reservoir_M); if (denom_mc != 0.0f) mc += nume_mc / denom_mc * confidence_multiplier; if (use_confidence_weights) return mi; else // In the defensive formulation, we want to divide by M, not M-1. // (Eq. 7.6 of "A Gentle Introduction to ReSTIR") // And we only want to divide if not using confidence weights // // M = 2 (center reservoir + temporal reservoir) return mi * 0.5f; } else { // Resampling the center pixel (initial candidates) if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else { // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! // In the defensive formulation, we want to divide by M, not M-1. // (Eq. 7.6 of "A Gentle Introduction to ReSTIR") if (ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights) return mc + static_cast(initial_candidates_reservoir.M) / static_cast(initial_candidates_reservoir.M + temporal_neighbor_reservoir.M); else return (1.0f + mc) * 0.5f; } } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; template struct ReSTIRTemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, ReSTIRReservoirType& temporal_neighbor_reservoir, ReSTIRReservoirType& initial_candidates_reservoir, ReSTIRSurface& center_pixel_surface, ReSTIRSurface& temporal_neighbor_surface, float neighbor_sample_target_function_at_center, int current_neighbor_index, Xorshift32Generator& random_number_generator) { if (current_neighbor_index == TEMPORAL_NEIGHBOR_ID) { // Resampling the temporal neighbor float target_function_neighbor_sample_at_neighbor = temporal_neighbor_reservoir.sample.target_function; float target_function_neighbor_sample_at_center = neighbor_sample_target_function_at_center; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float temporal_neighbor_M = use_confidence_weights ? temporal_neighbor_reservoir.M : 1; float center_reservoir_M = use_confidence_weights ? initial_candidates_reservoir.M : 1; float neighbors_confidence_sum = use_confidence_weights ? temporal_neighbor_M : 1; // Eq. 15 of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, 2024] generalized // with confidence weights float difference_function = symmetric_ratio_MIS_weights_difference_function(target_function_neighbor_sample_at_center, target_function_neighbor_sample_at_neighbor, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float nume_mi = difference_function * temporal_neighbor_M; float denom_mi = center_reservoir_M + neighbors_confidence_sum * difference_function; float mi = nume_mi / denom_mi; float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, initial_candidates_reservoir.sample, temporal_neighbor_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { // If this is an envmap path the jacobian is just 1 so this is not needed if (!initial_candidates_reservoir.sample.is_envmap_path()) { float jacobian = get_jacobian_determinant_reconnection_shift(initial_candidates_reservoir.sample.sample_point, initial_candidates_reservoir.sample.sample_point_geometric_normal.unpack(), temporal_neighbor_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, initial_candidates_reservoir.sample, temporal_neighbor_surface, random_number_generator); float target_function_center_sample_at_center = initial_candidates_reservoir.sample.target_function; float nume_mc = center_reservoir_M; float denom_mc = center_reservoir_M + neighbors_confidence_sum * symmetric_ratio_MIS_weights_difference_function(target_function_center_sample_at_neighbor, target_function_center_sample_at_center, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float confidence_weights_multiplier; if (use_confidence_weights) { if (neighbors_confidence_sum == 0.0f) confidence_weights_multiplier = 0.0f; else confidence_weights_multiplier = temporal_neighbor_M / neighbors_confidence_sum; } else confidence_weights_multiplier = 1.0f; mc += confidence_weights_multiplier * nume_mc / denom_mc; return mi; } else { // Resampling the center pixel (initial candidates) if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! return mc; } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; template struct ReSTIRTemporalResamplingMISWeight { HIPRT_HOST_DEVICE float get_resampling_MIS_weight(const HIPRTRenderData& render_data, ReSTIRReservoirType& temporal_neighbor_reservoir, ReSTIRReservoirType& initial_candidates_reservoir, ReSTIRSurface& center_pixel_surface, ReSTIRSurface& temporal_neighbor_surface, float neighbor_sample_target_function_at_center, int current_neighbor_index, Xorshift32Generator& random_number_generator) { if (current_neighbor_index == TEMPORAL_NEIGHBOR_ID) { // Resampling a neighbor float target_function_neighbor_sample_at_neighbor = temporal_neighbor_reservoir.sample.target_function; float target_function_center_sample_at_center = initial_candidates_reservoir.sample.target_function; bool use_confidence_weights = ReSTIRSettingsHelper::get_restir_settings(render_data).use_confidence_weights; float temporal_neighbor_M = use_confidence_weights ? temporal_neighbor_reservoir.M : 1; float center_reservoir_M = use_confidence_weights ? initial_candidates_reservoir.M : 1; float neighbors_confidence_sum = use_confidence_weights ? temporal_neighbor_M : 1; // Eq. 15 of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, 2024] generalized // with confidence weights float difference_function = symmetric_ratio_MIS_weights_difference_function(neighbor_sample_target_function_at_center, target_function_neighbor_sample_at_neighbor, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); float nume_mi, denom_mi; // Eq. 16 of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, 2024] generalized // with confidence weights if (neighbor_sample_target_function_at_center <= target_function_neighbor_sample_at_neighbor) { nume_mi = difference_function * temporal_neighbor_M; denom_mi = center_reservoir_M + neighbors_confidence_sum * difference_function; } else { nume_mi = difference_function * temporal_neighbor_M; denom_mi = center_reservoir_M + neighbors_confidence_sum; } float mi = nume_mi / denom_mi; float target_function_center_sample_at_neighbor; if constexpr (IsReSTIRGI) { // ReSTIR GI target function target_function_center_sample_at_neighbor = ReSTIR_GI_evaluate_target_function(render_data, initial_candidates_reservoir.sample, temporal_neighbor_surface, random_number_generator); // Because we're using the target function as a PDF here, we need to scale the PDF // by the jacobian. That's p_hat_from_i, Eq. 5.9 of "A Gentle Introduction to ReSTIR" // Only doing this if we at least have a target function to scale by the jacobian if (target_function_center_sample_at_neighbor > 0.0f) { // If this is an envmap path the jacobian is just 1 so this is not needed if (!initial_candidates_reservoir.sample.is_envmap_path()) { float jacobian = get_jacobian_determinant_reconnection_shift(initial_candidates_reservoir.sample.sample_point, initial_candidates_reservoir.sample.sample_point_geometric_normal.unpack(), temporal_neighbor_surface.shading_point, center_pixel_surface.shading_point, render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); if (jacobian == 0.0f) // Clamping at 0.0f so that if the jacobian returned is -1.0f (meaning that the jacobian doesn't match the threshold // and has been rejected), the target function is set to 0 target_function_center_sample_at_neighbor = 0.0f; else target_function_center_sample_at_neighbor *= jacobian; } } } else // ReSTIR DI target function target_function_center_sample_at_neighbor = ReSTIR_DI_evaluate_target_function(render_data, initial_candidates_reservoir.sample, temporal_neighbor_surface, random_number_generator); float nume_mc, denom_mc; float difference_function_mc = symmetric_ratio_MIS_weights_difference_function(target_function_center_sample_at_neighbor, target_function_center_sample_at_center, ReSTIRSettingsHelper::get_restir_settings(render_data).symmetric_ratio_mis_weights_beta_exponent); if (target_function_center_sample_at_center <= target_function_center_sample_at_neighbor) { nume_mc = difference_function_mc * temporal_neighbor_M; denom_mc = center_reservoir_M + neighbors_confidence_sum * difference_function_mc; } else { nume_mc = difference_function_mc * temporal_neighbor_M; denom_mc = center_reservoir_M + neighbors_confidence_sum; } /*float confidence_weights_multiplier; if (use_confidence_weights) { if (neighbors_confidence_sum == 0.0f) confidence_weights_multiplier = 0.0f; else confidence_weights_multiplier = reservoir_resampled_M / neighbors_confidence_sum; } else confidence_weights_multiplier = 1.0f / valid_neighbors_count;*/ mc += nume_mc / denom_mc; return mi; } else { // Resampling the center pixel if (mc == 0.0f) // If there was no neighbor resampling (and mc hasn't been accumulated), // then the MIS weight should be 1 for the center pixel. It gets all the weight // since no neighbor was resampled return 1.0f; else // Returning the weight accumulated so far when resampling the neighbors. // // !!! This assumes that the center pixel is resampled last (which it is in this ReSTIR implementation) !!! // // This is Eq. 16 of the paper: y not in R: m_i(y) = 1 - Sum(...) / |R| // mc here is the sum // and |R| is 1 return 1.0f - mc; } } // Weight for the canonical sample (center pixel) float mc = 0.0f; }; #endif ================================================ FILE: src/Device/includes/ReSTIR/TemporalNormalizationWeight.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_NORMALIZATION_WEIGHT_H #define DEVICE_RESTIR_DI_NORMALIZATION_WEIGHT_H #include "Device/includes/ReSTIR/Utils.h" #include "HostDeviceCommon/KernelOptions/KernelOptions.h" // By convention, the temporal neighbor is the first one to be resampled in for loops // (for looping over the neighbors when resampling / computing MIS weights) // So instead of hardcoding 0 everywhere in the code, we just basically give it a name // with a #define #define TEMPORAL_NEIGHBOR_ID 0 // Same when resampling the initial candidates #define INITIAL_CANDIDATES_ID 1 /** * This structure here is only meant to encapsulate one method that * returns the numerator and denominator for normalizing a reservoir at * the end of the temporal / spatial reuse pass. * * This is cleaner that having a single function with a ton of * * #if BiasCorrectionmode == 1_OVER_M * #elif BiasCorrectionmode == 1_OVER_Z * #elif BiasCorrectionmode == MIS_LIKE * .... * * We now have one structure per bias correction method one #if / #elif */ template struct ReSTIRTemporalNormalizationWeight {}; template struct ReSTIRTemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float final_reservoir_weight_sum, int initial_candidates_M, int temporal_neighbor_M, float& out_normalization_nume, float& out_normalization_denom) { if (final_reservoir_weight_sum <= 0) { // Invalid reservoir, returning directly out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; return; } // 1/M MIS weights are basically confidence weights only i.e. c_i / sum(c_j) with // c_i = r_i.M out_normalization_nume = 1.0f; // We're simply going to divide by the sum of all the M values of all the neighbors we resampled (including the center pixel) // so we're only going to set the denominator to that and the numerator isn't going to change out_normalization_denom = initial_candidates_M + temporal_neighbor_M; } }; template struct ReSTIRTemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(const HIPRTRenderData& render_data, const ReSTIRSampleType& final_reservoir_sample, float final_reservoir_weight_sum, int initial_candidates_M, int temporal_neighbor_M, ReSTIRSurface& center_pixel_surface, ReSTIRSurface& temporal_neighbor_surface, float& out_normalization_nume, float& out_normalization_denom, Xorshift32Generator& random_number_generator) { if (final_reservoir_weight_sum <= 0) { // Invalid reservoir, returning directly out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; return; } out_normalization_nume = 1.0f; // Checking how many of our neighbors could have produced the sample that we just picked // and we're going to divide by the sum of M values of those neighbors out_normalization_denom = 0.0f; // We're resampling from two reservoirs (the initial candidates and the temporal neighbor). // Either of these two reservoirs could have potentially produced the sample that we retained // in the 'reservoir' parameter. // // The question is: how many neighbors could have produced that sample? // The sample could have been produced by a neighbor if the target function of the neighbor with // that sample is > so we're going to check both target function here. // Evaluating the target function at the center pixel because this is the pixel of the initial candidates float center_pixel_target_function; if constexpr (IsReSTIRGI) // ReSTIR GI target function center_pixel_target_function = ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, center_pixel_surface, random_number_generator); else // ReSTIR DI target function center_pixel_target_function = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, center_pixel_surface, random_number_generator); // if the sample contained in our final reservoir (the 'reservoir' parameter) could have been produced by the center // pixel, we're adding the confidence of that pixel to the denominator for normalization out_normalization_denom += (center_pixel_target_function > 0) * initial_candidates_M; if (temporal_neighbor_M > 0) { // We only want to check if the temporal could have produced the sample if we actually have a temporal neighbor float temporal_neighbor_target_function; if constexpr (IsReSTIRGI) // ReSTIR GI target function temporal_neighbor_target_function = ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, temporal_neighbor_surface, random_number_generator); else // ReSTIR DI target function temporal_neighbor_target_function = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, temporal_neighbor_surface, random_number_generator); out_normalization_denom += (temporal_neighbor_target_function > 0) * temporal_neighbor_M; } } }; template struct ReSTIRTemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(const HIPRTRenderData& render_data, const ReSTIRSampleType& final_reservoir_sample, float final_reservoir_weight_sum, int initial_candidates_M, int temporal_neighbor_M, ReSTIRSurface& center_pixel_surface, ReSTIRSurface& temporal_neighbor_surface, int selected_neighbor, float& out_normalization_nume, float& out_normalization_denom, Xorshift32Generator& random_number_generator) { if (final_reservoir_weight_sum <= 0) { // Invalid reservoir, returning directly out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; return; } float center_pixel_target_function; if constexpr (IsReSTIRGI) // ReSTIR GI target function center_pixel_target_function = ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, center_pixel_surface, random_number_generator); else // ReSTIR DI target function center_pixel_target_function = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, center_pixel_surface, random_number_generator); float temporal_neighbor_target_function = 0.0f; if (temporal_neighbor_M > 0) { // Only evaluating the target function if we actually have a temporal neighbor because if we don't, // this means that no temporal neighbor contributed to the resampling of the sample in 'reservoir' // and if the temporal neighbor didn't contribute to the resampling, then this is not, in MIS terms, // a sampling technique/strategy to take into account in the MIS weight if constexpr (IsReSTIRGI) // ReSTIR GI target function temporal_neighbor_target_function = ReSTIR_GI_evaluate_target_function(render_data, final_reservoir_sample, temporal_neighbor_surface, random_number_generator); else // ReSTIR DI target function temporal_neighbor_target_function = ReSTIR_DI_evaluate_target_function(render_data, final_reservoir_sample, temporal_neighbor_surface, random_number_generator); } if (selected_neighbor == INITIAL_CANDIDATES_ID) // The point of the MIS-like MIS weights is to have the weight of the sample that we picked // in the numerator and the sum of everyone in the denominator. // // So if this is the sample that we picked, we're putting its target function value in the numerator // // Not multiplying by M here because this is done already during the resampling (in the resampling MIS weights) out_normalization_nume = center_pixel_target_function; else // Otherwise, if the sample that we picked is from the temporal neighbor, then the temporal // neighbor's target function is the one in the numerator out_normalization_nume = temporal_neighbor_target_function; if (!render_data.render_settings.restir_di_settings.use_confidence_weights) { // If not using confidence weights, settings the weights to 1 so that everyone has the same weight initial_candidates_M = 1; temporal_neighbor_M = 1; } out_normalization_denom = center_pixel_target_function * initial_candidates_M + temporal_neighbor_target_function * temporal_neighbor_M; } }; template struct ReSTIRTemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the // neighbors with balance heuristic MIS weights in the m_i terms out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRTemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the // neighbors. Everything is already in the MIS weights m_i. out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRTemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the // neighbors. Everything is already in the MIS weights m_i. out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRTemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the // neighbors. Everything is already in the MIS weights m_i. out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; template struct ReSTIRTemporalNormalizationWeight { HIPRT_HOST_DEVICE void get_normalization(float& out_normalization_nume, float& out_normalization_denom) { // Nothing more to normalize, everything is already handled when resampling the // neighbors. Everything is already in the MIS weights m_i. out_normalization_nume = 1.0f; out_normalization_denom = 1.0f; } }; #endif ================================================ FILE: src/Device/includes/ReSTIR/Utils.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_UTILS_H #define DEVICE_RESTIR_DI_UTILS_H #include "Device/includes/Dispatcher.h" #include "Device/includes/LightSampling/Envmap.h" #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/ReSTIR/Surface.h" #include "Device/includes/ReSTIR/NeighborSimilarity.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/ReSTIRSettingsHelper.h" /** * 'last_primitive_hit_index' is the index of the triangle we're currently sitting * on and that we're shooting a ray from. This is used to avoid self intersections. * * Returns true if the reservoir was killed, false otherwise */ HIPRT_HOST_DEVICE HIPRT_INLINE bool ReSTIR_DI_visibility_test_kill_reservoir(const HIPRTRenderData& render_data, ReSTIRDIReservoir& reservoir, float3 shading_point, int last_primitive_hit_index, Xorshift32Generator& random_number_generator) { if (reservoir.UCW <= 0.0f && reservoir.weight_sum <= 0.0f) return false; else if (reservoir.sample.flags & ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED) // The sample is already unoccluded, no need to test for visibility return false; float distance_to_light; float3 sample_direction; if (reservoir.sample.is_envmap_sample()) { sample_direction = matrix_X_vec(render_data.world_settings.envmap_to_world_matrix, reservoir.sample.point_on_light_source); distance_to_light = 1.0e35f; } else { sample_direction = reservoir.sample.point_on_light_source - shading_point; sample_direction /= (distance_to_light = hippt::length(sample_direction)); } hiprtRay shadow_ray; shadow_ray.origin = shading_point; shadow_ray.direction = sample_direction; bool visible = !evaluate_shadow_ray_occluded(render_data, shadow_ray, distance_to_light, last_primitive_hit_index, /* bounce. Always 0 for ReSTIR DI*/ 0, random_number_generator); if (!visible) { // Setting to -1 here so that we know when debugging that this is because of visibility reuse reservoir.UCW = -1.0f; return true; } else { // Visible so the sample is unoccluded reservoir.sample.flags |= RESTIR_DI_FLAGS_UNOCCLUDED; return false; } } /** * Tests the visibility of the sample containde in 'reservoir' from the given shading point and kills the reservoir * if the visibility is occluded * * Returns true if the reservoir was killed, false otherwise */ HIPRT_HOST_DEVICE HIPRT_INLINE bool ReSTIR_GI_visibility_validation(const HIPRTRenderData& render_data, ReSTIRGIReservoir& reservoir, float3 shading_point, int last_hit_primitive_index, Xorshift32Generator& random_number_generator) { if (reservoir.UCW <= 0.0f && reservoir.weight_sum <= 0.0f) return false; float distance_to_sample_point; float3 sample_direction; if (reservoir.sample.is_envmap_path()) { // For envmap path, the direction is stored in the 'sample_point' value sample_direction = reservoir.sample.sample_point; distance_to_sample_point = 1.0e35f; } else { // Not an envmap path, the direction is the difference between the current shading // point and the reconnection point sample_direction = reservoir.sample.sample_point - shading_point; distance_to_sample_point = hippt::length(sample_direction); if (distance_to_sample_point <= 1.0e-6f) { // To avoid numerical instabilities, killing the reservoir reservoir.UCW = 0.0f; return true; } sample_direction /= distance_to_sample_point; } hiprtRay shadow_ray; shadow_ray.origin = shading_point; shadow_ray.direction = sample_direction; bool visible = !evaluate_shadow_ray_occluded(render_data, shadow_ray, distance_to_sample_point, last_hit_primitive_index, /* bounce. Always 1 for ReSTIR GI from visible point to sample point */ 1, random_number_generator); if (!visible) { // Setting to -1 here so that we know when debugging that this is because of visibility reuse reservoir.UCW = 0.0f; return true; } return false; } #endif ================================================ FILE: src/Device/includes/ReSTIR/UtilsSpatial.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_UTILS_SPATIAL_H #define DEVICE_RESTIR_UTILS_SPATIAL_H #include "Device/includes/PathTracing.h" #include "Device/includes/ReSTIR/Surface.h" #include "HostDeviceCommon/KernelOptions/ReSTIRGIOptions.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/ReSTIR/ReSTIRCommonSettings.h" #include "HostDeviceCommon/ReSTIRSettingsHelper.h" template HIPRT_DEVICE void setup_adaptive_directional_spatial_reuse(HIPRTRenderData& render_data, unsigned int center_pixel_index, Xorshift32Generator& random_number_generator) { ReSTIRCommonSpatialPassSettings& spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); // Generating a unique seed per pixel that will be used to generate the spatial neighbors of that pixel if Hammersley isn't used spatial_pass_settings.spatial_neighbors_rng_seed = random_number_generator.xorshift32(); if (spatial_pass_settings.do_adaptive_directional_spatial_reuse(render_data.render_settings.accumulate)) { spatial_pass_settings.reuse_radius = spatial_pass_settings.per_pixel_spatial_reuse_radius[center_pixel_index]; // Storing the direction reuse mask in the 'current_pixel_directions_reuse_mask' field of the spatial // reuse settings so that we don't have to carry that parameter around in function calls everywhere... // // This parameter will be read by later by the function that samples a neighbor based on the allowed directions spatial_pass_settings.current_pixel_directions_reuse_mask = ReSTIRSettingsHelper::get_spatial_reuse_direction_mask_ull(render_data, center_pixel_index); if (spatial_pass_settings.reuse_radius == 0) spatial_pass_settings.reuse_neighbor_count = 0; } } template HIPRT_DEVICE HIPRT_INLINE bool do_include_visibility_term_or_not(const HIPRTRenderData& render_data, int current_neighbor_index) { const ReSTIRCommonSpatialPassSettings& spatial_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); bool visibility_only_on_last_pass = spatial_settings.do_visibility_only_last_pass; bool is_last_pass = spatial_settings.spatial_pass_index == spatial_settings.number_of_passes - 1; // Only using the visibility term on the last pass if so desired bool include_target_function_visibility = visibility_only_on_last_pass && is_last_pass; // Also allowing visibility if we want it at every pass include_target_function_visibility |= !spatial_settings.do_visibility_only_last_pass; // Only doing visibility for a few neighbors depending on 'neighbor_visibility_count' include_target_function_visibility &= current_neighbor_index < spatial_settings.neighbor_visibility_count; // Only doing visibility if we want it at all include_target_function_visibility &= (IsReSTIRGI ? ReSTIR_GI_SpatialTargetFunctionVisibility : ReSTIR_DI_SpatialTargetFunctionVisibility); // We don't want visibility for the center pixel because we're going to reuse the // target function stored in the reservoir anyways // Note: the center pixel has index 'spatial_settings.reuse_neighbor_count' // while actual *neighbors* have index between [0, spatial_settings.reuse_neighbor_count - 1] include_target_function_visibility &= current_neighbor_index != spatial_settings.reuse_neighbor_count; return include_target_function_visibility; } /** * Returns a pair of random numbers that should be used to sample the spatial neighbor disk of the current pixel * (i.e. pass the returned float2 to 'sample_in_disk_uv'). * * This function samples UVs for sampling in a disk such that the point sampled is only sampled in the allowed * directions of a pixel (according to its direction reuse masks). * * Note that this function will sample the first sector if there are no sectors available around the given pixel */ HIPRT_DEVICE float2 sample_spatial_neighbor_from_allowed_directions(const HIPRTRenderData& render_data, const ReSTIRCommonSpatialPassSettings& spatial_pass_settings, int2 center_pixel_coords, Xorshift32Generator& rng) { unsigned long long int directions_mask = spatial_pass_settings.current_pixel_directions_reuse_mask; int number_of_allowed_sectors = hippt::popc(directions_mask); unsigned char random_sector_index = rng.random_index(number_of_allowed_sectors); // Now that we have our random sector, we need to find what theta rotation corresponds // to that sector // // So we're counting how many sectors come before our 'random_sector_index' and we're going to // multiply that sector count by 2Pi / 32 (or / 64 if using 64 bits) unsigned char count_left_to_go = random_sector_index + 1; // Counting how many sectors there before we reach our 'random_sector_index' int sector_index = 0; unsigned char bit_count_so_far = 0; if (hippt::popc(directions_mask) == ReSTIR_GI_SpatialDirectionalReuseBitCount) // Fast path if all the directions are allowed sector_index = random_sector_index; else { // A naive implementation of this would go something like // // for (i = 0; i < ReSTIR_GI_SpatialDirectionalReuseBitCount; i++) // { // if (directions_mask & (1ull << i)) // { // --count_left_to_go; // // if (count_left_to_go == 0) // break; // } // } // sector_index = i; // // i.e., counting the bits one by one until we counted the number of bits we needed // // // But here we're going to count the sectors 'count_left_to_go' by 'count_left_to_go' to get things // a bit faster. // // So if we have the directions mask: // - 01110000 // // and we want the 4th valid sector, i.e. random_sector_index == 3, then we can just go ahead and // count bits 4 by 4: // // 11110000 <--- 'directions_mask' // & // 00001111 <--- 'mask' // = // 00000000. // --> popc(00000000) = 0 -----> 0 bits found // // We move the mask to the left by the number of bits we still have to find (which is still 4): // 11110000 <--- 'directions_mask' // & // 11110000 <--- 'mask' // = // 11110000. // --> popc(11110000) = 4 -----> 4 bits found --> we found all the bits we needed so the sector index // is in position '10000000' = 7 here while (count_left_to_go > 0) { unsigned char mask_length = count_left_to_go; unsigned long long int mask = ((1ull << mask_length) - 1ull) << bit_count_so_far; int count_mask = hippt::popc(directions_mask & mask); count_left_to_go -= count_mask; bit_count_so_far += mask_length; } sector_index = --bit_count_so_far; } float theta_start = sector_index / (float)ReSTIR_GI_SpatialDirectionalReuseBitCount; // Generating a random theta in between theta_start and the start of the next sector (which is 1.0f / 32.0f wide) // i.e. a random theta inside our disk sector float random_theta = theta_start + rng() * (1.0f / (float)ReSTIR_GI_SpatialDirectionalReuseBitCount); return make_float2(random_theta, rng()); } /** * Returns the linear index that can be used directly to index a buffer * of render_data of the 'neighbor_number'th neighbor that we're going * to spatially reuse from * * 'neighbor_number' is in [0, neighbor_reuse_count] * 'neighbor_reuse_count' is in [1, ReSTIRCommonSpatialPassSettings.reuse_neighbor_count] * 'neighbor_reuse_radius' is the radius of the disk within which the neighbors are sampled * 'center_pixel_coords' is the coordinates of the center pixel that is currently * doing the resampling of its neighbors. Neighbors will be spatially sampled * around that position * 'res' is the resolution of the viewport. This is used to check whether the generated * neighbor location is outside of the viewport or not * 'rng' is a random generator used for generating spatial neighbor positions if not using a Hammersley * point set. * * Only used if render_data.render_settings.restir_settings.common_spatial_pass.use_hammersley == false */ template HIPRT_DEVICE HIPRT_INLINE int get_spatial_neighbor_pixel_index(const HIPRTRenderData& render_data, int neighbor_index, int2 center_pixel_coords, Xorshift32Generator& rng) { const ReSTIRCommonSpatialPassSettings& spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); int neighbor_pixel_index; if (neighbor_index == spatial_pass_settings.reuse_neighbor_count) { // If this is the last neighbor, we set it to ourselves // This is why our loop on the neighbors goes up to 'i < NEIGHBOR_REUSE_COUNT + 1' // It's so that when i == NEIGHBOR_REUSE_COUNT, we resample ourselves neighbor_pixel_index = center_pixel_coords.x + center_pixel_coords.y * render_data.render_settings.render_resolution.x; } else { // +1 and +1 here because we want to skip the first point as it is always (0, 0) // which means that we would be resampling ourselves (the center pixel) --> // pointless because we already resample ourselves "manually" (that's why there's that // "if (neighbor_index == neighbor_reuse_count)" above, to resample the center pixel) float2 uv; if (spatial_pass_settings.do_adaptive_directional_spatial_reuse(render_data.render_settings.accumulate)) uv = sample_spatial_neighbor_from_allowed_directions(render_data, spatial_pass_settings, center_pixel_coords, rng); else uv = make_float2(rng(), rng()); float2 neighbor_offset_in_disk = sample_in_disk_uv(spatial_pass_settings.reuse_radius, uv); int2 neighbor_offset_int = make_int2(static_cast(roundf(neighbor_offset_in_disk.x)), static_cast(roundf(neighbor_offset_in_disk.y))); int2 neighbor_pixel_coords; if (spatial_pass_settings.debug_neighbor_location) { int2 offset; if (spatial_pass_settings.debug_neighbor_location_direction == 0) // Horizontal offset = make_int2(spatial_pass_settings.reuse_radius, 0); else if (spatial_pass_settings.debug_neighbor_location_direction == 1) // Vertical offset = make_int2(0, spatial_pass_settings.reuse_radius); else // Diagonal offset = make_int2(spatial_pass_settings.reuse_radius, spatial_pass_settings.reuse_radius); neighbor_pixel_coords = center_pixel_coords + offset; } else neighbor_pixel_coords = center_pixel_coords + neighbor_offset_int; if (neighbor_pixel_coords.x < 0 || neighbor_pixel_coords.x >= render_data.render_settings.render_resolution.x || neighbor_pixel_coords.y < 0 || neighbor_pixel_coords.y >= render_data.render_settings.render_resolution.y) // Rejecting the sample if it's outside of the viewport return -1; neighbor_pixel_index = neighbor_pixel_coords.x + neighbor_pixel_coords.y * render_data.render_settings.render_resolution.x; if (render_data.render_settings.enable_adaptive_sampling && render_data.render_settings.sample_number >= render_data.render_settings.adaptive_sampling_min_samples) { // If adaptive sampling is enabled, we only want to reuse a converged neighbor if the user allowed it // We also check whether or not we've reached the minimum amount of samples of adaptive sampling because // if adaptive sampling hasn't kicked in yet, there's no need to check whether the neighbor has converged or not yet if (spatial_pass_settings.allow_converged_neighbors_reuse) { // If we're allowing the reuse of converged neighbors, only doing so with a certain probability Xorshift32Generator rng_converged_neighbor_reuse(render_data.random_number); if (rng_converged_neighbor_reuse() > spatial_pass_settings.converged_neighbor_reuse_probability) { // We didn't pass the probability check, we are not allowed to reuse the neighbor if it // has converged if (render_data.aux_buffers.pixel_converged_sample_count[neighbor_pixel_index] != -1) // The neighbor is indeed converged, returning invalid neighbor with -1 return -1; } } else if (render_data.aux_buffers.pixel_converged_sample_count[neighbor_pixel_index] != -1) // The user doesn't allow reusing converged neighbors and the neighbor is indeed converged // Returning -1 for invalid neighbor return -1; } } return neighbor_pixel_index; } template HIPRT_DEVICE void spatial_neighbor_advance_rng(const HIPRTRenderData& render_data, Xorshift32Generator& rng) { const ReSTIRCommonSpatialPassSettings& spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); if (spatial_pass_settings.do_adaptive_directional_spatial_reuse(render_data.render_settings.accumulate)) { // If not using Hammersley, then each point is generated with 3 random numbers // // One for the random sector in the disk // One for the random theta within that sector // One for the random radius // // See the 'sample_spatial_neighbor_from_allowed_directions' function rng(); rng(); rng(); } else { // Two random numbers for sampling a neighbor in the disk rng(); rng(); } } /** * Counts how many neighbors are eligible for reuse. * This is needed for proper normalization by pairwise MIS weights. * * A neighbor is not eligible if it is outside of the viewport or if * it doesn't satisfy the normal/plane/roughness heuristics * * 'out_valid_neighbor_M_sum' is the sum of the M values (confidences) of the * valid neighbors. Used by confidence-weights pairwise MIS weights * * The bits of 'out_neighbor_heuristics_cache' are 1 or 0 depending on whether or not * the corresponding neighbor was valid or not (can be reused later to avoid having to * re-evauate the heuristics). Neighbor 0 is LSB. */ template HIPRT_DEVICE HIPRT_INLINE void count_valid_spatial_neighbors(const HIPRTRenderData& render_data, const ReSTIRSurface& center_pixel_surface, int2 center_pixel_coords, int& out_valid_neighbor_count, int& out_valid_neighbor_M_sum, int& out_neighbor_heuristics_cache) { out_valid_neighbor_count = 0; const ReSTIRCommonSpatialPassSettings& spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); Xorshift32Generator spatial_neighbors_rng(spatial_pass_settings.spatial_neighbors_rng_seed); int center_pixel_index = center_pixel_coords.x + center_pixel_coords.y * render_data.render_settings.render_resolution.x; int reused_neighbors_count = spatial_pass_settings.reuse_neighbor_count; for (int neighbor_index = 0; neighbor_index < reused_neighbors_count; neighbor_index++) { unsigned long long int* spatial_reuse_hit_rate_hits = nullptr; unsigned long long int* spatial_reuse_hit_rate_total = nullptr; if (spatial_pass_settings.compute_spatial_reuse_hit_rate) hippt::atomic_fetch_add(spatial_pass_settings.spatial_reuse_hit_rate_total, 1ull); int neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor_index, center_pixel_coords, spatial_neighbors_rng); if (neighbor_pixel_index == -1) // Neighbor out of the viewport continue; if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface))) continue; if (spatial_pass_settings.compute_spatial_reuse_hit_rate) hippt::atomic_fetch_add(spatial_pass_settings.spatial_reuse_hit_rate_hits, 1ull); out_valid_neighbor_M_sum += ReSTIRSettingsHelper::get_restir_spatial_pass_input_reservoir_M(render_data, neighbor_pixel_index); out_valid_neighbor_count++; out_neighbor_heuristics_cache |= (1 << neighbor_index); } } #endif ================================================ FILE: src/Device/includes/ReSTIR/UtilsTemporal.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_UTILS_TEMPORAL_H #define DEVICE_RESTIR_UTILS_TEMPORAL_H #include "HostDeviceCommon/RenderData.h" HIPRT_HOST_DEVICE HIPRT_INLINE int2 apply_permutation_sampling(int2 pixel_position, int random_bits) { int2 offset = make_int2(random_bits & 3, (random_bits >> 2) & 3); pixel_position += offset; pixel_position.x ^= 3; pixel_position.y ^= 3; pixel_position -= offset; return pixel_position; } /** * Returns a triplet (x, y, z) with * x the linear index that can be used directly to index a buffer * of render_data for getting data of the temporal neighbor. x is -1 * if there is no valid temporal neighbor (disoccluion / occlusion / out of viewport) * * (y, z) the pixel coordinates of the backprojected temporal neighbor position * These two values will always be filled even if the temporal neighbor is invalid * (disoccluion / occlusion / out of viewport) */ template HIPRT_HOST_DEVICE HIPRT_INLINE int3 find_temporal_neighbor_index(const HIPRTRenderData& render_data, const float3& current_shading_point, const float3& current_normal, int center_pixel_index, Xorshift32Generator& random_number_generator) { if (render_data.render_settings.accumulate) // If accumulating, the camera isn't moving, just returning // the current pixel index return make_int3(center_pixel_index, center_pixel_index % render_data.render_settings.render_resolution.x, center_pixel_index / render_data.render_settings.render_resolution.x); const ReSTIRCommonTemporalPassSettings& temporal_pass_settings = ReSTIRSettingsHelper::get_restir_temporal_pass_settings(render_data); float3 previous_screen_space_point_xyz = matrix_X_point(render_data.prev_camera.view_projection, current_shading_point); float2 previous_screen_space_point = make_float2(previous_screen_space_point_xyz.x, previous_screen_space_point_xyz.y); // Bringing back in [0, 1] from [-1, 1] previous_screen_space_point += make_float2(1.0f, 1.0f); previous_screen_space_point *= make_float2(0.5f, 0.5f); int2 resolution = render_data.render_settings.render_resolution; float2 prev_pixel_float = make_float2(previous_screen_space_point.x * resolution.x, previous_screen_space_point.y * resolution.y); // Bringing back in the center of the pixel prev_pixel_float -= make_float2(0.5f, 0.5f); // We're going to randomly look for an acceptable neighbor around the back-projected pixel location to find // in a given radius int temporal_neighbor_index = -1; for (int i = 0; i < temporal_pass_settings.max_neighbor_search_count + 1; i++) { float2 offset = make_float2(0.0f, 0.0f); if (i > 0) // Only randomly looking after we've at least checked whether or not the exact temporally reprojected location // is valid or not offset = make_float2(random_number_generator() - 0.5f, random_number_generator() - 0.5f) * temporal_pass_settings.neighbor_search_radius; int2 temporal_neighbor_screen_pixel_pos = make_int2(round(prev_pixel_float.x + offset.x), round(prev_pixel_float.y + offset.y)); if (temporal_pass_settings.use_permutation_sampling && i == 0) // If we're looking at the direct temporal neighbor (without random offset), applying // permutation sampling if enabled temporal_neighbor_screen_pixel_pos = apply_permutation_sampling(temporal_neighbor_screen_pixel_pos, temporal_pass_settings.permutation_sampling_random_bits); if (temporal_neighbor_screen_pixel_pos.x < 0 || temporal_neighbor_screen_pixel_pos.x >= resolution.x || temporal_neighbor_screen_pixel_pos.y < 0 || temporal_neighbor_screen_pixel_pos.y >= resolution.y) // Previous pixel is out of the current viewport continue; temporal_neighbor_index = temporal_neighbor_screen_pixel_pos.x + temporal_neighbor_screen_pixel_pos.y * resolution.x; // We always want to read from the previous frame g-buffer for temporal neighbors bool use_previous_frame_g_buffer = true; // except if we're accumulating because then the camera is not moving --> no motion // --> temporal neighbor are on the same surface as the current -> the previous // g-buffer is the same as the current frame's --> no need to read from previous // frame g-buffer --> the previous frame G-buffer is deallocated to save VRAM use_previous_frame_g_buffer &= render_data.render_settings.use_prev_frame_g_buffer(); if (check_neighbor_similarity_heuristics(render_data, temporal_neighbor_index, center_pixel_index, current_shading_point, current_normal, use_previous_frame_g_buffer)) // We found a good neighbor break; // We didn't break so we didn't find a good neighbor temporal_neighbor_index = -1; } return make_int3(temporal_neighbor_index, static_cast(round(prev_pixel_float.x)), static_cast(round(prev_pixel_float.y))); } #endif ================================================ FILE: src/Device/includes/RussianRoulette.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef RUSSIAN_ROULETTE_H #define RUSSIAN_ROULETTE_H #include "HostDeviceCommon/RenderSettings.h" /** * Returns false if the ray should be killed. * * This overload returns in 'throughput_scaling' the throughput scaling that * has been applied to 'ray_throughput' to account for the russian roulette surviving * probability * * If russian roulette wasn't applied, 'throughput_scaling' is left untouched */ HIPRT_HOST_DEVICE HIPRT_INLINE bool do_russian_roulette(const HIPRTRenderSettings& render_settings, int bounce, ColorRGB32F& ray_throughput, float& throughput_scaling, const ColorRGB32F& current_weight, Xorshift32Generator& random_number_generator) { if (bounce >= render_settings.russian_roulette_min_depth && render_settings.do_russian_roulette) { float survive_probability = 0.0f; if (render_settings.path_russian_roulette_method == PathRussianRoulette::MAX_THROUGHPUT) // Easy max throughput threshold survive_probability = ray_throughput.max_component(); else if (render_settings.path_russian_roulette_method == PathRussianRoulette::ARNOLD_2014) { // Reference: // [Physically Based Shader Design in Arnold, Langlands, 2014] survive_probability = (ray_throughput * current_weight).max_component() / ray_throughput.max_component(); survive_probability = sqrtf(survive_probability); } // Clamping anything above one back to 1 survive_probability = hippt::min(survive_probability, 1.0f); if (random_number_generator() > survive_probability) // Kill the ray return false; throughput_scaling = 1.0f / survive_probability; if (render_settings.russian_roulette_throughput_clamp > 0.0f) // Clamping the throughput increase to avoid fireflies by // rays that still pass the russian roulette with very low // probabilities throughput_scaling = hippt::min(throughput_scaling, render_settings.russian_roulette_throughput_clamp); ray_throughput *= throughput_scaling; } // The ray survived return true; } /** * Returns false if the ray should be killed. */ HIPRT_HOST_DEVICE HIPRT_INLINE bool do_russian_roulette(const HIPRTRenderSettings& render_settings, int bounce, ColorRGB32F& ray_throughput, const ColorRGB32F& current_weight, Xorshift32Generator& random_number_generator) { float unused_throughput_scaling; return do_russian_roulette(render_settings, bounce, ray_throughput, unused_throughput_scaling, current_weight, random_number_generator); } #endif ================================================ FILE: src/Device/includes/Sampling.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_SAMPLING_H #define DEVICE_SAMPLING_H #include "Device/includes/Fresnel.h" #include "Device/includes/ONB.h" #include "Device/includes/Texture.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/KernelOptions/KernelOptions.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/Xorshift.h" /** * Returns the radical inverse base 2 of a given number. * Used for generating 2D points following the Hammersley point set * * Reference: [Holger Dammertz, Hammersley Points on the Hemisphere] http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html */ HIPRT_HOST_DEVICE HIPRT_INLINE float radical_inverse_base_2(unsigned int index) { index = (index << 16u) | (index >> 16u); index = ((index & 0x55555555u) << 1u) | ((index & 0xAAAAAAAAu) >> 1u); index = ((index & 0x33333333u) << 2u) | ((index & 0xCCCCCCCCu) >> 2u); index = ((index & 0x0F0F0F0Fu) << 4u) | ((index & 0xF0F0F0F0u) >> 4u); index = ((index & 0x00FF00FFu) << 8u) | ((index & 0xFF00FF00u) >> 8u); return float(index) * 2.3283064365386963e-10f; // / 0x100000000 } /** * Generates a 2D point of the Hammersley point set given the total number * of points that are going to be sampled and the index of the point * (in [0, number_of_points -1]) that we're sampling right now */ HIPRT_HOST_DEVICE HIPRT_INLINE float2 sample_hammersley_2D(unsigned int number_of_points, unsigned int point_index) { return make_float2(static_cast(point_index) / static_cast(number_of_points), radical_inverse_base_2(point_index)); } /** * Returns float pixel coordinates offset from the center of the disk * given the radius of the disk and two random numbers in [0, 1] u and v * * uv.x is used as theta for sampling the disk * uv.y is used for sampling the distance from the center of the disk */ HIPRT_HOST_DEVICE HIPRT_INLINE float2 sample_in_disk_uv(float radius, float2 uv) { float r_sqrt_v = radius * sqrtf(uv.y); float x = r_sqrt_v * cos(M_TWO_PI * uv.x); float y = r_sqrt_v * sin(M_TWO_PI * uv.x); return make_float2(x, y); } /** * Returns integer pixel coordinates offset from the center of the disk of radius 'radius' */ HIPRT_HOST_DEVICE HIPRT_INLINE float2 sample_in_disk(float radius, Xorshift32Generator& random_number_generator) { float u1 = random_number_generator(); float u2 = random_number_generator(); return sample_in_disk_uv(radius, make_float2(u1, u2)); } /** * Power heuristic with a hardcoded Beta exponent of 2 and two sampling strategies only * * This implementation already contains the 1/nb_pdf_a fraction of the MIS estimator. This means * that you should not divide by 1/nb_pdf_a in the evaluation of your function where you use * the MIS weight */ HIPRT_HOST_DEVICE HIPRT_INLINE float power_heuristic(float pdf_a, int nb_pdf_a, float pdf_b, int nb_pdf_b) { float p_a_sqr = (nb_pdf_a * pdf_a) * (nb_pdf_a * pdf_a); float p_b_sqr = (nb_pdf_b * pdf_b) * (nb_pdf_b * pdf_b); // Note that we should have a multiplication by nb_pdf_a^2 in the // numerator but because we're going to divide by nb_pdf_a in the // function evaluation that use this MIS weight according to the // MIS estimator, we're only multiplying by nb_pdf_a (not squared) // since the squared nb_pdf_a would be cancelled by the division by // nb_pdf_a return nb_pdf_a * pdf_a * pdf_a / (p_a_sqr + p_b_sqr); } HIPRT_HOST_DEVICE HIPRT_INLINE float power_heuristic(float pdf_a, float pdf_b) { return power_heuristic(pdf_a, 1, pdf_b, 1); } /** * Balance heuristic for MIS weights computation * * This implementation already contains the 1/nb_pdf_a fraction of the MIS estimator. This means * that you should not divide by 1/nb_pdf_a in the evaluation of your function where you use * the MIS weight */ HIPRT_HOST_DEVICE HIPRT_INLINE float balance_heuristic(float pdf_a, float nb_pdf_a, float pdf_b, float nb_pdf_b) { if (pdf_a == 0.0f) return 0.0f; // Note that we should have a multiplication by nb_pdf_a in the // numerator but because we're going to divide by nb_pdf_a in the // function evaluation that use this MIS weight according to the // MIS estimator, this multiplication in the numerator that we // would have here would be canceled and that would be basically // wasted maths so we're not doing it and we should not do it // in the function evaluation either. return pdf_a / (nb_pdf_a * pdf_a + nb_pdf_b * pdf_b); } /** * Balance heuristic for 3 strategies */ HIPRT_HOST_DEVICE HIPRT_INLINE float balance_heuristic(float pdf_a, int nb_pdf_a, float pdf_b, int nb_pdf_b, int pdf_c, int nb_pdf_c) { // Note that we should have a multiplication by nb_pdf_a in the // numerator but because we're going to divide by nb_pdf_a in the // function evaluation that use this MIS weight according to the // MIS estimator, this multiplication in the numerator that we // would have here would be canceled and that would be basically // wasted maths so we're not doing it and we should not do it // in the function evaluation either. return pdf_a / (nb_pdf_a * pdf_a + nb_pdf_b * pdf_b + nb_pdf_c * pdf_c); } HIPRT_HOST_DEVICE HIPRT_INLINE float balance_heuristic(float pdf_a, float pdf_b) { return balance_heuristic(pdf_a, 1, pdf_b, 1); } HIPRT_HOST_DEVICE HIPRT_INLINE float balance_heuristic(float pdf_a, float pdf_b, float pdf_c) { return balance_heuristic(pdf_a, 1, pdf_b, 1, pdf_c, 1); } /** * Reflects a ray about a normal. This function requires that dot(ray_direction, surface_normal) > 0 i.e. * ray_direction and surface_normal are in the same hemisphere */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 reflect_ray(const float3& ray_direction, const float3& surface_normal) { return 2.0f * hippt::dot(ray_direction, surface_normal) * surface_normal - ray_direction; } /** * Refracts a ray about a normal. This function requires that dot(ray_direction, surface_normal) > 0 i.e. * ray_direction and surface_normal are in the same hemisphere * * relative_eta here must be eta_t / eta_i * * No total internal reflection is assumed */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 refract_ray(const float3& ray_direction, const float3& surface_normal, float relative_eta) { float NoI = hippt::dot(ray_direction, surface_normal); float sin_theta_i_2 = 1.0f - NoI * NoI; float root_term = 1.0f - sin_theta_i_2 / (relative_eta * relative_eta); float cos_theta_t = sqrt(root_term); float3 refract_direction = -ray_direction / relative_eta + (NoI / relative_eta - cos_theta_t) * surface_normal; return refract_direction; } /** * Reference: * * [1] [Lambertian Reflection Without Tangents], Edd Biddulph https://fizzer.neocities.org/lambertnotangent * * The sampled direction is returned in world space */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 cosine_weighted_sample_around_normal_world_space(const float3& normal, Xorshift32Generator& random_number_generator) { float rand_1 = random_number_generator(); float rand_2 = 2.0f * random_number_generator() - 1.0f; if (rand_1 < 1.0e-8f && rand_2 < -0.999999f && normal.z > 0.999999f) { // Slight perturbation when this would result in a singularity: // When rand_1 is 0.0f and rand_2 is -1.0f, this results in a theta // of 0.0f which then gives sphere_point = {0.0f, 0.0f, -1.0f}. In // conjunction with a normal of {0.0f, 0.0f, 1.0f}, we get a null vector // at the return statement that is then normalized --> NaN rand_1 += 1.0e-7f; rand_2 += 1.0e-7f; } float theta = M_TWO_PI * rand_1; float2 xy = sqrt(1.0f - rand_2 * rand_2) * make_float2(cos(theta), sin(theta)); float3 sphere_point = make_float3(xy.x, xy.y, rand_2); return hippt::normalize(normal + sphere_point); } /** * Reference: * * [1] [Global Illumination Compendium], https://people.cs.kuleuven.be/~philip.dutre/GI/TotalCompendium.pdf * * The sampled direction is returned in a local frame with Z as the up axis */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 cosine_weighted_sample_z_up_frame(Xorshift32Generator& random_number_generator) { float r1 = random_number_generator(); float r2 = random_number_generator(); float phi = M_TWO_PI * r1; float cos_theta = sqrt(r2); float sin_theta = sqrt(1 - cos_theta * cos_theta); return hippt::normalize(make_float3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta)); } #endif ================================================ FILE: src/Device/includes/SanityCheck.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_SANITY_CHECK_H #define DEVICE_INCLUDES_SANITY_CHECK_H #include "Device/includes/FixIntellisense.h" #include "HostDeviceCommon/RenderData.h" #ifndef __KERNELCC__ #include "Utils/Utils.h" // For debugbreak in sanity_check() // For logging stuff on the CPU and avoid everything being mixed // up in the terminal because of multithreading #include std::mutex g_mutex; #endif HIPRT_HOST_DEVICE HIPRT_INLINE void debug_set_final_color(const HIPRTRenderData& render_data, int x, int y, ColorRGB32F final_color) { if (render_data.render_settings.sample_number == 0) render_data.buffers.accumulated_ray_colors[y * render_data.render_settings.render_resolution.x + x] = final_color; else render_data.buffers.accumulated_ray_colors[y * render_data.render_settings.render_resolution.x + x] = final_color * render_data.render_settings.sample_number; } /** * Returns true if the color has a negative component. * False otherwise */ HIPRT_HOST_DEVICE HIPRT_INLINE bool check_for_negative_color(ColorRGB32F ray_color, int x, int y, int sample) { // To remove 'unused variable' warnings of the GPU compiler because these variables are only used // in the std::cout of the CPU (void)x; (void)y; (void)sample; if (ray_color.r < 0 || ray_color.g < 0 || ray_color.b < 0) { #ifndef __KERNELCC__ std::cout << "Negative color at [" << x << ", " << y << "], sample " << sample << std::endl; #endif return true; } return false; } /** * Returns true if the color has a NaN or INF component. * False otherwise */ HIPRT_HOST_DEVICE HIPRT_INLINE bool check_for_nan(ColorRGB32F ray_color, int x, int y, int sample) { // To avoid unused variables on the GPU (void)x; (void)y; (void)sample; if (hippt::is_nan(ray_color.r) || hippt::is_nan(ray_color.g) || hippt::is_nan(ray_color.b) || hippt::is_inf(ray_color.r) || hippt::is_inf(ray_color.g) || hippt::is_inf(ray_color.b)) { #ifndef __KERNELCC__ std::lock_guard logging_lock(g_mutex); std::cout << "NaN/INF at [" << x << ", " << y << "], sample" << sample << std::endl; #endif return true; } return false; } template HIPRT_HOST_DEVICE HIPRT_INLINE bool sanity_check(const HIPRTRenderData& render_data, ColorRGB32F& in_out_color, int x, int y) { if constexpr (CheckOnlyOnCPU) { #ifdef __KERNELCC__ return true; #endif } bool valid = true; valid &= !check_for_negative_color(in_out_color, x, y, render_data.render_settings.sample_number); valid &= !check_for_nan(in_out_color, x, y, render_data.render_settings.sample_number); if (!valid) { #ifndef __KERNELCC__ Utils::debugbreak(); #endif if (render_data.render_settings.display_NaNs) debug_set_final_color(render_data, x, y, ColorRGB32F(1.0e30f, 0.0f, 1.0e30f)); else in_out_color = ColorRGB32F(0.0f); } return valid; } template HIPRT_HOST_DEVICE HIPRT_INLINE bool sanity_check(const HIPRTRenderData& render_data, const ColorRGB32F& in_out_color, int x, int y) { ColorRGB32F copy = in_out_color; return sanity_check(render_data, copy, x, y); } #endif ================================================ FILE: src/Device/includes/Texture.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_TEXTURE_H #define DEVICE_TEXTURE_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/TriangleStructures.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/RenderData.h" #ifndef __KERNELCC__ #include "Image/Image.h" #endif #ifdef __KERNELCC__ // Dummy usings so that the GPU compiler doesn't complain that Image8Bit / Image32Bit don't exist. // It's okay to dummy use them as int because they are not used on the GPU side anyway, this is // purely for the compiler to be happy using Image8Bit = int; using Image32Bit = int; #endif /** * Templated here so that the CPU can cast the texture_buffer into Image8Bit or Image32Bit * for proper sampling in unsigned char or float respectively. * This template argument isn't used on the GPU and that's why Image8Bit and Image32Bit * are being defined as 'ints' * * If 'flip_uv_y' is true, then UV (0, 0) is the bottom left corner of the texture * and the texture must use a wrapping address mode for the V coordinate. * * If 'flip_uv_y' is true, then the UV coordinates are just used as is */ template HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F sample_texture_rgba(const void* texture_buffer, int texture_index, bool is_srgb, float2 uv, bool flip_uv_y = true) { ColorRGBA32F rgba; #ifdef __KERNELCC__ // We're doing the UV addressing ourselves since it seems to be broken in Orochi... float u = uv.x; float v = uv.y; if (flip_uv_y) v = -v; if (reinterpret_cast(texture_buffer)[texture_index] == 0) return ColorRGBA32F(0.0f); rgba = ColorRGBA32F(tex2D(reinterpret_cast(texture_buffer)[texture_index], u, v)); #else const ImageType& texture = reinterpret_cast(texture_buffer)[texture_index]; rgba = texture.sample_rgba32f(uv); #endif // sRGB to linear conversion // Doing the conversion manually instead of using the hardware // because it's unavailable in Orochi (again) :( if (is_srgb) return intrin_pow(rgba, 2.2f); else return rgba; } /** * If 'flip_uv_y' is true, then UV(0, 0) is the bottom left corner of the texture * and the texture must use a wrapping address mode for the V coordinate. * * If 'flip_uv_y' is true, then the UV coordinates are just used as is * * 'flip_uv_y' should basically be set to true in most cases and. * It should be set to false if your texture addressing mode isn't 'warping' * or when you know what you're doing and why you need to have it to false */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F sample_texture_rgb_8bits(const void* texture_buffer, int texture_index, bool is_srgb, float2 uv, bool flip_uv_y = true) { ColorRGBA32F rgba = sample_texture_rgba(texture_buffer, texture_index, is_srgb, uv, flip_uv_y); return ColorRGB32F(rgba.r, rgba.g, rgba.b); } /** * Samples a texture given by indexing the texture array 'texture_buffer' with 'texture_buffer[texture_index]'. * * To read from a single texture, pass the pointer to the texture in 'texture_buffer' and * pass texture_index = 0 * * Not that on the GPU, 'texture_buffer' must be of type oroTextureObject_t*, i.e. it's a pointer on oroTextureObject_t * this means that if the pointer is set in RenderData with OrochiTexture::get_device_texture() on the CPU, then * &get_device_texture() must be passed to this function for 'texture_buffer' * * If 'flip_uv_y' is true, then UV(0, 0) is the bottom left corner of the texture * and the texture must use a wrapping address mode for the V coordinate. * * If 'flip_uv_y' is true, then the UV coordinates are just used as is * * 'flip_uv_y' should basically be set to true in most cases and. * It should be set to false if your texture addressing mode isn't 'warping' * or when you know what you're doing and why you need to have it to false */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F sample_texture_rgb_32bits(const void* texture_buffer, int texture_index, bool is_srgb, float2 uv, bool flip_uv_y = true) { ColorRGBA32F rgba = sample_texture_rgba(texture_buffer, texture_index, is_srgb, uv, flip_uv_y); return ColorRGB32F(rgba.r, rgba.g, rgba.b); } #ifdef __KERNELCC__ /** * Bilinearly samples around x & y on the layer z of a 3D texture configured for * nearest neighbor sampling * * uv is supposed to be in [0, 1] already */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F internal_bilinear_sample_on_3D_texture(const oroTextureObject_t texture, int3 ires, float2 uv, int z) { // Reference: https://iquilezles.org/articles/hwinterpolation/ float2 res_f = make_float2(ires.x, ires.y); float2 st = (uv - 0.5f / res_f) * res_f; int2 i = make_int2(floorf(st.x), floorf(st.y)); float2 w = make_float2(hippt::fract(st.x), hippt::fract(st.y)); ColorRGBA32F a = ColorRGBA32F(tex3D(texture, i.x + 0, i.y + 0, z)); ColorRGBA32F b = ColorRGBA32F(tex3D(texture, i.x + 1, i.y + 0, z)); ColorRGBA32F c = ColorRGBA32F(tex3D(texture, i.x + 0, i.y + 1, z)); ColorRGBA32F d = ColorRGBA32F(tex3D(texture, i.x + 1, i.y + 1, z)); return hippt::lerp(hippt::lerp(a, b, w.x), hippt::lerp(c, d, w.x), w.y); } #endif /** * This function samples a 3D texture given in the 'texture' parameter * This parameter should be an oroTextureObject_t on the GPU, not a * pointer 'oroTextureObject_t*' as is the case for 'sample_texture_rgb_32bits' */ HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F sample_texture_3D_rgb_32bits(void* texture, int3 texture_dims, float3 uvw, bool hardware_interpolation = false) { if (texture == nullptr) return ColorRGB32F(0.0f); #ifdef __KERNELCC__ // Sampling in repeat mode so we're just keeping the fractional part float u = uvw.x; if (u != 1.0f) // Only doing that if u != 1.0f because if we actually have // uv.x == 1.0f, then subtracting static_cast(uv.x) will // give us 0.0f even though we actually want 1.0f (which is correct). // // Basically, 1.0f gets transformed into 0.0f even though 1.0f is a correct // U coordinate which needs not to be wrapped u = hippt::fract(uvw.x); float v = uvw.y; if (v != 1.0f) // Same for v v = hippt::fract(uvw.y); float w = uvw.z; if (w != 1.0f) // Same for w w = hippt::fract(uvw.z); // For negative UVs, we also want to repeat and we want, for example, // -0.1f to behave as 0.9f u = u < 0 ? 1.0f + u : u; v = v < 0 ? 1.0f + v : v; w = w < 0 ? 1.0f + w : w; // Sampling with [0, 0] bottom-left convention v = 1.0f - v; if (hardware_interpolation) { float x = (u * (texture_dims.x - 1)); float y = (v * (texture_dims.y - 1)); float z = (w * (texture_dims.z - 1)); return ColorRGB32F(ColorRGBA32F(tex3D(reinterpret_cast(texture), x, y, z))); } else { float z = (w * (texture_dims.z - 1)); // Whether or not we need to interpolate with layer z+1 or z-1 bool z_layer_up = hippt::fract(w * texture_dims.z) > 0.5f; int z0 = z; int z1 = z_layer_up ? z0 + 1 : z0 - 1; ColorRGBA32F rgba0 = internal_bilinear_sample_on_3D_texture(reinterpret_cast(texture), texture_dims, make_float2(u, v), z0); ColorRGBA32F rgba1 = internal_bilinear_sample_on_3D_texture(reinterpret_cast(texture), texture_dims, make_float2(u, v), z1); return ColorRGB32F(hippt::lerp(rgba0, rgba1, w)); } #else const Image32Bit3D& image = *reinterpret_cast(texture); ColorRGBA32F rgba = image.sample_rgba32f(uvw); return ColorRGB32F(rgba); #endif } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F sample_environment_map_texture(const WorldSettings& world_settings, float2 uv) { #if EnvmapSamplingDoBilinearFiltering == KERNEL_OPTION_TRUE float x = uv.x * (world_settings.envmap_width - 1); float y = uv.y * (world_settings.envmap_height - 1); float x_frac = hippt::fract(x); float y_frac = hippt::fract(y); int x_i = (int)x; int x_i_1 = hippt::min(x_i + 1, (int)world_settings.envmap_width - 1); int y_i = (int)y; int y_i_1 = hippt::min(y_i + 1, (int)world_settings.envmap_height - 1); int index_x0y0 = x_i + y_i * world_settings.envmap_width; int index_x1y0 = x_i_1 + y_i * world_settings.envmap_width; int index_x0y1 = x_i + y_i_1 * world_settings.envmap_width; int index_x1y1 = x_i_1 + y_i_1 * world_settings.envmap_width; ColorRGB32F color_x0y0 = world_settings.envmap[index_x0y0].unpack() * world_settings.envmap_intensity; ColorRGB32F color_x1y0 = world_settings.envmap[index_x1y0].unpack() * world_settings.envmap_intensity; ColorRGB32F color_x0y1 = world_settings.envmap[index_x0y1].unpack() * world_settings.envmap_intensity; ColorRGB32F color_x1y1 = world_settings.envmap[index_x1y1].unpack() * world_settings.envmap_intensity; return hippt::lerp(hippt::lerp(color_x0y0, color_x1y0, x_frac), hippt::lerp(color_x0y1, color_x1y1, x_frac), y_frac); #else int x = uv.x * (world_settings.envmap_width - 1); int y = uv.y * (world_settings.envmap_height - 1); int index = x + y * world_settings.envmap_width; return world_settings.envmap[index].unpack() * world_settings.envmap_intensity; #endif } /** * Given the indices of the vertices of a triangle, interpolates the vertices data found * in the 'data' buffer passed as argument as the given UV coordinates */ template HIPRT_HOST_DEVICE HIPRT_INLINE T uv_interpolate(int vertex_A_index, int vertex_B_index, int vertex_C_index, T* data, float2 uv) { return data[vertex_B_index] * uv.x + data[vertex_C_index] * uv.y + data[vertex_A_index] * (1.0f - uv.x - uv.y); } /** * Given the indices of the vertices of a triangle, interpolates the vertices data found * in the 'data' buffer passed as argument as the given UV coordinates */ template HIPRT_HOST_DEVICE HIPRT_INLINE T uv_interpolate(TriangleIndices triangle_vertex_indices, T* data, float2 uv) { return uv_interpolate(triangle_vertex_indices.x, triangle_vertex_indices.y, triangle_vertex_indices.z, data, uv); } /** * Just a simple "specialization" for when we're interpolating texcoords */ HIPRT_HOST_DEVICE HIPRT_INLINE float2 uv_interpolate(TriangleTexcoords texcoords, float2 uv) { return texcoords.y * uv.x + texcoords.z * uv.y + texcoords.x * (1.0f - uv.x - uv.y); } /** * Same as the overloads above but you can call this one when you don't already have the vertex indices * from the place you're calling this function from. This function will then fetch the vertex indices again */ template HIPRT_HOST_DEVICE HIPRT_INLINE T uv_interpolate(int* vertex_indices, int primitive_index, T* data, float2 uv) { int vertex_A_index = vertex_indices[primitive_index * 3 + 0]; int vertex_B_index = vertex_indices[primitive_index * 3 + 1]; int vertex_C_index = vertex_indices[primitive_index * 3 + 2]; return uv_interpolate(vertex_A_index, vertex_B_index, vertex_C_index, data, uv); } #endif ================================================ FILE: src/Device/includes/TriangleStructures.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_TRIANGLE_STRUCTURES_H #define DEVICE_TRIANGLE_STRUCTURES_H #include "HostDeviceCommon/Math.h" /** * Structure that contains the vertex index (in the vertex buffer) of the 3 vertices of a triangle */ struct TriangleIndices { int x; // vertex A int y; // vertex B int z; // vertex C }; HIPRT_HOST_DEVICE HIPRT_INLINE TriangleIndices load_triangle_vertex_indices(int* triangle_indices_buffer, int primitive_index) { int primitive_index_3 = primitive_index * 3; return TriangleIndices { triangle_indices_buffer[primitive_index_3 + 0], triangle_indices_buffer[primitive_index_3 + 1], triangle_indices_buffer[primitive_index_3 + 2] }; } /** * Structure that contains the UV texcoords a the 3 vertices of a triangle */ struct TriangleTexcoords { float2 x; // vertex A float2 y; // vertex B float2 z; // vertex C }; HIPRT_HOST_DEVICE HIPRT_INLINE TriangleTexcoords load_triangle_texcoords(float2* texcoords_buffer, TriangleIndices triangle_vertex_indices) { return TriangleTexcoords { texcoords_buffer[triangle_vertex_indices.x], texcoords_buffer[triangle_vertex_indices.y], texcoords_buffer[triangle_vertex_indices.z] }; } #endif ================================================ FILE: src/Device/includes/WarpDirectionReuse.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_INCLUDES_WARP_DIRECTION_REUSE_H #define DEVICE_INCLUDES_WARP_DIRECTION_REUSE_H #include "Device/includes/Dispatcher.h" #include "Device/includes/FixIntellisense.h" #include "HostDeviceCommon/HitInfo.h" /** * Experimental implementation of [Generate Coherent Rays Directly, Liu et al., 2024] * * This incomplete implementation supposes that all threads in the warp have the same material * type and this does not implement the "interleaved groups" approach to reduce correlation * * Preliminary results only show a 10% boost in perf, even without correlation reduction and on * the Bistro (which is an expensive scene to trace). Because the correlations were pretty bad, * the implementation of the paper was discontinued */ HIPRT_HOST_DEVICE HIPRT_INLINE void warp_direction_reuse(const HIPRTRenderData& render_data, const HitInfo& closest_hit_info, RayPayload& ray_payload, float3 view_direction, float3& in_out_bounce_direction, ColorRGB32F& out_bsdf_color, float& out_bsdf_pdf, int bounce, Xorshift32Generator& random_number_generator) { if (bounce == 0) { // Direction reuse is only done on the first bounce because the efficiency largely decreases at later bounces unsigned int active_mask = hippt::warp_activemask(); unsigned int first_active_thread_index = hippt::ffs(active_mask) - 1; float3 local_direction = world_to_local_frame(closest_hit_info.shading_normal, in_out_bounce_direction); local_direction.x = hippt::warp_shfl(local_direction.x, first_active_thread_index); local_direction.y = hippt::warp_shfl(local_direction.y, first_active_thread_index); local_direction.z = hippt::warp_shfl(local_direction.z, first_active_thread_index); in_out_bounce_direction = local_to_world_frame(closest_hit_info.shading_normal, local_direction); BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, in_out_bounce_direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness); out_bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, out_bsdf_pdf, random_number_generator); } } #endif ================================================ FILE: src/Device/kernel_parameters/NEE++/NEEPlusPlusCachingPrepassParameters.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef NEE_PLUS_PLUS_CACHING_PREPASS_KERNEL_PARAMETERS_H #define NEE_PLUS_PLUS_CACHING_PREPASS_KERNEL_PARAMETERS_H #include "Device/includes/NEE++/NEE++.h" struct NEEPlusPlusCachingPrepassParameters { NEEPlusPlusDevice nee_plus_plus; HIPRTCamera current_camera; unsigned int random_number = 42; }; #endif ================================================ FILE: src/Device/kernel_parameters/ReSTIR/DI/LightPresamplingParameters.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef LIGHT_PRESAMPLING_KERNEL_PARAMETERS_H #define LIGHT_PRESAMPLING_KERNEL_PARAMETERS_H #include "Device/includes/ReSTIR/DI/PresampledLight.h" #include "Device/includes/ReSTIR/DI/Reservoir.h" #include "HostDeviceCommon/Material/MaterialPackedSoA.h" #include "HostDeviceCommon/WorldSettings.h" struct LightPresamplingParameters { /** * Parameters specific to the kernel */ // From all the lights of the scene, how many subsets to presample int number_of_subsets = 128; // How many lights to presample in each subset int subset_size = 1024; // Buffer that holds the presampled lights ReSTIRDIPresampledLight* out_light_samples; // For each presampled light, the probability that this is going to be an envmap sample float envmap_sampling_probability; }; #endif ================================================ FILE: src/Device/kernels/Baking/GGXConductorDirectionalAlbedo.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Renderer/Baker/GGXConductorDirectionalAlbedoSettings.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/BSDFs/Microfacet.h" #include "HostDeviceCommon/RenderData.h" /* References: * [1][Practical multiple scattering compensation for microfacet models, Turquin, 2019] * [2][Revisiting Physically Based Shading at Imageworks, Kulla & Conty, SIGGRAPH 2017] * [3][Dassault Enterprise PBR 2025 Specification] * [4][Google - Physically Based Rendering in Filament] * [5][MaterialX codebase on Github] * [6][Blender's Cycles codebase on Github] * * This kernel computes the directional albedo of a conductor BRDF for use * in energy compensation code (MicrofacetEnergyCompensation.h) as proposed in * [Practical multiple scattering compensation for microfacet models, Turquin, 2019] * * The kernel outputs its results in one buffer (which is then written to disk as a texture). * The texture is parameterized by cos_theta_o (cosine view direction) and the roughness of * the BRDF */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) inline GGXConductorDirectionalAlbedoBake(int kernel_iterations, int current_iteration, GGXConductorDirectionalAlbedoSettings bake_settings, float* out_buffer) #else GLOBAL_KERNEL_SIGNATURE(void) inline GGXConductorDirectionalAlbedoBake(int kernel_iterations, int current_iteration, GGXConductorDirectionalAlbedoSettings bake_settings, float* out_buffer, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif const uint32_t pixel_index = (x + y * bake_settings.texture_size_cos_theta); if (x >= bake_settings.texture_size_cos_theta || y >= bake_settings.texture_size_roughness) return; Xorshift32Generator random_number_generator(wang_hash(pixel_index + 1) * current_iteration); float roughness = 1.0f / (bake_settings.texture_size_roughness - 1.0f) * y; roughness = hippt::max(roughness, 1.0e-4f); float cos_theta_o = 1.0f / (bake_settings.texture_size_cos_theta - 1.0f) * x; cos_theta_o = hippt::max(GGX_DOT_PRODUCTS_CLAMP, cos_theta_o); float sin_theta_o = sin(acos(cos_theta_o)); float3 local_view_direction = hippt::normalize(make_float3(cos(0.0f) * sin_theta_o, sin(0.0f) * sin_theta_o, cos_theta_o)); int iterations_per_kernel = floor(hippt::max(1.0f, (float)GPUBakerConstants::COMPUTE_ELEMENT_PER_BAKE_KERNEL_LAUNCH / (float)(bake_settings.texture_size_cos_theta * bake_settings.texture_size_roughness))); int nb_kernel_launch = ceil(bake_settings.integration_sample_count / (float)iterations_per_kernel); int nb_samples = nb_kernel_launch * iterations_per_kernel; for (int sample = 0; sample < kernel_iterations; sample++) { float3 sampled_local_to_light_direction = microfacet_GGX_sample_reflection(roughness, 0.0f, local_view_direction, random_number_generator); if (sampled_local_to_light_direction.z < 0) // Sampled direction below surface continue; HIPRTRenderData dummy_render_data; // Just updating the masking shadowing term dummy_render_data.bsdfs_data.GGX_masking_shadowing = bake_settings.masking_shadowing_term; float eval_pdf; float directional_albedo = torrance_sparrow_GGX_eval_reflect<0>(dummy_render_data, roughness, 0.0f, false, /* fresnel */ ColorRGB32F(1.0f), local_view_direction, sampled_local_to_light_direction, hippt::normalize(local_view_direction + sampled_local_to_light_direction), eval_pdf, MaterialUtils::SPECULAR_PEAK_SAMPLED, 0).r; directional_albedo /= eval_pdf; directional_albedo *= sampled_local_to_light_direction.z; out_buffer[pixel_index] += directional_albedo / nb_samples; } } ================================================ FILE: src/Device/kernels/Baking/GGXFresnelDirectionalAlbedo.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Renderer/Baker/GGXFresnelDirectionalAlbedoSettings.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/BSDFs/Microfacet.h" #include "HostDeviceCommon/RenderData.h" /* References: * [1][Practical multiple scattering compensation for microfacet models, Turquin, 2019] * [2][Revisiting Physically Based Shading at Imageworks, Kulla & Conty, SIGGRAPH 2017] * [3][Dassault Enterprise PBR 2025 Specification] * [4][Google - Physically Based Rendering in Filament] * [5][MaterialX codebase on Github] * [6][Blender's Cycles codebase on Github] * * This kernel computes the directional albedo of the Torrance Sparrow BRDF * with the GGX distribution with a varying fresnel term (i.e. a dielectric GGX lobe) */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) inline GGXFresnelDirectionalAlbedoBake(int kernel_iterations, int current_iteration, GGXFresnelDirectionalAlbedoSettings bake_settings, float* out_buffer) #else GLOBAL_KERNEL_SIGNATURE(void) inline GGXFresnelDirectionalAlbedoBake(int kernel_iterations, int current_iteration, GGXFresnelDirectionalAlbedoSettings bake_settings, float* out_buffer, int x, int y, int z) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; const uint32_t z = blockIdx.z * blockDim.z + threadIdx.z; #endif const uint32_t pixel_index = (x + y * bake_settings.texture_size_cos_theta + z * bake_settings.texture_size_cos_theta * bake_settings.texture_size_roughness); if (x >= bake_settings.texture_size_cos_theta || y >= bake_settings.texture_size_roughness || z >= bake_settings.texture_size_ior) return; Xorshift32Generator random_number_generator(wang_hash(pixel_index + 1) * current_iteration); float roughness = 1.0f / (bake_settings.texture_size_roughness - 1.0f) * y; roughness = hippt::max(roughness, 1.0e-4f); float cos_theta_o = 1.0f / (bake_settings.texture_size_cos_theta - 1.0f) * x; cos_theta_o = hippt::max(GGX_DOT_PRODUCTS_CLAMP, cos_theta_o); cos_theta_o = powf(cos_theta_o, 2.5f); float sin_theta_o = sin(acos(cos_theta_o)); // Integrates for interface reflectivities of IORs between 1.0f and 3.0f float F0 = 1.0f / (bake_settings.texture_size_ior - 1.0f) * z; // Relative eta (eta_t / eta_i) from F0 // Using F0^4 to get more precision near 0 F0 *= F0; // F0^2 F0 *= F0; // F0^4 float sqrt_F0 = sqrtf(hippt::clamp(0.0f, 0.99f, F0)); float relative_ior = (1.0f + sqrt_F0) / (1.0f - sqrt_F0); float3 local_view_direction = hippt::normalize(make_float3(cos(0.0f) * sin_theta_o, sin(0.0f) * sin_theta_o, cos_theta_o)); int iterations_per_kernel = floor(hippt::max(1.0f, (float)GPUBakerConstants::COMPUTE_ELEMENT_PER_BAKE_KERNEL_LAUNCH / (float)(bake_settings.texture_size_cos_theta * bake_settings.texture_size_roughness))); int nb_kernel_launch = ceil(bake_settings.integration_sample_count / (float)iterations_per_kernel); int nb_samples = nb_kernel_launch * iterations_per_kernel; for (int sample = 0; sample < kernel_iterations; sample++) { float3 sampled_local_to_light_direction = microfacet_GGX_sample_reflection(roughness, 0.0f, local_view_direction, random_number_generator); if (sampled_local_to_light_direction.z < 0) // Sampled direction below surface continue; ColorRGB32F F = ColorRGB32F(full_fresnel_dielectric(sampled_local_to_light_direction.z, relative_ior)); HIPRTRenderData dummy_render_data; // Just updating the masking shadowing term dummy_render_data.bsdfs_data.GGX_masking_shadowing = bake_settings.masking_shadowing_term; float eval_pdf; float directional_albedo = torrance_sparrow_GGX_eval_reflect<0>(dummy_render_data, roughness, /* anisotropy */ 0.0f, false, F, local_view_direction, sampled_local_to_light_direction, hippt::normalize(local_view_direction + sampled_local_to_light_direction), eval_pdf, MaterialUtils::SPECULAR_PEAK_SAMPLED, 0).r; directional_albedo /= eval_pdf; directional_albedo *= sampled_local_to_light_direction.z; out_buffer[pixel_index] += directional_albedo / nb_samples; } } ================================================ FILE: src/Device/kernels/Baking/GGXGlassDirectionalAlbedo.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/BSDFs/Principled.h" #include "HostDeviceCommon/RenderData.h" #include "Renderer/Baker/GGXGlassDirectionalAlbedoSettings.h" /* References: * [1][Practical multiple scattering compensation for microfacet models, Turquin, 2019] * [2][Revisiting Physically Based Shading at Imageworks, Kulla & Conty, SIGGRAPH 2017] * [3][Dassault Enterprise PBR 2025 Specification] * [4][Google - Physically Based Rendering in Filament] * [5][MaterialX codebase on Github] * [6][Blender's Cycles codebase on Github] * * This kernel computes the directional albedo of a glass BSDF for use * in energy compensation code (MicrofacetEnergyCompensation.h) as proposed in * [Practical multiple scattering compensation for microfacet models, Turquin, 2019] * * The kernel outputs its results in two buffers (which are then written to disk as textures). * The two textures are parameterized by cos_theta_o (cosine view direction), the roughness of * the BSDF and the reflectance at normal incidence F0 which relates to the relative IOR at * the interface of the BSDF * * The first texture is the directional albedo precomputation when hitting the object * from the outside * The second texture is used when inside the object: its IOR is simply inversed */ HIPRT_HOST_DEVICE HIPRT_INLINE float GGX_glass_E_eval(float relative_ior, float roughness, const float3& local_view_direction, const float3& local_to_light_direction, float& pdf, GGXMaskingShadowingFlavor masking_shadowing_term) { pdf = 0.0f; float NoV = local_view_direction.z; float NoL = local_to_light_direction.z; if (hippt::abs(NoL) < 1.0e-8f) // Check to avoid dividing by 0 later on return 0.0f; // We're in the case of reflection if the view direction and the bounced ray (light direction) are in the same hemisphere bool reflecting = NoL * NoV > 0; if (hippt::abs(relative_ior - 1.0f) < 1.0e-5f) relative_ior = 1.0f + 1.0e-5f; // Computing the generalized (that takes refraction into account) half vector float3 local_half_vector; if (reflecting) local_half_vector = local_to_light_direction + local_view_direction; else // We need to take the relative_ior into account when refracting to compute // the half vector (this is the "generalized" part of the half vector computation) local_half_vector = local_to_light_direction * relative_ior + local_view_direction; local_half_vector = hippt::normalize(local_half_vector); if (local_half_vector.z < 0.0f) // Because the rest of the function we're going to compute here assume // that the microfacet normal is in the same hemisphere as the surface // normal, we're going to flip it if needed local_half_vector = -local_half_vector; float HoL = hippt::dot(local_to_light_direction, local_half_vector); float HoV = hippt::dot(local_view_direction, local_half_vector); if (HoL * NoL < 0.0f || HoV * NoV < 0.0f) // Backfacing microfacets when the microfacet normal isn't in the same // hemisphere as the view dir or light dir return 0.0f; float albedo; float F = full_fresnel_dielectric(hippt::dot(local_view_direction, local_half_vector), relative_ior); if (reflecting) { HIPRTRenderData render_data; render_data.bsdfs_data.GGX_masking_shadowing = masking_shadowing_term; albedo = torrance_sparrow_GGX_eval_reflect<0>(render_data, roughness, 0.0f, false, ColorRGB32F(F), local_view_direction, local_to_light_direction, local_half_vector, pdf, MaterialUtils::SPECULAR_PEAK_SAMPLED, 0).r; // Scaling the PDF by the probability of being here (reflection of the ray and not transmission) pdf *= F; } else { float dot_prod = HoL + HoV / relative_ior; float dot_prod2 = dot_prod * dot_prod; float denom = dot_prod2 * NoL * NoV; float alpha_x; float alpha_y; MaterialUtils::get_alphas(roughness, 0.0f, alpha_x, alpha_y); float D = GGX_anisotropic(alpha_x, alpha_y, local_half_vector); float G1_V = G1_Smith(alpha_x, alpha_y, local_view_direction); float G1_L = G1_Smith(alpha_x, alpha_y, local_to_light_direction); float G2 = G1_V * G1_L; float dwm_dwi = hippt::abs(HoL) / dot_prod2; float D_pdf = G1_V / hippt::abs(NoV) * D * hippt::abs(HoV); pdf = dwm_dwi * D_pdf; // Taking refraction probability into account pdf *= (1.0f - F); // We added a check a few lines above to "avoid dividing by 0 later on". This is where. // When NoL is 0, denom is 0 too and we're dividing by 0. // The PDF of this case is as low as 1.0e-9 (light direction sampled perpendicularly to the normal) // so this is an extremely rare case. // The PDF being non-zero, we could actualy compute it, it's valid but not with floats :D albedo = D * (1 - F) * G2 * hippt::abs(HoL * HoV / denom); } return albedo; } /** * The sampled direction is returned in the local shading frame of the basis used for 'local_view_direction' */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 GGX_glass_E_sample(float relative_ior, float roughness, const float3& local_view_direction, Xorshift32Generator& random_number_generator) { if (hippt::abs(relative_ior - 1.0f) < 1.0e-5f) relative_ior = 1.0f + 1.0e-5f; float alpha_x; float alpha_y; MaterialUtils::get_alphas(roughness, /* ignoring anisotropy */ 0.0f, alpha_x, alpha_y); float3 microfacet_normal = GGX_anisotropic_sample_microfacet(local_view_direction, alpha_x, alpha_y, random_number_generator); float F = full_fresnel_dielectric(hippt::dot(local_view_direction, microfacet_normal), relative_ior); float rand_1 = random_number_generator(); float3 sampled_direction; if (rand_1 < F) // Reflection sampled_direction = reflect_ray(local_view_direction, microfacet_normal); else { // Refraction if (hippt::dot(microfacet_normal, local_view_direction) < 0.0f) // For the refraction operation that follows, we want the direction to refract (the view // direction here) to be in the same hemisphere as the normal (the microfacet normal here) // so we're flipping the microfacet normal in case it wasn't in the same hemisphere as // the view direction // Relative_eta as already been flipped above in the code microfacet_normal = -microfacet_normal; sampled_direction = refract_ray(local_view_direction, microfacet_normal, relative_ior); } return sampled_direction; } HIPRT_HOST_DEVICE HIPRT_INLINE void glass_directional_albedo_integration(int kernel_iterations, int current_iteration, uint32_t x, uint32_t y, uint32_t z, uint32_t pixel_index, GGXGlassDirectionalAlbedoSettings bake_settings, float* out_buffer, bool exiting_surface) { Xorshift32Generator random_number_generator(wang_hash(pixel_index + 1) * current_iteration); float cos_theta_o = 1.0f / (bake_settings.texture_size_cos_theta_o - 1.0f) * x; cos_theta_o = hippt::max(GGX_DOT_PRODUCTS_CLAMP, cos_theta_o); cos_theta_o = powf(cos_theta_o, 2.5f); float sin_theta_o = sin(acos(cos_theta_o)); float roughness = 1.0f / (bake_settings.texture_size_roughness - 1.0f) * y; roughness = hippt::max(roughness, 1.0e-4f); // Integrates for interface reflectivities of IORs between 1.0f and 3.0f float F0 = 1.0f / (bake_settings.texture_size_ior - 1.0f) * z; // Relative eta (eta_t / eta_i) from F0 // Using F0^4 to get more precision near 0 F0 *= F0; // F0^2 F0 *= F0; // F0^4 float sqrt_F0 = sqrtf(hippt::clamp(0.0f, 0.99f, F0)); float relative_ior = (1.0f + sqrt_F0) / (1.0f - sqrt_F0); float3 local_view_direction = hippt::normalize(make_float3(cos(0.0f) * sin_theta_o, sin(0.0f) * sin_theta_o, cos_theta_o)); if (exiting_surface) // Inverting the relative IOR in case we're inside the surface relative_ior = 1.0f / relative_ior; int iterations_per_kernel = floor(hippt::max(1.0f, GPUBakerConstants::COMPUTE_ELEMENT_PER_BAKE_KERNEL_LAUNCH / static_cast(bake_settings.texture_size_cos_theta_o * bake_settings.texture_size_roughness * bake_settings.texture_size_ior))); int nb_kernel_launch = ceil(bake_settings.integration_sample_count / static_cast(iterations_per_kernel)); int nb_samples = nb_kernel_launch * iterations_per_kernel; for (int sample = 0; sample < kernel_iterations; sample++) { float3 sampled_local_to_light_direction = GGX_glass_E_sample(relative_ior, roughness, local_view_direction, random_number_generator); float eval_pdf; float directional_albedo = GGX_glass_E_eval(relative_ior, roughness, local_view_direction, sampled_local_to_light_direction, eval_pdf, bake_settings.masking_shadowing_term); if (eval_pdf == 0.0f) continue; directional_albedo /= eval_pdf; directional_albedo *= hippt::abs(sampled_local_to_light_direction.z); out_buffer[pixel_index] += directional_albedo / nb_samples; } } #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) inline GGXGlassDirectionalAlbedoBakeEntering(int kernel_iterations, int current_iteration, GGXGlassDirectionalAlbedoSettings bake_settings, float* out_buffer) #else GLOBAL_KERNEL_SIGNATURE(void) inline GGXGlassDirectionalAlbedoBakeEntering(int kernel_iterations, int current_iteration, GGXGlassDirectionalAlbedoSettings bake_settings, float* out_buffer, int x, int y, int z) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; const uint32_t z = blockIdx.z * blockDim.z + threadIdx.z; #endif const uint32_t pixel_index = (x + y * bake_settings.texture_size_cos_theta_o + z * bake_settings.texture_size_cos_theta_o * bake_settings.texture_size_roughness); if (x >= bake_settings.texture_size_cos_theta_o || y >= bake_settings.texture_size_roughness || z >= bake_settings.texture_size_ior) return; glass_directional_albedo_integration(kernel_iterations, current_iteration, x, y, z, pixel_index, bake_settings, out_buffer, false); } #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) inline GGXGlassDirectionalAlbedoBakeExiting(int kernel_iterations, int current_iteration, GGXGlassDirectionalAlbedoSettings bake_settings, float* out_buffer) #else GLOBAL_KERNEL_SIGNATURE(void) inline GGXGlassDirectionalAlbedoBakeExiting(int kernel_iterations, int current_iteration, GGXGlassDirectionalAlbedoSettings bake_settings, float* out_buffer, int x, int y, int z) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; const uint32_t z = blockIdx.z * blockDim.z + threadIdx.z; #endif const uint32_t pixel_index = (x + y * bake_settings.texture_size_cos_theta_o + z * bake_settings.texture_size_cos_theta_o * bake_settings.texture_size_roughness); if (x >= bake_settings.texture_size_cos_theta_o || y >= bake_settings.texture_size_roughness || z >= bake_settings.texture_size_ior) return; glass_directional_albedo_integration(kernel_iterations, current_iteration, x, y, z, pixel_index, bake_settings, out_buffer, true); } ================================================ FILE: src/Device/kernels/Baking/GGXThinGlassDirectionalAlbedo.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/BSDFs/Principled.h" #include "HostDeviceCommon/RenderData.h" #include "Renderer/Baker/GGXThinGlassDirectionalAlbedoSettings.h" /* References: * [1][Practical multiple scattering compensation for microfacet models, Turquin, 2019] * [2][Revisiting Physically Based Shading at Imageworks, Kulla & Conty, SIGGRAPH 2017] * [3][Dassault Enterprise PBR 2025 Specification] * [4][Google - Physically Based Rendering in Filament] * [5][MaterialX codebase on Github] * [6][Blender's Cycles codebase on Github] * * This kernel computes the directional albedo of a thin glass BSDF for use * in energy compensation code (MicrofacetEnergyCompensation.h) as proposed in * [Practical multiple scattering compensation for microfacet models, Turquin, 2019] * * The kernel outputs its results in one buffer (which is then written to disk as a texture). * The texture is parameterized by cos_theta_o (cosine view direction) and the roughness of * the thin BSDF and its IOR (F0 actually) */ HIPRT_HOST_DEVICE HIPRT_INLINE float3 thin_glass_sample(float relative_eta, float roughness, const float3& local_view_direction, Xorshift32Generator& random_number_generator) { // To avoid sampling directions that would lead to a null half_vector. // Explained in more details in principled_glass_eval. if (hippt::abs(relative_eta - 1.0f) < 1.0e-5f) relative_eta = 1.0f + 1.0e-5f; float alpha_x; float alpha_y; MaterialUtils::get_alphas(roughness, /* anisotropy */ 0.0f, alpha_x, alpha_y); float3 microfacet_normal = GGX_anisotropic_sample_microfacet(local_view_direction, alpha_x, alpha_y, random_number_generator); float HoV = hippt::dot(local_view_direction, microfacet_normal); float F = full_fresnel_dielectric(HoV, relative_eta); // Reference: Dielectric BSDF, PBR Book 4ed: https://pbr-book.org/4ed/Reflection_Models/Dielectric_BSDF // // Adjusting fresnel reflectance for thin walled material but not above 0.1f roughness // because above that, that scaling starts to be off (this scaling is only meant for roughness 0 // actually) if (roughness < 0.1f) F += hippt::square(1.0f - F) * F / (1.0f - hippt::square(F)); float rand_1 = random_number_generator(); float3 sampled_direction; if (rand_1 < F) // Reflection sampled_direction = reflect_ray(local_view_direction, microfacet_normal); else { // Refraction if (hippt::dot(microfacet_normal, local_view_direction) < 0.0f) // For the refraction operation that follows, we want the direction to refract (the view // direction here) to be in the same hemisphere as the normal (the microfacet normal here) // so we're flipping the microfacet normal in case it wasn't in the same hemisphere as // the view direction microfacet_normal = -microfacet_normal; // Because the interface is thin (and so we refract twice, "cancelling" the bending the light), // the refraction direction is just the incoming (view direction) reflected // and flipped about the normal plane float3 reflected = reflect_ray(local_view_direction, microfacet_normal); // Now flipping reflected.z *= -1.0f; return reflected; } return sampled_direction; } HIPRT_HOST_DEVICE HIPRT_INLINE float thin_glass_eval(float relative_eta, float roughness, const float3& local_view_direction, const float3& local_to_light_direction, float& pdf, GGXMaskingShadowingFlavor masking_shadowing_term) { pdf = 0.0f; float NoV = local_view_direction.z; float NoL = local_to_light_direction.z; if (hippt::abs(NoL) < 1.0e-8f) // Check to avoid dividing by 0 later on return 0.0f; // We're in the case of reflection if the view direction and the bounced ray (light direction) are in the same hemisphere bool reflecting = NoL * NoV > 0; // relative_eta can be 1 when refracting from a volume into another volume of the same IOR. // This in conjunction with the view direction and the light direction being the negative of // one another will lead the microfacet normal to be the null vector which then causes // NaNs. // // Example: // The view and light direction can be the negative of one another when looking straight at a // flat window for example. The view direction is aligned with the normal of the window // in this configuration whereas the refracting light direction (and it is very likely to refract // in this configuration) is going to point exactly away from the view direction and the normal. // // We then have // // half_vector = light_dir + relative_eta * view_dir // = light_dir + 1.0f * view_dir // = light_dir + view_dir = (0, 0, 0) // // Normalizing this null vector then leads to a NaNs because of the zero-length. // // We're settings relative_eta to 1.00001f to avoid this issue if (hippt::abs(relative_eta - 1.0f) < 1.0e-5f) relative_eta = 1.0f + 1.0e-5f; // Computing the generalized (that takes refraction into account) half vector float3 local_half_vector; if (reflecting) local_half_vector = local_to_light_direction + local_view_direction; else // Thin walled material refract without light bending (because both refractions interfaces are simulated in one layer of material) // just refract straight through i.e. light_direction = -view_direction // It can be as si local_half_vector = local_to_light_direction * make_float3(1.0f, 1.0f, -1.0f) + local_view_direction; local_half_vector = hippt::normalize(local_half_vector); if (local_half_vector.z < 0.0f) // Because the rest of the function we're going to compute here assume // that the microfacet normal is in the same hemisphere as the surface // normal, we're going to flip it if needed local_half_vector = -local_half_vector; float HoL = hippt::dot(local_to_light_direction, local_half_vector); float HoV = hippt::dot(local_view_direction, local_half_vector); if (HoL * NoL < 0.0f || HoV * NoV < 0.0f) // Backfacing microfacets when the microfacet normal isn't in the same // hemisphere as the view dir or light dir return 0.0f; float F = full_fresnel_dielectric(HoV, relative_eta); // Reference: Dielectric BSDF, PBR Book 4ed: https://pbr-book.org/4ed/Reflection_Models/Dielectric_BSDF // // Adjusting fresnel reflectance for thin walled material but not above 0.1f roughness // because above that, that scaling starts to be off (this scaling is only meant for roughness 0 // actually) if (roughness < 0.1f) F += hippt::square(1.0f - F) * F / (1.0f - hippt::square(F)); ColorRGB32F color; if (reflecting) { HIPRTRenderData fake_render_data; fake_render_data.bsdfs_data.GGX_masking_shadowing = masking_shadowing_term; float color = torrance_sparrow_GGX_eval_reflect<0>(fake_render_data, roughness, /* anisotropy */ 0.0f, false, ColorRGB32F(F), local_view_direction, local_to_light_direction, local_half_vector, pdf, MaterialUtils::SPECULAR_PEAK_SAMPLED, 0).r; // Scaling the PDF by the probability of being here (reflection of the ray and not transmission) pdf *= F; return color; } else { float dot_prod = HoL + HoV / relative_eta; float dot_prod2 = dot_prod * dot_prod; float denom = dot_prod2 * NoL * NoV; float alpha_x; float alpha_y; MaterialUtils::get_alphas(roughness, /* anisotropy */ 0.0f, alpha_x, alpha_y); float D = GGX_anisotropic(alpha_x, alpha_y, local_half_vector); float G1_V = G1_Smith(alpha_x, alpha_y, local_view_direction); float G1_L = G1_Smith(alpha_x, alpha_y, local_to_light_direction); float G2 = G1_V * G1_L; float dwm_dwi = hippt::abs(HoL) / dot_prod2; float D_pdf = G1_V / hippt::abs(NoV) * D * hippt::abs(HoV); pdf = dwm_dwi * D_pdf; // Taking refraction probability into account pdf *= 1.0f - F; // We added a check a few lines above to "avoid dividing by 0 later on". This is where. // When NoL is 0, denom is 0 too and we're dividing by 0. // The PDF of this case is as low as 1.0e-9 (light direction sampled perpendicularly to the normal) // so this is an extremely rare case. // The PDF being non-zero, we could actualy compute it, it's valid but not with floats :D return D * (1.0f - F) * G2 * hippt::abs(HoL * HoV / denom); } } #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) inline GGXThinGlassDirectionalAlbedoBake(int kernel_iterations, int current_iteration, GGXThinGlassDirectionalAlbedoSettings bake_settings, float* out_buffer) #else GLOBAL_KERNEL_SIGNATURE(void) inline GGXThinGlassDirectionalAlbedoBake(int kernel_iterations, int current_iteration, GGXThinGlassDirectionalAlbedoSettings bake_settings, float* out_buffer, int x, int y, int z) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; const uint32_t z = blockIdx.z * blockDim.z + threadIdx.z; #endif const uint32_t pixel_index = (x + y * bake_settings.texture_size_cos_theta_o + z * bake_settings.texture_size_cos_theta_o * bake_settings.texture_size_roughness); if (x >= bake_settings.texture_size_cos_theta_o || y >= bake_settings.texture_size_roughness || z >= bake_settings.texture_size_ior) return; Xorshift32Generator random_number_generator(wang_hash(pixel_index + 1) * current_iteration); float cos_theta_o = 1.0f / (bake_settings.texture_size_cos_theta_o - 1.0f) * x; cos_theta_o = hippt::max(GGX_DOT_PRODUCTS_CLAMP, cos_theta_o); //cos_theta_o = powf(cos_theta_o, 2.5f); float sin_theta_o = sin(acos(cos_theta_o)); float roughness = 1.0f / (bake_settings.texture_size_roughness - 1.0f) * y; roughness = hippt::max(roughness, 1.0e-4f); // Integrates for interface reflectivities of IORs between 1.0f and 3.0f float F0 = 1.0f / (bake_settings.texture_size_ior - 1.0f) * z; // Relative eta (eta_t / eta_i) from F0 // Using F0^4 to get more precision near 0 F0 *= F0; // F0^2 F0 *= F0; // F0^4 float sqrt_F0 = sqrtf(hippt::clamp(0.0f, 0.99f, F0)); float relative_ior = (1.0f + sqrt_F0) / (1.0f - sqrt_F0); float3 local_view_direction = hippt::normalize(make_float3(cos(0.0f) * sin_theta_o, sin(0.0f) * sin_theta_o, cos_theta_o)); int nb_kernel_launch = ceil(bake_settings.integration_sample_count / (float)kernel_iterations); int nb_samples = nb_kernel_launch * kernel_iterations; // Entering surface for (int sample = 0; sample < kernel_iterations; sample++) { float thin_walled_roughness = MaterialUtils::get_thin_walled_roughness(true, roughness, relative_ior); float3 sampled_local_to_light_direction = thin_glass_sample(relative_ior, thin_walled_roughness, local_view_direction, random_number_generator); float eval_pdf = 0.0f; float directional_albedo = thin_glass_eval(relative_ior, thin_walled_roughness, local_view_direction, sampled_local_to_light_direction, eval_pdf, bake_settings.masking_shadowing_term); if (eval_pdf == 0.0f) continue; directional_albedo /= eval_pdf; directional_albedo *= hippt::abs(sampled_local_to_light_direction.z); out_buffer[pixel_index] += directional_albedo / nb_samples; } } ================================================ FILE: src/Device/kernels/Baking/GlossyDielectricDirectionalAlbedo.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Device/includes/BSDFs/Lambertian.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/BSDFs/Microfacet.h" #include "HostDeviceCommon/RenderData.h" #include "Renderer/Baker/GlossyDielectricDirectionalAlbedoSettings.h" /* References: * [1][Practical multiple scattering compensation for microfacet models, Turquin, 2019] * [2][Revisiting Physically Based Shading at Imageworks, Kulla & Conty, SIGGRAPH 2017] * [3][Dassault Enterprise PBR 2025 Specification] * [4][Google - Physically Based Rendering in Filament] * [5][MaterialX codebase on Github] * [6][Blender's Cycles codebase on Github] * * The kernel outputs its results in one buffer (which is then written to disk as a texture). * The texture is parameterized by cos_theta_o (cosine view direction), the roughness of * the specular GGX layer and its IOR */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) inline GlossyDielectricDirectionalAlbedoBake(int kernel_iterations, int current_iteration, GlossyDielectricDirectionalAlbedoSettings bake_settings, float* out_buffer) #else GLOBAL_KERNEL_SIGNATURE(void) inline GlossyDielectricDirectionalAlbedoBake(int kernel_iterations, int current_iteration, GlossyDielectricDirectionalAlbedoSettings bake_settings, float* out_buffer, int x, int y, int z) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; const uint32_t z = blockIdx.z * blockDim.z + threadIdx.z; #endif const uint32_t pixel_index = (x + y * bake_settings.texture_size_cos_theta_o + z * bake_settings.texture_size_cos_theta_o * bake_settings.texture_size_roughness); if (x >= bake_settings.texture_size_cos_theta_o || y >= bake_settings.texture_size_roughness || z >= bake_settings.texture_size_ior) return; Xorshift32Generator random_number_generator(wang_hash(pixel_index + 1)* current_iteration); float cos_theta_o = 1.0f / (bake_settings.texture_size_cos_theta_o - 1.0f) * x; cos_theta_o = hippt::max(GGX_DOT_PRODUCTS_CLAMP, cos_theta_o); cos_theta_o = powf(cos_theta_o, 2.5f); float sin_theta_o = sin(acos(cos_theta_o)); float roughness = 1.0f / (bake_settings.texture_size_roughness - 1.0f) * y; roughness = hippt::max(roughness, 1.0e-4f); // Integrates for interface reflectivities of IORs between 1.0f and 3.0f float F0 = 1.0f / (bake_settings.texture_size_ior - 1.0f) * z; // Relative eta (eta_t / eta_i) from F0 // Using F0^4 to get more precision near 0 F0 *= F0; // F0^2 F0 *= F0; // F0^4 float sqrt_F0 = sqrtf(hippt::clamp(0.0f, 0.99f, F0)); float relative_ior = (1.0f + sqrt_F0) / (1.0f - sqrt_F0); float3 local_view_direction = hippt::normalize(make_float3(cos(0.0f) * sin_theta_o, sin(0.0f) * sin_theta_o, cos_theta_o)); int iterations_per_kernel = floor(hippt::max(1.0f, GPUBakerConstants::COMPUTE_ELEMENT_PER_BAKE_KERNEL_LAUNCH / static_cast(bake_settings.texture_size_cos_theta_o * bake_settings.texture_size_roughness * bake_settings.texture_size_ior))); int nb_kernel_launch = ceil(bake_settings.integration_sample_count / static_cast(iterations_per_kernel)); int nb_samples = nb_kernel_launch * iterations_per_kernel; for (int sample = 0; sample < kernel_iterations; sample++) { // Sampling the specular GGX lobe or diffuse lobe float rand_lobe = random_number_generator(); float3 sampled_local_to_light_direction; if (rand_lobe < 0.5f) { // Sampling the specular lobe sampled_local_to_light_direction = microfacet_GGX_sample_reflection(roughness, /* anisotropy */ 0.0f, local_view_direction, random_number_generator); if (sampled_local_to_light_direction.z < 0) // Sampled direction below surface, this can happen with microfacet // sampling continue; } else // Sampling the diffuse lobe sampled_local_to_light_direction = cosine_weighted_sample_z_up_frame(random_number_generator); float3 microfacet_normal = hippt::normalize(local_view_direction + sampled_local_to_light_direction); float total_pdf = 0.0f; HIPRTRenderData render_data; render_data.bsdfs_data.GGX_masking_shadowing = bake_settings.masking_shadowing_term; float F = full_fresnel_dielectric(hippt::dot(microfacet_normal, sampled_local_to_light_direction), relative_ior); float eval_pdf_specular; float directional_albedo_specular = torrance_sparrow_GGX_eval_reflect<0>(render_data, roughness, /* aniso */ 0.0f, false, ColorRGB32F(F), local_view_direction, sampled_local_to_light_direction, microfacet_normal, eval_pdf_specular, MaterialUtils::SPECULAR_PEAK_SAMPLED, 0).r; // Multiplying the PDF by 0.5f because we have a 50% chance to sample the specular lobe total_pdf += eval_pdf_specular * 0.5f; float specular_layer_throughput = 1.0f; specular_layer_throughput *= 1.0f - full_fresnel_dielectric(sampled_local_to_light_direction.z, relative_ior); specular_layer_throughput *= 1.0f - full_fresnel_dielectric(local_view_direction.z, relative_ior); // A material with the base color defined is the only thing needed for // lambertian_brdf_eval() DeviceUnpackedEffectiveMaterial mat; mat.base_color = ColorRGB32F(1.0f); float eval_pdf_diffuse; float directional_albedo_diffuse = lambertian_brdf_eval(mat, sampled_local_to_light_direction.z, eval_pdf_diffuse).r; // Multiplying the PDF by 0.5f because we have a 50% chance to sample the diffuse lobe total_pdf += eval_pdf_diffuse * 0.5f; // Only the fraction of light that got through the specular layer // and that can get back to the viewer contributes to the illumination // we get from the diffuse layer directional_albedo_diffuse *= specular_layer_throughput; float final_albedo = directional_albedo_specular + directional_albedo_diffuse; final_albedo *= sampled_local_to_light_direction.z; final_albedo /= total_pdf; out_buffer[pixel_index] += final_albedo / nb_samples; } #ifndef __KERNELCC__ // Some sanity checks on the CPU float threshold = 1.1f; if (out_buffer[pixel_index] > threshold || out_buffer[pixel_index] < 0 || std::isinf(out_buffer[pixel_index]) || std::isnan(out_buffer[pixel_index])) std::cout << "Error at x, y, z = [" << x << ", " << y << ", " << z << "]. Value = " << out_buffer[pixel_index] << std::endl; #endif } ================================================ FILE: src/Device/kernels/CameraRays.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_CAMERA_RAY_H #define KERNELS_CAMERA_RAY_H #include "Device/includes/AdaptiveSampling.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/Intersect.h" #include "Device/includes/RayPayload.h" #include "Device/includes/ReSTIR/ReGIR/Representative.h" #include "HostDeviceCommon/HIPRTCamera.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/RenderData.h" HIPRT_HOST_DEVICE HIPRT_INLINE void reset_render(const HIPRTRenderData& render_data, uint32_t pixel_index) { if (render_data.aux_buffers.restir_gi_reservoir_buffer_1 != nullptr) { // Same for ReSTIR GI if (render_data.aux_buffers.restir_gi_reservoir_buffer_1) render_data.aux_buffers.restir_gi_reservoir_buffer_1[pixel_index] = ReSTIRGIReservoir(); if (render_data.aux_buffers.restir_gi_reservoir_buffer_2) render_data.aux_buffers.restir_gi_reservoir_buffer_2[pixel_index] = ReSTIRGIReservoir(); if (render_data.aux_buffers.restir_gi_reservoir_buffer_3) render_data.aux_buffers.restir_gi_reservoir_buffer_3[pixel_index] = ReSTIRGIReservoir(); } if (render_data.render_settings.has_access_to_adaptive_sampling_buffers()) { // These buffers are only available when either the adaptive sampling or the stop noise threshold is enabled render_data.aux_buffers.pixel_sample_count[pixel_index] = 0; render_data.aux_buffers.pixel_squared_luminance[pixel_index] = 0; render_data.aux_buffers.pixel_converged_sample_count[pixel_index] = -1; } // Resetting the G-Buffer render_data.g_buffer.first_hit_prim_index[pixel_index] = -1; render_data.g_buffer.geometric_normals[pixel_index] = Octahedral24BitNormalPadded32b::pack_static(make_float3(0.0f, 0.0f, 0.0f)); render_data.g_buffer.shading_normals[pixel_index] = Octahedral24BitNormalPadded32b::pack_static(make_float3(0.0f, 0.0f, 0.0f)); render_data.g_buffer.primary_hit_position[pixel_index] = make_float3(0.0f, 0.0f, 0.0f); render_data.g_buffer.materials[pixel_index] = DevicePackedEffectiveMaterial::pack(DeviceUnpackedEffectiveMaterial()); // Resetting the previous frame G-Buffer if we have it if (render_data.render_settings.use_prev_frame_g_buffer()) { render_data.g_buffer_prev_frame.first_hit_prim_index[pixel_index] = -1; render_data.g_buffer_prev_frame.geometric_normals[pixel_index] = Octahedral24BitNormalPadded32b::pack_static(make_float3(0.0f, 0.0f, 0.0f)); render_data.g_buffer_prev_frame.shading_normals[pixel_index] = Octahedral24BitNormalPadded32b::pack_static(make_float3(0.0f, 0.0f, 0.0f)); render_data.g_buffer_prev_frame.primary_hit_position[pixel_index] = make_float3(0.0f, 0.0f, 0.0f); render_data.g_buffer_prev_frame.materials[pixel_index] = DevicePackedEffectiveMaterial::pack(DeviceUnpackedEffectiveMaterial()); } } HIPRT_HOST_DEVICE HIPRT_INLINE void rescale_samples(HIPRTRenderData& render_data, uint32_t pixel_index) { // Because when displaying the framebuffer, we're dividing by the number of samples to // rescale the color of a pixel, we're going to have a problem if some pixels stopped samping // at 10 samples while the other pixels are still being sampled and have 100 samples for example. // The pixels that only received 10 samples are going to be divided by 100 at display time, making them // appear too dark. // We're rescaling the color of the pixels that stopped sampling here for correct display float float_sample_number = static_cast(render_data.render_settings.sample_number); render_data.buffers.accumulated_ray_colors[pixel_index] = render_data.buffers.accumulated_ray_colors[pixel_index] / float_sample_number * (render_data.render_settings.sample_number + 1); if (render_data.buffers.gmon_estimator.sets != nullptr) { int2 res = render_data.render_settings.render_resolution; // GMoN is enabled, we're also going to scale the GMoN samples for the same reason for (int set_index = 0; set_index < GMoNMSetsCount; set_index++) // TODO this is slow render_data.buffers.gmon_estimator.sets[set_index * res.x * res.y + pixel_index] = render_data.buffers.gmon_estimator.sets[set_index * res.x * res.y + pixel_index] / float_sample_number * (render_data.render_settings.sample_number + 1); } } #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) CameraRays(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline CameraRays(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t pixel_index = x + y * render_data.render_settings.render_resolution.x; if (render_data.render_settings.need_to_reset) reset_render(render_data, pixel_index); // 'Render low resolution' means that the user is moving the camera for example // so we're going to reduce the quality of the render for increased framerates // while moving if (render_data.render_settings.do_render_low_resolution()) { int res_scaling = render_data.render_settings.render_low_resolution_scaling; // If rendering at low resolution, only one pixel out of res_scaling^2 will be rendered if (x % res_scaling != 0 || y % res_scaling != 0) { render_data.aux_buffers.pixel_active[pixel_index] = false; return; } pixel_index /= res_scaling; } if (render_data.render_settings.use_prev_frame_g_buffer()) { render_data.g_buffer_prev_frame.geometric_normals[pixel_index] = render_data.g_buffer.geometric_normals[pixel_index]; render_data.g_buffer_prev_frame.shading_normals[pixel_index] = render_data.g_buffer.shading_normals[pixel_index]; render_data.g_buffer_prev_frame.materials[pixel_index] = render_data.g_buffer.materials[pixel_index]; render_data.g_buffer_prev_frame.primary_hit_position[pixel_index] = render_data.g_buffer.primary_hit_position[pixel_index]; render_data.g_buffer_prev_frame.first_hit_prim_index[pixel_index] = render_data.g_buffer.first_hit_prim_index[pixel_index]; } bool sampling_needed = true; bool pixel_converged = false; sampling_needed = adaptive_sampling(render_data, pixel_index, pixel_converged); if (pixel_converged || !sampling_needed) { if (render_data.render_settings.do_update_status_buffers) // Updating if we have the right to (when do_update_status_buffers is true). // do_update_status_buffers is only true on the last sample of a frame // // Indicating that this pixel has reached the threshold in render_settings.stop_noise_threshold hippt::atomic_fetch_add(render_data.aux_buffers.pixel_count_converged_so_far, 1u); } if (render_data.render_settings.has_access_to_adaptive_sampling_buffers()) { if (!sampling_needed) { rescale_samples(render_data, pixel_index); render_data.aux_buffers.pixel_active[pixel_index] = false; return; } else render_data.aux_buffers.pixel_sample_count[pixel_index]++; } unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(pixel_index + 1); else seed = wang_hash((pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); // Direction to the center of the pixel float x_ray_point_direction = (x + 0.5f); float y_ray_point_direction = (y + 0.5f); if (render_data.current_camera.do_jittering) { // Jitter randomly around the center x_ray_point_direction += random_number_generator() - 0.5f; y_ray_point_direction += random_number_generator() - 0.5f; } hiprtRay ray = render_data.current_camera.get_camera_ray(x_ray_point_direction, y_ray_point_direction, render_data.render_settings.render_resolution); RayPayload ray_payload; HitInfo closest_hit_info; bool intersection_found = trace_main_path_ray(render_data, ray, ray_payload, closest_hit_info, /* camera ray = no previous primitive hit */ -1, /* bounce. Always 0 for camera rays*/ 0, random_number_generator); if (intersection_found) { render_data.g_buffer.geometric_normals[pixel_index].pack(closest_hit_info.geometric_normal); render_data.g_buffer.shading_normals[pixel_index].pack(closest_hit_info.shading_normal); render_data.g_buffer.materials[pixel_index] = DevicePackedEffectiveMaterial::pack(ray_payload.material); render_data.g_buffer.primary_hit_position[pixel_index] = closest_hit_info.inter_point; } else // Special case when not hitting anything // // The view directions are reconstructed from the primary hit and the camera position // but if we didn't hit anything, there's no primary hit. // // But we're still going to need to be able to reconstruct the view direction // so we're faking the primary hit with the point the ray was directed to instead. // // If you're wondering: "yeah but then the rest of the ray tracing passes are going to use a wrong primary hit position?" // --> No because the 'first_hit_prim_index' indicates whether we have a primary hit or not. // If we don't have a primary hit, we're never going to use the float3 in the 'primary_hit_position' // buffer as an actual position, render_data.g_buffer.primary_hit_position[pixel_index] = ray.origin + ray.direction; render_data.g_buffer.first_hit_prim_index[pixel_index] = intersection_found ? closest_hit_info.primitive_index : -1; render_data.aux_buffers.pixel_active[pixel_index] = true; ReGIR_update_representative_data(render_data, closest_hit_info.inter_point, closest_hit_info.geometric_normal, render_data.current_camera, closest_hit_info.primitive_index, true, ray_payload.material); // If we got here, this means that we still have at least one ray active if (render_data.render_settings.do_update_status_buffers) { // Updating if we have the right to (when do_update_status_buffers is true). // do_update_status_buffers is only true on the last sample of a frame render_data.aux_buffers.still_one_ray_active[0] = 1; } } #endif ================================================ FILE: src/Device/kernels/Experimentations/RegistersTest.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ /** * This kernel is a playground for understanding what kind of optimizations the GPU compiler is able to do on variable usage ---> register pressure. * * This allows the validation of simple intuitions such as: "The compiler optimizes away unused variables". * * But what if the variable is passed in a function that itself doesn't use it? Try it out: the compiler optimizes it away too. * Fun fact: the variable is also optimized if used but not initialized. * * You get the idea. */ /** * Here's a rundown of all that I tested already: * * - Unused variable: optimized away, no register cost. * - Variable passed to a function that doesn't use it: optimized away, no register cost. * - Precomputing a result in a temporary variable to avoid recomputing many times: no register cost. * This must be because using a temporary variable or not, the result of the calculation must be in * a register anyway so it's only 1 register in both cases * - Two different variables equal to the same value: only using one register * - Variable declared in a structure. That structure is passed to a function that doesn't use the variable ---> variable optimized away. */ #include "Device/includes/FixIntellisense.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/HIPRTCamera.h" #include "HostDeviceCommon/Xorshift.h" HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int wang_hash(unsigned int seed) { seed = (seed ^ 61) ^ (seed >> 16); seed *= 9; seed = seed ^ (seed >> 4); seed *= 0x27d4eb2d; seed = seed ^ (seed >> 15); return seed; } //struct DataStruct //{ // unsigned char a, b, c, d; //}; // //HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int wang_hash(unsigned int seed) //{ // seed = (seed ^ 61) ^ (seed >> 16); // seed *= 9; // seed = seed ^ (seed >> 4); // seed *= 0x27d4eb2d; // seed = seed ^ (seed >> 15); // return seed; //} // //HIPRT_HOST_DEVICE HIPRT_INLINE int sumFunction(DataStruct& data) //{ // return (int)data.a % (int)data.b + (int)data.c + (int)data.d; //} // //#ifdef __KERNELCC__ //GLOBAL_KERNEL_SIGNATURE(void) TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera) //#else //GLOBAL_KERNEL_SIGNATURE(void) inline TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera, int x, int y) //#endif //{ //#ifdef __KERNELCC__ // const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; // const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; //#endif // const uint32_t threadId = (x + y * res.x); // // if (threadId >= res.x * res.y) // return; // // Xorshift32Generator randomGenerator(wang_hash(threadId + 1)); // // DataStruct data; // // unsigned char rand_a = (unsigned char)randomGenerator.xorshift32(); // unsigned char rand_b = (unsigned char)randomGenerator.xorshift32(); // unsigned char rand_c = (unsigned char)randomGenerator.xorshift32(); // unsigned char rand_d = (unsigned char)randomGenerator.xorshift32(); // // data.a = rand_a; // data.b = rand_b; // data.c = rand_c; // data.d = rand_d; // // int result = sumFunction(data); // // render_data.buffers.pixels[threadId] = ColorRGB32F(result); //} // 9 registers //struct DataStruct //{ // int packed; //}; // //HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int wang_hash(unsigned int seed) //{ // seed = (seed ^ 61) ^ (seed >> 16); // seed *= 9; // seed = seed ^ (seed >> 4); // seed *= 0x27d4eb2d; // seed = seed ^ (seed >> 15); // return seed; //} // //HIPRT_HOST_DEVICE HIPRT_INLINE int sumFunction(DataStruct& data) //{ // return ((data.packed >> 0) | 0b11111111) % // a // ((data.packed >> 8) | 0b11111111) + // b // ((data.packed >> 16) | 0b11111111) + // c // ((data.packed >> 24) | 0b11111111); // d //} // //#ifdef __KERNELCC__ //GLOBAL_KERNEL_SIGNATURE(void) TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera) //#else //GLOBAL_KERNEL_SIGNATURE(void) inline TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera, int x, int y) //#endif //{ //#ifdef __KERNELCC__ // const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; // const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; //#endif // const uint32_t threadId = (x + y * res.x); // // if (threadId >= res.x * res.y) // return; // // Xorshift32Generator randomGenerator(wang_hash(threadId + 1)); // // DataStruct data; // // data.packed = (int)randomGenerator.xorshift32(); // int result = sumFunction(data); // // render_data.buffers.pixels[threadId] = ColorRGB32F(result); //} // 7 registers //struct DataStruct //{ // short int a; // short int b; // short int c; // short int d; //}; // //HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int wang_hash(unsigned int seed) //{ // seed = (seed ^ 61) ^ (seed >> 16); // seed *= 9; // seed = seed ^ (seed >> 4); // seed *= 0x27d4eb2d; // seed = seed ^ (seed >> 15); // return seed; //} // //HIPRT_HOST_DEVICE HIPRT_INLINE int sumFunction(DataStruct& data) //{ // return data.a % data.b + data.c + data.d; //} // //#ifdef __KERNELCC__ //GLOBAL_KERNEL_SIGNATURE(void) TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera) //#else //GLOBAL_KERNEL_SIGNATURE(void) inline TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera, int x, int y) //#endif //{ //#ifdef __KERNELCC__ // const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; // const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; //#endif // const uint32_t threadId = (x + y * res.x); // // if (threadId >= res.x * res.y) // return; // // Xorshift32Generator randomGenerator(wang_hash(threadId + 1)); // // DataStruct data; // data.a = (short int)randomGenerator.xorshift32(); // data.b = (short int)randomGenerator.xorshift32(); // data.c = (short int)randomGenerator.xorshift32(); // data.d = (short int)randomGenerator.xorshift32(); // int result = sumFunction(data); // // render_data.buffers.pixels[threadId] = ColorRGB32F(result); //} //// 10 registers //struct DataStruct //{ // int packed1; // int packed2; //}; // //HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int wang_hash(unsigned int seed) //{ // seed = (seed ^ 61) ^ (seed >> 16); // seed *= 9; // seed = seed ^ (seed >> 4); // seed *= 0x27d4eb2d; // seed = seed ^ (seed >> 15); // return seed; //} // //HIPRT_HOST_DEVICE HIPRT_INLINE int sumFunction(DataStruct& data) //{ // return ((data.packed1 >> 0) | 0xFFFF) % ((data.packed1 >> 16) | 0xFFFF) + ((data.packed2 >> 0) | 0xFFFF) + ((data.packed2 >> 16) | 0xFFFF); //} // //#ifdef __KERNELCC__ //GLOBAL_KERNEL_SIGNATURE(void) TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera) //#else //GLOBAL_KERNEL_SIGNATURE(void) inline TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera, int x, int y) //#endif //{ //#ifdef __KERNELCC__ // const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; // const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; //#endif // const uint32_t threadId = (x + y * res.x); // // if (threadId >= res.x * res.y) // return; // // Xorshift32Generator randomGenerator(wang_hash(threadId + 1)); // // DataStruct data; // data.packed1 = randomGenerator.xorshift32(); // data.packed2 = randomGenerator.xorshift32(); // int result = sumFunction(data); // // render_data.buffers.pixels[threadId] = ColorRGB32F(result); //} // 7 registers //struct DataStruct //{ // int a; // int b; // int c; // int d; // int e; // int f; //}; // //HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int wang_hash(unsigned int seed) //{ // seed = (seed ^ 61) ^ (seed >> 16); // seed *= 9; // seed = seed ^ (seed >> 4); // seed *= 0x27d4eb2d; // seed = seed ^ (seed >> 15); // return seed; //} // //HIPRT_HOST_DEVICE HIPRT_INLINE int sumFunction(DataStruct& data) //{ // return data.a % data.b + data.c + data.d + data.e + data.f; //} // //#ifdef __KERNELCC__ //GLOBAL_KERNEL_SIGNATURE(void) TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera) //#else //GLOBAL_KERNEL_SIGNATURE(void) inline TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera, int x, int y) //#endif //{ //#ifdef __KERNELCC__ // const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; // const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; //#endif // const uint32_t threadId = (x + y * res.x); // // if (threadId >= res.x * res.y) // return; // // Xorshift32Generator randomGenerator(wang_hash(threadId + 1)); // // DataStruct data; // data.a = randomGenerator.xorshift32(); // data.b = randomGenerator.xorshift32(); // data.c = randomGenerator.xorshift32(); // data.d = randomGenerator.xorshift32(); // data.e = randomGenerator.xorshift32(); // data.f = randomGenerator.xorshift32(); // int result = sumFunction(data); // // render_data.buffers.pixels[threadId] = ColorRGB32F(result); //} // 10 registers //struct DataStruct //{ // int ab; // int cd; // int ef; //}; // //HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int wang_hash(unsigned int seed) //{ // seed = (seed ^ 61) ^ (seed >> 16); // seed *= 9; // seed = seed ^ (seed >> 4); // seed *= 0x27d4eb2d; // seed = seed ^ (seed >> 15); // return seed; //} // //HIPRT_HOST_DEVICE HIPRT_INLINE int sumFunction(DataStruct& data) //{ // int a = data.ab & (0xFFFF << 0); // int b = data.ab & (0xFFFF << 16); // int c = data.cd & (0xFFFF << 0); // int d = data.cd & (0xFFFF << 16); // int e = data.ef & (0xFFFF << 0); // int f = data.ef & (0xFFFF << 16); // return a % b + c + d + e + f; //} // //#ifdef __KERNELCC__ //GLOBAL_KERNEL_SIGNATURE(void) TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera) //#else //GLOBAL_KERNEL_SIGNATURE(void) inline TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera, int x, int y) //#endif //{ //#ifdef __KERNELCC__ // const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; // const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; //#endif // const uint32_t threadId = (x + y * res.x); // // if (threadId >= res.x * res.y) // return; // // Xorshift32Generator randomGenerator(wang_hash(threadId + 1)); // // DataStruct data; // data.ab = randomGenerator.xorshift32(); // data.cd = randomGenerator.xorshift32(); // data.ef = randomGenerator.xorshift32(); // int result = sumFunction(data); // // render_data.buffers.pixels[threadId] = ColorRGB32F(result); //} //#include "Device/includes/Dispatcher.h" // //#ifdef __KERNELCC__ //GLOBAL_KERNEL_SIGNATURE(void) TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera) //#else //GLOBAL_KERNEL_SIGNATURE(void) inline TestFunction(HIPRTRenderData render_data, int2 res, HIPRTCamera camera, int x, int y) //#endif //{ //#ifdef __KERNELCC__ // const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; // const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; //#endif // const uint32_t threadId = (x + y * res.x); // // if (threadId >= res.x * res.y) // return; // // Xorshift32Generator randomGenerator(wang_hash(threadId + 1)); // // float pdf; // int mat_index = (int)(threadId * randomGenerator() * 50); // DeviceTexturedMaterial mat = render_data.buffers.materials_buffer[(int)(threadId * randomGenerator() * 50) % 10]; // ColorRGB32F eval_out = bsdf_dispatcher_eval(render_data.buffers.materials_buffer, mat, render_data.g_buffer.ray_volume_states[threadId], make_float3(0.5, 1.0, 2), make_float3(0.5, 1.0, 2), make_float3(0.5, 1.0, 2), pdf); // // int incident, outgoing; // bool leaving; // render_data.g_buffer.ray_volume_states[threadId].interior_stack.push(incident, outgoing, leaving, mat_index, render_data.buffers.materials_buffer[mat_index].dielectric_priority); // render_data.g_buffer.ray_volume_states[threadId].interior_stack.push(incident, outgoing, leaving, mat_index + 5, render_data.buffers.materials_buffer[mat_index + 5].dielectric_priority); // render_data.g_buffer.ray_volume_states[threadId].interior_stack.push(incident, outgoing, leaving, mat_index * 5, render_data.buffers.materials_buffer[mat_index * 5].dielectric_priority); // render_data.g_buffer.ray_volume_states[threadId].interior_stack.push(incident, outgoing, leaving, mat_index * 25, render_data.buffers.materials_buffer[mat_index * 25].dielectric_priority); // // render_data.buffers.pixels[threadId] = ColorRGB32F(render_data.g_buffer.ray_volume_states[threadId].interior_stack.stack_entries[1].odd_parity) * eval_out; //} ================================================ FILE: src/Device/kernels/Experimentations/Test3DTexture.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ /** * Kernel for testing that creating and reading from a 3D texture happens correctly. * The 3D texture is just written to a linear buffer and the linear buffer is then * expected to contain the data of the texture, basically just a copy of it. */ #include "Device/includes/FixIntellisense.h" #include #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) Test3DTexture(oroTextureObject_t texture_3D, int tex_size, float* out_buffer) #else GLOBAL_KERNEL_SIGNATURE(void) Test3DTexture(oroTextureObject_t texture_3D, int tex_size, float* out_buffer, int x, int y, int z) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; const uint32_t z = blockIdx.z * blockDim.z + threadIdx.z; #endif if (x >= tex_size || y >= tex_size || z >= tex_size) return; const uint32_t thread_index = (x + y * tex_size + z * tex_size * tex_size); out_buffer[thread_index * 4] = tex3D(texture_3D, x + 0.35f, y + 0.35f, z + 0.35f).y; } ================================================ FILE: src/Device/kernels/Experimentations/TestCopyKernelAlignment.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ /** * Kernel for testing that creating and reading from a 3D texture happens correctly. * The 3D texture is just written to a linear buffer and the linear buffer is then * expected to contain the data of the texture, basically just a copy of it. */ #include "Device/includes/FixIntellisense.h" #include "HostDeviceCommon/Color.h" #include #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) TestCopyKernelAlignment(ColorRGB32F* __restrict__ buffer_a, const ColorRGB32F* __restrict__ buffer_b, size_t buffer_size) #else GLOBAL_KERNEL_SIGNATURE(void) TestCopyKernelAlignment(ColorRGB32F* __restrict__ buffer_a, const ColorRGB32F* __restrict__ buffer_b, size_t buffer_size, int x) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; #endif uint32_t offset = 13; uint32_t index = x + offset; if (index >= buffer_size) return; buffer_a[index] = buffer_b[index]; } ================================================ FILE: src/Device/kernels/Experimentations/TestCopyKernelRestrict.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ /** * Kernel for testing that creating and reading from a 3D texture happens correctly. * The 3D texture is just written to a linear buffer and the linear buffer is then * expected to contain the data of the texture, basically just a copy of it. */ #include "Device/includes/FixIntellisense.h" #include #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) TestCopyKernelRestrict(float* buffer_a, float* buffer_b, float* buffer_c, float* buffer_d, size_t buffer_size) #else GLOBAL_KERNEL_SIGNATURE(void) TestCopyKernelRestrict(float* __restrict__ buffer_a, const float* __restrict__ buffer_b, float* __restrict__ buffer_c, float* __restrict__ buffer_d, size_t buffer_size, int x) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; #endif if (x >= buffer_size) return; buffer_a[x] = buffer_a[x] + buffer_b[x]; buffer_d[x] = buffer_a[x] * buffer_b[x]; buffer_d[x] *= buffer_c[x]; } ================================================ FILE: src/Device/kernels/Experimentations/TestCopyKernelSimple.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Device/includes/FixIntellisense.h" #include "Device/includes/ReSTIR/DI/Reservoir.h" #include "Device/includes/ReSTIR/GI/Reservoir.h" #include "HostDeviceCommon/Color.h" #include using TEST_COPY_KERNEL_SIMPLE_BUFFER_TYPE = DeviceUnpackedEffectiveMaterial; struct TestCopyKernelSimpleInputData { TEST_COPY_KERNEL_SIMPLE_BUFFER_TYPE* buffer_a; TEST_COPY_KERNEL_SIMPLE_BUFFER_TYPE* buffer_b; }; HIPRT_HOST_DEVICE HIPRT_INLINE void copy_function(const TEST_COPY_KERNEL_SIMPLE_BUFFER_TYPE* __restrict__ input_buffer, TEST_COPY_KERNEL_SIMPLE_BUFFER_TYPE* __restrict__ output_buffer, uint32_t tIdx) { output_buffer[tIdx] = input_buffer[tIdx]; } #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) TestCopyKernelSimple(TestCopyKernelSimpleInputData input, size_t buffer_size) #else GLOBAL_KERNEL_SIGNATURE(void) TestCopyKernelSimple(TestCopyKernelSimpleInputData input, size_t buffer_size, int x) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; #endif uint32_t offset = 13; uint32_t index = x + offset; if (index >= buffer_size) return; copy_function(input.buffer_b, input.buffer_a, index); } ================================================ FILE: src/Device/kernels/GMoN/GMoNComputeMedianOfMeans.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_GMON_H #define KERNELS_GMON_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/GMoN/GMoN.h" #include "HostDeviceCommon/RenderData.h" /** * Kernel for the implementation of GMoN * * Reference: * [1] [Firefly removal in Monte Carlo rendering with adaptive Median of meaNs, Buisine et al., 2021] */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) GMoNComputeMedianOfMeans(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline GMoNComputeMedianOfMeans(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t pixel_index = x + y * render_data.render_settings.render_resolution.x; if (render_data.render_settings.sample_number == 0) { // For sample 0, this is a special case where we're just going to // copy the current pixel color (which is only 1 sample accumulated) // to the output framebuffer such that we don't get a black // viewport while the full GMoN median of means computation // hasn't been launched render_data.buffers.gmon_estimator.result_framebuffer[pixel_index] = render_data.buffers.accumulated_ray_colors[pixel_index]; return; } ColorRGB32F GMoN_color = gmon_compute_median_of_means(render_data.buffers.gmon_estimator, pixel_index, render_data.render_settings.sample_number, render_data.render_settings.render_resolution); render_data.buffers.gmon_estimator.result_framebuffer[pixel_index] = GMoN_color; } #endif ================================================ FILE: src/Device/kernels/Megakernel.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_MEGAKERNEL_H #define KERNELS_MEGAKERNEL_H #include "Device/includes/AdaptiveSampling.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/LightSampling/Lights.h" #include "Device/includes/LightSampling/Envmap.h" #include "Device/includes/Hash.h" #include "Device/includes/Material.h" #include "Device/includes/PathTracing.h" #include "Device/includes/RayPayload.h" #include "Device/includes/Sampling.h" #include "Device/includes/SanityCheck.h" #include "HostDeviceCommon/Xorshift.h" #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) MegaKernel(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline MegaKernel(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t pixel_index = x + y * render_data.render_settings.render_resolution.x; if (!render_data.aux_buffers.pixel_active[pixel_index]) return; if (render_data.render_settings.do_render_low_resolution()) // Reducing the number of bounces to 3 if rendering at low resolution // for better interactivity render_data.render_settings.nb_bounces = hippt::min(3, render_data.render_settings.nb_bounces); #if ViewportColorOverriden == 1 // If some kernel option is going to debug some color in the viewport, // then we're clearing the viewport buffer here render_data.buffers.accumulated_ray_colors[pixel_index] = ColorRGB32F(); #endif unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(pixel_index + 1); else seed = wang_hash((pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); // Initializing the closest hit info the information from the camera ray pass HitInfo closest_hit_info; closest_hit_info.inter_point = render_data.g_buffer.primary_hit_position[pixel_index]; closest_hit_info.geometric_normal = hippt::normalize(render_data.g_buffer.geometric_normals[pixel_index].unpack()); closest_hit_info.shading_normal = hippt::normalize(render_data.g_buffer.shading_normals[pixel_index].unpack()); closest_hit_info.primitive_index = render_data.g_buffer.first_hit_prim_index[pixel_index]; // Initializing the ray with the information from the camera ray pass hiprtRay ray; ray.direction = hippt::normalize(-render_data.g_buffer.get_view_direction(render_data.current_camera.position, pixel_index)); RayPayload ray_payload; ray_payload.next_ray_state = RayState::BOUNCE; ray_payload.material = render_data.g_buffer.materials[pixel_index].unpack(); // Because this is the camera hit (and assuming the camera isn't inside volumes for now), // the ray volume state after the camera hit is just an empty interior stack but with // the material index that we hit pushed onto the stack. That's it. Because it is that // simple, we don't have the ray volume state in the GBuffer but rather we can // reconstruct the ray volume state on the fly ray_payload.volume_state.reconstruct_first_hit( ray_payload.material, render_data.buffers.material_indices, closest_hit_info.primitive_index, random_number_generator); // + 1 to nb_bounces here because we want "0" bounces to still act as one // hit and to return some color bool intersection_found = closest_hit_info.primitive_index != -1; for (int& bounce = ray_payload.bounce; bounce < render_data.render_settings.nb_bounces + 1; bounce++) { if (ray_payload.next_ray_state != RayState::MISSED) { if (bounce > 0) intersection_found = path_tracing_find_indirect_bounce_intersection(render_data, ray, ray_payload, closest_hit_info, random_number_generator); if (intersection_found) { if (bounce == 0) store_denoiser_AOVs(render_data, pixel_index, closest_hit_info.shading_normal, ray_payload.material.base_color); else if (bounce > 0) { bool ReGIR_primary_hit = render_data.render_settings.regir_settings.compute_is_primary_hit(ray_payload); // Storing data for ReGIR representative points ReGIR_update_representative_data(render_data, closest_hit_info.inter_point, closest_hit_info.geometric_normal, render_data.current_camera, closest_hit_info.primitive_index, ReGIR_primary_hit, ray_payload.material); } // TODO REMOVE THE DEBUG IF if (bounce > 0 || render_data.render_settings.enable_direct) { ray_payload.ray_color += estimate_direct_lighting(render_data, ray_payload, closest_hit_info, -ray.direction, x, y, random_number_generator); sanity_check(render_data, ray_payload.ray_color, x, y); } BSDFIncidentLightInfo sampled_light_info; // This variable is never used, this is just for debugging on the CPU so that we know what the BSDF sampled bool valid_indirect_bounce = path_tracing_compute_next_indirect_bounce(render_data, ray_payload, closest_hit_info, -ray.direction, ray, random_number_generator, &sampled_light_info); if (!valid_indirect_bounce) // Bad BSDF sample (under the surface), killed by russian roulette, ... break; } else { ray_payload.ray_color += path_tracing_miss_gather_envmap(render_data, ray_payload, ray.direction, pixel_index); ray_payload.next_ray_state = RayState::MISSED; sanity_check(render_data, ray_payload.ray_color, x, y); } } else if (ray_payload.next_ray_state == RayState::MISSED) break; } // Checking for NaNs / negative value samples. Output if (!sanity_check(render_data, ray_payload.ray_color, x, y)) return; // If we got here, this means that we still have at least one ray active // This is a concurrent write by the way but we don't really care, everyone is writing // the same value render_data.aux_buffers.still_one_ray_active[0] = 1; path_tracing_accumulate_debug_view_color(render_data, ray_payload, pixel_index, random_number_generator); path_tracing_accumulate_color(render_data, ray_payload.ray_color, pixel_index); } #endif ================================================ FILE: src/Device/kernels/NEE++/GridPrepopulate.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_NEE_PLUS_PLUS_GRID_PREPOPULATE_H #define KERNELS_NEE_PLUS_PLUS_GRID_PREPOPULATE_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/LightSampling/Lights.h" #include "Device/includes/LightSampling/Envmap.h" #include "Device/includes/Hash.h" #include "Device/includes/Material.h" #include "Device/includes/PathTracing.h" #include "Device/includes/RayPayload.h" #include "Device/includes/Sampling.h" #include "Device/includes/SanityCheck.h" #include "HostDeviceCommon/Xorshift.h" HIPRT_DEVICE void accumulate_NEE_plus_plus(HIPRTRenderData& render_data, const hiprtRay& ray, const HitInfo& closest_hit_info, RayPayload& ray_payload, Xorshift32Generator& random_number_generator) { // Just making sure that this is not set to false render_data.nee_plus_plus.m_update_visibility_map = true; for (int sample = 0; sample < 100; sample++) { constexpr int SAMPLING_STRATEGY = DirectLightSamplingBaseStrategy == LSS_BASE_REGIR ? ReGIR_GridFillLightSamplingBaseStrategy : DirectLightSamplingBaseStrategy; LightSampleInformation light_sample = sample_one_emissive_triangle(render_data, closest_hit_info.inter_point, -ray.direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, closest_hit_info.primitive_index, ray_payload, random_number_generator); if (light_sample.area_measure_pdf <= 0.0f) // Can happen for very small triangles continue; float3 shadow_ray_origin = closest_hit_info.inter_point; float3 shadow_ray_direction = light_sample.point_on_light - shadow_ray_origin; float distance_to_light = hippt::length(shadow_ray_direction); float3 shadow_ray_direction_normalized = shadow_ray_direction / distance_to_light; hiprtRay shadow_ray; shadow_ray.origin = shadow_ray_origin; shadow_ray.direction = shadow_ray_direction_normalized; ColorRGB32F light_source_radiance; // abs() here to allow backfacing light sources float dot_light_source = compute_cosine_term_at_light_source(light_sample.light_source_normal, -shadow_ray.direction); if (dot_light_source > 0.0f) { NEEPlusPlusContext nee_plus_plus_context; nee_plus_plus_context.point_on_light = light_sample.point_on_light; nee_plus_plus_context.shaded_point = shadow_ray_origin; bool in_shadow = evaluate_shadow_ray_nee_plus_plus(render_data, shadow_ray, distance_to_light, closest_hit_info.primitive_index, nee_plus_plus_context, random_number_generator, ray_payload.bounce); } } } #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) NEEPlusPlus_Grid_Prepopulate(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline NEEPlusPlus_Grid_Prepopulate(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = (blockIdx.x * blockDim.x + threadIdx.x) * ReGIR_GridPrepopulationResolutionDownscale; const uint32_t y = (blockIdx.y * blockDim.y + threadIdx.y) * ReGIR_GridPrepopulationResolutionDownscale; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t pixel_index = x + y * render_data.render_settings.render_resolution.x; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(pixel_index + 1); else seed = wang_hash((pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); // Direction to the center of the pixel float x_ray_point_direction = (x + 0.5f); float y_ray_point_direction = (y + 0.5f); if (render_data.current_camera.do_jittering) { // Jitter randomly around the center x_ray_point_direction += random_number_generator() - 0.5f; y_ray_point_direction += random_number_generator() - 0.5f; } hiprtRay ray = render_data.current_camera.get_camera_ray(x_ray_point_direction, y_ray_point_direction, render_data.render_settings.render_resolution); RayPayload ray_payload; HitInfo closest_hit_info; bool intersection_found = trace_main_path_ray(render_data, ray, ray_payload, closest_hit_info, /* camera ray = no previous primitive hit */ -1, /* bounce. Always 0 for camera rays*/ 0, random_number_generator); if (!intersection_found) return; for (int& bounce = ray_payload.bounce; bounce < render_data.render_settings.nb_bounces + 1; bounce++) { if (ray_payload.next_ray_state != RayState::MISSED) { if (bounce > 0) intersection_found = path_tracing_find_indirect_bounce_intersection(render_data, ray, ray_payload, closest_hit_info, random_number_generator); if (intersection_found) { accumulate_NEE_plus_plus(render_data, ray, closest_hit_info, ray_payload, random_number_generator); BSDFIncidentLightInfo sampled_light_info; // This variable is never used, this is just for debugging on the CPU so that we know what the BSDF sampled bool valid_indirect_bounce = path_tracing_compute_next_indirect_bounce(render_data, ray_payload, closest_hit_info, -ray.direction, ray, random_number_generator, &sampled_light_info); if (!valid_indirect_bounce) // Bad BSDF sample (under the surface), killed by russian roulette, ... break; } else return; } } } #endif ================================================ FILE: src/Device/kernels/NEE++/NEEPlusPlusFinalizeAccumulation.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_NEE_PLUS_PLUS_FINALIZE_ACCUMULATION_H #define KERNELS_NEE_PLUS_PLUS_FINALIZE_ACCUMULATION_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/NEE++/NEE++.h" #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) NEEPlusPlusFinalizeAccumulation(NEEPlusPlusDevice nee_plus_plus_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline NEEPlusPlusFinalizeAccumulation(NEEPlusPlusDevice nee_plus_plus_data, int x) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; #endif uint32_t pixel_index = x; if (x >= nee_plus_plus_data.m_total_number_of_cells) return; // nee_plus_plus_data.copy_accumulation_buffers(pixel_index); } #endif ================================================ FILE: src/Device/kernels/ReSTIR/DI/FusedSpatiotemporalReuse.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_SPATIOTEMPORAL_REUSE_H #define DEVICE_RESTIR_DI_SPATIOTEMPORAL_REUSE_H #include "Device/includes/Dispatcher.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/ReSTIR/SpatialMISWeight.h" #include "Device/includes/ReSTIR/SpatiotemporalMISWeight.h" #include "Device/includes/ReSTIR/SpatiotemporalNormalizationWeight.h" #include "Device/includes/ReSTIR/Surface.h" #include "Device/includes/ReSTIR/DI/TargetFunction.h" #include "Device/includes/ReSTIR/Utils.h" #include "Device/includes/ReSTIR/UtilsSpatial.h" #include "Device/includes/ReSTIR/UtilsTemporal.h" #include "Device/includes/Sampling.h" #include "HostDeviceCommon/HIPRTCamera.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/RenderData.h" /** References: * * [1] [Spatiotemporal reservoir resampling for real-time ray tracing with dynamic direct lighting] https://research.nvidia.com/labs/rtr/publication/bitterli2020spatiotemporal/ * [2] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time] https://intro-to-restir.cwyman.org/ * [3] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time - SIGGRAPH 2023 Presentation Video] https://dl.acm.org/doi/10.1145/3587423.3595511#sec-supp * [4] [NVIDIA RTX DI SDK - Github] https://github.com/NVIDIAGameWorks/RTXDI * [5] [Generalized Resampled Importance Sampling Foundations of ReSTIR] https://research.nvidia.com/publication/2022-07_generalized-resampled-importance-sampling-foundations-restir * [6] [Uniform disk sampling] https://rh8liuqy.github.io/Uniform_Disk.html * [7] [Reddit Post for the Jacobian term needed] https://www.reddit.com/r/GraphicsProgramming/comments/1eo5hqr/restir_di_light_sample_pdf_confusion/ * [8] [Rearchitecting Spatiotemporal Resampling for Production] https://research.nvidia.com/publication/2021-07_rearchitecting-spatiotemporal-resampling-production */ #define TEMPORAL_NEIGHBOR_ID 0 HIPRT_HOST_DEVICE HIPRT_INLINE bool do_include_spatial_visibility_term_or_not(const HIPRTRenderData& render_data, int current_neighbor_index) { const ReSTIRCommonSpatialPassSettings& spatial_settings = render_data.render_settings.restir_di_settings.common_spatial_pass; bool visibility_only_on_last_pass = spatial_settings.do_visibility_only_last_pass; bool is_last_pass = spatial_settings.spatial_pass_index == spatial_settings.number_of_passes - 1; // Only using the visibility term on the last pass if so desired bool include_target_function_visibility = visibility_only_on_last_pass && is_last_pass; // Also allowing visibility if we want it at every pass include_target_function_visibility |= !spatial_settings.do_visibility_only_last_pass; // Only doing visibility for a few neighbors depending on 'neighbor_visibility_count' include_target_function_visibility &= current_neighbor_index < spatial_settings.neighbor_visibility_count; // Only doing visibility if we want it at all include_target_function_visibility &= ReSTIR_DI_SpatialTargetFunctionVisibility; // We don't want visibility for the center pixel because we're going to reuse the // target function stored in the reservoir anyways // Note: the center pixel has index 'spatial_settings.reuse_neighbor_count' // while actual *neighbors* have index between [0, spatial_settings.reuse_neighbor_count - 1] include_target_function_visibility &= current_neighbor_index != spatial_settings.reuse_neighbor_count; return include_target_function_visibility; } /** * Returns -1 if there is no valid temporal neighbor. * The linear buffer index of the temporal neighbor otherwise */ HIPRT_HOST_DEVICE HIPRT_INLINE int3 load_spatiotemporal_neighbor_data(const HIPRTRenderData& render_data, const ReSTIRSurface& center_pixel_surface, int center_pixel_index, ReSTIRDIReservoir& out_temporal_neighbor_reservoir, ReSTIRSurface& out_temporal_neighbor_surface, Xorshift32Generator& random_number_generator) { int3 temporal_neighbor_pixel_index_and_pos = find_temporal_neighbor_index(render_data, render_data.g_buffer.primary_hit_position[center_pixel_index], center_pixel_surface.shading_normal, center_pixel_index, random_number_generator); if (temporal_neighbor_pixel_index_and_pos.x == -1 || render_data.render_settings.freeze_random) // Temporal occlusion / disoccusion --> temporal neighbor is invalid, // we're only going to resample the initial candidates so let's set that as // the output right away // // We're also 'disabling' temporal accumulation if the renderer's random is frozen otherwise // very strong correlations will creep up, corrupt the render and potentially invalidate // performance measurements (which we're probably trying to measure since we froze the random) return temporal_neighbor_pixel_index_and_pos; out_temporal_neighbor_reservoir = render_data.render_settings.restir_di_settings.temporal_pass.input_reservoirs[temporal_neighbor_pixel_index_and_pos.x]; if (out_temporal_neighbor_reservoir.M == 0) // No temporal neighbor return temporal_neighbor_pixel_index_and_pos; // Reading from the previous g-buffer or not depending on whether or not the prev g-buffer is available // (it may not be if we're accumulating because then, it's useless since there is no motion) out_temporal_neighbor_surface = get_pixel_surface(render_data, temporal_neighbor_pixel_index_and_pos.x, render_data.render_settings.use_prev_frame_g_buffer(), random_number_generator); if (out_temporal_neighbor_surface.material.is_emissive()) // Can't resample the temporal neighbor if it's emissive return temporal_neighbor_pixel_index_and_pos; return temporal_neighbor_pixel_index_and_pos; } /** * Counts how many neighbors are eligible for reuse. * This is needed for proper normalization by pairwise MIS weights. * * A neighbor is not eligible if it is outside of the viewport or if * it doesn't satisfy the normal/plane/roughness heuristics * * 'out_valid_neighbor_M_sum' is the sum of the M values (confidences) of the * valid neighbors. Used by confidence-weights pairwise MIS weights * * The bits of 'out_neighbor_heuristics_cache' are 1 or 0 depending on whether or not * the corresponding neighbor was valid or not (can be reused later to avoid having to * re-evauate the heuristics). Neighbor 0 is LSB. */ HIPRT_HOST_DEVICE HIPRT_INLINE void count_valid_spatiotemporal_neighbors(const HIPRTRenderData& render_data, const ReSTIRSurface& center_pixel_surface, int center_pixel_index, int2 temporal_neighbor_position, int& out_valid_neighbor_count, int& out_valid_neighbor_M_sum, int& out_neighbor_heuristics_cache) { int reused_neighbors_count = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data).reuse_neighbor_count; // The RNG for generating the neighbors. It's important to use the same RNG here as the one used in the main for-loop // of the spatial reuse such that we count the right neighbors Xorshift32Generator spatial_neighbors_rng(render_data.render_settings.restir_di_settings.common_spatial_pass.spatial_neighbors_rng_seed); out_valid_neighbor_count = 0; for (int neighbor_index = 0; neighbor_index < reused_neighbors_count; neighbor_index++) { int neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor_index, temporal_neighbor_position, spatial_neighbors_rng); if (neighbor_pixel_index == -1) // Neighbor out of the viewport / invalid continue; if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface), render_data.render_settings.use_prev_frame_g_buffer())) continue; out_valid_neighbor_M_sum += render_data.render_settings.restir_di_settings.spatial_pass.input_reservoirs[neighbor_pixel_index].M; out_valid_neighbor_count++; out_neighbor_heuristics_cache |= (1 << neighbor_index); } } #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_DI_SpatiotemporalReuse(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_DI_SpatiotemporalReuse(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t center_pixel_index = (x + y * render_data.render_settings.render_resolution.x); if (!render_data.aux_buffers.pixel_active[center_pixel_index] || render_data.g_buffer.first_hit_prim_index[center_pixel_index] == -1) // Pixel inactive because of adaptive sampling, returning // Or also we don't have a primary hit return; // Initializing the random generator // TODO try having multiply instead of XOR again unsigned int seed = render_data.render_settings.freeze_random ? wang_hash(center_pixel_index + 1) : wang_hash(((center_pixel_index + 1) * (render_data.render_settings.sample_number + 1)) ^ render_data.random_number); Xorshift32Generator random_number_generator(seed); // Generating a unique seed per pixel that will be used to generate the spatial neighbors of that pixel if Hammersley isn't used render_data.render_settings.restir_di_settings.common_spatial_pass.spatial_neighbors_rng_seed = random_number_generator.xorshift32(); int2 center_pixel_coords = make_int2(x, y); if (render_data.render_settings.restir_di_settings.common_temporal_pass.temporal_buffer_clear_requested) // We requested a temporal buffer clear for ReSTIR DI render_data.render_settings.restir_di_settings.temporal_pass.input_reservoirs[center_pixel_index] = ReSTIRDIReservoir(); // Surface data of the center pixel ReSTIRSurface center_pixel_surface = get_pixel_surface(render_data, center_pixel_index, random_number_generator); ReSTIRDIReservoir temporal_neighbor_reservoir; ReSTIRSurface temporal_neighbor_surface; int3 temporal_neighbor_pixel_index_and_pos = load_spatiotemporal_neighbor_data(render_data, center_pixel_surface, center_pixel_index, temporal_neighbor_reservoir, temporal_neighbor_surface, random_number_generator); if ((temporal_neighbor_pixel_index_and_pos.x == -1 || temporal_neighbor_reservoir.M <= 1) && render_data.render_settings.restir_di_settings.common_spatial_pass.do_disocclusion_reuse_boost) // Increasing the number of spatial samples for disocclusions render_data.render_settings.restir_di_settings.common_spatial_pass.reuse_neighbor_count = render_data.render_settings.restir_di_settings.common_spatial_pass.disocclusion_reuse_count; setup_adaptive_directional_spatial_reuse(render_data, center_pixel_index, random_number_generator); // 'selected_neighbor' is only used with MIS-like weight // // Will keep the index of the neighbor that has been selected by resampling. int selected_neighbor = 0; int neighbor_heuristics_cache = 0; int valid_neighbors_count = 0; int valid_neighbors_M_sum = 0; count_valid_spatiotemporal_neighbors(render_data, center_pixel_surface, center_pixel_index, make_int2(temporal_neighbor_pixel_index_and_pos.y, temporal_neighbor_pixel_index_and_pos.z), valid_neighbors_count, valid_neighbors_M_sum, neighbor_heuristics_cache); if (temporal_neighbor_pixel_index_and_pos.x != -1 && temporal_neighbor_reservoir.M > 0) { // Adding the temporal neighbor to the count valid_neighbors_count++; valid_neighbors_M_sum += temporal_neighbor_reservoir.M; } ReSTIRDIReservoir spatiotemporal_output_reservoir; ReSTIRDIReservoir initial_candidates_reservoir = render_data.render_settings.restir_di_settings.initial_candidates.output_reservoirs[center_pixel_index]; ReSTIRSpatiotemporalResamplingMISWeight mis_weight_function; if (temporal_neighbor_pixel_index_and_pos.x != -1) { // Resampling the temporal neighbor if (temporal_neighbor_reservoir.M > 0) { float target_function_at_center = 0.0f; if (temporal_neighbor_reservoir.UCW > 0.0f) // Only resampling if the temporal neighbor isn't empty // // If the temporal neighbor's reservoir is empty, then we do not get // inside that if() and the target function stays at 0.0f which eliminates // most of the computations afterwards // // Matching the visibility used here with the bias correction mode for ease // of use (and because manually handling the visibility in the target // function of the temporal reuse is tricky for the user to use in // combination with other parameters and on top of that, it makes little // technical sense since our temporal neighbor is supposed to be unoccluded // (unless geometry moves around in the scene but that's another problem) target_function_at_center = ReSTIR_DI_evaluate_target_function(render_data, temporal_neighbor_reservoir.sample, center_pixel_surface, random_number_generator); #if ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_M float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(temporal_neighbor_reservoir.M); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(temporal_neighbor_reservoir.M); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_LIKE float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir.M); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_GBH float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir.UCW, temporal_neighbor_reservoir.sample, center_pixel_surface, temporal_neighbor_surface, TEMPORAL_NEIGHBOR_ID, initial_candidates_reservoir.M, temporal_neighbor_reservoir.M, center_pixel_index, make_int2(temporal_neighbor_pixel_index_and_pos.y, temporal_neighbor_pixel_index_and_pos.z), random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE bool update_mc = initial_candidates_reservoir.M > 0 && initial_candidates_reservoir.UCW > 0.0f; float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir.M, temporal_neighbor_reservoir.sample.target_function, initial_candidates_reservoir.M, initial_candidates_reservoir.sample.target_function, initial_candidates_reservoir.sample, target_function_at_center, temporal_neighbor_pixel_index_and_pos.x, valid_neighbors_count, valid_neighbors_M_sum, update_mc, /* resample canonical */ false, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO bool update_mc = initial_candidates_reservoir.M > 0 && initial_candidates_reservoir.UCW > 0.0f; float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir.M, temporal_neighbor_reservoir.sample.target_function, initial_candidates_reservoir.M, initial_candidates_reservoir.sample.target_function, initial_candidates_reservoir.sample, center_pixel_surface, target_function_at_center, temporal_neighbor_pixel_index_and_pos.x, valid_neighbors_count, valid_neighbors_M_sum, update_mc, /* resample canonical */ false, random_number_generator); #else #error "Unsupported bias correction mode" #endif // Combining as in Alg. 6 of the paper float jacobian_determinant = 1.0f; if (spatiotemporal_output_reservoir.combine_with(temporal_neighbor_reservoir, temporal_neighbor_resampling_mis_weight, target_function_at_center, jacobian_determinant, random_number_generator)) { // Only used with MIS-like weight selected_neighbor = TEMPORAL_NEIGHBOR_ID; // Using ReSTIR_DI_BiasCorrectionUseVisibility here because that's what we use in the resampling target function #if ReSTIR_DI_BiasCorrectionUseVisibility == KERNEL_OPTION_FALSE // We cannot be certain that the visibility of the temporal neighbor // chosen is exactly the same so we're clearing the unoccluded flag spatiotemporal_output_reservoir.sample.flags &= ~ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; #else // However, if we're using the visibility in the target function, then // the temporal neighobr could never have been selected unless it is // unoccluded so we can add the flag spatiotemporal_output_reservoir.sample.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; #endif } } spatiotemporal_output_reservoir.sanity_check(center_pixel_coords); } ReSTIRDIReservoir* spatial_input_reservoir_buffer = render_data.render_settings.restir_di_settings.spatial_pass.input_reservoirs; Xorshift32Generator spatial_neighbors_rng(render_data.render_settings.restir_di_settings.common_spatial_pass.spatial_neighbors_rng_seed); // Resampling the neighbors. Using neighbors + 1 here so that // we can use the last iteration of the loop to resample the *initial candidates reservoir* int reused_neighbors_count = render_data.render_settings.restir_di_settings.common_spatial_pass.reuse_neighbor_count; int start_index = 0; if (valid_neighbors_M_sum == 0) // No spatial resampling to do, only the initial candidate reservoir (potentially) // so we can directly start there start_index = reused_neighbors_count; for (int spatial_neighbor_index = start_index; spatial_neighbor_index < reused_neighbors_count + 1; spatial_neighbor_index++) { // We can already check whether or not this neighbor is going to be // accepted at all by checking the heuristic cache if (spatial_neighbor_index < reused_neighbors_count && reused_neighbors_count <= 32) // If not the center pixel, we can check the heuristics, otherwise there's no need to, // we know that the center pixel will be accepted // // Our heuristics cache is a 32bit int so we can only cache 32 values are we're // going to have issues if we try to read more than that. if ((neighbor_heuristics_cache & (1 << spatial_neighbor_index)) == 0) { // Advancing the rng for generating the spatial neighbors since if we "continue" here, the spatial neighbors rng // isn't going to be advanced by the call to 'get_spatial_neighbor_pixel_index' below so we're doing it manually spatial_neighbor_advance_rng(render_data, spatial_neighbors_rng); // Neighbor not passing the heuristics tests, skipping it right away continue; } int neighbor_pixel_index = -1; if (spatial_neighbor_index == reused_neighbors_count) // Last iteration, resampling the initial candidates neighbor_pixel_index = center_pixel_index; else // Resampling around the temporal neighbor location neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, spatial_neighbor_index, make_int2(temporal_neighbor_pixel_index_and_pos.y, temporal_neighbor_pixel_index_and_pos.z), spatial_neighbors_rng); if (neighbor_pixel_index == -1) // Neighbor out of the viewport continue; if (spatial_neighbor_index < reused_neighbors_count && reused_neighbors_count > 32) // If not the center pixel, we can check the heuristics // // Only checking the heuristic if we have more than 32 neighbors (does not fit in the heuristic cache) // If we have less than 32 neighbors, we've already checked the cache at the beginning of this for loop if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface), render_data.render_settings.use_prev_frame_g_buffer())) { continue; } // Neighbor surface needed for roughness m-capping and jacobian determinant ReSTIRDIReservoir neighbor_reservoir; if (spatial_neighbor_index == reused_neighbors_count) // Last iteration, resampling the initial candidates neighbor_reservoir = initial_candidates_reservoir; else neighbor_reservoir = spatial_input_reservoir_buffer[neighbor_pixel_index]; float target_function_at_center = 0.0f; bool do_neighbor_target_function_visibility = do_include_spatial_visibility_term_or_not(render_data, spatial_neighbor_index); if (neighbor_reservoir.UCW > 0.0f) { if (spatial_neighbor_index == reused_neighbors_count) // No need to evaluate the center sample at the center pixel, that's exactly // the target function of the center reservoir target_function_at_center = neighbor_reservoir.sample.target_function; else if (do_neighbor_target_function_visibility) target_function_at_center = ReSTIR_DI_evaluate_target_function(render_data, neighbor_reservoir.sample, center_pixel_surface, random_number_generator); else target_function_at_center = ReSTIR_DI_evaluate_target_function(render_data, neighbor_reservoir.sample, center_pixel_surface, random_number_generator); } ReSTIRSurface neighbor_surface = get_pixel_surface(render_data, neighbor_pixel_index, render_data.render_settings.use_prev_frame_g_buffer(), random_number_generator); #if ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_M float mis_weight = mis_weight_function.get_resampling_MIS_weight(neighbor_reservoir.M); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z float mis_weight = mis_weight_function.get_resampling_MIS_weight(neighbor_reservoir.M); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_LIKE float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.M); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_GBH // Using 'spatial_neighbor_index + 1' in this function call because the index // 0 is for the temporal neighbor so we start at 1 by using '+ 1' float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.UCW, neighbor_reservoir.sample, center_pixel_surface, temporal_neighbor_surface, spatial_neighbor_index + 1, initial_candidates_reservoir.M, temporal_neighbor_reservoir.M, center_pixel_index, make_int2(temporal_neighbor_pixel_index_and_pos.y, temporal_neighbor_pixel_index_and_pos.z), random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE bool update_mc = initial_candidates_reservoir.M > 0 && initial_candidates_reservoir.UCW > 0.0f; float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.M, neighbor_reservoir.sample.target_function, initial_candidates_reservoir.M, initial_candidates_reservoir.sample.target_function, initial_candidates_reservoir.sample, target_function_at_center, neighbor_pixel_index, valid_neighbors_count, valid_neighbors_M_sum, update_mc, spatial_neighbor_index == reused_neighbors_count, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO bool update_mc = initial_candidates_reservoir.M > 0 && initial_candidates_reservoir.UCW > 0.0f; float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.M, neighbor_reservoir.sample.target_function, initial_candidates_reservoir.M, initial_candidates_reservoir.sample.target_function, initial_candidates_reservoir.sample, center_pixel_surface, target_function_at_center, neighbor_pixel_index, valid_neighbors_count, valid_neighbors_M_sum, update_mc, spatial_neighbor_index == reused_neighbors_count, random_number_generator); #else #error "Unsupported bias correction mode" #endif // Combining as in Alg. 6 of the paper if (spatiotemporal_output_reservoir.combine_with(neighbor_reservoir, mis_weight, target_function_at_center, 1.0f, random_number_generator)) { // Only used with MIS-like weight // // + 1 here because we've already resampled the temporal neighbor so we need to account for that selected_neighbor = spatial_neighbor_index + 1; if (do_neighbor_target_function_visibility) // If we resampled the neighbor with visibility, then we are sure that we can set the flag spatiotemporal_output_reservoir.sample.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; else { // If we didn't resample the neighbor with visibility if (spatial_neighbor_index == reused_neighbors_count) // If we just resampled the center pixel, then we can copy the visibility flag spatiotemporal_output_reservoir.sample.flags |= neighbor_reservoir.sample.flags & ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; else // This was not the center pixel, we cannot be certain what the visibility at the center // pixel of the neighbor sample we just resample is so we're clearing the bit spatiotemporal_output_reservoir.sample.flags &= ~ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; } } spatiotemporal_output_reservoir.sanity_check(center_pixel_coords); } float normalization_numerator = 1.0f; float normalization_denominator = 1.0f; ReSTIRSpatiotemporalNormalizationWeight normalization_function; #if ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_M normalization_function.get_normalization(render_data, spatiotemporal_output_reservoir.weight_sum, initial_candidates_reservoir.M, center_pixel_surface, temporal_neighbor_reservoir.M, center_pixel_index, make_int2(temporal_neighbor_pixel_index_and_pos.y, temporal_neighbor_pixel_index_and_pos.z), normalization_numerator, normalization_denominator, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z normalization_function.get_normalization(render_data, spatiotemporal_output_reservoir.sample, spatiotemporal_output_reservoir.weight_sum, center_pixel_surface, temporal_neighbor_surface, initial_candidates_reservoir.M, temporal_neighbor_reservoir.M, center_pixel_index, make_int2(temporal_neighbor_pixel_index_and_pos.y, temporal_neighbor_pixel_index_and_pos.z), normalization_numerator, normalization_denominator, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_LIKE normalization_function.get_normalization(render_data, spatiotemporal_output_reservoir.sample, spatiotemporal_output_reservoir.weight_sum, center_pixel_surface, temporal_neighbor_surface, selected_neighbor, initial_candidates_reservoir.M, temporal_neighbor_reservoir.M, center_pixel_index, make_int2(temporal_neighbor_pixel_index_and_pos.y, temporal_neighbor_pixel_index_and_pos.z), normalization_numerator, normalization_denominator, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_GBH normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO normalization_function.get_normalization(normalization_numerator, normalization_denominator); #else #error "Unsupported bias correction mode" #endif spatiotemporal_output_reservoir.end_with_normalization(normalization_numerator, normalization_denominator); spatiotemporal_output_reservoir.sanity_check(center_pixel_coords); // Only these 3 weighting schemes are affected #if (ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z \ || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS \ || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE \ || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO \ || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO) \ && ReSTIR_DI_BiasCorrectionUseVisibility == KERNEL_OPTION_TRUE \ && (ReSTIR_DI_DoVisibilityReuse == KERNEL_OPTION_TRUE || (ReSTIR_DI_InitialTargetFunctionVisibility == KERNEL_OPTION_TRUE && ReSTIR_DI_SpatialTargetFunctionVisibility == KERNEL_OPTION_TRUE)) // Why is this needed? // // Picture the case where we have visibility reuse (at the end of the initial candidates sampling pass), // visibility term in the bias correction target function (when counting the neighbors that could // have produced the picked sample) and 2 spatial reuse passes. // // The first spatial reuse pass reuses from samples that were produced with visibility in mind // (because of the visibility reuse pass that discards occluded samples). This means that we need // the visibility in the target function used when counting the neighbors that could have produced // the picked sample otherwise we may think that our neighbor could have produced the picked // sample where actually it couldn't because the sample is occluded at the neighbor. We would // then have a Z denominator (with 1/Z weights) that is too large and we'll end up with darkening. // // Now at the end of the first spatial reuse pass, the center pixel ends up with a sample that may // or may not be occluded from the center's pixel point of view. We didn't include the visibility // in the target function when resampling the neighbors (only when counting the "correct" neighbors // but that's all) so we are not giving a 0 weight to occluded resampled neighbors --> it is possible // that we picked an occluded sample. // // In the second spatial reuse pass, we are now going to resample from our neighbors and get some // samples that were not generated with occlusion in mind (because the resampling target function of // the first spatial reuse doesn't include visibility). Yet, we are going to weight them with occlusion // in mind. This means that we are probably going to discard samples because of occlusion that could // have been generated because they are generated without occlusion test. We end up discarding too many // samples --> brightening bias. // // With the visibility reuse at the end of each spatial pass, we force samples at the end of each // spatial reuse to take visibility into account so that when we weight them with visibility testing, // everything goes well // // As an optimization, we also do this for the pairwise MIS because pairwise MIS evaluates the target function // of reservoirs at their own location. Doing the visibility reuse here ensures that a reservoir sample at its own location // includes visibility and so we do not need to recompute the target function of the neighbors in this case. We can just // reuse the target function stored in the reservoir // // We also give the user the choice to remove bias using this option or not as it introduces very little bias // in practice (but noticeable when switching back and forth between reference image/biased image) // // We only need this if we're going to temporally reuse (because then the output of the spatial reuse must be correct // for the temporal reuse pass) or if we have multiple spatial reuse passes and this is not the last spatial pass if (render_data.render_settings.restir_di_settings.common_temporal_pass.do_temporal_reuse_pass || render_data.render_settings.restir_di_settings.common_spatial_pass.number_of_passes - 1 != render_data.render_settings.restir_di_settings.common_spatial_pass.spatial_pass_index) ReSTIR_DI_visibility_test_kill_reservoir(render_data, spatiotemporal_output_reservoir, center_pixel_surface.shading_point, center_pixel_surface.primitive_index, random_number_generator); #endif // M-capping so that we don't have to M-cap when reading reservoirs on the next frame if (render_data.render_settings.restir_di_settings.m_cap > 0) // M-capping the temporal neighbor if an M-cap has been given spatiotemporal_output_reservoir.M = hippt::min(spatiotemporal_output_reservoir.M, render_data.render_settings.restir_di_settings.m_cap); render_data.render_settings.restir_di_settings.spatial_pass.output_reservoirs[center_pixel_index] = spatiotemporal_output_reservoir; } #endif ================================================ FILE: src/Device/kernels/ReSTIR/DI/InitialCandidates.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_RESTIR_DI_INITIAL_CANDIDATES_H #define KERNELS_RESTIR_DI_INITIAL_CANDIDATES_H #include "Device/includes/Dispatcher.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/ReSTIR/Utils.h" #include "Device/includes/ReSTIR/DI/PresampledLight.h" #include "Device/includes/ReSTIR/DI/TargetFunction.h" #include "HostDeviceCommon/HIPRTCamera.h" #include "HostDeviceCommon/Math.h" #include "HostDeviceCommon/RenderData.h" #include "HostDeviceCommon/KernelOptions/ReSTIRDIOptions.h" #define LIGHT_DOESNT_CONTRIBUTE_ENOUGH -42.0f /** * Reference: https://en.wikipedia.org/wiki/Pairing_function */ HIPRT_HOST_DEVICE HIPRT_INLINE int cantor_pairing_function(int x, int y) { return (x + y + 1) * (x + y) / 2 + y; } HIPRT_HOST_DEVICE HIPRT_INLINE ReSTIRDISample use_presampled_light_candidate(const HIPRTRenderData& render_data, const int2& pixel_coords, const float3& evaluated_point, const float3& shading_normal, ColorRGB32F& out_sample_radiance, float& out_sample_cosine_term, float& out_sample_pdf, float& out_distance_to_light, float3& out_to_light_direction, Xorshift32Generator& random_number_generator) { const ReSTIRDILightPresamplingSettings& light_presampling_settings = render_data.render_settings.restir_di_settings.light_presampling; // We want all threads in a block of light_presampling_settings.tile_size * light_presampling_settings.tile_size // pixels to sample from the same random subset of lights. // We compute a unique number per each light_presampling_settings.tile_size * light_presampling_settings.tile_size // tile of pixels and use that unique number as seed for our random number generator int tile_index_seed = cantor_pairing_function(pixel_coords.x / light_presampling_settings.tile_size, pixel_coords.y / light_presampling_settings.tile_size); Xorshift32Generator subset_rng(render_data.random_number * (tile_index_seed + 1)); int random_subset_index = subset_rng.random_index(light_presampling_settings.number_of_subsets); int random_light_index_in_subset = random_number_generator.random_index(light_presampling_settings.subset_size); int light_sample_index = random_subset_index * light_presampling_settings.subset_size + random_light_index_in_subset; ReSTIRDIPresampledLight presampled_light_sample = light_presampling_settings.light_samples[light_sample_index]; ReSTIRDISample light_sample; light_sample.emissive_triangle_index = presampled_light_sample.emissive_triangle_index; light_sample.point_on_light_source = presampled_light_sample.point_on_light_source; light_sample.flags = presampled_light_sample.flags; out_sample_radiance = presampled_light_sample.radiance; out_sample_pdf = presampled_light_sample.pdf; if (light_sample.is_envmap_sample()) { out_to_light_direction = matrix_X_vec(render_data.world_settings.envmap_to_world_matrix, light_sample.point_on_light_source); out_distance_to_light = 1.0e35f; } else { out_to_light_direction = light_sample.point_on_light_source - evaluated_point; out_to_light_direction = out_to_light_direction / (out_distance_to_light = hippt::length(out_to_light_direction)); // Normalization } out_sample_cosine_term = hippt::dot(shading_normal, out_to_light_direction); if (!light_sample.is_envmap_sample()) { bool contributes_enough = check_minimum_light_contribution(render_data.render_settings.minimum_light_contribution, out_sample_radiance * out_sample_cosine_term / out_sample_pdf); if (!contributes_enough) { // Early check that the light contributes enough to the point, and if it doesn't, skip that light sample // Setting it to LIGHT_DOESNT_CONTRIBUTE_ENOUGH so that we know that the sample is invalid when the caller of this // function will look at the target function's value light_sample.target_function = LIGHT_DOESNT_CONTRIBUTE_ENOUGH; return light_sample; } } return light_sample; } HIPRT_HOST_DEVICE HIPRT_INLINE ReSTIRDISample sample_fresh_light_candidate(const HIPRTRenderData& render_data, float envmap_candidate_probability, const float3& view_direction, const HitInfo& closest_hit_info, RayPayload& ray_payload, ColorRGB32F& out_sample_radiance, float& out_sample_cosine_term, float& out_sample_pdf, Xorshift32Generator& random_number_generator) { ReSTIRDISample light_sample; float3 evaluated_point = closest_hit_info.inter_point; if (random_number_generator() > envmap_candidate_probability) { // Light sample LightSampleInformation light_sample_info = sample_one_emissive_triangle(render_data, closest_hit_info.inter_point, view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, closest_hit_info.primitive_index, ray_payload, random_number_generator); light_sample.emissive_triangle_index = light_sample_info.emissive_triangle_index; light_sample.point_on_light_source = light_sample_info.point_on_light; out_sample_pdf = light_sample_info.area_measure_pdf; if (out_sample_pdf > 0.0f) { // It can happen that the light PDF returned by the emissive triangle // sampling function is 0 because of emissive triangles that are so // small that we cannot compute their normal and their area (the cross // product of their edges gives a quasi-null vector --> length of 0.0f --> area of 0) float distance_to_light; float3 to_light_direction = light_sample.point_on_light_source - evaluated_point; to_light_direction = to_light_direction / (distance_to_light = hippt::length(to_light_direction)); // Normalization out_sample_cosine_term = hippt::max(0.0f, hippt::dot(closest_hit_info.shading_normal, to_light_direction)); float cosine_at_light_source = compute_cosine_term_at_light_source(light_sample_info.light_source_normal, -to_light_direction); bool contributes_enough = check_minimum_light_contribution(render_data.render_settings.minimum_light_contribution, light_sample_info.emission * out_sample_cosine_term / out_sample_pdf); if (!contributes_enough) { // Early check that the light contributes enough to the point, and if it doesn't, skip that light sample // Setting it to LIGHT_DOESNT_CONTRIBUTE_ENOUGH so that we know that the sample is invalid when the caller of this // function will look at the target function's value light_sample.target_function = LIGHT_DOESNT_CONTRIBUTE_ENOUGH; return light_sample; } // Accounting for the probability of sampling a light, not the envmap // (which has probability 'envmap_candidate_probability') out_sample_pdf *= (1.0f - envmap_candidate_probability); out_sample_radiance = light_sample_info.emission; } } else { // Envmap sample float3 envmap_sampled_direction; out_sample_radiance = envmap_sample(render_data.world_settings, envmap_sampled_direction, out_sample_pdf, random_number_generator); out_sample_cosine_term = hippt::max(0.0f, hippt::dot(envmap_sampled_direction, closest_hit_info.shading_normal)); bool contributes_enough = check_minimum_light_contribution(render_data.render_settings.minimum_light_contribution, out_sample_radiance * out_sample_cosine_term / out_sample_pdf); if (!contributes_enough) { // Early check that the envmap sample contributes enough to the point, and if it doesn't, skip it // Setting it to LIGHT_DOESNT_CONTRIBUTE_ENOUGH so that we know that the sample is invalid when the caller of this // function will look at the target function's value light_sample.target_function = LIGHT_DOESNT_CONTRIBUTE_ENOUGH; return light_sample; } // Taking into account the fact that we only have a 1 in 'envmap_candidate_probability' chance to sample // the envmap out_sample_pdf *= envmap_candidate_probability; light_sample.emissive_triangle_index = -1; // Storing in envmap space light_sample.point_on_light_source = matrix_X_vec(render_data.world_settings.world_to_envmap_matrix, envmap_sampled_direction); light_sample.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_ENVMAP_SAMPLE; } return light_sample; } HIPRT_HOST_DEVICE HIPRT_INLINE void sample_light_candidates(const HIPRTRenderData& render_data, const HitInfo& closest_hit_info, RayPayload& ray_payload, ReSTIRDIReservoir& reservoir, int nb_light_candidates, int nb_bsdf_candidates, float envmap_candidate_probability, const float3& view_direction, Xorshift32Generator& random_number_generator, const int2& pixel_coords) { for (int i = 0; i < nb_light_candidates; i++) { ColorRGB32F sample_radiance; float sample_cosine_term = 0.0f; float light_pdf_area_measure = 0.0f; float distance_to_light = 0.0f; float3 to_light_direction{ 0.0f, 0.0f, 0.0f }; #if ReSTIR_DI_DoLightPresampling == KERNEL_OPTION_TRUE ReSTIRDISample light_sample = use_presampled_light_candidate(render_data, pixel_coords, closest_hit_info.inter_point, closest_hit_info.shading_normal, sample_radiance, sample_cosine_term, light_pdf_area_measure, distance_to_light, to_light_direction, random_number_generator); #else ReSTIRDISample light_sample = sample_fresh_light_candidate(render_data, envmap_candidate_probability, view_direction, closest_hit_info, ray_payload, sample_radiance, sample_cosine_term, light_pdf_area_measure, random_number_generator); if (light_sample.is_envmap_sample()) { to_light_direction = matrix_X_vec(render_data.world_settings.envmap_to_world_matrix, light_sample.point_on_light_source); distance_to_light = 1.0e35f; } else { to_light_direction = light_sample.point_on_light_source - closest_hit_info.inter_point; to_light_direction = to_light_direction / (distance_to_light = hippt::length(to_light_direction)); // Normalization } #endif if (light_sample.target_function == LIGHT_DOESNT_CONTRIBUTE_ENOUGH) continue; float candidate_weight = 0.0f; if (sample_cosine_term > 0.0f && light_pdf_area_measure > 0.0f) { float bsdf_pdf_solid_angle; BSDFIncidentLightInfo incident_light_info; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, to_light_direction, incident_light_info, ray_payload.volume_state, false, ray_payload.material, ray_payload.bounce, ray_payload.accumulated_roughness); ColorRGB32F bsdf_color = bsdf_dispatcher_eval(render_data, bsdf_context, bsdf_pdf_solid_angle, random_number_generator); // Filling a surface to give to 'ReSTIR_DI_evaluate_target_function' ReSTIRSurface surface; surface.geometric_normal = closest_hit_info.geometric_normal; surface.primitive_index = closest_hit_info.primitive_index; surface.material = ray_payload.material; surface.ray_volume_state = ray_payload.volume_state; surface.shading_normal = closest_hit_info.shading_normal; surface.shading_point = closest_hit_info.inter_point; surface.view_direction = view_direction; float target_function = ReSTIR_DI_evaluate_target_function(render_data, light_sample, surface, random_number_generator); if (bsdf_pdf_solid_angle <= 0.0f || !check_minimum_light_contribution(render_data.render_settings.minimum_light_contribution, target_function / light_pdf_area_measure / bsdf_pdf_solid_angle)) target_function = 0.0f; else { float light_pdf_solid_angle; if (light_sample.is_envmap_sample()) // For envmap sample, the PDF is already in solid angle light_pdf_solid_angle = light_pdf_area_measure; else { float3 light_normal = get_triangle_normal_not_normalized(render_data, light_sample.emissive_triangle_index); float normal_length = hippt::length(light_normal); float light_area = normal_length * 0.5f; light_normal /= normal_length; // Converting from area measure to solid angle measure so that we use the balance heuristic we the same measure PDFs // (same measure for the BSDF PDF and the light PDF) // // Removing the envmap proba to avoid double counting it below in light_pdf_solid_angle = area_to_solid_angle_pdf(light_pdf_area_measure / (1.0f - envmap_candidate_probability), distance_to_light, compute_cosine_term_at_light_source(light_normal, -to_light_direction)); light_pdf_solid_angle *= (1.0f - envmap_candidate_probability); } float mis_weight = balance_heuristic(light_pdf_solid_angle, nb_light_candidates, bsdf_pdf_solid_angle, nb_bsdf_candidates); candidate_weight = mis_weight * target_function / light_pdf_area_measure; sanity_check(render_data, ColorRGB32F(candidate_weight), 0, 0); light_sample.target_function = target_function; } } #if ReSTIR_DI_InitialTargetFunctionVisibility == KERNEL_OPTION_TRUE if (!render_data.render_settings.do_render_low_resolution() && light_sample.target_function > 0.0f) { // Only doing visiblity if we're render at low resolution // (meaning we're moving the camera) for better movement framerates // Also, only testing visibility if we got a valid sample hiprtRay shadow_ray; shadow_ray.origin = closest_hit_info.inter_point; shadow_ray.direction = to_light_direction; bool visible = !evaluate_shadow_ray_occluded(render_data, shadow_ray, distance_to_light, closest_hit_info.primitive_index, /* bounce. Always 0 for ReSTIR DI*/ 0, random_number_generator); if (!visible) { // Sample occluded, it is not going to be resampled anyways because it is // going to have a 0 contribution so we just take it into account in the // reservoir (because even if it has zero-contribution, this is still a resampled sample) reservoir.M++; // And we go onto the next sample continue; } // We are now sure that if the sample survived, it is unoccluded light_sample.flags |= RESTIR_DI_FLAGS_UNOCCLUDED; } #endif reservoir.add_one_candidate(light_sample, candidate_weight, random_number_generator); reservoir.sanity_check(make_int2(-1, -1)); } } HIPRT_HOST_DEVICE HIPRT_INLINE void sample_bsdf_candidates(const HIPRTRenderData& render_data, const HitInfo& closest_hit_info, RayPayload& ray_payload, ReSTIRDIReservoir& reservoir, int nb_light_candidates, int nb_bsdf_candidates, float envmap_candidate_probability, const float3& view_direction, Xorshift32Generator& random_number_generator) { // Sampling the BSDF candidates for (int i = 0; i < nb_bsdf_candidates; i++) { float bsdf_sample_pdf_solid_angle = 0.0f; float3 bsdf_sampled_direction; BSDFIncidentLightInfo sampled_lobe_info; BSDFContext bsdf_context(view_direction, closest_hit_info.shading_normal, closest_hit_info.geometric_normal, make_float3(0.0f, 0.0f, 0.0f), sampled_lobe_info, ray_payload.volume_state, false, ray_payload.material, /* bounce */ 0, ray_payload.accumulated_roughness); ColorRGB32F bsdf_color = bsdf_dispatcher_sample(render_data, bsdf_context, bsdf_sampled_direction, bsdf_sample_pdf_solid_angle, random_number_generator); if (bsdf_sample_pdf_solid_angle > 0.0f) { hiprtRay bsdf_ray; bsdf_ray.origin = closest_hit_info.inter_point; bsdf_ray.direction = bsdf_sampled_direction; BSDFLightSampleRayHitInfo shadow_light_ray_hit_info; bool hit_found = evaluate_bsdf_light_sample_ray(render_data, bsdf_ray, 1.0e35f, shadow_light_ray_hit_info, closest_hit_info.primitive_index, /* bounce. Always 0 for ReSTIR */ 0, random_number_generator); if (hit_found && !shadow_light_ray_hit_info.hit_emission.is_black()) { // If we intersected an emissive material, compute the weight. // Otherwise, the weight is 0 because of the emision being 0 so we just don't compute it // Filling a surface to give to 'ReSTIR_DI_evaluate_target_function' ReSTIRSurface surface; surface.geometric_normal = closest_hit_info.geometric_normal; surface.primitive_index = closest_hit_info.primitive_index; surface.material = ray_payload.material; surface.ray_volume_state = ray_payload.volume_state; surface.shading_normal = closest_hit_info.shading_normal; surface.shading_point = closest_hit_info.inter_point; surface.view_direction = view_direction; ReSTIRDISample bsdf_RIS_sample; bsdf_RIS_sample.emissive_triangle_index = shadow_light_ray_hit_info.hit_prim_index; bsdf_RIS_sample.point_on_light_source = bsdf_ray.origin + bsdf_ray.direction * shadow_light_ray_hit_info.hit_distance; bsdf_RIS_sample.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; bsdf_RIS_sample.flags |= ReSTIRDISample::flags_from_BSDF_incident_light_info(sampled_lobe_info); bsdf_RIS_sample.target_function = ReSTIR_DI_evaluate_target_function(render_data, bsdf_RIS_sample, surface, random_number_generator); float light_pdf_solid_angle = 0.0f; bool refraction_sampled = hippt::dot(bsdf_sampled_direction, closest_hit_info.shading_normal) < 0.0f; if (!refraction_sampled) { // TODO we should just allow refraction light samples instead of this // Only computing the light PDF if we're not refracting // // Why? // // Because right now, we allow sampling BSDF refractions. This means that we can sample a light // that is inside an object with a *BSDF sample*. However, a *light sample* to the same light cannot // be sampled because there's is going to be the surface of the object we're currently on in-between. // Basically, we are not allowing light sample refractions and so they should have a MIS weight of 0 which // is what we're doing here: the pdf of a *light sample* that refracts through a surface is 0. // // If not doing that, we're going to have bad MIS weights that don't sum up to 1 // (because the BSDF sample, that should have weight 1 [or to be precise: 1 / nb_bsdf_samples] // will have weight 1 / (1 + nb_light_samples) [or to be precise: 1 / (nb_bsdf_samples + nb_light_samples)] // and this is going to cause darkening as the number of light samples grows) #if ReSTIR_DI_DoLightPresampling == KERNEL_OPTION_TRUE light_pdf_solid_angle = pdf_of_emissive_triangle_hit_solid_angle(render_data, shadow_light_ray_hit_info, bsdf_sampled_direction); #else light_pdf_solid_angle = pdf_of_emissive_triangle_hit_solid_angle(render_data, shadow_light_ray_hit_info, bsdf_sampled_direction); #endif } float target_function = bsdf_RIS_sample.target_function; if (bsdf_sample_pdf_solid_angle <= 0.0f || !check_minimum_light_contribution(render_data.render_settings.minimum_light_contribution, target_function / light_pdf_solid_angle / bsdf_sample_pdf_solid_angle)) continue; // Our light sampler is only chosen with probability '1.0f - envmap_candidate_probability' // so we multiply that here to take that into account light_pdf_solid_angle *= (1.0f - envmap_candidate_probability); float mis_weight = balance_heuristic(bsdf_sample_pdf_solid_angle, nb_bsdf_candidates, light_pdf_solid_angle, nb_light_candidates); float bsdf_sample_pdf_area_measure = bsdf_sample_pdf_solid_angle; bsdf_sample_pdf_area_measure /= (shadow_light_ray_hit_info.hit_distance * shadow_light_ray_hit_info.hit_distance); bsdf_sample_pdf_area_measure *= compute_cosine_term_at_light_source(shadow_light_ray_hit_info.hit_geometric_normal, -bsdf_sampled_direction); float candidate_weight = mis_weight * target_function / bsdf_sample_pdf_area_measure; reservoir.add_one_candidate(bsdf_RIS_sample, candidate_weight, random_number_generator); reservoir.sanity_check(make_int2(-1, -1)); } else if (!hit_found && render_data.world_settings.ambient_light_type == AmbientLightType::ENVMAP) { // Envmap hit, this becomes an envmap sample // Not allowing refraction envmap samples here // TODO fixthis, we should allow them if (hippt::dot(closest_hit_info.shading_normal, bsdf_sampled_direction) > 0.0f) { float envmap_pdf; ColorRGB32F envmap_radiance = envmap_eval(render_data, bsdf_sampled_direction, envmap_pdf); // Filling a surface to give to 'ReSTIR_DI_evaluate_target_function' ReSTIRSurface surface; surface.geometric_normal = closest_hit_info.geometric_normal; surface.primitive_index = closest_hit_info.primitive_index; surface.material = ray_payload.material; surface.ray_volume_state = ray_payload.volume_state; surface.shading_normal = closest_hit_info.shading_normal; surface.shading_point = closest_hit_info.inter_point; surface.view_direction = view_direction; ReSTIRDISample bsdf_RIS_sample; bsdf_RIS_sample.emissive_triangle_index = -1; // Storing in envmap space bsdf_RIS_sample.point_on_light_source = matrix_X_vec(render_data.world_settings.world_to_envmap_matrix, bsdf_sampled_direction); bsdf_RIS_sample.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; bsdf_RIS_sample.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_ENVMAP_SAMPLE; bsdf_RIS_sample.flags |= ReSTIRDISample::flags_from_BSDF_incident_light_info(sampled_lobe_info); bsdf_RIS_sample.target_function = ReSTIR_DI_evaluate_target_function(render_data, bsdf_RIS_sample, surface, random_number_generator); float target_function = bsdf_RIS_sample.target_function; // Not taking the light sampling PDF into account in the balance heuristic because a envmap hit // (not a light surface hit) can never be sampled by a light-surface sampler and so the PDF // of the current envmap sample is always 0 for a light sampler. if (bsdf_sample_pdf_solid_angle <= 0.0f || !check_minimum_light_contribution(render_data.render_settings.minimum_light_contribution, target_function / bsdf_sample_pdf_solid_angle)) continue; // We're evaluating the probability of choosing that BSDF-sample direction with the envmap sampler. // Because our envmap sampler is chosen only with probability 'envmap_candidate_probability', we multiply // that here to account for that envmap_pdf *= envmap_candidate_probability; float mis_weight = balance_heuristic(bsdf_sample_pdf_solid_angle, nb_bsdf_candidates, envmap_pdf, nb_light_candidates); float candidate_weight = mis_weight * target_function / bsdf_sample_pdf_solid_angle; reservoir.add_one_candidate(bsdf_RIS_sample, candidate_weight, random_number_generator); reservoir.sanity_check(make_int2(-1, -1)); } } } } } HIPRT_HOST_DEVICE HIPRT_INLINE ReSTIRDIReservoir sample_initial_candidates(const HIPRTRenderData& render_data, const int2& pixel_coords, RayPayload& ray_payload, const HitInfo closest_hit_info, const float3& view_direction, Xorshift32Generator& random_number_generator) { // If we're rendering at low resolution, only doing 1 candidate of each // for better interactive framerates int initial_nb_light_cand = render_data.render_settings.restir_di_settings.initial_candidates.number_of_initial_light_candidates; int initial_nb_bsdf_cand = render_data.render_settings.restir_di_settings.initial_candidates.number_of_initial_bsdf_candidates; int nb_light_candidates = render_data.render_settings.do_render_low_resolution() ? hippt::min(1, initial_nb_light_cand) : initial_nb_light_cand; int nb_bsdf_candidates = render_data.render_settings.do_render_low_resolution() ? hippt::min(1, initial_nb_bsdf_cand) : initial_nb_bsdf_cand; float envmap_candidate_probability = 0.0f; if (render_data.world_settings.ambient_light_type == AmbientLightType::ENVMAP) { if (render_data.buffers.emissive_triangles_count == 0) // Only the envmap to sample envmap_candidate_probability = 1.0f; else envmap_candidate_probability = render_data.render_settings.restir_di_settings.initial_candidates.envmap_candidate_probability; } // Sampling candidates with weighted reservoir sampling ReSTIRDIReservoir reservoir; sample_light_candidates(render_data, closest_hit_info, ray_payload, reservoir, nb_light_candidates, nb_bsdf_candidates, envmap_candidate_probability, view_direction, random_number_generator, pixel_coords); sample_bsdf_candidates(render_data, closest_hit_info, ray_payload, reservoir, nb_light_candidates, nb_bsdf_candidates, envmap_candidate_probability, view_direction, random_number_generator); reservoir.end(); reservoir.sanity_check(pixel_coords); // There's no need to keep M > 1 here, if you have 4 light candidates and 1 BSDF candidates, that's 5 samples. // But if you divide everyone by 5, everything stays correct. That allows manipulating the M-cap without having // to take the number of initial candidates into account reservoir.M = 1; return reservoir; } #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_DI_InitialCandidates(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_DI_InitialCandidates(HIPRTRenderData render_data, int x, int y) #endif { if (render_data.buffers.emissive_triangles_count == 0 && render_data.world_settings.ambient_light_type != AmbientLightType::ENVMAP) // No initial candidates to sample since no lights return; #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t pixel_index = (x + y * render_data.render_settings.render_resolution.x); DevicePackedEffectiveMaterial material = render_data.g_buffer.materials[pixel_index]; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(pixel_index + 1); else seed = wang_hash((pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); if (!render_data.aux_buffers.pixel_active[pixel_index] || render_data.g_buffer.first_hit_prim_index[pixel_index] == -1) // Pixel inactive because of adaptive sampling, returning // Or also we don't have a primary hit return; HitInfo hit_info; hit_info.geometric_normal = render_data.g_buffer.geometric_normals[pixel_index].unpack(); hit_info.shading_normal = render_data.g_buffer.shading_normals[pixel_index].unpack(); hit_info.inter_point = render_data.g_buffer.primary_hit_position[pixel_index]; hit_info.primitive_index = render_data.g_buffer.first_hit_prim_index[pixel_index]; RayPayload ray_payload; ray_payload.material = material.unpack(); // Because this is the camera hit (and assuming the camera isn't inside volumes for now), // the ray volume state after the camera hit is just an empty interior stack but with // the material index that we hit pushed onto the stack. That's it. Because it is that // simple, we don't have the ray volume state in the GBuffer but rather we can // reconstruct the ray volume state on the fly ray_payload.volume_state.reconstruct_first_hit( ray_payload.material, render_data.buffers.material_indices, render_data.g_buffer.first_hit_prim_index[pixel_index], random_number_generator); float3 view_direction = render_data.g_buffer.get_view_direction(render_data.current_camera.position, pixel_index); // Producing and storing the reservoir ReSTIRDIReservoir initial_candidates_reservoir = sample_initial_candidates(render_data, make_int2(x, y), ray_payload, hit_info, view_direction, random_number_generator); #if ReSTIR_DI_DoVisibilityReuse == KERNEL_OPTION_TRUE ReSTIR_DI_visibility_test_kill_reservoir(render_data, initial_candidates_reservoir, hit_info.inter_point, hit_info.primitive_index, random_number_generator); #endif render_data.render_settings.restir_di_settings.initial_candidates.output_reservoirs[pixel_index] = initial_candidates_reservoir; } #endif ================================================ FILE: src/Device/kernels/ReSTIR/DI/LightsPresampling.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_RESTIR_DI_LIGHTS_PRESAMPLING_H #define KERNELS_RESTIR_DI_LIGHTS_PRESAMPLING_H #include "Device/includes/LightSampling/Envmap.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/kernel_parameters/ReSTIR/DI/LightPresamplingParameters.h" #include "HostDeviceCommon/RenderData.h" /** References: * * [1] [Rearchitecting Spatiotemporal Resampling for Production] https://research.nvidia.com/publication/2021-07_rearchitecting-spatiotemporal-resampling-production */ HIPRT_HOST_DEVICE HIPRT_INLINE ReSTIRDIPresampledLight presample_envmap(const WorldSettings& world_settings, float envmap_sampling_probability, Xorshift32Generator& random_number_generator) { ReSTIRDIPresampledLight presampled_envmap; presampled_envmap.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_ENVMAP_SAMPLE; ColorRGB32F radiance = envmap_sample(world_settings, presampled_envmap.point_on_light_source, presampled_envmap.pdf, random_number_generator); // Moving the direction to envmap space because that's what we use for ReSTIR DI presampled_envmap.point_on_light_source = matrix_X_vec(world_settings.world_to_envmap_matrix, presampled_envmap.point_on_light_source); presampled_envmap.radiance = radiance; presampled_envmap.pdf *= envmap_sampling_probability; return presampled_envmap; } HIPRT_HOST_DEVICE HIPRT_INLINE ReSTIRDIPresampledLight presample_emissive_triangle(const HIPRTRenderData& render_data, float light_sampling_probability, Xorshift32Generator& random_number_generator) { ReSTIRDIPresampledLight presampled_light; // We're passing 0.0f as the position here because we do not have a position when presampling lights in screen space // The position is only used for "spatial sampling" schemes such as ReGIR or light trees for example and such schemes are not // compatible with ReSTIR light presampling anyways LightSampleInformation light_sample = sample_one_emissive_triangle(render_data, random_number_generator); if (light_sample.area_measure_pdf > 0.0f) { presampled_light.point_on_light_source = light_sample.point_on_light; presampled_light.light_source_normal = light_sample.light_source_normal; presampled_light.emissive_triangle_index = light_sample.emissive_triangle_index; // PDF in area measure presampled_light.pdf = light_sample.area_measure_pdf; presampled_light.pdf *= light_sampling_probability; presampled_light.radiance = light_sample.emission; } return presampled_light; } // TODO try just passing LightPresamplingParameters in there instead of everything individually HIPRT_HOST_DEVICE HIPRT_INLINE ReSTIRDIPresampledLight ReSTIR_DI_presample_one_light(const HIPRTRenderData& render_data, const LightPresamplingParameters& parameters, float envmap_sampling_probability, Xorshift32Generator& random_number_generator) { ReSTIRDIPresampledLight presampled_light; if (random_number_generator() < envmap_sampling_probability) presampled_light = presample_envmap(render_data.world_settings, envmap_sampling_probability, random_number_generator); else presampled_light = presample_emissive_triangle(render_data, 1.0f - envmap_sampling_probability, random_number_generator); return presampled_light; } #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_DI_LightsPresampling(LightPresamplingParameters presampling_parameters, HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_DI_LightsPresampling(LightPresamplingParameters presampling_parameters, HIPRTRenderData render_data, int x) #endif { if (render_data.buffers.emissive_triangles_count == 0 && render_data.world_settings.ambient_light_type != AmbientLightType::ENVMAP) // No initial candidates to sample since no lights return; #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; #endif if (x >= presampling_parameters.subset_size * presampling_parameters.number_of_subsets) return; uint32_t thread_index = x; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(thread_index + 1); else seed = wang_hash((thread_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); float envmap_candidate_probability = 0.0f; if (render_data.world_settings.ambient_light_type == AmbientLightType::ENVMAP) { if (render_data.buffers.emissive_triangles_count == 0) // Only the envmap to sample envmap_candidate_probability = 1.0f; else envmap_candidate_probability = presampling_parameters.envmap_sampling_probability; } presampling_parameters.out_light_samples[x] = ReSTIR_DI_presample_one_light(render_data, presampling_parameters, envmap_candidate_probability, random_number_generator); } #endif ================================================ FILE: src/Device/kernels/ReSTIR/DI/SpatialReuse.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_SPATIAL_REUSE_H #define DEVICE_RESTIR_DI_SPATIAL_REUSE_H #include "Device/includes/Dispatcher.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/ReSTIR/Jacobian.h" #include "Device/includes/ReSTIR/NeighborSimilarity.h" #include "Device/includes/ReSTIR/OptimalVisibilitySampling.h" #include "Device/includes/ReSTIR/SpatialMISWeight.h" #include "Device/includes/ReSTIR/SpatialNormalizationWeight.h" #include "Device/includes/ReSTIR/Surface.h" #include "Device/includes/ReSTIR/Utils.h" #include "Device/includes/ReSTIR/UtilsSpatial.h" #include "Device/includes/ReSTIR/DI/TargetFunction.h" #include "Device/includes/Sampling.h" #include "HostDeviceCommon/HIPRTCamera.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/RenderData.h" /** References: * * [1] [Spatiotemporal reservoir resampling for real-time ray tracing with dynamic direct lighting] https://research.nvidia.com/labs/rtr/publication/bitterli2020spatiotemporal/ * [2] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time] https://intro-to-restir.cwyman.org/ * [3] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time - SIGGRAPH 2023 Presentation Video] https://dl.acm.org/doi/10.1145/3587423.3595511#sec-supp * [4] [NVIDIA RTX DI SDK - Github] https://github.com/NVIDIAGameWorks/RTXDI * [5] [Generalized Resampled Importance Sampling Foundations of ReSTIR] https://research.nvidia.com/publication/2022-07_generalized-resampled-importance-sampling-foundations-restir * [6] [Uniform disk sampling] https://rh8liuqy.github.io/Uniform_Disk.html * [7] [Reddit Post for the Jacobian term needed] https://www.reddit.com/r/GraphicsProgramming/comments/1eo5hqr/restir_di_light_sample_pdf_confusion/ * [8] [Rearchitecting Spatiotemporal Resampling for Production] https://research.nvidia.com/publication/2021-07_rearchitecting-spatiotemporal-resampling-production */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_DI_SpatialReuse(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_DI_SpatialReuse(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t center_pixel_index = (x + y * render_data.render_settings.render_resolution.x); if (!render_data.aux_buffers.pixel_active[center_pixel_index] || render_data.g_buffer.first_hit_prim_index[center_pixel_index] == -1) // Pixel inactive because of adaptive sampling, returning // Or also we don't have a primary hit return; // Initializing the random generator unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(center_pixel_index + 1); else if (render_data.render_settings.restir_gi_settings.common_spatial_pass.coalesced_spatial_reuse) seed = wang_hash((render_data.render_settings.sample_number + 1) * render_data.random_number); else seed = wang_hash(((center_pixel_index + 1) * (render_data.render_settings.sample_number + 1)) * render_data.random_number); Xorshift32Generator random_number_generator(seed); ReSTIRDIReservoir* input_reservoir_buffer = render_data.render_settings.restir_di_settings.spatial_pass.input_reservoirs; ReSTIRDIReservoir spatial_reuse_output_reservoir; int2 center_pixel_coords = make_int2(x, y); ReSTIRDIReservoir center_pixel_reservoir = input_reservoir_buffer[center_pixel_index]; if ((center_pixel_reservoir.M <= 1) && render_data.render_settings.restir_di_settings.common_spatial_pass.do_disocclusion_reuse_boost) // Increasing the number of spatial samples for disocclusions render_data.render_settings.restir_di_settings.common_spatial_pass.reuse_neighbor_count = render_data.render_settings.restir_di_settings.common_spatial_pass.disocclusion_reuse_count; ReSTIRSurface center_pixel_surface = get_pixel_surface(render_data, center_pixel_index, random_number_generator); setup_adaptive_directional_spatial_reuse(render_data, center_pixel_index, random_number_generator); // Only used with MIS-like weight int selected_neighbor = 0; int neighbor_heuristics_cache = 0; int valid_neighbors_count = 0; int valid_neighbors_M_sum = 0; count_valid_spatial_neighbors(render_data, center_pixel_surface, center_pixel_coords, valid_neighbors_count, valid_neighbors_M_sum, neighbor_heuristics_cache); ReSTIRSpatialResamplingMISWeight mis_weight_function; Xorshift32Generator spatial_neighbors_rng(render_data.render_settings.restir_di_settings.common_spatial_pass.spatial_neighbors_rng_seed); // Resampling the neighbors. Using neighbors + 1 here so that // we can use the last iteration of the loop to resample ourselves (the center pixel) // // See the implementation of get_spatial_neighbor_pixel_index() in ReSTIR/DI/Utils.h int reused_neighbors_count = render_data.render_settings.restir_di_settings.common_spatial_pass.reuse_neighbor_count; int start_index = 0; if (valid_neighbors_M_sum == 0) // No valid neighbor to resample from, skip to the initial candidate right away start_index = reused_neighbors_count; for (int neighbor_index = start_index; neighbor_index < reused_neighbors_count + 1; neighbor_index++) { // We can already check whether or not this neighbor is going to be // accepted at all by checking the heuristic cache if (neighbor_index < reused_neighbors_count && reused_neighbors_count <= 32) { // If not the center pixel, we can check the heuristics, otherwise there's no need to, // we know that the center pixel will be accepted // // Our heuristics cache is a 32bit int so we can only cache 32 values are we're // going to have issues if we try to read more than that. if ((neighbor_heuristics_cache & (1 << neighbor_index)) == 0) { // Advancing the rng for generating the spatial neighbors since if we "continue" here, the spatial neighbors rng // isn't going to be advanced by the call to 'get_spatial_neighbor_pixel_index' below so we're doing it manually spatial_neighbor_advance_rng(render_data, spatial_neighbors_rng); // Neighbor not passing the heuristics tests, skipping it right away continue; } } int neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor_index, center_pixel_coords, spatial_neighbors_rng); if (neighbor_pixel_index == -1) // Neighbor out of the viewport continue; if (neighbor_index < reused_neighbors_count && reused_neighbors_count > 32) // If not the center pixel, we can check the heuristics // // Only checking the heuristic if we have more than 32 neighbors (does not fit in the heuristic cache) // If we have less than 32 neighbors, we've already checked the cache at the beginning of this for loop if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface))) continue; ReSTIRDIReservoir neighbor_reservoir = input_reservoir_buffer[neighbor_pixel_index]; float target_function_at_center = 0.0f; bool do_neighbor_target_function_visibility = do_include_visibility_term_or_not(render_data, neighbor_index); if (neighbor_reservoir.UCW > 0.0f) { if (neighbor_index == reused_neighbors_count) // No need to evaluate the center sample at the center pixel, that's exactly // the target function of the center reservoir target_function_at_center = neighbor_reservoir.sample.target_function; else { if (do_neighbor_target_function_visibility) target_function_at_center = ReSTIR_DI_evaluate_target_function(render_data, neighbor_reservoir.sample, center_pixel_surface, random_number_generator); else target_function_at_center = ReSTIR_DI_evaluate_target_function(render_data, neighbor_reservoir.sample, center_pixel_surface, random_number_generator); } } #if ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_M float mis_weight = mis_weight_function.get_resampling_MIS_weight(neighbor_reservoir.M); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z float mis_weight = mis_weight_function.get_resampling_MIS_weight(neighbor_reservoir.M); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_LIKE float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.M); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_GBH float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.UCW, neighbor_reservoir.sample, center_pixel_surface, neighbor_index, center_pixel_coords, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE bool update_mc = center_pixel_reservoir.M > 0 && center_pixel_reservoir.UCW > 0.0f; float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.M, neighbor_reservoir.sample.target_function, center_pixel_reservoir.sample, center_pixel_reservoir.M, center_pixel_reservoir.sample.target_function, neighbor_reservoir, center_pixel_surface, target_function_at_center, neighbor_pixel_index, valid_neighbors_count, valid_neighbors_M_sum, update_mc,/* resampling canonical */ neighbor_index == reused_neighbors_count, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO bool update_mc = center_pixel_reservoir.M > 0 && center_pixel_reservoir.UCW > 0.0f; float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.M, neighbor_reservoir.sample.target_function, center_pixel_reservoir.sample, center_pixel_reservoir.M, center_pixel_reservoir.sample.target_function, neighbor_reservoir, center_pixel_surface, target_function_at_center, neighbor_pixel_index, valid_neighbors_count, valid_neighbors_M_sum, update_mc,/* resampling canonical */ neighbor_index == reused_neighbors_count, random_number_generator); #else #error "Unsupported bias correction mode" #endif // Combining as in Alg. 6 of the paper float jacobian_determinant = 1.0f; if (spatial_reuse_output_reservoir.combine_with(neighbor_reservoir, mis_weight, target_function_at_center, jacobian_determinant, random_number_generator)) { // Only used with MIS-like weight selected_neighbor = neighbor_index; if (do_neighbor_target_function_visibility) // If we resampled the neighbor with visibility, then we are sure that we can set the flag spatial_reuse_output_reservoir.sample.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; else { // If we didn't resample the neighbor with visibility if (neighbor_index == reused_neighbors_count) // If we just resampled the center pixel, then we can copy the visibility flag spatial_reuse_output_reservoir.sample.flags |= neighbor_reservoir.sample.flags & ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; else // This was not the center pixel, we cannot be certain what the visibility at the center // pixel of the neighbor sample we just resample is so we're clearing the bit spatial_reuse_output_reservoir.sample.flags &= ~ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; } } spatial_reuse_output_reservoir.sanity_check(center_pixel_coords); ReSTIR_optimal_visibility_sampling(render_data, spatial_reuse_output_reservoir, center_pixel_reservoir, center_pixel_surface, neighbor_index, reused_neighbors_count, random_number_generator); } float normalization_numerator = 1.0f; float normalization_denominator = 1.0f; ReSTIRSpatialNormalizationWeight normalization_function; #if ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_M normalization_function.get_normalization(render_data, spatial_reuse_output_reservoir.weight_sum, center_pixel_surface, center_pixel_coords, normalization_numerator, normalization_denominator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z normalization_function.get_normalization(render_data, spatial_reuse_output_reservoir.sample, spatial_reuse_output_reservoir.weight_sum, center_pixel_surface, center_pixel_coords, normalization_numerator, normalization_denominator, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_LIKE normalization_function.get_normalization(render_data, spatial_reuse_output_reservoir.sample, spatial_reuse_output_reservoir.weight_sum, center_pixel_surface, selected_neighbor, center_pixel_coords, normalization_numerator, normalization_denominator, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_GBH normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO normalization_function.get_normalization(normalization_numerator, normalization_denominator); #else #error "Unsupported bias correction mode" #endif spatial_reuse_output_reservoir.end_with_normalization(normalization_numerator, normalization_denominator); spatial_reuse_output_reservoir.sanity_check(center_pixel_coords); // Only these 3 weighting schemes are affected #if (ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z \ || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS \ || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE \ || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO \ || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO) \ && ReSTIR_DI_BiasCorrectionUseVisibility == KERNEL_OPTION_TRUE \ && (ReSTIR_DI_DoVisibilityReuse == KERNEL_OPTION_TRUE || (ReSTIR_DI_InitialTargetFunctionVisibility == KERNEL_OPTION_TRUE && ReSTIR_DI_SpatialTargetFunctionVisibility == KERNEL_OPTION_TRUE)) // Why is this needed? // // Picture the case where we have visibility reuse (at the end of the initial candidates sampling pass), // visibility term in the bias correction target function (when counting the neighbors that could // have produced the picked sample) and 2 spatial reuse passes. // // The first spatial reuse pass reuses from samples that were produced with visibility in mind // (because of the visibility reuse pass that discards occluded samples). This means that we need // the visibility in the target function used when counting the neighbors that could have produced // the picked sample otherwise we may think that our neighbor could have produced the picked // sample where actually it couldn't because the sample is occluded at the neighbor. We would // then have a Z denominator (with 1/Z weights) that is too large and we'll end up with darkening. // // Now at the end of the first spatial reuse pass, the center pixel ends up with a sample that may // or may not be occluded from the center's pixel point of view. We didn't include the visibility // in the target function when resampling the neighbors (only when counting the "correct" neighbors // but that's all) so we are not giving a 0 weight to occluded resampled neighbors --> it is possible // that we picked an occluded sample. // // In the second spatial reuse pass, we are now going to resample from our neighbors and get some // samples that were not generated with occlusion in mind (because the resampling target function of // the first spatial reuse doesn't include visibility). Yet, we are going to weight them with occlusion // in mind. This means that we are probably going to discard samples because of occlusion that could // have been generated because they are generated without occlusion test. We end up discarding too many // samples --> brightening bias. // // With the visibility reuse at the end of each spatial pass, we force samples at the end of each // spatial reuse to take visibility into account so that when we weight them with visibility testing, // everything goes well // // As an optimization, we also do this for the pairwise MIS because pairwise MIS evaluates the target function // of reservoirs at their own location. Doing the visibility reuse here ensures that a reservoir sample at its own location // includes visibility and so we do not need to recompute the target function of the neighbors in this case. We can just // reuse the target function stored in the reservoir // // We also give the user the choice to remove bias using this option or not as it introduces very little bias // in practice (but noticeable when switching back and forth between reference image/biased image) // // We only need this if we're going to temporally reuse (because then the output of the spatial reuse must be correct // for the temporal reuse pass) or if we have multiple spatial reuse passes and this is not the last spatial pass bool not_last_spatial_pass = render_data.render_settings.restir_di_settings.common_spatial_pass.number_of_passes - 1 != render_data.render_settings.restir_di_settings.common_spatial_pass.spatial_pass_index; if (render_data.render_settings.restir_di_settings.common_temporal_pass.do_temporal_reuse_pass || not_last_spatial_pass) ReSTIR_DI_visibility_test_kill_reservoir(render_data, spatial_reuse_output_reservoir, center_pixel_surface.shading_point, center_pixel_surface.primitive_index, random_number_generator); #endif // M-capping so that we don't have to M-cap when reading reservoirs on the next frame if (render_data.render_settings.restir_di_settings.m_cap > 0) // M-capping the temporal neighbor if an M-cap has been given spatial_reuse_output_reservoir.M = hippt::min(spatial_reuse_output_reservoir.M, render_data.render_settings.restir_di_settings.m_cap); render_data.render_settings.restir_di_settings.spatial_pass.output_reservoirs[center_pixel_index] = spatial_reuse_output_reservoir; } #endif ================================================ FILE: src/Device/kernels/ReSTIR/DI/TemporalReuse.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_DI_TEMPORAL_REUSE_H #define DEVICE_RESTIR_DI_TEMPORAL_REUSE_H #include "Device/includes/Dispatcher.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/Intersect.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/ReSTIR/TemporalMISWeight.h" #include "Device/includes/ReSTIR/TemporalNormalizationWeight.h" #include "Device/includes/ReSTIR/Surface.h" #include "Device/includes/ReSTIR/DI/TargetFunction.h" #include "Device/includes/ReSTIR/Utils.h" #include "Device/includes/ReSTIR/UtilsTemporal.h" #include "Device/includes/Sampling.h" #include "HostDeviceCommon/HIPRTCamera.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/HitInfo.h" #include "HostDeviceCommon/RenderData.h" /** References: * * [1] [Spatiotemporal reservoir resampling for real-time ray tracing with dynamic direct lighting] https://research.nvidia.com/labs/rtr/publication/bitterli2020spatiotemporal/ * [2] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time] https://intro-to-restir.cwyman.org/ * [3] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time - SIGGRAPH 2023 Presentation Video] https://dl.acm.org/doi/10.1145/3587423.3595511#sec-supp * [4] [NVIDIA RTX DI SDK - Github] https://github.com/NVIDIAGameWorks/RTXDI * [5] [Generalized Resampled Importance Sampling Foundations of ReSTIR] https://research.nvidia.com/publication/2022-07_generalized-resampled-importance-sampling-foundations-restir * [6] [Uniform disk sampling] https://rh8liuqy.github.io/Uniform_Disk.html * [7] [Reddit Post for the Jacobian Term needed] https://www.reddit.com/r/GraphicsProgramming/comments/1eo5hqr/restir_di_light_sample_pdf_confusion/ * [8] [Rearchitecting Spatiotemporal Resampling for Production] https://research.nvidia.com/publication/2021-07_rearchitecting-spatiotemporal-resampling-production * [9] [Adventures in Hybrid Rendering] https://diharaw.github.io/post/adventures_in_hybrid_rendering/ * [10] [NVIDIA ReBLUR - Fast Denoising with Self Stabilizing Recurrent Blurs] https://developer.nvidia.com/gtc/2020/video/s22699-vid */ // By convention, the temporal neighbor is the first one to be resampled in for loops // (for looping over the neighbors when resampling / computing MIS weights) // So instead of hardcoding 0 everywhere in the code, we just basically give it a name // with a #define #define TEMPORAL_NEIGHBOR_ID 0 // Same when resampling the initial candidates #define INITIAL_CANDIDATES_ID 1 #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_DI_TemporalReuse(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_DI_TemporalReuse(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t center_pixel_index = (x + y * render_data.render_settings.render_resolution.x); if (!render_data.aux_buffers.pixel_active[center_pixel_index] || render_data.g_buffer.first_hit_prim_index[center_pixel_index] == -1) // Pixel inactive because of adaptive sampling, returning // Or also we don't have a primary hit return; // Initializing the random generator unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(center_pixel_index + 1); else seed = wang_hash((center_pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); if (render_data.render_settings.restir_di_settings.common_temporal_pass.temporal_buffer_clear_requested) // We requested a temporal buffer clear for ReSTIR DI render_data.render_settings.restir_di_settings.temporal_pass.input_reservoirs[center_pixel_index] = ReSTIRDIReservoir(); // Surface data of the center pixel ReSTIRSurface center_pixel_surface = get_pixel_surface(render_data, center_pixel_index, random_number_generator); int temporal_neighbor_pixel_index = find_temporal_neighbor_index(render_data, render_data.g_buffer.primary_hit_position[center_pixel_index], center_pixel_surface.shading_normal, center_pixel_index, random_number_generator).x; if (temporal_neighbor_pixel_index == -1 || render_data.render_settings.freeze_random) { // Temporal occlusion / disoccusion, temporal neighbor is invalid, // we're only going to resample the initial candidates so let's set that as // the output right away // // We're also 'disabling' temporal accumulation if the random is frozen otherwise // very strong correlations will creep up, corrupt the render and potentially invalidate // performance measurements (which we're probably trying to measure since we froze the random) // The output of this temporal pass is just the initial candidates reservoir render_data.render_settings.restir_di_settings.temporal_pass.output_reservoirs[center_pixel_index] = render_data.render_settings.restir_di_settings.initial_candidates.output_reservoirs[center_pixel_index]; return; } ReSTIRDIReservoir temporal_neighbor_reservoir = render_data.render_settings.restir_di_settings.temporal_pass.input_reservoirs[temporal_neighbor_pixel_index]; if (temporal_neighbor_reservoir.M == 0) { // No temporal neighbor, the output of this temporal pass is just the initial candidates reservoir render_data.render_settings.restir_di_settings.temporal_pass.output_reservoirs[center_pixel_index] = render_data.render_settings.restir_di_settings.initial_candidates.output_reservoirs[center_pixel_index]; return; } ReSTIRDIReservoir temporal_reuse_output_reservoir; ReSTIRSurface temporal_neighbor_surface = get_pixel_surface(render_data, temporal_neighbor_pixel_index, render_data.render_settings.use_prev_frame_g_buffer(), random_number_generator); if (temporal_neighbor_surface.material.is_emissive()) { // Can't resample the temporal neighbor if it's emissive so output the initial candidates right away render_data.render_settings.restir_di_settings.temporal_pass.output_reservoirs[center_pixel_index] = render_data.render_settings.restir_di_settings.initial_candidates.output_reservoirs[center_pixel_index]; return; } ReSTIRTemporalResamplingMISWeight mis_weight_function; // Only used with MIS-like weight // // Will keep the index of the neighbor that has been selected by resampling. // Either 0 or 1 for the temporal resampling pass int selected_neighbor = 0; // /* ------------------------------- */ // Resampling the temporal neighbor // /* ------------------------------- */ ReSTIRDIReservoir initial_candidates_reservoir = render_data.render_settings.restir_di_settings.initial_candidates.output_reservoirs[center_pixel_index]; if (temporal_neighbor_reservoir.M > 0) { float target_function_at_center = 0.0f; if (temporal_neighbor_reservoir.UCW > 0.0f) // Only resampling if the temporal neighbor isn't empty // // If the temporal neighbor's reservoir is empty, then we do not get // inside that if() and the target function stays at 0.0f which eliminates // most of the computations afterwards // // Matching the visibility used here with the bias correction mode for ease // of use (and because manually handling the visibility in the target // function of the temporal reuse is tricky for the user to use in // combination with other parameters and on top of that, it makes little // technical sense since our temporal neighbor is supposed to be unoccluded // (unless geometry moves around in the scene but that's another problem) target_function_at_center = ReSTIR_DI_evaluate_target_function(render_data, temporal_neighbor_reservoir.sample, center_pixel_surface, random_number_generator); #if ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_M float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(temporal_neighbor_reservoir); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(temporal_neighbor_reservoir); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_LIKE float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_GBH float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir.sample, initial_candidates_reservoir.M, temporal_neighbor_surface, center_pixel_surface, temporal_neighbor_reservoir.M, TEMPORAL_NEIGHBOR_ID, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir, initial_candidates_reservoir, center_pixel_surface, temporal_neighbor_surface, target_function_at_center, TEMPORAL_NEIGHBOR_ID, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir, initial_candidates_reservoir, center_pixel_surface, temporal_neighbor_surface, target_function_at_center, TEMPORAL_NEIGHBOR_ID, random_number_generator); #else #error "Unsupported bias correction mode" #endif // Combining as in Alg. 6 of the paper float jacobian_determinant = 1.0f; if (temporal_reuse_output_reservoir.combine_with(temporal_neighbor_reservoir, temporal_neighbor_resampling_mis_weight, target_function_at_center, jacobian_determinant, random_number_generator)) { // Only used with MIS-like weight selected_neighbor = TEMPORAL_NEIGHBOR_ID; // Using ReSTIR_DI_BiasCorrectionUseVisibility here because that's what we use in the resampling target function #if ReSTIR_DI_BiasCorrectionUseVisibility == KERNEL_OPTION_FALSE // We cannot be certain that the visibility of the temporal neighbor // chosen is exactly the same so we're clearing the unoccluded flag temporal_reuse_output_reservoir.sample.flags &= ~ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; #else // However, if we're using the visibility in the target function, then // the temporal neighobr could never have been selected unless it is // unoccluded so we can add the flag temporal_reuse_output_reservoir.sample.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; #endif } temporal_reuse_output_reservoir.sanity_check(make_int2(x, y)); } // /* ------------------------------- */ // Resampling the initial candidates // /* ------------------------------- */ #if ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_M float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(initial_candidates_reservoir); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(initial_candidates_reservoir); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_LIKE float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, initial_candidates_reservoir); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_GBH float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, initial_candidates_reservoir.sample, initial_candidates_reservoir.M, temporal_neighbor_surface, center_pixel_surface, temporal_neighbor_reservoir.M, INITIAL_CANDIDATES_ID, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir, initial_candidates_reservoir, center_pixel_surface, temporal_neighbor_surface, /* unused */ 0.0f, INITIAL_CANDIDATES_ID, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO ||ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir, initial_candidates_reservoir, center_pixel_surface, temporal_neighbor_surface, /* unused */ 0.0f, INITIAL_CANDIDATES_ID, random_number_generator); #else #error "Unsupported bias correction mode" #endif if (temporal_reuse_output_reservoir.combine_with(initial_candidates_reservoir, initial_candidates_mis_weight, initial_candidates_reservoir.sample.target_function, /* jacobian is 1 when reusing at the exact same spot */ 1.0f, random_number_generator)) { // Only used with MIS-like weight selected_neighbor = INITIAL_CANDIDATES_ID; // Using ReSTIR_DI_BiasCorrectionUseVisibility here because that's what we use in the resampling target function #if ReSTIR_DI_BiasCorrectionUseVisibility == KERNEL_OPTION_FALSE // We resampled the center pixel so we can copy the unoccluded flag temporal_reuse_output_reservoir.sample.flags |= initial_candidates_reservoir.sample.flags & ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; #else // However, if we're using the visibility in the target function, then // we are sure that the sample is now unoccluded temporal_reuse_output_reservoir.sample.flags |= ReSTIRDISampleFlags::RESTIR_DI_FLAGS_UNOCCLUDED; #endif } temporal_reuse_output_reservoir.sanity_check(make_int2(x, y)); float normalization_numerator = 1.0f; float normalization_denominator = 1.0f; ReSTIRTemporalNormalizationWeight normalization_function; #if ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_M normalization_function.get_normalization(temporal_reuse_output_reservoir.weight_sum, initial_candidates_reservoir.M, temporal_neighbor_reservoir.M, normalization_numerator, normalization_denominator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_1_OVER_Z normalization_function.get_normalization(render_data, temporal_reuse_output_reservoir.sample, temporal_reuse_output_reservoir.weight_sum, initial_candidates_reservoir.M, temporal_neighbor_reservoir.M, center_pixel_surface, temporal_neighbor_surface, normalization_numerator, normalization_denominator, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_LIKE normalization_function.get_normalization(render_data, temporal_reuse_output_reservoir.sample, temporal_reuse_output_reservoir.weight_sum, initial_candidates_reservoir.M, temporal_neighbor_reservoir.M, center_pixel_surface, temporal_neighbor_surface, selected_neighbor, normalization_numerator, normalization_denominator, random_number_generator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_MIS_GBH normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_DI_BiasCorrectionWeights == RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO normalization_function.get_normalization(normalization_numerator, normalization_denominator); #else #error "Unsupported bias correction mode" #endif temporal_reuse_output_reservoir.end_with_normalization(normalization_numerator, normalization_denominator); temporal_reuse_output_reservoir.sanity_check(make_int2(x, y)); // M-capping so that we don't have to M-cap when reading reservoirs on the next frame if (render_data.render_settings.restir_di_settings.m_cap > 0) // M-capping the temporal neighbor if an M-cap has been given temporal_reuse_output_reservoir.M = hippt::min(temporal_reuse_output_reservoir.M, render_data.render_settings.restir_di_settings.m_cap); render_data.render_settings.restir_di_settings.temporal_pass.output_reservoirs[center_pixel_index] = temporal_reuse_output_reservoir; } #endif ================================================ FILE: src/Device/kernels/ReSTIR/DirectionalReuseCompute.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_RESTIR_DIRECTIONAL_REUSE_COMPUTE_H #define KERNELS_RESTIR_DIRECTIONAL_REUSE_COMPUTE_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/ReSTIR/NeighborSimilarity.h" #include "Device/includes/ReSTIR/UtilsSpatial.h" #include "HostDeviceCommon/RenderData.h" #define NB_RADIUS 32 #if ComputingSpatialDirectionalReuseForReSTIRGI == KERNEL_OPTION_TRUE #define NB_SAMPLES_PER_RADIUS_INTERNAL ReSTIR_GI_SpatialDirectionalReuseBitCount // CHANGE THIS ONE #else #define NB_SAMPLES_PER_RADIUS_INTERNAL ReSTIR_DI_SpatialDirectionalReuseBitCount // CHANGE THIS ONE #endif #define NB_SAMPLES_PER_RADIUS (NB_SAMPLES_PER_RADIUS_INTERNAL > 64 ? 64 : NB_SAMPLES_PER_RADIUS_INTERNAL) // Max to 64 for unsigned long long int #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_Directional_Reuse_Compute(HIPRTRenderData render_data, unsigned int* __restrict__ out_directional_reuse_masks_buffer_u, unsigned long long int* __restrict__ out_directional_reuse_masks_buffer_ull, unsigned char* __restrict__ out_adaptive_radius_buffer) #else template GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_Directional_Reuse_Compute(HIPRTRenderData render_data, int x, int y, unsigned int* __restrict__ out_directional_reuse_masks_buffer_u, unsigned long long int* __restrict__ out_directional_reuse_masks_buffer_ull, unsigned char* __restrict__ out_adaptive_radius_buffer) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t center_pixel_index = x + y * render_data.render_settings.render_resolution.x; if (!render_data.aux_buffers.pixel_active[center_pixel_index]) // Pixel isn't active because of adaptive sampling or render resolution scaling return; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(center_pixel_index + 1); else seed = wang_hash((center_pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); // Clearing previous data #if NB_SAMPLES_PER_RADIUS > 32 out_directional_reuse_masks_buffer_ull[center_pixel_index] = 0; #else out_directional_reuse_masks_buffer_u[center_pixel_index] = 0; #endif out_adaptive_radius_buffer[center_pixel_index] = 0; #ifdef __KERNELCC__ // If on the GPU, using the 'ComputingSpatialDirectionalReuseForReSTIRGI' macro // (that is passed to the compiler in the ReSTIRDI/GI RenderPass.cpp) // // To get the settings ReSTIRCommonSpatialPassSettings spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); #else // On the CPU, it is the template argument that dictates whether this is for ReSTIR DI or GI ReSTIRCommonSpatialPassSettings spatial_pass_settings = ReSTIRSettingsHelper::get_restir_spatial_pass_settings(render_data); #endif float3 center_shading_point = render_data.g_buffer.primary_hit_position[center_pixel_index]; #ifdef __KERNELCC__ float3 center_normal = ReSTIRSettingsHelper::get_restir_neighbor_similarity_settings(render_data).reject_using_geometric_normals ? render_data.g_buffer.geometric_normals[center_pixel_index].unpack() : render_data.g_buffer.shading_normals[center_pixel_index].unpack(); #else float3 center_normal = ReSTIRSettingsHelper::get_restir_neighbor_similarity_settings(render_data).reject_using_geometric_normals ? render_data.g_buffer.geometric_normals[center_pixel_index].unpack() : render_data.g_buffer.shading_normals[center_pixel_index].unpack(); #endif float best_area = 0.0f; int best_radius_index = 0; // Each long long int in there contains, in each bit, whether or not the direction for that radius is reusable or not unsigned long long int valid_samples_per_radius[NB_RADIUS] = { 0 }; for (int radius_index = 0; radius_index < NB_RADIUS; radius_index++) { float current_radius = spatial_pass_settings.minimum_per_pixel_reuse_radius + (radius_index / (float)NB_RADIUS) * (spatial_pass_settings.reuse_radius - spatial_pass_settings.minimum_per_pixel_reuse_radius); float current_radius_circle_area = M_PI * current_radius * current_radius; // Now sampling a bunch of neighbors *on* that radius, exactly at that radius distance from the center (i.e. *not* within the disk of that radius) float area_at_current_radius = 0.0f; for (int sample_index = 0; sample_index < NB_SAMPLES_PER_RADIUS; sample_index++) { if (radius_index > 0) if (!(valid_samples_per_radius[radius_index - 1] & (1ull << sample_index))) // If this direction wasn't accepted at the previous radius continue; float theta = sample_index / (float)NB_SAMPLES_PER_RADIUS * M_TWO_PI; float x_circle = current_radius * cosf(theta); float y_circle = current_radius * sinf(theta); int2 neighbor_offset_in_disk = make_int2(static_cast(roundf(x_circle)), static_cast(roundf(y_circle))); int2 neighbor_pixel_coords = make_int2(x, y) + neighbor_offset_in_disk; if (neighbor_pixel_coords.x < 0 || neighbor_pixel_coords.x >= render_data.render_settings.render_resolution.x || neighbor_pixel_coords.y < 0 || neighbor_pixel_coords.y >= render_data.render_settings.render_resolution.y) // Rejecting the sample if it's outside of the viewport continue; int neighbor_index = neighbor_pixel_coords.x + neighbor_pixel_coords.y * render_data.render_settings.render_resolution.x; #ifdef __KERNELCC__ // If on the GPU, using the 'ComputingSpatialDirectionalReuseForReSTIRGI' macro // (that is passed to the compiler in the ReSTIRDI/GI RenderPass.cpp) // // To determine whether this is for ReSTIR DI or GI if (!check_neighbor_similarity_heuristics(render_data, neighbor_index, center_pixel_index, center_shading_point, center_normal)) continue; #else // On the CPU, it is the template argument that dictates whether this is for ReSTIR DI or GI if (!check_neighbor_similarity_heuristics(render_data, neighbor_index, center_pixel_index, center_shading_point, center_normal)) continue; #endif valid_samples_per_radius[radius_index] |= (1ull << sample_index); area_at_current_radius += current_radius_circle_area * (1.0f / NB_SAMPLES_PER_RADIUS); } if (best_area < area_at_current_radius) { best_area = area_at_current_radius; best_radius_index = radius_index; } } // Computing the actual radius from the best radius index float best_radius = spatial_pass_settings.minimum_per_pixel_reuse_radius + (best_radius_index / (float)NB_RADIUS) * (spatial_pass_settings.reuse_radius - spatial_pass_settings.minimum_per_pixel_reuse_radius); if (best_area == 0.0f) best_radius = 0.0f; out_adaptive_radius_buffer[center_pixel_index] = (unsigned char)best_radius; #if NB_SAMPLES_PER_RADIUS > 32 out_directional_reuse_masks_buffer_ull[center_pixel_index] = valid_samples_per_radius[best_radius_index]; #else // Extracting the low 32 bits out_directional_reuse_masks_buffer_u[center_pixel_index] = (unsigned int)(valid_samples_per_radius[best_radius_index] & 0x00000000FFFFFFFFF); #endif } #endif ================================================ FILE: src/Device/kernels/ReSTIR/GI/InitialCandidates.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_RESTIR_GI_INITIAL_CANDIDATES_H #define KERNELS_RESTIR_GI_INITIAL_CANDIDATES_H #include "Device/includes/LightSampling/Envmap.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/LightSampling/Lights.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/ReSTIR/GI/InitialCandidatesUtils.h" #include "Device/includes/ReSTIR/GI/Reservoir.h" #include "Device/includes/ReSTIR/GI/TargetFunction.h" #include "Device/includes/SanityCheck.h" #include "HostDeviceCommon/Xorshift.h" #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_GI_InitialCandidates(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_GI_InitialCandidates(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t pixel_index = x + y * render_data.render_settings.render_resolution.x; if (!render_data.aux_buffers.pixel_active[pixel_index]) // Pixel isn't active because of adaptive sampling or render resolution scaling return; if (render_data.render_settings.do_render_low_resolution()) // Reducing the number of bounces to 3 if rendering at low resolution // for better interactivity render_data.render_settings.nb_bounces = hippt::min(3, render_data.render_settings.nb_bounces); unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(pixel_index + 1); else seed = wang_hash((pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); // Initializing the closest hit info the information from the camera ray pass HitInfo closest_hit_info; closest_hit_info.inter_point = render_data.g_buffer.primary_hit_position[pixel_index]; closest_hit_info.geometric_normal = render_data.g_buffer.geometric_normals[pixel_index].unpack(); closest_hit_info.shading_normal = render_data.g_buffer.shading_normals[pixel_index].unpack(); closest_hit_info.primitive_index = render_data.g_buffer.first_hit_prim_index[pixel_index]; // Initializing the ray with the information from the camera ray pass hiprtRay ray; ray.direction = -render_data.g_buffer.get_view_direction(render_data.current_camera.position, pixel_index); RayPayload ray_payload; ray_payload.next_ray_state = RayState::BOUNCE; ray_payload.material = render_data.g_buffer.materials[pixel_index].unpack(); // Because this is the camera hit (and assuming the camera isn't inside volumes for now), // the ray volume state after the camera hit is just an empty interior stack but with // the material index that we hit pushed onto the stack. That's it. Because it is that // simple, we don't have the ray volume state in the GBuffer but rather we can // reconstruct the ray volume state on the fly ray_payload.volume_state.reconstruct_first_hit( ray_payload.material, render_data.buffers.material_indices, closest_hit_info.primitive_index, random_number_generator); bool intersection_found = closest_hit_info.primitive_index != -1; ReSTIRSurface initial_surface; initial_surface.geometric_normal = closest_hit_info.geometric_normal; initial_surface.shading_normal = closest_hit_info.shading_normal; initial_surface.primitive_index = closest_hit_info.primitive_index; initial_surface.material = ray_payload.material; initial_surface.ray_volume_state = ray_payload.volume_state; initial_surface.shading_point = closest_hit_info.inter_point; initial_surface.view_direction = -ray.direction; float bsdf_sample_pdf = 0.0f; ReSTIRGISample restir_gi_initial_sample; ColorRGB32F incoming_radiance_to_visible_point; ColorRGB32F incoming_radiance_to_sample_point; ColorRGB32F throughput_to_visible_point = ColorRGB32F(1.0f); // + 1 to nb_bounces here because we want "0" bounces to still act as one // hit and to return some color for (int& bounce = ray_payload.bounce; bounce < render_data.render_settings.nb_bounces + 1; bounce++) { if (ray_payload.next_ray_state != RayState::MISSED) { if (bounce > 0) { if (bounce == 1) // This is going to be tracing the ray from the visible point to the sample: // we're saving the random seed used during the BVH traversal to be able to reproduce // alpha tests restir_gi_initial_sample.visible_to_sample_point_alpha_test_random_seed = random_number_generator.m_state.seed; intersection_found = path_tracing_find_indirect_bounce_intersection(render_data, ray, ray_payload, closest_hit_info, random_number_generator); } if (intersection_found) { if (bounce == 0) store_denoiser_AOVs(render_data, pixel_index, closest_hit_info.shading_normal, ray_payload.material.base_color); else if (bounce > 0) { bool ReGIR_primary_hit = render_data.render_settings.regir_settings.compute_is_primary_hit(ray_payload); // Storing data for ReGIR representative points ReGIR_update_representative_data(render_data, closest_hit_info.inter_point, closest_hit_info.geometric_normal, render_data.current_camera, closest_hit_info.primitive_index, ReGIR_primary_hit, ray_payload.material); } if (bounce == 1) { restir_gi_initial_sample.sample_point_geometric_normal.pack(closest_hit_info.geometric_normal); restir_gi_initial_sample.sample_point = closest_hit_info.inter_point; restir_gi_initial_sample.sample_point_primitive_index = closest_hit_info.primitive_index; restir_gi_initial_sample.sample_point_rough_enough = MaterialUtils::can_do_light_sampling(ray_payload.material, render_data.render_settings.restir_gi_settings.neighbor_sample_point_roughness_threshold); } if (bounce > 0) { // Estimating with a throughput of 1.0f here because we're going to apply the throughput ourselves ColorRGB32F direct_lighting_estimation = estimate_direct_lighting(render_data, ray_payload, ColorRGB32F(1.0f), closest_hit_info, -ray.direction, x, y, random_number_generator); // Updating the cumulated outgoing radiance of our path to the visible point incoming_radiance_to_visible_point += clamp_direct_lighting_estimation(direct_lighting_estimation * throughput_to_visible_point, render_data.render_settings.indirect_contribution_clamp, bounce); } float bsdf_pdf; BSDFIncidentLightInfo incident_light_info; bool valid_indirect_bounce = restir_gi_compute_next_indirect_bounce(render_data, ray_payload, throughput_to_visible_point, closest_hit_info, -ray.direction, ray, random_number_generator, &incident_light_info, &bsdf_pdf); if (!valid_indirect_bounce) // Bad BSDF sample (under the surface), killed by russian roulette, ... break; if (bounce == 0) { restir_gi_initial_sample.incident_light_info_at_visible_point = incident_light_info; bsdf_sample_pdf = bsdf_pdf; } } else { if (bounce == 1) { // For envmap path, the direction is stored in the hit point restir_gi_initial_sample.sample_point = ray.direction; // -1 for the primitive index indicates that this is an envmap sample restir_gi_initial_sample.sample_point_primitive_index = -1; } incoming_radiance_to_visible_point += path_tracing_miss_gather_envmap(render_data, throughput_to_visible_point, ray.direction, ray_payload.bounce, pixel_index); ray_payload.next_ray_state = RayState::MISSED; } } else if (ray_payload.next_ray_state == RayState::MISSED) break; } // Checking for NaNs / negative value samples. Output if (!sanity_check(render_data, ray_payload.ray_color, x, y)) return; // If we got here, this means that we still have at least one ray active // This is a concurrent write by the way but we don't really care, everyone is writing // the same value render_data.aux_buffers.still_one_ray_active[0] = 1; restir_gi_initial_sample.incoming_radiance_to_visible_point.pack(incoming_radiance_to_visible_point); restir_gi_initial_sample.target_function = ReSTIR_GI_evaluate_target_function(render_data, restir_gi_initial_sample, initial_surface, random_number_generator); float resampling_weight = 0.0f; float mis_weight = 1.0f; float target_function = restir_gi_initial_sample.target_function; float source_pdf = bsdf_sample_pdf; if (source_pdf > 0.0f) resampling_weight = mis_weight * restir_gi_initial_sample.target_function / source_pdf; ReSTIRGIReservoir restir_gi_initial_reservoir; restir_gi_initial_reservoir.add_one_candidate(restir_gi_initial_sample, resampling_weight, random_number_generator); restir_gi_initial_reservoir.end(); restir_gi_initial_reservoir.sanity_check(make_int2(x, y)); render_data.render_settings.restir_gi_settings.initial_candidates.initial_candidates_buffer[pixel_index] = restir_gi_initial_reservoir; } #endif ================================================ FILE: src/Device/kernels/ReSTIR/GI/Shading.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_RESTIR_GI_SHADING_H #define KERNELS_RESTIR_GI_SHADING_H #include "Device/includes/LightSampling/Envmap.h" #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/LightSampling/Lights.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/PathTracing.h" #include "Device/includes/ReSTIR/GI/Reservoir.h" #include "Device/includes/ReSTIR/GI/TargetFunction.h" #include "Device/includes/ReSTIR/UtilsSpatial.h" #include "Device/includes/SanityCheck.h" #include "HostDeviceCommon/Xorshift.h" // ReSTIR GI shading/resampling is still a bit broken, there's still some brightening bias coming from // I don't know where, supposedly when the BRDF starts to include smooth/glossy BRDFs // // This manifests the most on specular 1 + roughness 0 everywhere in the scene // Maybe the bias is also there with a Lambertian BRDF but I could never see it. Maybe it's there but it's just so subtle that it's invisible // // -------------------------- WHAT WE KNOW -------------------------- // - Still biased with no alpha tests // - Do we absolutely have correct convergence on Lambertian & Oren Nayar? --> hard to verify, looks like it? // - Is it the glass that is biased? -------> No // - 1/Z is also biased, even without the jacobian rejection heuristic // - It's not the adaptive sampling that is messed up // - Definitely has some bias (very little but there) with everything using a metallic BRDF, roughness 0.1, 50 bounces. Contemporary bedroom // - There is some bias in the contemporary bedroom at 1 bounce, everything specular, 0 roughness, with RIS light sampling + envmap sampling // - Can't see any bias with lambertian/oren nayar contemporary bedroom + NEE + Envmap // - There is still some bias with a roughness 1.0f metallic // - Not a normal mapping issue? // - With everything specular at IOR 1.0f but roughness 1.0f, there's basically no bias. Even though the specular layer has no effect because of IOR 1.0f. So if the roughness of an inexistant layer changes the bias, it can only be a PDF issue? // - Because there is no bias on full Lambertian, this isn't a jacobian issue? // // With a specular IOR 1 + diffuse lobe setup // - increasing the roughness of the specular (still at IOR 1) reduces the bias // - artificially fixing the proba of sampling the specular to 90 % and diffuse 10 % increases the bias quiiite a lot(but still converges correctly without ReSTIR and when reusing 0 neighbor).Even more so when approaching 100 % specular(but not quite 100 %) // - sampling the specular & diffuse lobe based on the fresnel reflectance yields a different bias vs.sampling 50 / 50 (or 90 / 10). // - resampling more spatial neighbors makes things a bit worse.But only up to a certain point.For example, 6 spatial reuse passes with 16 candidates each(which is huge) is barely worse than 1 spatial pass @ 16 candidates // - with 0 spatial neighbor reuse, it converges correctly, no matter the BSDF / sampling probas / ... // - there seems to be no bias with only 1 bounce(i.e.with paths, being at most : "camera -> first hit -> second hit").Bias only comes in with # of bounce >= 2 // -------------------------- WHAT WE KNOW -------------------------- // // -------------------------- DIRTY FIX RIGHT NOW -------------------------- // - No double BSDF shading // - No double BSDF in target function // - Reuse on specular is ok // - Using rejection heuristics is better // -------------------------- DIRTY FIX RIGHT NOW -------------------------- #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_GI_Shading(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_GI_Shading(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t pixel_index = x + y * render_data.render_settings.render_resolution.x; if (!render_data.aux_buffers.pixel_active[pixel_index]) return; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(pixel_index + 1); else seed = wang_hash((pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); hiprtRay ray; ray.direction = -render_data.g_buffer.get_view_direction(render_data.current_camera.position, pixel_index); HitInfo closest_hit_info; closest_hit_info.primitive_index = render_data.g_buffer.first_hit_prim_index[pixel_index]; if (closest_hit_info.primitive_index == -1) { // Geometry miss, directly into the envmap ColorRGB32F envmap_radiance = path_tracing_miss_gather_envmap(render_data, ColorRGB32F(1.0f), ray.direction, 0, pixel_index); path_tracing_accumulate_color(render_data, envmap_radiance, pixel_index); return; } closest_hit_info.inter_point = render_data.g_buffer.primary_hit_position[pixel_index]; closest_hit_info.shading_normal = render_data.g_buffer.shading_normals[pixel_index].unpack(); // Initializing the ray with the information from the camera ray pass RayPayload ray_payload; ray_payload.next_ray_state = RayState::BOUNCE; // Loading the first hit in the ray payload ray_payload.material = render_data.g_buffer.materials[pixel_index].unpack(); ray_payload.volume_state.reconstruct_first_hit( ray_payload.material, render_data.buffers.material_indices, closest_hit_info.primitive_index, random_number_generator); float3 view_direction = render_data.g_buffer.get_view_direction(render_data.current_camera.position, pixel_index); ColorRGB32F camera_outgoing_radiance; if (render_data.render_settings.enable_direct) // Adding the direct lighting contribution at the first hit in the direction of the camera camera_outgoing_radiance += estimate_direct_lighting(render_data, ray_payload, closest_hit_info, view_direction, x, y, random_number_generator); ReSTIRGIReservoir resampling_reservoir = render_data.render_settings.restir_gi_settings.restir_output_reservoirs[pixel_index]; if (render_data.render_settings.nb_bounces > 0) { // Only doing the ReSTIR GI stuff if we have more than 1 bounce if (resampling_reservoir.UCW > 0.0f) { // Only doing the shading if we do actually have a sample float3 geometric_normal = render_data.g_buffer.geometric_normals[pixel_index].unpack(); float3 restir_resampled_indirect_direction; if (resampling_reservoir.sample.is_envmap_path()) restir_resampled_indirect_direction = resampling_reservoir.sample.sample_point; else restir_resampled_indirect_direction = hippt::normalize(resampling_reservoir.sample.sample_point - closest_hit_info.inter_point); // Computing the BSDF throughput at the first hit // - view direction: towards the camera // - incident light direction: towards the sample point float bsdf_pdf_first_hit; BSDFContext bsdf_first_hit_context(view_direction, closest_hit_info.shading_normal, geometric_normal, restir_resampled_indirect_direction, resampling_reservoir.sample.incident_light_info_at_visible_point, ray_payload.volume_state, false, ray_payload.material, 0, 0.0f); ColorRGB32F bsdf_color_first_hit = bsdf_dispatcher_eval(render_data, bsdf_first_hit_context, bsdf_pdf_first_hit, random_number_generator); ColorRGB32F first_hit_throughput; if (bsdf_pdf_first_hit > 0.0f) first_hit_throughput = bsdf_color_first_hit * hippt::abs(hippt::dot(restir_resampled_indirect_direction, closest_hit_info.shading_normal)) * resampling_reservoir.UCW; if (resampling_reservoir.sample.is_envmap_path()) camera_outgoing_radiance += path_tracing_miss_gather_envmap(render_data, first_hit_throughput, restir_resampled_indirect_direction, 1, pixel_index); else camera_outgoing_radiance += first_hit_throughput * resampling_reservoir.sample.incoming_radiance_to_visible_point.unpack(); } } // Setting the 'camera_outgoing_radiance' into the ray color just for the call to 'sanity_check' ray_payload.ray_color = camera_outgoing_radiance; if (!sanity_check(render_data, ray_payload.ray_color, x, y)) return; if (render_data.render_settings.restir_gi_settings.debug_view == ReSTIRGIDebugView::FINAL_RESERVOIR_UCW) path_tracing_accumulate_color(render_data, ColorRGB32F(resampling_reservoir.UCW) * render_data.render_settings.restir_gi_settings.debug_view_scale_factor, pixel_index); else if (render_data.render_settings.restir_gi_settings.debug_view == ReSTIRGIDebugView::TARGET_FUNCTION) path_tracing_accumulate_color(render_data, ColorRGB32F(resampling_reservoir.sample.target_function) * render_data.render_settings.restir_gi_settings.debug_view_scale_factor, pixel_index); else if (render_data.render_settings.restir_gi_settings.debug_view == ReSTIRGIDebugView::WEIGHT_SUM) path_tracing_accumulate_color(render_data, ColorRGB32F(resampling_reservoir.weight_sum) * render_data.render_settings.restir_gi_settings.debug_view_scale_factor, pixel_index); else if (render_data.render_settings.restir_gi_settings.debug_view == ReSTIRGIDebugView::M_COUNT) path_tracing_accumulate_color(render_data, ColorRGB32F(resampling_reservoir.M) * render_data.render_settings.restir_gi_settings.debug_view_scale_factor, pixel_index); else if (render_data.render_settings.restir_gi_settings.debug_view == ReSTIRGIDebugView::PER_PIXEL_REUSE_RADIUS && render_data.render_settings.restir_gi_settings.common_spatial_pass.per_pixel_spatial_reuse_radius != nullptr) { float radius_percentage = (render_data.render_settings.restir_gi_settings.common_spatial_pass.per_pixel_spatial_reuse_radius[pixel_index] / (float)render_data.render_settings.restir_gi_settings.common_spatial_pass.reuse_radius); ColorRGB32F debug_color = hippt::lerp(ColorRGB32F(2.0f, 0.0f, 0.0f), ColorRGB32F(0.0f, 2.0f, 0.0f), radius_percentage); debug_set_final_color(render_data, x, y, debug_color); } else if (render_data.render_settings.restir_gi_settings.debug_view == ReSTIRGIDebugView::PER_PIXEL_VALID_DIRECTIONS_PERCENTAGE && render_data.render_settings.restir_gi_settings.common_spatial_pass.per_pixel_spatial_reuse_radius != nullptr) { unsigned char accepted_directions = hippt::popc(ReSTIRSettingsHelper::get_spatial_reuse_direction_mask_ull(render_data, pixel_index)); float accepted_percentage = accepted_directions / 32.0f; ColorRGB32F debug_color = hippt::lerp(ColorRGB32F(2.0f, 0.0f, 0.0f), ColorRGB32F(0.0f, 2.0f, 0.0f), accepted_percentage); debug_set_final_color(render_data, x, y, debug_color); } else // Regular output path_tracing_accumulate_color(render_data, camera_outgoing_radiance, pixel_index); } #endif ================================================ FILE: src/Device/kernels/ReSTIR/GI/SpatialReuse.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_ReSTIR_GI_SPATIAL_REUSE_H #define DEVICE_ReSTIR_GI_SPATIAL_REUSE_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/ReSTIR/Jacobian.h" #include "Device/includes/ReSTIR/NeighborSimilarity.h" #include "Device/includes/ReSTIR/OptimalVisibilitySampling.h" #include "Device/includes/ReSTIR/SpatialMISWeight.h" #include "Device/includes/ReSTIR/SpatialNormalizationWeight.h" #include "Device/includes/ReSTIR/Utils.h" #include "Device/includes/ReSTIR/UtilsSpatial.h" #include "Device/includes/ReSTIR/GI/Reservoir.h" #include "Device/includes/ReSTIR/GI/TargetFunction.h" #include "HostDeviceCommon/KernelOptions/KernelOptions.h" #include "HostDeviceCommon/RenderData.h" /** References: * * [1] [ReSTIR GI: Path Resampling for Real-Time Path Tracing] https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing * [2] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time] https://intro-to-restir.cwyman.org/ * [3] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time - SIGGRAPH 2023 Presentation Video] https://dl.acm.org/doi/10.1145/3587423.3595511#sec-supp */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_GI_SpatialReuse(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_GI_SpatialReuse(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t center_pixel_index = (x + y * render_data.render_settings.render_resolution.x); int2 center_pixel_coords = make_int2(x, y); if (!render_data.aux_buffers.pixel_active[center_pixel_index] || render_data.g_buffer.first_hit_prim_index[center_pixel_index] == -1) { // Pixel inactive because of adaptive sampling, returning // Or also we don't have a primary hit render_data.render_settings.restir_gi_settings.spatial_pass.output_reservoirs[center_pixel_index] = ReSTIRGIReservoir(); return; } // Initializing the random generator unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(center_pixel_index + 1); else if (render_data.render_settings.restir_gi_settings.common_spatial_pass.coalesced_spatial_reuse) seed = wang_hash((render_data.render_settings.sample_number + 1) * render_data.random_number); else seed = wang_hash(((center_pixel_index + 1) * (render_data.render_settings.sample_number + 1)) * render_data.random_number); Xorshift32Generator random_number_generator(seed); ReSTIRGIReservoir* input_reservoir_buffer = render_data.render_settings.restir_gi_settings.spatial_pass.input_reservoirs; ReSTIRGIReservoir center_pixel_reservoir = input_reservoir_buffer[center_pixel_index]; if ((center_pixel_reservoir.M <= 1) && render_data.render_settings.restir_gi_settings.common_spatial_pass.do_disocclusion_reuse_boost) // Increasing the number of spatial samples for disocclusions render_data.render_settings.restir_gi_settings.common_spatial_pass.reuse_neighbor_count = render_data.render_settings.restir_gi_settings.common_spatial_pass.disocclusion_reuse_count; // Surface data of the center pixel ReSTIRSurface center_pixel_surface = get_pixel_surface(render_data, center_pixel_index, random_number_generator); setup_adaptive_directional_spatial_reuse(render_data, center_pixel_index, random_number_generator); // Only used with MIS-like weight int selected_neighbor = 0; int neighbor_heuristics_cache = 0; int valid_neighbors_count = 0; int valid_neighbors_M_sum = 0; count_valid_spatial_neighbors(render_data, center_pixel_surface, center_pixel_coords, valid_neighbors_count, valid_neighbors_M_sum, neighbor_heuristics_cache); int reused_neighbors_count = render_data.render_settings.restir_gi_settings.common_spatial_pass.reuse_neighbor_count; int start_index = 0; if (valid_neighbors_M_sum == 0) // No valid neighbor to resample from, skip to the initial candidate right away start_index = reused_neighbors_count; ReSTIRGIReservoir spatial_reuse_output_reservoir; ReSTIRSpatialResamplingMISWeight mis_weight_function; Xorshift32Generator spatial_neighbors_rng(render_data.render_settings.restir_gi_settings.common_spatial_pass.spatial_neighbors_rng_seed); // Resampling the neighbors. Using neighbors + 1 here so that // we can use the last iteration of the loop to resample ourselves (the center pixel) // // See the implementation of get_spatial_neighbor_pixel_index() in ReSTIR/UtilsSpatial.h for (int neighbor_index = start_index; neighbor_index < reused_neighbors_count + 1; neighbor_index++) { const bool is_center_pixel = neighbor_index == reused_neighbors_count; // We can already check whether or not this neighbor is going to be // accepted at all by checking the heuristic cache if (neighbor_index < reused_neighbors_count && reused_neighbors_count <= 32) { // If not the center pixel, we can check the heuristics, otherwise there's no need to, // we know that the center pixel will be accepted // // Our heuristics cache is a 32bit int so we can only cache 32 values are we're // going to have issues if we try to read more than that. if ((neighbor_heuristics_cache & (1 << neighbor_index)) == 0) { // Advancing the rng for generating the spatial neighbors since if we "continue" here, the spatial neighbors rng // isn't going to be advanced by the call to 'get_spatial_neighbor_pixel_index' below so we're doing it manually spatial_neighbor_advance_rng(render_data, spatial_neighbors_rng); // Neighbor not passing the heuristics tests, skipping it right away continue; } } int neighbor_pixel_index = get_spatial_neighbor_pixel_index(render_data, neighbor_index, center_pixel_coords, spatial_neighbors_rng); if (neighbor_pixel_index == -1) // Neighbor out of the viewport continue; if (!is_center_pixel && reused_neighbors_count > 32) // If not the center pixel, we can check the heuristics // // Only checking the heuristic if we have more than 32 neighbors (does not fit in the heuristic cache) // If we have less than 32 neighbors, we've already checked the cache at the beginning of this for loop if (!check_neighbor_similarity_heuristics(render_data, neighbor_pixel_index, center_pixel_index, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface))) continue; ReSTIRGIReservoir neighbor_reservoir = input_reservoir_buffer[neighbor_pixel_index]; float shift_mapping_jacobian = 1.0f; if (neighbor_reservoir.UCW > 0.0f && !is_center_pixel && !neighbor_reservoir.sample.is_envmap_path()) { // Only attempting the shift if the neighbor reservoir is valid // // Also, if this is the last neighbor resample (meaning that it is the center pixel), // the shift mapping is going to be an identity shift with a jacobian of 1 so we don't need to do it shift_mapping_jacobian = get_jacobian_determinant_reconnection_shift(neighbor_reservoir.sample.sample_point, neighbor_reservoir.sample.sample_point_geometric_normal.unpack(), center_pixel_surface.shading_point, render_data.g_buffer.primary_hit_position[neighbor_pixel_index], render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); } float target_function_at_center = 0.0f; bool do_neighbor_target_function_visibility = do_include_visibility_term_or_not(render_data, neighbor_index); if (neighbor_reservoir.UCW > 0.0f) { if (is_center_pixel) // No need to evaluate the center sample at the center pixel, that's exactly // the target function of the center reservoir target_function_at_center = neighbor_reservoir.sample.target_function; else { if (do_neighbor_target_function_visibility) target_function_at_center = ReSTIR_GI_evaluate_target_function(render_data, neighbor_reservoir.sample, center_pixel_surface, random_number_generator); else target_function_at_center = ReSTIR_GI_evaluate_target_function(render_data, neighbor_reservoir.sample, center_pixel_surface, random_number_generator); } } #if ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_1_OVER_M float mis_weight = mis_weight_function.get_resampling_MIS_weight(neighbor_reservoir.M); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_1_OVER_Z float mis_weight = mis_weight_function.get_resampling_MIS_weight(neighbor_reservoir.M); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_MIS_LIKE float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.M); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_MIS_GBH float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.UCW, neighbor_reservoir.sample, center_pixel_surface, neighbor_index, center_pixel_coords, random_number_generator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE bool update_mc = center_pixel_reservoir.M > 0 && center_pixel_reservoir.UCW > 0.0f; float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.M, neighbor_reservoir.sample.target_function, center_pixel_reservoir.sample, center_pixel_reservoir.M, center_pixel_reservoir.sample.target_function, neighbor_reservoir, center_pixel_surface, target_function_at_center * shift_mapping_jacobian, neighbor_pixel_index, valid_neighbors_count, valid_neighbors_M_sum, update_mc,/* resampling canonical */ is_center_pixel, random_number_generator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_ASYMMETRIC_RATIO bool update_mc = center_pixel_reservoir.M > 0 && center_pixel_reservoir.UCW > 0.0f; float mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, neighbor_reservoir.M, neighbor_reservoir.sample.target_function, center_pixel_reservoir.sample, center_pixel_reservoir.M, center_pixel_reservoir.sample.target_function, neighbor_reservoir, center_pixel_surface, target_function_at_center * shift_mapping_jacobian, neighbor_pixel_index, valid_neighbors_count, valid_neighbors_M_sum, update_mc,/* resampling canonical */ is_center_pixel, random_number_generator); #else #error "Unsupported bias correction mode" #endif // Combining as in Alg. 1 of the ReSTIR GI paper if (spatial_reuse_output_reservoir.combine_with(neighbor_reservoir, mis_weight, target_function_at_center, shift_mapping_jacobian, random_number_generator)) // Only used with MIS-like MIS weights selected_neighbor = neighbor_index; spatial_reuse_output_reservoir.sanity_check(center_pixel_coords); ReSTIR_optimal_visibility_sampling(render_data, spatial_reuse_output_reservoir, center_pixel_reservoir, center_pixel_surface, neighbor_index, reused_neighbors_count, random_number_generator); } float normalization_numerator = 1.0f; float normalization_denominator = 1.0f; ReSTIRSpatialNormalizationWeight normalization_function; #if ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_1_OVER_M normalization_function.get_normalization(render_data, spatial_reuse_output_reservoir.weight_sum, center_pixel_surface, center_pixel_coords, normalization_numerator, normalization_denominator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_1_OVER_Z normalization_function.get_normalization(render_data, spatial_reuse_output_reservoir.sample, spatial_reuse_output_reservoir.weight_sum, center_pixel_surface, center_pixel_coords, normalization_numerator, normalization_denominator, random_number_generator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_MIS_LIKE normalization_function.get_normalization(render_data, spatial_reuse_output_reservoir.sample, spatial_reuse_output_reservoir.weight_sum, center_pixel_surface, selected_neighbor, center_pixel_coords, normalization_numerator, normalization_denominator, random_number_generator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_MIS_GBH normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_ASYMMETRIC_RATIO normalization_function.get_normalization(normalization_numerator, normalization_denominator); #else #error "Unsupported bias correction mode" #endif spatial_reuse_output_reservoir.end_with_normalization(normalization_numerator, normalization_denominator); spatial_reuse_output_reservoir.sanity_check(center_pixel_coords); // Validating that the sample point resampled is visible from our visible point // TODO use a flag in the sample reservoir to indicate whether we are unoccluded or not // (we are always unoccluded if we resampled the canonical sample for example, in which case we don't have to do the validation) // It would also probably be beneficial to have another kernel do the validation such that samples that don't need the validation // (resampled the canonical neighbor) don't do the validation at all ReSTIR_GI_visibility_validation(render_data, spatial_reuse_output_reservoir, center_pixel_surface.shading_point, center_pixel_surface.primitive_index, random_number_generator); // M-capping so that we don't have to M-cap when reading reservoirs on the next frame if (render_data.render_settings.restir_gi_settings.m_cap > 0) // M-capping the spatial neighbor if an M-cap has been given spatial_reuse_output_reservoir.M = hippt::min(spatial_reuse_output_reservoir.M, render_data.render_settings.restir_gi_settings.m_cap); render_data.render_settings.restir_gi_settings.spatial_pass.output_reservoirs[center_pixel_index] = spatial_reuse_output_reservoir; } #endif ================================================ FILE: src/Device/kernels/ReSTIR/GI/TemporalReuse.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RESTIR_GI_SPATIAL_REUSE_H #define DEVICE_RESTIR_GI_SPATIAL_REUSE_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/PathTracing.h" #include "Device/includes/ReSTIR/Surface.h" #include "Device/includes/ReSTIR/TemporalMISWeight.h" #include "Device/includes/ReSTIR/TemporalNormalizationWeight.h" #include "Device/includes/ReSTIR/Utils.h" #include "Device/includes/ReSTIR/UtilsTemporal.h" #include "Device/includes/ReSTIR/GI/Reservoir.h" #include "HostDeviceCommon/RenderData.h" /** References: * * [1] [ReSTIR GI: Path Resampling for Real-Time Path Tracing] https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing * [2] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time] https://intro-to-restir.cwyman.org/ * [3] [A Gentle Introduction to ReSTIR: Path Reuse in Real-time - SIGGRAPH 2023 Presentation Video] https://dl.acm.org/doi/10.1145/3587423.3595511#sec-supp */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReSTIR_GI_TemporalReuse(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReSTIR_GI_TemporalReuse(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t center_pixel_index = (x + y * render_data.render_settings.render_resolution.x); if (!render_data.aux_buffers.pixel_active[center_pixel_index] || render_data.g_buffer.first_hit_prim_index[center_pixel_index] == -1) { // Pixel inactive because of adaptive sampling, returning // Or also we don't have a primary hit render_data.render_settings.restir_gi_settings.temporal_pass.output_reservoirs[center_pixel_index] = ReSTIRGIReservoir(); return; } if (render_data.render_settings.restir_gi_settings.common_temporal_pass.temporal_buffer_clear_requested) // We requested a temporal buffer clear for ReSTIR GI render_data.render_settings.restir_gi_settings.temporal_pass.input_reservoirs[center_pixel_index] = ReSTIRGIReservoir(); if (render_data.render_settings.sample_number == 0 && render_data.render_settings.accumulate) // First frame of accumulation, no temporal history, just outputting the initial candidates render_data.render_settings.restir_gi_settings.temporal_pass.output_reservoirs[center_pixel_index] = render_data.render_settings.restir_gi_settings.initial_candidates.initial_candidates_buffer[center_pixel_index]; // Initializing the random generator unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(center_pixel_index + 1); else seed = wang_hash((center_pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); // Surface data of the center pixel ReSTIRSurface center_pixel_surface = get_pixel_surface(render_data, center_pixel_index, random_number_generator); int temporal_neighbor_pixel_index = find_temporal_neighbor_index(render_data, center_pixel_surface.shading_point, ReSTIRSettingsHelper::get_normal_for_rejection_heuristic(render_data, center_pixel_surface), center_pixel_index, random_number_generator).x; if (temporal_neighbor_pixel_index == -1 || render_data.render_settings.freeze_random) { // Temporal occlusion / disoccusion, temporal neighbor is invalid, // we're only going to resample the initial candidates so let's set that as // the output right away // // We're also 'disabling' temporal accumulation if the random is frozen otherwise // very strong correlations will creep up, corrupt the render and potentially invalidate // performance measurements (which we're probably trying to measure since we froze the random) // The output of this temporal pass is just the initial candidates reservoir render_data.render_settings.restir_gi_settings.temporal_pass.output_reservoirs[center_pixel_index] = render_data.render_settings.restir_gi_settings.initial_candidates.initial_candidates_buffer[center_pixel_index]; return; } if (temporal_neighbor_pixel_index < 0 || temporal_neighbor_pixel_index >= render_data.render_settings.render_resolution.x * render_data.render_settings.render_resolution.y) return; ReSTIRGIReservoir temporal_neighbor_reservoir = render_data.render_settings.restir_gi_settings.temporal_pass.input_reservoirs[temporal_neighbor_pixel_index]; if (temporal_neighbor_reservoir.M == 0) { // No temporal neighbor, the output of this temporal pass is just the initial candidates reservoir render_data.render_settings.restir_gi_settings.temporal_pass.output_reservoirs[center_pixel_index] = render_data.render_settings.restir_gi_settings.initial_candidates.initial_candidates_buffer[center_pixel_index]; return; } ReSTIRGIReservoir temporal_reuse_output_reservoir; ReSTIRSurface temporal_neighbor_surface = get_pixel_surface(render_data, temporal_neighbor_pixel_index, render_data.render_settings.use_prev_frame_g_buffer(), random_number_generator); ReSTIRTemporalResamplingMISWeight mis_weight_function; // /* ------------------------------- */ // Resampling the temporal neighbor // /* ------------------------------- */ ReSTIRGIReservoir initial_candidates_reservoir = render_data.render_settings.restir_gi_settings.initial_candidates.initial_candidates_buffer[center_pixel_index]; if (temporal_neighbor_reservoir.M > 0) { float target_function_at_center = 0.0f; if (temporal_neighbor_reservoir.UCW > 0.0f) // Only resampling if the temporal neighbor isn't empty // // If the temporal neighbor's reservoir is empty, then we do not get // inside that if() and the target function stays at 0.0f which eliminates // most of the computations afterwards // // Matching the visibility used here with the bias correction mode for ease // of use (and because manually handling the visibility in the target // function of the temporal reuse is tricky for the user to use in // combination with other parameters target_function_at_center = ReSTIR_GI_evaluate_target_function(render_data, temporal_neighbor_reservoir.sample, center_pixel_surface, random_number_generator); float shift_mapping_jacobian = 1.0f; if (temporal_neighbor_reservoir.UCW > 0.0f && !temporal_neighbor_reservoir.sample.is_envmap_path()) { // Only attempting the shift if the neighbor reservoir is valid // // Also, if this is the last neighbor resample (meaning that it is the center pixel), // the shift mapping is going to be an identity shift with a jacobian of 1 so we don't need to do it shift_mapping_jacobian = get_jacobian_determinant_reconnection_shift(temporal_neighbor_reservoir.sample.sample_point, temporal_neighbor_reservoir.sample.sample_point_geometric_normal.unpack(), center_pixel_surface.shading_point, render_data.g_buffer_prev_frame.primary_hit_position[temporal_neighbor_pixel_index], render_data.render_settings.restir_gi_settings.get_jacobian_heuristic_threshold()); } #if ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_1_OVER_M float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(temporal_neighbor_reservoir); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_1_OVER_Z float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(temporal_neighbor_reservoir); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_MIS_LIKE float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_MIS_GBH float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir, initial_candidates_reservoir, temporal_neighbor_surface, center_pixel_surface, temporal_neighbor_reservoir.M, TEMPORAL_NEIGHBOR_ID, random_number_generator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir, initial_candidates_reservoir, center_pixel_surface, temporal_neighbor_surface, target_function_at_center * shift_mapping_jacobian, TEMPORAL_NEIGHBOR_ID, random_number_generator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_ASYMMETRIC_RATIO float temporal_neighbor_resampling_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir, initial_candidates_reservoir, center_pixel_surface, temporal_neighbor_surface, target_function_at_center * shift_mapping_jacobian, TEMPORAL_NEIGHBOR_ID, random_number_generator); #else #error "Unsupported bias correction mode" #endif // Combining as in Alg. 6 of the paper temporal_reuse_output_reservoir.combine_with(temporal_neighbor_reservoir, temporal_neighbor_resampling_mis_weight, target_function_at_center, shift_mapping_jacobian, random_number_generator); temporal_reuse_output_reservoir.sanity_check(make_int2(x, y)); } // /* ------------------------------- */ // Resampling the initial candidates // /* ------------------------------- */ #if ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_1_OVER_M float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(initial_candidates_reservoir); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_1_OVER_Z float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(initial_candidates_reservoir); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_MIS_LIKE float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, initial_candidates_reservoir); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_MIS_GBH float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, initial_candidates_reservoir, initial_candidates_reservoir, temporal_neighbor_surface, center_pixel_surface, temporal_neighbor_reservoir.M, INITIAL_CANDIDATES_ID, random_number_generator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir, initial_candidates_reservoir, center_pixel_surface, temporal_neighbor_surface, /* unused */ 0.0f, INITIAL_CANDIDATES_ID, random_number_generator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_ASYMMETRIC_RATIO float initial_candidates_mis_weight = mis_weight_function.get_resampling_MIS_weight(render_data, temporal_neighbor_reservoir, initial_candidates_reservoir, center_pixel_surface, temporal_neighbor_surface, /* unused */ 0.0f, INITIAL_CANDIDATES_ID, random_number_generator); #else #error "Unsupported bias correction mode" #endif temporal_reuse_output_reservoir.combine_with(initial_candidates_reservoir, initial_candidates_mis_weight, initial_candidates_reservoir.sample.target_function, /* jacobian is 1 when reusing at the exact same spot */ 1.0f, random_number_generator); temporal_reuse_output_reservoir.sanity_check(make_int2(x, y)); float normalization_numerator = 1.0f; float normalization_denominator = 1.0f; ReSTIRTemporalNormalizationWeight normalization_function; #if ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_MIS_GBH normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE normalization_function.get_normalization(normalization_numerator, normalization_denominator); #elif ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_SYMMETRIC_RATIO || ReSTIR_GI_BiasCorrectionWeights == RESTIR_GI_BIAS_CORRECTION_ASYMMETRIC_RATIO normalization_function.get_normalization(normalization_numerator, normalization_denominator); #else #error "Unsupported bias correction mode" #endif temporal_reuse_output_reservoir.end_with_normalization(normalization_numerator, normalization_denominator); temporal_reuse_output_reservoir.sanity_check(make_int2(x, y)); // M-capping so that we don't have to M-cap when reading reservoirs on the next frame if (render_data.render_settings.restir_gi_settings.m_cap > 0) // M-capping the temporal neighbor if an M-cap has been given temporal_reuse_output_reservoir.M = hippt::min(temporal_reuse_output_reservoir.M, render_data.render_settings.restir_gi_settings.m_cap); render_data.render_settings.restir_gi_settings.temporal_pass.output_reservoirs[center_pixel_index] = temporal_reuse_output_reservoir; } #endif ================================================ FILE: src/Device/kernels/ReSTIR/ReGIR/GridFillTemporalReuse.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_GRID_FILL_H #define DEVICE_KERNELS_REGIR_GRID_FILL_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/ReSTIR/ReGIR/Settings.h" #include "Device/includes/ReSTIR/ReGIR/TargetFunction.h" #include "HostDeviceCommon/KernelOptions/ReGIROptions.h" #include "HostDeviceCommon/RenderData.h" HIPRT_DEVICE LightSampleInformation sample_one_presampled_light(const HIPRTRenderData& render_data, const ReGIRSettings& regir_settings, unsigned int hash_grid_cell_index, int reservoir_index_in_cell, bool primary_hit, Xorshift32Generator& rng) { float presampled_light_pdf; ReGIRPresampledLight light_sample = regir_settings.sample_one_presampled_light(hash_grid_cell_index, reservoir_index_in_cell, primary_hit, presampled_light_pdf, rng); LightSampleInformation full_sample_information; full_sample_information.emissive_triangle_index = light_sample.emissive_triangle_index; full_sample_information.light_source_normal = light_sample.normal.unpack(); full_sample_information.light_area = light_sample.triangle_area; //full_sample_information.emission = light_sample.emission; full_sample_information.emission = render_data.buffers.materials_buffer.get_emission(render_data.buffers.material_indices[light_sample.emissive_triangle_index]); full_sample_information.point_on_light = light_sample.point_on_light; // PDF of that point on that triangle full_sample_information.area_measure_pdf = 1.0f / full_sample_information.light_area; #if ReGIR_GridFillLightSamplingBaseStrategy == LSS_BASE_UNIFORM // PDF of sampling that triangle uniformly full_sample_information.area_measure_pdf *= 1.0f / render_data.buffers.emissive_triangles_count; #elif ReGIR_GridFillLightSamplingBaseStrategy == LSS_BASE_POWER // PDF of sampling that triangle according to its power full_sample_information.area_measure_pdf *= (full_sample_information.emission.luminance() * full_sample_information.light_area) / render_data.buffers.emissives_power_alias_table.sum_elements; #endif return full_sample_information; } template HIPRT_DEVICE ReGIRReservoir grid_fill(const HIPRTRenderData& render_data, const ReGIRSettings& regir_settings, unsigned int hash_grid_cell_index, int reservoir_index_in_cell, const ReGIRGridFillSurface& surface, bool primary_hit, Xorshift32Generator& rng) { ReGIRReservoir grid_fill_reservoir; bool reservoir_is_canonical = regir_settings.get_grid_fill_settings(primary_hit).reservoir_index_in_cell_is_canonical(reservoir_index_in_cell); int retries = 0; for (int light_sample_index = 0; light_sample_index < regir_settings.get_grid_fill_settings(primary_hit).light_sample_count_per_cell_reservoir; light_sample_index++) { LightSampleInformation light_sample; if constexpr (ReGIR_GridFillDoLightPresampling == KERNEL_OPTION_TRUE && !accumulatePreIntegration) // Never using presampling lights for pre integration because pre integration needs // different samples to pre integrate properly and using presampled lights severely restricts // the number of different samples we have available light_sample = sample_one_presampled_light(render_data, regir_settings, hash_grid_cell_index, reservoir_index_in_cell, primary_hit, rng); else light_sample = sample_one_emissive_triangle(render_data, rng); if (light_sample.emissive_triangle_index == -1) continue; float target_function; if (reservoir_is_canonical) // This reservoir is canonical, simple target function to keep it canonical (no visibility / cosine terms) target_function = ReGIR_grid_fill_evaluate_canonical_target_function(render_data, surface, primary_hit, light_sample.emission, light_sample.light_source_normal, light_sample.point_on_light, rng); else target_function = ReGIR_grid_fill_evaluate_non_canonical_target_function(render_data, surface, primary_hit, light_sample.emission, light_sample.light_source_normal, light_sample.point_on_light, rng); float mis_weight = 1.0f / regir_settings.get_grid_fill_settings(primary_hit).light_sample_count_per_cell_reservoir; float source_pdf = light_sample.area_measure_pdf; grid_fill_reservoir.stream_sample(mis_weight, target_function, source_pdf, light_sample, rng); } return grid_fill_reservoir; } template HIPRT_DEVICE void grid_fill_pre_integration_accumulation(HIPRTRenderData& render_data, const ReGIRReservoir& output_reservoir, bool reservoir_is_canonical, unsigned int hash_grid_cell_index, bool primary_hit) { if constexpr (accumulatePreIntegration) { ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; // Only doing the pre integration on the first sample of the frame // and if we don't have spatial reuse. If we have the spatial reuse, it's // the spatial reuse pass that will do the pre integration accumulation if (!regir_settings.spatial_reuse.do_spatial_reuse) { float normalization; if (reservoir_is_canonical) normalization = regir_settings.get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell() * render_data.render_settings.DEBUG_REGIR_PRE_INTEGRATION_ITERATIONS; else normalization = regir_settings.get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell() * render_data.render_settings.DEBUG_REGIR_PRE_INTEGRATION_ITERATIONS; float integration_increment = hippt::max(0.0f, output_reservoir.sample.target_function * output_reservoir.UCW) / normalization; if (reservoir_is_canonical) hippt::atomic_fetch_add(®ir_settings.get_canonical_pre_integration_factor_buffer(primary_hit)[hash_grid_cell_index], integration_increment); else hippt::atomic_fetch_add(®ir_settings.get_non_canonical_pre_integration_factor_buffer(primary_hit)[hash_grid_cell_index], integration_increment); } } } /** * This kernel is in charge of resetting (when necessary) and filling the ReGIR grid. * * This kernel also does the temporal reuse if enabled. */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) ReGIR_Grid_Fill_Temporal_Reuse(HIPRTRenderData render_data, ReGIRHashGridSoADevice output_reservoirs_grid, unsigned int number_of_cells_alive, bool primary_hit) #else template GLOBAL_KERNEL_SIGNATURE(void) inline ReGIR_Grid_Fill_Temporal_Reuse(HIPRTRenderData render_data, ReGIRHashGridSoADevice output_reservoirs_grid, int thread_index, unsigned int number_of_cells_alive, bool primary_hit) #endif { if (render_data.buffers.emissive_triangles_count == 0) // No initial candidates to sample since no lights return; ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; #ifdef __KERNELCC__ uint32_t thread_index = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t thread_count = gridDim.x * blockDim.x; #endif while (thread_index < regir_settings.get_number_of_reservoirs_per_cell(primary_hit) * number_of_cells_alive) { int reservoir_index = thread_index; unsigned int reservoir_index_in_cell = reservoir_index % regir_settings.get_number_of_reservoirs_per_cell(primary_hit); unsigned int cell_alive_index = reservoir_index / regir_settings.get_number_of_reservoirs_per_cell(primary_hit); // If all cells are alive, the cell index is straightforward // // Not all cells are alive, what we have is cell_alive_index which is the index of the cell in the alive list // so we can fetch the index of the cell in the grid cells alive list with that cell_alive_index unsigned int hash_grid_cell_index = regir_settings.get_hash_cell_data_soa(primary_hit).grid_cells_alive_list[cell_alive_index]; unsigned int reservoir_index_in_grid = hash_grid_cell_index * regir_settings.get_number_of_reservoirs_per_cell(primary_hit) + reservoir_index_in_cell; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(reservoir_index_in_grid + 1); else seed = wang_hash((reservoir_index_in_grid + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); ReGIRReservoir output_reservoir; ReGIRGridFillSurface cell_surface = ReGIR_get_cell_surface(render_data, hash_grid_cell_index, primary_hit); // Grid fill #ifdef __KERNELCC__ constexpr bool ACCUMULATE_PRE_INTEGRATION_OPTION = ReGIR_GridFillSpatialReuse_AccumulatePreIntegration; #else constexpr bool ACCUMULATE_PRE_INTEGRATION_OPTION = accumulatePreIntegration; #endif output_reservoir = grid_fill(render_data, regir_settings, hash_grid_cell_index, reservoir_index_in_cell, cell_surface, primary_hit, random_number_generator); // Normalizing the reservoir output_reservoir.finalize_resampling(1.0f, 1.0f); regir_settings.store_reservoir_custom_buffer_opt(output_reservoirs_grid, output_reservoir, hash_grid_cell_index, reservoir_index_in_cell); grid_fill_pre_integration_accumulation(render_data, output_reservoir, regir_settings.get_grid_fill_settings(primary_hit).reservoir_index_in_cell_is_canonical(reservoir_index_in_cell), hash_grid_cell_index, primary_hit); #ifndef __KERNELCC__ // We're dispatching exactly one thread per reservoir to compute on the CPU so no need // for the work queue style of things that is only needed on the GPU, we can just exit here break; #else // We need to compute the next reservoir index for the next iteration thread_index += thread_count; #endif } } #endif ================================================ FILE: src/Device/kernels/ReSTIR/ReGIR/GridPrepopulate.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_REGIR_GRID_PREPOPULATE_H #define KERNELS_REGIR_GRID_PREPOPULATE_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/PathTracing.h" #include "Device/includes/RayPayload.h" #include "Device/includes/SanityCheck.h" #include "HostDeviceCommon/Xorshift.h" #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReGIR_Grid_Prepopulate(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReGIR_Grid_Prepopulate(HIPRTRenderData render_data, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = (blockIdx.x * blockDim.x + threadIdx.x) * ReGIR_GridPrepopulationResolutionDownscale; const uint32_t y = (blockIdx.y * blockDim.y + threadIdx.y) * ReGIR_GridPrepopulationResolutionDownscale; #endif if (x >= render_data.render_settings.render_resolution.x || y >= render_data.render_settings.render_resolution.y) return; uint32_t pixel_index = x + y * render_data.render_settings.render_resolution.x; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(pixel_index + 1); else seed = wang_hash((pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); // Direction to the center of the pixel float x_ray_point_direction = (x + 0.5f); float y_ray_point_direction = (y + 0.5f); if (render_data.current_camera.do_jittering) { // Jitter randomly around the center x_ray_point_direction += random_number_generator() - 0.5f; y_ray_point_direction += random_number_generator() - 0.5f; } hiprtRay camera_ray = render_data.current_camera.get_camera_ray(x_ray_point_direction, y_ray_point_direction, render_data.render_settings.render_resolution); RayPayload ray_payload; HitInfo closest_hit_info; bool intersection_found = trace_main_path_ray(render_data, camera_ray, ray_payload, closest_hit_info, /* camera ray = no previous primitive hit */ -1, /* bounce. Always 0 for camera rays*/ 0, random_number_generator); if (!intersection_found) return; ReGIR_update_representative_data(render_data, closest_hit_info.inter_point, closest_hit_info.geometric_normal, render_data.current_camera, closest_hit_info.primitive_index, true, ray_payload.material); for (int& bounce = ray_payload.bounce; bounce < render_data.render_settings.nb_bounces + 1; bounce++) { if (ray_payload.next_ray_state != RayState::MISSED) { if (bounce > 0) intersection_found = path_tracing_find_indirect_bounce_intersection(render_data, camera_ray, ray_payload, closest_hit_info, random_number_generator); if (intersection_found) { if (bounce > 0) { bool ReGIR_primary_hit = render_data.render_settings.regir_settings.compute_is_primary_hit(ray_payload); // Storing data for ReGIR representative points ReGIR_update_representative_data(render_data, closest_hit_info.inter_point, closest_hit_info.geometric_normal, render_data.current_camera, closest_hit_info.primitive_index, ReGIR_primary_hit, ray_payload.material); } BSDFIncidentLightInfo sampled_light_info; // This variable is never used, this is just for debugging on the CPU so that we know what the BSDF sampled bool valid_indirect_bounce = path_tracing_compute_next_indirect_bounce(render_data, ray_payload, closest_hit_info, -camera_ray.direction, camera_ray, random_number_generator, &sampled_light_info); if (!valid_indirect_bounce) // Bad BSDF sample (under the surface), killed by russian roulette, ... break; } else return; } } } #endif ================================================ FILE: src/Device/kernels/ReSTIR/ReGIR/LightPresampling.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_LIGHT_PRESAMPLING_H #define DEVICE_KERNELS_REGIR_LIGHT_PRESAMPLING_H #include "Device/includes/LightSampling/LightUtils.h" #include "HostDeviceCommon/RenderData.h" /** * This kernel inserts the keys of the input hash table into the output hash table * * This is used when the hash table has been resized and we need to re-insert the keys * of the old (smaller) hash table into the new (larger) hash table */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) ReGIR_Light_Presampling(HIPRTRenderData render_data) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReGIR_Light_Presampling(HIPRTRenderData render_data, int thread_index) #endif { ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; #ifdef __KERNELCC__ const uint32_t thread_index = blockIdx.x * blockDim.x + threadIdx.x; #endif if (thread_index >= render_data.render_settings.regir_settings.presampled_lights.get_presampled_light_count()) return; Xorshift32Generator rng(wang_hash(thread_index ^ render_data.random_number)); LightSampleInformation light_sample = sample_one_emissive_triangle(render_data, rng); ReGIRPresampledLight presampled_light; presampled_light.emissive_triangle_index = light_sample.emissive_triangle_index; presampled_light.point_on_light = light_sample.point_on_light; presampled_light.normal.pack(light_sample.light_source_normal); presampled_light.triangle_area = light_sample.light_area; presampled_light.emission = light_sample.emission; render_data.render_settings.regir_settings.presampled_lights.store_one_presampled_light(presampled_light, thread_index); } #endif ================================================ FILE: src/Device/kernels/ReSTIR/ReGIR/PreIntegration.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_REGIR_PRE_INTEGRATION_H #define KERNELS_REGIR_PRE_INTEGRATION_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/PathTracing.h" #include "Device/includes/RayPayload.h" #include "Device/includes/SanityCheck.h" #include "HostDeviceCommon/Xorshift.h" #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) __launch_bounds__(64) ReGIR_Pre_integration(HIPRTRenderData render_data, unsigned int number_of_cells_alive, bool primary_hit) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReGIR_Pre_integration(HIPRTRenderData render_data, unsigned int number_of_cells_alive, bool primary_hit, int thread_index) #endif { if (render_data.buffers.emissive_triangles_count == 0) // No initial candidates to sample since no lights return; ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; #ifdef __KERNELCC__ uint32_t thread_index = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t thread_count = gridDim.x * blockDim.x; #endif while (thread_index < number_of_cells_alive) { int cell_alive_index = thread_index; unsigned int hash_grid_cell_index = number_of_cells_alive == regir_settings.get_total_number_of_cells_per_grid(primary_hit) ? cell_alive_index : regir_settings.get_hash_cell_data_soa(primary_hit).grid_cells_alive_list[cell_alive_index]; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(hash_grid_cell_index + 1); else seed = wang_hash((hash_grid_cell_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); ReGIRGridFillSurface surface = ReGIR_get_cell_surface(render_data, hash_grid_cell_index, primary_hit); // This kernel always uses a Lambertian BRDF where the view direction is not used so it can be set to zero float3 view_direction = make_float3(0.0f, 0.0f, 0.0f); float non_canonical_cell_integration_sum = 0.0f; for (int i = 0; i < regir_settings.get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell(); i++) { bool invalid_sample = false; ReGIRReservoir non_canonical_reservoir = regir_settings.get_cell_non_canonical_reservoir_from_index(hash_grid_cell_index, primary_hit, i, &invalid_sample); if (invalid_sample || non_canonical_reservoir.UCW <= 0.0f) continue; LightSampleInformation light_sample; light_sample.area_measure_pdf = 1.0f / non_canonical_reservoir.UCW; light_sample.emission = get_emission_of_triangle_from_index(render_data, non_canonical_reservoir.sample.emissive_triangle_index); light_sample.emissive_triangle_index = non_canonical_reservoir.sample.emissive_triangle_index; light_sample.light_area = triangle_area(render_data, non_canonical_reservoir.sample.emissive_triangle_index); light_sample.light_source_normal = hippt::normalize(get_triangle_normal_not_normalized(render_data, non_canonical_reservoir.sample.emissive_triangle_index)); light_sample.point_on_light = non_canonical_reservoir.sample.point_on_light; if (light_sample.area_measure_pdf <= 0.0f) // Can happen for very small triangles continue; float non_canonical_target_function = ReGIR_grid_fill_evaluate_non_canonical_target_function(render_data, surface, primary_hit, light_sample.emission, light_sample.light_source_normal, light_sample.point_on_light, random_number_generator); if (non_canonical_target_function <= 0.0f) continue; non_canonical_cell_integration_sum += non_canonical_target_function * non_canonical_reservoir.UCW; } regir_settings.get_non_canonical_pre_integration_factor_buffer(primary_hit)[hash_grid_cell_index] += non_canonical_cell_integration_sum / (regir_settings.get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell()) / render_data.render_settings.DEBUG_REGIR_PRE_INTEGRATION_ITERATIONS; float canonical_cell_integration_sum = 0.0f; for (int i = 0; i < regir_settings.get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell(); i++) { bool invalid_sample = false; ReGIRReservoir canonical_reservoir = regir_settings.get_cell_canonical_reservoir_from_index(hash_grid_cell_index, primary_hit, i, &invalid_sample); if (invalid_sample || canonical_reservoir.UCW <= 0.0f) continue; LightSampleInformation light_sample; light_sample.area_measure_pdf = 1.0f / canonical_reservoir.UCW; light_sample.emission = get_emission_of_triangle_from_index(render_data, canonical_reservoir.sample.emissive_triangle_index); light_sample.emissive_triangle_index = canonical_reservoir.sample.emissive_triangle_index; light_sample.light_area = triangle_area(render_data, canonical_reservoir.sample.emissive_triangle_index); light_sample.light_source_normal = hippt::normalize(get_triangle_normal_not_normalized(render_data, canonical_reservoir.sample.emissive_triangle_index)); light_sample.point_on_light = canonical_reservoir.sample.point_on_light; if (light_sample.area_measure_pdf <= 0.0f) // Can happen for very small triangles continue; float canonical_target_function = ReGIR_grid_fill_evaluate_canonical_target_function(render_data, surface, primary_hit, light_sample.emission, light_sample.light_source_normal, light_sample.point_on_light, random_number_generator); if (canonical_target_function <= 0.0f) continue; canonical_cell_integration_sum += canonical_target_function * canonical_reservoir.UCW; } regir_settings.get_canonical_pre_integration_factor_buffer(primary_hit)[hash_grid_cell_index] += canonical_cell_integration_sum / (regir_settings.get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell()) / render_data.render_settings.DEBUG_REGIR_PRE_INTEGRATION_ITERATIONS; #ifndef __KERNELCC__ // We're dispatching exactly one thread per reservoir to compute on the CPU so no need // for the work queue style of things that is only needed on the GPU, we can just exit here break; #else // We need to compute the next reservoir index for the next iteration thread_index += thread_count; #endif } } #endif ================================================ FILE: src/Device/kernels/ReSTIR/ReGIR/Rehash.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_REHASH_KERNEL_H #define DEVICE_KERNELS_REGIR_REHASH_KERNEL_H #include "Device/includes/ReSTIR/ReGIR/Settings.h" /** * This kernel inserts the keys of the input hash table into the output hash table * * This is used when the hash table has been resized and we need to re-insert the keys * of the old (smaller) hash table into the new (larger) hash table */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) ReGIR_Rehash( HIPRTCamera current_camera, ReGIRHashGrid new_hash_grid, ReGIRHashGridSoADevice new_hash_grid_soa, ReGIRHashCellDataSoADevice new_hash_cell_data, ReGIRHashCellDataSoADevice old_hash_cell_data, unsigned int* old_grid_cells_alive_list, unsigned int old_cell_count, bool primary_hit) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReGIR_Rehash( HIPRTCamera current_camera, ReGIRHashGrid new_hash_grid, ReGIRHashGridSoADevice new_hash_grid_soa, ReGIRHashCellDataSoADevice new_hash_cell_data, ReGIRHashCellDataSoADevice old_hash_cell_data, unsigned int* old_grid_cells_alive_list, unsigned int old_cell_count, bool primary_hit, unsigned int cell_index ) #endif { #ifdef __KERNELCC__ const uint32_t cell_index = blockIdx.x * blockDim.x + threadIdx.x; #endif if (cell_index >= old_cell_count) return; unsigned int cell_alive_index = old_grid_cells_alive_list[cell_index]; float3 world_position = old_hash_cell_data.world_points[cell_alive_index]; float3 shading_normal = old_hash_cell_data.world_normals[cell_alive_index].unpack(); int primitive_index = old_hash_cell_data.hit_primitive[cell_alive_index]; DeviceUnpackedEffectiveMaterial material; material.roughness = old_hash_cell_data.roughness[cell_alive_index] / 255.0f; material.metallic = old_hash_cell_data.metallic[cell_alive_index] / 255.0f; material.specular = old_hash_cell_data.specular[cell_alive_index] / 255.0f; ReGIRSettings::insert_hash_cell_data_static( new_hash_grid, new_hash_grid_soa, new_hash_cell_data, world_position, shading_normal, current_camera, primitive_index, primary_hit, material); } #endif ================================================ FILE: src/Device/kernels/ReSTIR/ReGIR/SpatialReuse.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_SPATIAL_REUSE_H #define DEVICE_KERNELS_REGIR_SPATIAL_REUSE_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/LightSampling/LightUtils.h" #include "Device/includes/ReSTIR/ReGIR/TargetFunction.h" #include "HostDeviceCommon/RenderData.h" #ifndef __KERNELCC__ #include "omp.h" #endif HIPRT_DEVICE unsigned int get_random_neighbor_hash_grid_cell_index_with_retries(HIPRTRenderData& render_data, bool primary_hit, float3 point_in_center_cell, float3 center_cell_normal, float center_cell_roughness, Xorshift32Generator& spatial_neighbor_rng) { ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; unsigned int neighbor_hash_grid_cell_index_in_grid; bool neighbor_invalid = true; int retry = 0; while (retry < regir_settings.spatial_reuse.retries_per_neighbor && neighbor_invalid) { float3 random_neighbor = make_float3(spatial_neighbor_rng(), spatial_neighbor_rng(), spatial_neighbor_rng()); float3 offset_float_radius_1 = random_neighbor * 2.0f - 1.0f; float3 offset_float_radius = offset_float_radius_1 * regir_settings.spatial_reuse.spatial_reuse_radius; float3 offset = make_float3(roundf(offset_float_radius.x), roundf(offset_float_radius.y), roundf(offset_float_radius.z)); float3 point_in_neighbor_cell = point_in_center_cell + offset * regir_settings.get_cell_size(point_in_center_cell, render_data.current_camera, center_cell_roughness, primary_hit); neighbor_hash_grid_cell_index_in_grid = regir_settings.get_hash_grid_cell_index_from_world_pos(point_in_neighbor_cell, center_cell_normal, render_data.current_camera, center_cell_roughness, primary_hit); if (neighbor_hash_grid_cell_index_in_grid != HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX && regir_settings.get_hash_cell_data_soa(primary_hit).grid_cell_alive[neighbor_hash_grid_cell_index_in_grid]) // Neighbor is inside of the grid and alive, we can use it neighbor_invalid = false; retry++; } if (neighbor_invalid) // We couldn't find a good neighbor return HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX; return neighbor_hash_grid_cell_index_in_grid; } HIPRT_DEVICE ReGIRReservoir spatial_reuse(HIPRTRenderData& render_data, ReGIRHashGridSoADevice& input_reservoirs, int reservoir_index_in_cell, int hash_grid_cell_index, bool primary_hit, float3 center_cell_point, float3 center_cell_normal, float center_cell_roughness, Xorshift32Generator& spatial_neighbor_rng, Xorshift32Generator& random_number_generator) { ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; ReGIRReservoir output_reservoir; int selected = 0; for (int neighbor_index = 0; neighbor_index < regir_settings.spatial_reuse.spatial_neighbor_count + 1; neighbor_index++) { bool is_center_cell = neighbor_index == regir_settings.spatial_reuse.spatial_neighbor_count; // Getting a random neighbor and retrying a certain amount of times // in case the neighbor that we picked was out of the grid, in a dead cell, ... // // This is to have more chance to get a reusable neighbor --> more reuse --> less variance float3 random_neighbor; int neighbor_hash_grid_cell_index_in_grid; if (is_center_cell) neighbor_hash_grid_cell_index_in_grid = hash_grid_cell_index; else { neighbor_hash_grid_cell_index_in_grid = get_random_neighbor_hash_grid_cell_index_with_retries(render_data, primary_hit, center_cell_point, center_cell_normal, center_cell_roughness, spatial_neighbor_rng); if (neighbor_hash_grid_cell_index_in_grid == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) // Could not find a valid neighbor continue; } for (int neighbor_reuse = 0; neighbor_reuse < regir_settings.spatial_reuse.reuse_per_neighbor_count; neighbor_reuse++) { // Picking a random reservoir in the neighbor cell // If our reservoir is canonical, we pick a random canonical reservoir in the neighbor cell. // Same for non-canonical int random_reservoir_index_in_cell; if (regir_settings.get_grid_fill_settings(primary_hit).reservoir_index_in_cell_is_canonical(reservoir_index_in_cell)) random_reservoir_index_in_cell = random_number_generator() * regir_settings.get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell() + regir_settings.get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell(); else random_reservoir_index_in_cell = random_number_generator() * regir_settings.get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell(); ReGIRReservoir neighbor_reservoir = regir_settings.get_reservoir_from_grid_cell_index(input_reservoirs, neighbor_hash_grid_cell_index_in_grid, random_reservoir_index_in_cell); if (neighbor_reservoir.UCW <= 0.0f) continue; ColorRGB32F emission = get_emission_of_triangle_from_index(render_data, neighbor_reservoir.sample.emissive_triangle_index); float3 point_on_light = neighbor_reservoir.sample.point_on_light; float3 light_source_normal = get_triangle_normal_not_normalized(render_data, neighbor_reservoir.sample.emissive_triangle_index); float light_source_area = hippt::length(light_source_normal) * 0.5f; light_source_normal /= light_source_area * 2.0f; float target_function_at_center; if (regir_settings.get_grid_fill_settings(primary_hit).reservoir_index_in_cell_is_canonical(reservoir_index_in_cell)) target_function_at_center = ReGIR_grid_fill_evaluate_canonical_target_function(render_data, hash_grid_cell_index, primary_hit, emission, light_source_normal, point_on_light, random_number_generator); else target_function_at_center = ReGIR_grid_fill_evaluate_non_canonical_target_function(render_data, hash_grid_cell_index, primary_hit, emission, light_source_normal, point_on_light, random_number_generator); // MIS weight is 1.0f because we're going to normalize at the end instead of during the resampling float mis_weight = 1.0f; output_reservoir.stream_reservoir(mis_weight, target_function_at_center, neighbor_reservoir, random_number_generator); } } return output_reservoir; } HIPRT_DEVICE int spatial_reuse_mis_weight(HIPRTRenderData& render_data, const ReGIRReservoir& output_reservoir, int reservoir_index_in_cell, int hash_grid_cell_index, bool primary_hit, float3 center_cell_point, float3 center_cell_normal, float center_cell_roughness, Xorshift32Generator& spatial_neighbor_rng, Xorshift32Generator& random_number_generator) { ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; // Now counting the number of neighbors that could have produced this sample for the MIS weight // This is 1/Z MIS weights int valid_neighbor_count = 0; if (output_reservoir.weight_sum > 0.0f) { ColorRGB32F emission = get_emission_of_triangle_from_index(render_data, output_reservoir.sample.emissive_triangle_index); float3 point_on_light = output_reservoir.sample.point_on_light; float3 light_source_normal = get_triangle_normal_not_normalized(render_data, output_reservoir.sample.emissive_triangle_index); float light_source_area = hippt::length(light_source_normal) * 0.5f; light_source_normal /= light_source_area * 2.0f; for (int neighbor_index = 0; neighbor_index < regir_settings.spatial_reuse.spatial_neighbor_count + 1; neighbor_index++) { bool is_center_cell = neighbor_index == regir_settings.spatial_reuse.spatial_neighbor_count; int neighbor_hash_grid_cell_index_in_grid; if (is_center_cell) neighbor_hash_grid_cell_index_in_grid = hash_grid_cell_index; else { neighbor_hash_grid_cell_index_in_grid = get_random_neighbor_hash_grid_cell_index_with_retries(render_data, primary_hit, center_cell_point, center_cell_normal, center_cell_roughness, spatial_neighbor_rng); if (neighbor_hash_grid_cell_index_in_grid == HashGrid::UNDEFINED_CHECKSUM_OR_GRID_INDEX) // Could not find a valid neighbor continue; } if (regir_settings.get_grid_fill_settings(primary_hit).reservoir_index_in_cell_is_canonical(reservoir_index_in_cell)) // A canonical reservoir can always be produced by anyone valid_neighbor_count += regir_settings.spatial_reuse.reuse_per_neighbor_count; else { // Non-canonical sample, we need to count how many neighbors could have produced it if (ReGIR_grid_fill_evaluate_non_canonical_target_function(render_data, neighbor_hash_grid_cell_index_in_grid, primary_hit, emission, light_source_normal, point_on_light, random_number_generator) > 0.0f) valid_neighbor_count += regir_settings.spatial_reuse.reuse_per_neighbor_count; } } } return valid_neighbor_count; } template HIPRT_DEVICE HIPRT_INLINE void spatial_reuse_pre_integration_accumulation(HIPRTRenderData& render_data, const ReGIRReservoir& output_reservoir, bool reservoir_is_canonical, unsigned int hash_grid_cell_index, bool primary_hit) { if constexpr (accumulatePreIntegration) { if (render_data.render_settings.regir_settings.spatial_reuse.spatial_reuse_pass_index == render_data.render_settings.regir_settings.spatial_reuse.spatial_reuse_pass_count - 1) { // Only accumulating pre-integration factors on the last spatial reuse pass ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; // Only doing the pre integration on the first sample of the frame float normalization; if (reservoir_is_canonical) normalization = regir_settings.get_grid_fill_settings(primary_hit).get_canonical_reservoir_count_per_cell() * render_data.render_settings.DEBUG_REGIR_PRE_INTEGRATION_ITERATIONS; else normalization = regir_settings.get_grid_fill_settings(primary_hit).get_non_canonical_reservoir_count_per_cell() * render_data.render_settings.DEBUG_REGIR_PRE_INTEGRATION_ITERATIONS; float integration_increment = hippt::max(0.0f, output_reservoir.sample.target_function * output_reservoir.UCW) / normalization; if (reservoir_is_canonical) hippt::atomic_fetch_add(®ir_settings.get_canonical_pre_integration_factor_buffer(primary_hit)[hash_grid_cell_index], integration_increment); else hippt::atomic_fetch_add(®ir_settings.get_non_canonical_pre_integration_factor_buffer(primary_hit)[hash_grid_cell_index], integration_increment); } } } /** * This kernel is in charge of the spatial reuse on the ReGIR grid. * * Each cell reuses from random cells adjacent to it */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) ReGIR_Spatial_Reuse(HIPRTRenderData render_data, ReGIRHashGridSoADevice input_reservoirs_grid, ReGIRHashGridSoADevice output_reservoirs_grid, ReGIRHashCellDataSoADevice output_reservoirs_hash_cell_data, unsigned int number_of_cells_alive, bool primary_hit) #else template GLOBAL_KERNEL_SIGNATURE(void) inline ReGIR_Spatial_Reuse(HIPRTRenderData render_data, ReGIRHashGridSoADevice input_reservoirs_grid, ReGIRHashGridSoADevice output_reservoirs_grid, ReGIRHashCellDataSoADevice output_reservoirs_hash_cell_data, unsigned int number_of_cells_alive, bool primary_hit, int thread_index) #endif { if (render_data.buffers.emissive_triangles_count == 0) // No initial candidates to sample since no lights return; ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; #ifdef __KERNELCC__ uint32_t thread_index = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t thread_count = gridDim.x * blockDim.x; #endif while (thread_index < regir_settings.get_number_of_reservoirs_per_cell(primary_hit) * number_of_cells_alive) { int reservoir_index = thread_index; int reservoir_index_in_cell = reservoir_index % regir_settings.get_grid_fill_settings(primary_hit).get_total_reservoir_count_per_cell(); int cell_alive_index = reservoir_index / regir_settings.get_number_of_reservoirs_per_cell(primary_hit); int hash_grid_cell_index = cell_alive_index; if (number_of_cells_alive == regir_settings.get_total_number_of_cells_per_grid(primary_hit)) // If all cells are alive, the cell index is straightforward hash_grid_cell_index = cell_alive_index; else // Not all cells are alive, what we have is cell_alive_index which is the index of the cell in the alive list // so we can fetch the index of the cell in the grid cells alive list with that cell_alive_index hash_grid_cell_index = regir_settings.get_hash_cell_data_soa(primary_hit).grid_cells_alive_list[cell_alive_index]; int reservoir_index_in_grid = hash_grid_cell_index * regir_settings.get_number_of_reservoirs_per_cell(primary_hit) + reservoir_index_in_cell; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(reservoir_index_in_grid + 1); else seed = wang_hash((reservoir_index_in_grid + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); float3 center_cell_point = ReGIR_get_cell_world_point(render_data, hash_grid_cell_index, primary_hit); float3 center_cell_normal = ReGIR_get_cell_world_normal(render_data, hash_grid_cell_index, primary_hit); float center_cell_roughness = ReGIR_get_cell_roughness(render_data, hash_grid_cell_index, primary_hit); if (regir_settings.get_hash_cell_data_soa(primary_hit).grid_cell_alive[hash_grid_cell_index] == 0) { // Grid cell wasn't used during shading in the last frame, let's not refill it // Storing an empty reservoir to clear the cell regir_settings.store_reservoir_custom_buffer_opt(output_reservoirs_grid, ReGIRReservoir(), hash_grid_cell_index, reservoir_index_in_cell); return; } unsigned int spatial_neighbor_rng_seed; if (regir_settings.spatial_reuse.do_coalesced_spatial_reuse) // Everyone is going to use the same RNG (the RNG doesn't depend on the pixel index) // such that memory accesses on the spatial neighbors are coalesced to improve performance spatial_neighbor_rng_seed = render_data.render_settings.freeze_random ? render_data.random_number : (render_data.render_settings.sample_number + 1) * render_data.random_number; else spatial_neighbor_rng_seed = wang_hash(seed); Xorshift32Generator spatial_neighbor_rng(spatial_neighbor_rng_seed); ReGIRReservoir output_reservoir = spatial_reuse(render_data, input_reservoirs_grid, reservoir_index_in_cell, hash_grid_cell_index, primary_hit, center_cell_point, center_cell_normal, center_cell_roughness, spatial_neighbor_rng, random_number_generator); spatial_neighbor_rng.m_state.seed = spatial_neighbor_rng_seed; int valid_neighbor_count = spatial_reuse_mis_weight(render_data, output_reservoir, reservoir_index_in_cell, hash_grid_cell_index, primary_hit, center_cell_point, center_cell_normal, center_cell_roughness, spatial_neighbor_rng, random_number_generator); // Normalizing the reservoirs to 1 output_reservoir.finalize_resampling(1.0f, valid_neighbor_count); regir_settings.store_reservoir_custom_buffer_opt(output_reservoirs_grid, output_reservoir, hash_grid_cell_index, reservoir_index_in_cell); #ifdef __KERNELCC__ spatial_reuse_pre_integration_accumulation(render_data, output_reservoir, regir_settings.get_grid_fill_settings(primary_hit).reservoir_index_in_cell_is_canonical(reservoir_index_in_cell), hash_grid_cell_index, primary_hit); #else spatial_reuse_pre_integration_accumulation(render_data, output_reservoir, regir_settings.get_grid_fill_settings(primary_hit).reservoir_index_in_cell_is_canonical(reservoir_index_in_cell), hash_grid_cell_index, primary_hit); #endif #ifndef __KERNELCC__ // We're dispatching exactly one thread per reservoir to compute on the CPU so no need // for the work queue style of things that is only needed on the GPU, we can just exit here break; #else // We need to compute the next reservoir index for the next iteration thread_index += thread_count; #endif } } #endif ================================================ FILE: src/Device/kernels/ReSTIR/ReGIR/SupersamplingCopy.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_KERNELS_REGIR_SUPERSAMPLING_COPY_H #define DEVICE_KERNELS_REGIR_SUPERSAMPLING_COPY_H #include "HostDeviceCommon/RenderData.h" /** * This kernel inserts the keys of the input hash table into the output hash table * * This is used when the hash table has been resized and we need to re-insert the keys * of the old (smaller) hash table into the new (larger) hash table */ #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) ReGIR_Supersampling_Copy(HIPRTRenderData render_data, ReGIRHashGridSoADevice input_reservoirs_to_copy) #else GLOBAL_KERNEL_SIGNATURE(void) inline ReGIR_Supersampling_Copy(HIPRTRenderData render_data, ReGIRHashGridSoADevice input_reservoirs_to_copy, int thread_index) #endif { ReGIRSettings& regir_settings = render_data.render_settings.regir_settings; #ifdef __KERNELCC__ const uint32_t thread_index = blockIdx.x * blockDim.x + threadIdx.x; #endif #ifdef __KERNELCC__ if (thread_index >= *render_data.render_settings.regir_settings.get_hash_cell_data_soa(true).grid_cells_alive_count * regir_settings.get_number_of_reservoirs_per_cell(true)) #else if (thread_index >= render_data.render_settings.regir_settings.get_hash_cell_data_soa(true).grid_cells_alive_count->load() * regir_settings.get_number_of_reservoirs_per_cell(true)) #endif { return; } unsigned int reservoir_index = thread_index; unsigned int reservoir_index_in_cell = reservoir_index % regir_settings.get_number_of_reservoirs_per_cell(true); unsigned int cell_alive_index = reservoir_index / regir_settings.get_number_of_reservoirs_per_cell(true); // If all cells are alive, the cell index is straightforward // // Not all cells are alive, what we have is cell_alive_index which is the index of the cell in the alive list // so we can fetch the index of the cell in the grid cells alive list with that cell_alive_index unsigned int hash_grid_cell_index = regir_settings.get_hash_cell_data_soa(true).grid_cells_alive_list[cell_alive_index]; unsigned int reservoir_index_in_grid = hash_grid_cell_index * regir_settings.get_number_of_reservoirs_per_cell(true) + reservoir_index_in_cell; ReGIRReservoir reservoir_to_copy = regir_settings.hash_grid.read_full_reservoir(input_reservoirs_to_copy, reservoir_index_in_grid); unsigned int reservoir_index_in_supersampling_grid = reservoir_index_in_grid + regir_settings.supersampling.correl_reduction_current_grid * regir_settings.get_number_of_reservoirs_per_grid(true); render_data.render_settings.regir_settings.hash_grid.store_full_reservoir(regir_settings.supersampling.correlation_reduction_grid, reservoir_to_copy, reservoir_index_in_supersampling_grid); } #endif ================================================ FILE: src/Device/kernels/TraceTest.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_TRACE_TEST_H #define KERNELS_TRACE_TEST_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/Hash.h" #include "Device/includes/Intersect.h" #include "HostDeviceCommon/RenderData.h" #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) TraceTest(HIPRTRenderData render_data, int2 res) #else GLOBAL_KERNEL_SIGNATURE(void) inline TraceTest(HIPRTRenderData render_data, int2 res, int x, int y) #endif { #ifdef __KERNELCC__ const uint32_t x = blockIdx.x * blockDim.x + threadIdx.x; const uint32_t y = blockIdx.y * blockDim.y + threadIdx.y; #endif if (x >= res.x || y >= res.y) return; uint32_t pixel_index = x + y * res.x; unsigned int seed; if (render_data.render_settings.freeze_random) seed = wang_hash(pixel_index + 1); else seed = wang_hash((pixel_index + 1) * (render_data.render_settings.sample_number + 1) * render_data.random_number); Xorshift32Generator random_number_generator(seed); // Direction to the center of the pixel float x_ray_point_direction = (x + 0.5f); float y_ray_point_direction = (y + 0.5f); if (render_data.current_camera.do_jittering) { // Jitter randomly around the center x_ray_point_direction += random_number_generator() - 0.5f; y_ray_point_direction += random_number_generator() - 0.5f; } hiprtRay ray = render_data.current_camera.get_camera_ray(x_ray_point_direction, y_ray_point_direction, res); hiprtHit hit; #ifdef __KERNELCC__ #if UseSharedStackBVHTraversal == KERNEL_OPTION_TRUE #if SharedStackBVHTraversalSize > 0 hiprtSharedStackBuffer shared_stack_buffer{ SharedStackBVHTraversalSize, shared_stack_cache }; #else hiprtSharedStackBuffer shared_stack_buffer{ 0, nullptr }; #endif hiprtGlobalStack global_stack(render_data.global_traversal_stack_buffer, shared_stack_buffer); hiprtGeomTraversalClosestCustomStack traversal(render_data.GPU_BVH, ray, global_stack, hiprtTraversalHintDefault); #else hiprtGeomTraversalClosest traversal(render_data.GPU_BVH, ray, hiprtTraversalHintDefault); #endif hit = traversal.getNextHit(); #endif render_data.g_buffer.first_hit_prim_index[pixel_index] = hit.hasHit() ? 1 : 0; } #endif ================================================ FILE: src/Device/kernels/Utils/RayVolumeStateSize.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef KERNELS_RAY_VOLUME_STATE_SIZE_H #define KERNELS_RAY_VOLUME_STATE_SIZE_H #include "Device/includes/FixIntellisense.h" #include "Device/includes/RayVolumeState.h" #include "HostDeviceCommon/Packing.h" #include "HostDeviceCommon/RenderSettings.h" #ifdef __KERNELCC__ GLOBAL_KERNEL_SIGNATURE(void) RayVolumeStateSize(size_t* out_buffer) #else GLOBAL_KERNEL_SIGNATURE(void) inline RayVolumeStateSize(size_t* out_buffer) #endif { out_buffer[0] = sizeof(RayVolumeState); } #endif ================================================ FILE: src/Experimentations/TestCopyKernelAlignment.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Compiler/GPUKernel.h" #include "HIPRT-Orochi/OrochiBuffer.h" #include "HIPRT-Orochi/HIPRTOrochiCtx.h" #include void TestCopyKernelAlignment() { std::shared_ptr hiprt_orochi_ctx = std::make_shared(0); GPUKernel test_copy_kernel; test_copy_kernel.set_kernel_file_path(DEVICE_KERNELS_DIRECTORY "/Experimentations/TestCopyKernelAlignment.h"); test_copy_kernel.set_kernel_function_name("TestCopyKernelAlignment"); test_copy_kernel.compile(hiprt_orochi_ctx); #define BUFFER_SIZE 500000000 #define ITERATIONS 1000 OrochiBuffer buffer_a(BUFFER_SIZE); OrochiBuffer buffer_b(BUFFER_SIZE); size_t buffer_size = BUFFER_SIZE; ColorRGB32F* buffer_a_ptr = buffer_a.get_device_pointer(); ColorRGB32F* buffer_b_ptr = buffer_b.get_device_pointer(); void* launch_args[] = { &buffer_a_ptr, &buffer_b_ptr, &buffer_size }; float average_sum = 0.0f; float min_exec_time = 1000000.0f; float max_exec_time = 0.0f; for (int i = 0; i < ITERATIONS; i++) { float execution_time; test_copy_kernel.launch_synchronous(256, 1, BUFFER_SIZE, 1, launch_args, &execution_time); min_exec_time = hippt::min(execution_time, min_exec_time); max_exec_time = hippt::max(execution_time, max_exec_time); average_sum += execution_time; } std::cout << "Min/max/average exec time:" << min_exec_time << " / " << max_exec_time << "/" << average_sum / ITERATIONS << " ms" << std::endl; } ================================================ FILE: src/Experimentations/TestCopyKernelAlignment.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef EXPERIMENTATIONS_TEST_COPY_KERNEL_ALIGNMENT_H #define EXPERIMENTATIONS_TEST_COPY_KERNEL_ALIGNMENT_H void TestCopyKernelAlignment(); #endif ================================================ FILE: src/Experimentations/TestCopyKernelRestrict.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Compiler/GPUKernel.h" #include "HIPRT-Orochi/OrochiBuffer.h" #include "HIPRT-Orochi/HIPRTOrochiCtx.h" #include void TestCopyKernelRestrict() { std::shared_ptr hiprt_orochi_ctx = std::make_shared(0); GPUKernel test_copy_kernel; test_copy_kernel.set_kernel_file_path(DEVICE_KERNELS_DIRECTORY "/Experimentations/TestCopyKernelRestrict.h"); test_copy_kernel.set_kernel_function_name("TestCopyKernelRestrict"); test_copy_kernel.compile(hiprt_orochi_ctx); #define BUFFER_SIZE 1000000000 #define ITERATIONS 100 OrochiBuffer buffer_a(BUFFER_SIZE); OrochiBuffer buffer_b(BUFFER_SIZE); OrochiBuffer buffer_c(BUFFER_SIZE); OrochiBuffer buffer_d(BUFFER_SIZE); buffer_a.memset_whole_buffer(1); buffer_b.memset_whole_buffer(1); buffer_c.memset_whole_buffer(1); buffer_d.memset_whole_buffer(1); size_t buffer_size = BUFFER_SIZE; float* buffer_a_ptr = buffer_a.get_device_pointer(); float* buffer_b_ptr = buffer_b.get_device_pointer(); float* buffer_c_ptr = buffer_c.get_device_pointer(); float* buffer_d_ptr = buffer_d.get_device_pointer(); void* launch_args[] = { &buffer_a_ptr, &buffer_b_ptr, &buffer_c_ptr, &buffer_c_ptr, &buffer_size }; float min_exec_time = 1000000.0f; float max_exec_time = 0.0f; for (int i = 0; i < ITERATIONS; i++) { float execution_time; test_copy_kernel.launch_synchronous(256, 1, BUFFER_SIZE, 1, launch_args, &execution_time); min_exec_time = hippt::min(execution_time, min_exec_time); max_exec_time = hippt::max(execution_time, max_exec_time); } std::cout << "Min/max exec time:" << min_exec_time << " / " << max_exec_time << " ms" << std::endl; } ================================================ FILE: src/Experimentations/TestCopyKernelRestrict.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef EXPERIMENTATIONS_TEST_COPY_KERNEL_RESTRICT_H #define EXPERIMENTATIONS_TEST_COPY_KERNEL_RESTRICT_H void TestCopyKernelRestrict(); #endif ================================================ FILE: src/Experimentations/TestCopyKernelSimple.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Compiler/GPUKernel.h" #include "Device/includes/ReSTIR/GI/Reservoir.h" #include "Device/kernels/Experimentations/TestCopyKernelSimple.h" #include "HIPRT-Orochi/OrochiBuffer.h" #include "HIPRT-Orochi/HIPRTOrochiCtx.h" #include void TestCopyKernelSimple() { std::shared_ptr hiprt_orochi_ctx = std::make_shared(0); GPUKernel test_copy_kernel; test_copy_kernel.set_kernel_file_path(DEVICE_KERNELS_DIRECTORY "/Experimentations/TestCopyKernelSimple.h"); test_copy_kernel.set_kernel_function_name("TestCopyKernelSimple"); test_copy_kernel.compile(hiprt_orochi_ctx); #define BUFFER_SIZE 2560*1440 #define ITERATIONS 4000 OrochiBuffer buffer_a(BUFFER_SIZE); OrochiBuffer buffer_b(BUFFER_SIZE); TestCopyKernelSimpleInputData input_data; input_data.buffer_a = buffer_a.get_device_pointer(); input_data.buffer_b = buffer_b.get_device_pointer(); size_t buffer_size = BUFFER_SIZE; void* launch_args[] = { &input_data, &buffer_size}; float average_sum = 0.0f; float min_exec_time = 1000000.0f; float max_exec_time = 0.0f; for (int i = 0; i < ITERATIONS; i++) { float execution_time; test_copy_kernel.launch_synchronous(256, 1, BUFFER_SIZE, 1, launch_args, &execution_time); min_exec_time = hippt::min(execution_time, min_exec_time); max_exec_time = hippt::max(execution_time, max_exec_time); average_sum += execution_time; } std::cout << "Min/max/average exec time:" << min_exec_time << " / " << max_exec_time << " / " << average_sum / ITERATIONS << " ms" << std::endl; } ================================================ FILE: src/Experimentations/TestCopyKernelSimple.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef EXPERIMENTATIONS_TEST_COPY_KERNEL_SIMPLE_H #define EXPERIMENTATIONS_TEST_COPY_KERNEL_SIMPLE_H void TestCopyKernelSimple(); #endif ================================================ FILE: src/HIPRT-Orochi/HIPRTOrochiCtx.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HIPRT_OROCHI_CTX_H #define HIPRT_OROCHI_CTX_H #include #include #include #include #include "HIPRT-Orochi/HIPRTOrochiUtils.h" #include "UI/ImGui/ImGuiLogger.h" #include "Utils/Utils.h" extern ImGuiLogger g_imgui_logger; struct HIPRTOrochiCtx { HIPRTOrochiCtx() {} HIPRTOrochiCtx(int device_index) { init(device_index); } #ifdef _WIN32 Utils::AddEnvVarError add_CUDA_PATH_to_PATH() { // On Windows + NVIDIA, adding the CUDA_PATH to the PATH environment variable just to be sure // that CUDA's DLLs are found in case the user indeed has installer the CUDA toolkit but their PATH // environment variable is not set correctly. return Utils::windows_add_ENV_var_to_PATH(L"CUDA_PATH", L"\\bin;"); } #endif void init(int device_index) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_INFO, "Initializing Orochi..."); #ifdef OROCHI_ENABLE_CUEW #ifdef _WIN32 Utils::AddEnvVarError error = add_CUDA_PATH_to_PATH(); #endif #endif #ifdef OROCHI_ENABLE_CUEW int error_initialize = oroInitialize((oroApi)(ORO_API_CUDA), 0); #else int error_initialize = oroInitialize((oroApi)(ORO_API_HIP), 0); #endif if (error_initialize != oroSuccess) { switch (error_initialize) { // Unable to load HIP/CUDA case ORO_API_HIPDRIVER: g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Unable to load HIP... Are your drivers up-to-date?"); break; case ORO_API_CUDADRIVER: g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Unable to load CUDA... Are your drivers up-to-date?"); break; // Unable to load HIP/CUDA case ORO_API_HIPRTC: g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Unable to load HIPRTC... Is the HIP SDK (Windows) or ROCm + HIP (Linux) installed?"); break; case ORO_API_CUDARTC: g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Unable to load CUDARTC... Is the CUDA Toolkit installed + is the CUDA_PATH " "environment variable set? (or have {CUDA_TOOLKIT_FOLDER/bin} in your " "PATH environment variable)"); break; } int trash = std::getchar(); std::exit(1); } OROCHI_CHECK_ERROR(oroInit(0)); OROCHI_CHECK_ERROR(oroDeviceGet(&orochi_device, device_index)); OROCHI_CHECK_ERROR(oroCtxCreate(&orochi_ctx, 0, orochi_device)); OROCHI_CHECK_ERROR(oroGetDeviceProperties(&device_properties, orochi_device)); g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_INFO, "HIPRT ver.%s", HIPRT_VERSION_STR); g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_INFO, "Executing on '%s'\n", device_properties.name); if (std::string(device_properties.name).find("NVIDIA") != std::string::npos) hiprt_ctx_input.deviceType = hiprtDeviceNVIDIA; else hiprt_ctx_input.deviceType = hiprtDeviceAMD; hiprt_ctx_input.ctxt = oroGetRawCtx(orochi_ctx); hiprt_ctx_input.device = oroGetRawDevice(orochi_device); hiprtSetLogLevel(hiprtLogLevelError); HIPRT_CHECK_ERROR(hiprtCreateContext(HIPRT_API_VERSION, hiprt_ctx_input, hiprt_ctx)); } hiprtContextCreationInput hiprt_ctx_input = { nullptr, -1, hiprtDeviceAMD }; oroCtx orochi_ctx = nullptr; oroDevice orochi_device = -1; oroDeviceProp device_properties = {}; hiprtContext hiprt_ctx = nullptr; }; #endif ================================================ FILE: src/HIPRT-Orochi/HIPRTOrochiUtils.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "HIPRT-Orochi/HIPRTOrochiUtils.h" #include "UI/ImGui/ImGuiLogger.h" #include "Utils/Utils.h" #include #include extern ImGuiLogger g_imgui_logger; void orochi_check_error(oroError res, const char* file, uint32_t line) { if (res != oroSuccess) { const char* msg; oroGetErrorString(res, &msg); g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Orochi error: '%s' on line %d in '%s'.", msg, line, file); Utils::debugbreak(); exit(EXIT_FAILURE); } } void orochi_rtc_check_error(orortcResult res, const char* file, uint32_t line) { if (res != ORORTC_SUCCESS) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "ORORTC error: '%s' [ %d ] on line %d in '%s'", orortcGetErrorString(res), res, line, file); Utils::debugbreak(); exit(EXIT_FAILURE); } } void hiprt_check_error(hiprtError res, const char* file, uint32_t line) { if (res != hiprtSuccess) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "HIPRT error: '%d' on line %d in '%s'.", res, line, file); Utils::debugbreak(); exit(EXIT_FAILURE); } } namespace HIPPTOrochiUtils { bool read_source_code(const std::string& path, std::string& sourceCode, std::vector* includes) { std::fstream f(path); if (f.is_open()) { size_t sizeFile; f.seekg(0, std::fstream::end); size_t size = sizeFile = (size_t)f.tellg(); f.seekg(0, std::fstream::beg); if (includes) { sourceCode.clear(); std::string line; char buf[512]; while (std::getline(f, line)) { if (strstr(line.c_str(), "#include") != 0) { const char* a = strstr(line.c_str(), "<"); const char* b = strstr(line.c_str(), ">"); if (!a) { // If we couldn't find a "<", trying to find a '"' a = strstr(line.c_str(), "\""); if (!a) { // Not even '"' was find, that's invalid #include syntax g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Unable to parse header name in line '%s'", line.c_str()); continue; } } // Same thing with the ending character, '>' or another '"' if (!b) { b = strstr(a + 1, "\""); if (!b) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Unable to parse header name in line '%s'", line.c_str()); continue; } } int n = b - a - 1; memcpy(buf, a + 1, n); buf[n] = '\0'; includes->push_back(buf); } sourceCode += line + '\n'; } } else { sourceCode.resize(size, ' '); f.read(&sourceCode[0], size); } f.close(); return true; } return false; } hiprtError build_trace_kernel(hiprtContext ctxt, const std::string& kernel_file_path, const std::string& function_name, hiprtApiFunction& kernel_function_out, const std::vector& additional_include_directories, const std::vector& compiler_options, unsigned int num_geom_types, unsigned int num_ray_types, bool use_compiler_cache, hiprtFuncNameSet* func_name_set, const std::string& additional_cache_key) { std::string kernel_source_code; read_source_code(kernel_file_path, kernel_source_code); std::vector compiler_options_cstr; for (const std::string& option : compiler_options) compiler_options_cstr.push_back(option.c_str()); std::vector additional_include_directories_options; additional_include_directories_options.reserve(additional_include_directories.size()); for (const std::string& additional_include_dir : additional_include_directories) { additional_include_directories_options.push_back("-I" + additional_include_dir); compiler_options_cstr.push_back(additional_include_directories_options.back().c_str()); } const char* func_name_cstr = function_name.c_str(); return hiprtBuildTraceKernels( ctxt, 1, &func_name_cstr, kernel_source_code.c_str(), kernel_file_path.c_str(), 0, nullptr, nullptr, compiler_options_cstr.size(), compiler_options_cstr.size() > 0 ? compiler_options_cstr.data() : nullptr, num_geom_types, num_ray_types, func_name_set, &kernel_function_out, nullptr, use_compiler_cache, additional_cache_key); } } ================================================ FILE: src/HIPRT-Orochi/HIPRTOrochiUtils.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HIPRTPT_OROCHI_UTILS_H #define HIPRTPT_OROCHI_UTILS_H #include #include #include #include #define OROCHI_CHECK_ERROR( error ) ( orochi_check_error( error, __FILE__, __LINE__ ) ) #define OROCHI_RTC_CHECK_ERROR( error ) ( orochi_rtc_check_error( error, __FILE__, __LINE__ ) ) #define HIPRT_CHECK_ERROR( error ) ( hiprt_check_error( error, __FILE__, __LINE__ ) ) // This flag isn't defined in Orochi for some reasons ? // It allows sampling textures with normalized coordinates in [0, 1[ instead of // [0, width[ #define ORO_TRSF_NORMALIZED_COORDINATES 0x02 namespace HIPPTOrochiUtils { /* * Reads a given file, outputs its code in 'sourceCode' and a list of the names * of the files included in the source file by #include directives in 'includes' * * If 'includes' is nullptr, then no include names will be returned */ bool read_source_code(const std::string& path, std::string& sourceCode, std::vector* includes = nullptr); /** * Note, the 'additional_include_directories' are expected to be given are relative folder * path "../../myIncludeDir" without any "-I" prefix */ hiprtError build_trace_kernel(hiprtContext ctxt, const std::string& kernel_file_path, const std::string& function_name, hiprtApiFunction& kernel_function_out, const std::vector& additional_include_directories, const std::vector& compiler_options, unsigned int num_geom_types, unsigned int num_ray_types, bool use_compiler_cache, hiprtFuncNameSet* func_name_set = nullptr, const std::string& additional_cache_key = ""); } void orochi_check_error(oroError res, const char* file, uint32_t line); void orochi_rtc_check_error(orortcResult res, const char* file, uint32_t line); void hiprt_check_error(hiprtError res, const char* file, uint32_t line); #endif ================================================ FILE: src/HIPRT-Orochi/HIPRTScene.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HIPRT_SCENE_H #define HIPRT_SCENE_H #include "HIPRT-Orochi/HIPRTOrochiUtils.h" #include "HIPRT-Orochi/OrochiTexture.h" #include "Renderer/GPUDataStructures/MaterialPackedSoAGPUData.h" #include "Renderer/CPUGPUCommonDataStructures/PrecomputedEmissiveTrianglesDataSoAHost.h" #include "UI/ImGui/ImGuiLogger.h" #include "hiprt/hiprt.h" #include "Orochi/Orochi.h" extern ImGuiLogger g_imgui_logger; struct HIPRTGeometry { HIPRTGeometry() : m_hiprt_ctx(nullptr) {} HIPRTGeometry(hiprtContext ctx) : m_hiprt_ctx(ctx) {} ~HIPRTGeometry() { if (m_mesh.triangleIndices) OROCHI_CHECK_ERROR(oroFree(reinterpret_cast(m_mesh.triangleIndices))); if (m_mesh.vertices && m_allow_free_mesh_vertices) OROCHI_CHECK_ERROR(oroFree(reinterpret_cast(m_mesh.vertices))); if (m_geometry) HIPRT_CHECK_ERROR(hiprtDestroyGeometry(m_hiprt_ctx, m_geometry)); } void upload_triangle_indices(const std::vector& triangles_indices) { int triangle_count = triangles_indices.size() / 3; // Allocating and initializing the indices buffer m_mesh.triangleCount = triangle_count; m_mesh.triangleStride = sizeof(int3); OROCHI_CHECK_ERROR(oroMalloc(reinterpret_cast(&m_mesh.triangleIndices), triangle_count * sizeof(int3))); OROCHI_CHECK_ERROR(oroMemcpy(reinterpret_cast(m_mesh.triangleIndices), triangles_indices.data(), triangle_count * sizeof(int3), oroMemcpyHostToDevice)); } std::vector download_triangle_indices() { if (m_mesh.vertices != nullptr) return OrochiBuffer::download_data(reinterpret_cast(m_mesh.triangleIndices), m_mesh.triangleCount * 3); return std::vector(); } void upload_vertices_positions(const std::vector& vertices_positions) { // Allocating and initializing the vertices positions buiffer m_mesh.vertexCount = vertices_positions.size(); m_mesh.vertexStride = sizeof(float3); OROCHI_CHECK_ERROR(oroMalloc(reinterpret_cast(&m_mesh.vertices), m_mesh.vertexCount * sizeof(float3))); OROCHI_CHECK_ERROR(oroMemcpy(reinterpret_cast(m_mesh.vertices), vertices_positions.data(), m_mesh.vertexCount * sizeof(float3), oroMemcpyHostToDevice)); } void copy_vertices_positions_from(const HIPRTGeometry& other_geometry) { m_mesh.vertexCount = other_geometry.m_mesh.vertexCount; m_mesh.vertexStride = other_geometry.m_mesh.vertexStride; m_mesh.vertices = other_geometry.m_mesh.vertices; // This structure is not going to free the mesh vertices because it is // managed by another HIPRTGeometry m_allow_free_mesh_vertices = false; } std::vector download_vertices_positions() { if (m_mesh.vertices != nullptr) return OrochiBuffer::download_data(reinterpret_cast(m_mesh.vertices), m_mesh.vertexCount); return std::vector(); } void log_bvh_building(hiprtBuildFlags build_flags) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_INFO, "Compiling BVH building kernels & building scene BVH..."); } void build_bvh(hiprtBuildFlags build_flags, bool do_compaction, bool disable_spatial_splits_on_OOM, oroStream_t build_stream) { auto start = std::chrono::high_resolution_clock::now(); if (m_geometry != nullptr) { HIPRT_CHECK_ERROR(hiprtDestroyGeometry(m_hiprt_ctx, m_geometry)); m_geometry = nullptr; } if (m_mesh.vertexCount == 0 || m_mesh.triangleCount == 0) // No BVH to build return; hiprtBuildOptions build_options; hiprtGeometryBuildInput geometry_build_input; size_t geometry_temp_size; hiprtDevicePtr geometry_temp; build_options.buildFlags = build_flags; geometry_build_input.type = hiprtPrimitiveTypeTriangleMesh; geometry_build_input.primitive.triangleMesh = m_mesh; // Geom type 0 here geometry_build_input.geomType = 0; log_bvh_building(build_options.buildFlags); // Getting the buffer sizes for the construction of the BVH HIPRT_CHECK_ERROR(hiprtGetGeometryBuildTemporaryBufferSize(m_hiprt_ctx, geometry_build_input, build_options, geometry_temp_size)); oroError_t error = oroMalloc(reinterpret_cast(&geometry_temp), geometry_temp_size); if (error != oroSuccess && error == oroErrorOutOfMemory && disable_spatial_splits_on_OOM) { if (error == oroErrorOutOfMemory && disable_spatial_splits_on_OOM) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_WARNING, "Out of memory while trying to build the BVH... Retrying without spatial splits. Tracing performance may suffer..."); build_options.buildFlags |= hiprtBuildFlagBitDisableSpatialSplits; HIPRT_CHECK_ERROR(hiprtGetGeometryBuildTemporaryBufferSize(m_hiprt_ctx, geometry_build_input, build_options, geometry_temp_size)); error = oroMalloc(reinterpret_cast(&geometry_temp), geometry_temp_size); if (error != oroSuccess) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_WARNING, "Error while trying to build the BVH even without spatial splits... Aborting..."); OROCHI_CHECK_ERROR(error); } } } else OROCHI_CHECK_ERROR(error); HIPRT_CHECK_ERROR(hiprtCreateGeometry(m_hiprt_ctx, geometry_build_input, build_options, m_geometry)); HIPRT_CHECK_ERROR(hiprtBuildGeometry(m_hiprt_ctx, hiprtBuildOperationBuild, geometry_build_input, build_options, geometry_temp, build_stream, m_geometry)); OROCHI_CHECK_ERROR(oroFree(reinterpret_cast(geometry_temp))); if (do_compaction) HIPRT_CHECK_ERROR(hiprtCompactGeometry(m_hiprt_ctx, 0, m_geometry, m_geometry)); auto stop = std::chrono::high_resolution_clock::now(); g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_INFO, "BVH built in %ldms", std::chrono::duration_cast(stop - start).count()); } hiprtContext m_hiprt_ctx = nullptr; hiprtTriangleMeshPrimitive m_mesh = { nullptr }; // One geometry for the whole scene for now hiprtGeometry m_geometry = nullptr; bool m_allow_free_mesh_vertices = true; }; struct HIPRTScene { void print_statistics(std::ostream& stream) { stream << "Scene statistics: " << std::endl; stream << "\t" << whole_scene_BLAS.m_mesh.vertexCount << " vertices" << std::endl; stream << "\t" << whole_scene_BLAS.m_mesh.triangleCount << " triangles" << std::endl; stream << "\t" << emissive_triangles_primitive_indices.size() << " emissive triangles" << std::endl; stream << "\t" << materials_buffer.m_element_count << " materials" << std::endl; stream << "\t" << orochi_materials_textures.size() << " textures" << std::endl; } HIPRTGeometry whole_scene_BLAS; HIPRTGeometry emissive_triangles_BLAS; OrochiBuffer triangle_areas; OrochiBuffer has_vertex_normals; OrochiBuffer vertex_normals; OrochiBuffer material_indices; DevicePackedTexturedMaterialSoAGPUData materials_buffer; // This vector contains true for a material that has a fully opaque base color texture. // Otherwise, the texture has some alpha transparency in it // // This vector isn't used on the GPU, it's only used by the CPU to basically remember which // materials had textures with some alpha in it std::vector material_has_opaque_base_color_texture; OrochiBuffer material_opaque; int emissive_triangles_count = 0; OrochiBuffer emissive_triangles_primitive_indices; OrochiBuffer emissive_triangles_indices_and_emissive_textures; OrochiBuffer emissive_power_alias_table_probas; OrochiBuffer emissive_power_alias_table_alias; // This is a remnant of some tests and it was actually not worth it PrecomputedEmissiveTrianglesDataSoAHost precomputed_emissive_triangles_data; // Vector to keep the textures data alive otherwise the OrochiTexture objects would // be destroyed which means that the underlying textures would be destroyed std::vector orochi_materials_textures; OrochiBuffer gpu_materials_textures; OrochiBuffer texcoords_buffer; }; #endif ================================================ FILE: src/HIPRT-Orochi/OrochiBuffer.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef OROCHI_BUFFER_H #define OROCHI_BUFFER_H #include "hiprt/hiprt.h" #include "HIPRT-Orochi/HIPRTOrochiUtils.h" #include "Orochi/Orochi.h" #include "UI/DisplayView/DisplayTextureType.h" #include "UI/ImGui/ImGuiLogger.h" #include "Utils/Utils.h" #include "GL/glew.h" #include "tracy/TracyOpenGL.hpp" extern ImGuiLogger g_imgui_logger; template class OrochiBuffer { public: using value_type = T; OrochiBuffer() : m_data_pointer(nullptr) {} OrochiBuffer(int element_count); OrochiBuffer(OrochiBuffer&& other); ~OrochiBuffer(); void operator=(OrochiBuffer&& other) noexcept; void memset_whole_buffer(T value); void resize(int new_element_count, size_t type_size_override = 0); void resize_host_pinned_mem(int new_element_count, size_t type_size_override = 0); size_t size() const; size_t get_byte_size() const; const T* get_device_pointer() const; T* get_device_pointer(); const T* get_host_pinned_pointer() const; T* get_host_pinned_pointer(); /** * Returns a pointer to the device buffer but cast into an AtomicType */ const AtomicType* get_atomic_device_pointer() const; AtomicType* get_atomic_device_pointer(); /** * data() is just an alias for get_device_pointer() */ const T* data() const; T* data(); bool is_allocated() const; /** * Static function for downloading from a device buffer when we * only have the address of the buffer (and not the OrochiBuffer object) */ static std::vector download_data(T* device_data_pointer, size_t element_count); std::vector download_data() const; /** * Download the data of the *whole* buffer directly to the given 'host_pointer' * * The given 'host_pointer' is supposed to be pointing to an allocated memory block * that is large enough to accomodate all the data of this buffer. Behavior is undefined * if this is not the case * * 'host_pointer' can also be the pointer returned by 'get_host_pinned_pointer()' if using host pinned * memory */ void download_data_into(T* host_pointer) const; /** * Downloads elements ['start_element_index', 'stop_element_index_excluded'[ from the buffer */ std::vector download_data_partial(int start_element_index, int stop_element_index_excluded) const; void download_data_async(void* out, oroStream_t stream) const; static void upload_data(T* device_data_pointer, const std::vector& data_to_upload, size_t element_count); static void upload_data(T* device_data_pointer, const T* data_to_upload, size_t element_count); /** * Uploads as many elements as returned by size from the data std::vector into the buffer. * The given std::vector must therefore contain at least size() elements. * * The overload using a void pointer reads sizeof(T) * size() bytes starting at * the given pointer address. The given pointer must therefore provide a contiguous access * to sizeof(T) * size() bytes of data */ void upload_data(const std::vector& data); void upload_data(const T* data); /** * Uploads 'element_count' elmements from 'data' starting (it will be overriden) at element number 'start_index' in the buffer */ void upload_data_partial(int start_index, const T* data, size_t element_count); void unpack_to_GL_texture(GLuint texture, GLint texture_unit, int width, int height, DisplayTextureType texture_type); /** * Copies the data in 'other' to this buffer. * * This copies the maximum amount of data from 'other' that can fit in this buffer */ void memcpy_from(const OrochiBuffer& other); void memcpy_from(T* data_source, size_t element_count_to_copy); /** * Frees the buffer. No effect if already freed / not allocated yet */ void free(); private: bool m_pinned_memory = false; T* m_data_pointer = nullptr; size_t m_element_count = 0; }; template OrochiBuffer::OrochiBuffer(int element_count) : m_element_count(element_count) { OROCHI_CHECK_ERROR(oroMalloc(reinterpret_cast(&m_data_pointer), sizeof(T) * element_count)); } template OrochiBuffer::OrochiBuffer(OrochiBuffer&& other) { m_data_pointer = other.m_data_pointer; m_element_count = other.m_element_count; other.m_data_pointer = nullptr; other.m_element_count = 0; } template OrochiBuffer::~OrochiBuffer() { if (m_data_pointer) free(); } template void OrochiBuffer::operator=(OrochiBuffer&& other) noexcept { if (m_data_pointer) free(); m_data_pointer = other.m_data_pointer; m_element_count = other.m_element_count; other.m_data_pointer = nullptr; other.m_element_count = 0; } template inline void OrochiBuffer::memset_whole_buffer(T value) { if (m_data_pointer == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to memset on an OrochiBuffer that hasn't been allocated yet!"); return; } std::vector data(m_element_count, value); upload_data(data); } template void OrochiBuffer::resize(int new_element_count, size_t type_size_override) { if (m_data_pointer) free(); size_t buffer_size = type_size_override != 0 ? (type_size_override * new_element_count) : (sizeof(T) * new_element_count); OROCHI_CHECK_ERROR(oroMalloc(reinterpret_cast(&m_data_pointer), buffer_size)); m_element_count = new_element_count; } template void OrochiBuffer::resize_host_pinned_mem(int new_element_count, size_t type_size_override) { if (m_data_pointer) free(); size_t buffer_size = type_size_override != 0 ? (type_size_override * new_element_count) : (sizeof(T) * new_element_count); OROCHI_CHECK_ERROR(oroHostMalloc(reinterpret_cast(&m_data_pointer), buffer_size, 0)); m_element_count = new_element_count; m_pinned_memory = true; } template size_t OrochiBuffer::size() const { return m_element_count; } template size_t OrochiBuffer::get_byte_size() const { return m_element_count * sizeof(T); } template const T* OrochiBuffer::get_device_pointer() const { if (m_data_pointer == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Getting the device_pointer of an OrochiBuffer that hasn't been allocated!"); return nullptr; } if (m_pinned_memory) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Getting the device_pointer of an OrochiBuffer that has been allocated with host pinned memory. Pinned host memory doesn't have device pointers. Use get_host_pinned_pointer()"); return nullptr; } else return m_data_pointer; } template T* OrochiBuffer::get_device_pointer() { if (m_data_pointer == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Getting the device_pointer of an OrochiBuffer that hasn't been allocated!"); return nullptr; } if (m_pinned_memory) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Getting the device_pointer of an OrochiBuffer that has been allocated with host pinned memory. Pinned host memory doesn't have device pointers. Use get_host_pinned_pointer()"); return nullptr; } else return m_data_pointer; } template const AtomicType* OrochiBuffer::get_atomic_device_pointer() const { return reinterpret_cast*>(get_device_pointer()); } template AtomicType* OrochiBuffer::get_atomic_device_pointer() { return reinterpret_cast*>(get_device_pointer()); } template const T* OrochiBuffer::get_host_pinned_pointer() const { if (m_data_pointer == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Getting the host_pinned_pointer of an OrochiBuffer that hasn't been allocated!"); return nullptr; } else if (!m_pinned_memory) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Getting the host_pinned_pointer of an OrochiBuffer that has been allocated for the device! Use get_device_pointer() instead"); return nullptr; } return m_data_pointer; } template T* OrochiBuffer::get_host_pinned_pointer() { if (m_data_pointer == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Getting the host_pinned_pointer of an OrochiBuffer that hasn't been allocated!"); return nullptr; } else if (!m_pinned_memory) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Getting the host_pinned_pointer of an OrochiBuffer that has been allocated for the device! Use get_device_pointer() instead"); return nullptr; } return m_data_pointer; } template const T* OrochiBuffer::data() const { return get_device_pointer(); } template T* OrochiBuffer::data() { return get_device_pointer(); } template bool OrochiBuffer::is_allocated() const { return m_data_pointer != nullptr; } // Static function template std::vector OrochiBuffer::download_data(T* device_data_pointer, size_t element_count) { if (!device_data_pointer) return std::vector(); std::vector data(element_count); OROCHI_CHECK_ERROR(oroMemcpyDtoH(data.data(), reinterpret_cast(device_data_pointer), sizeof(T) * element_count)); return data; } template std::vector OrochiBuffer::download_data() const { if (!m_data_pointer) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to download data from a non-allocated buffer!"); return std::vector(); } std::vector data(m_element_count); OROCHI_CHECK_ERROR(oroMemcpyDtoH(data.data(), reinterpret_cast(m_data_pointer), sizeof(T) * m_element_count)); return data; } template void OrochiBuffer::download_data_into(T* host_pointer) const { if (!m_data_pointer) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to download data into a host pinned buffer from a non-allocated buffer!"); return; } OROCHI_CHECK_ERROR(oroMemcpyDtoH(host_pointer, reinterpret_cast(m_data_pointer), sizeof(T) * m_element_count)); } template inline std::vector OrochiBuffer::download_data_partial(int start_element_index, int stop_element_index_excluded) const { if (!m_data_pointer) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to download data from a non-allocated buffer!"); return std::vector(); } if (start_element_index == stop_element_index_excluded || stop_element_index_excluded < start_element_index) return std::vector(); size_t element_count = stop_element_index_excluded - start_element_index; std::vector data(element_count); OROCHI_CHECK_ERROR(oroMemcpyDtoH(data.data() + start_element_index, reinterpret_cast(m_data_pointer), sizeof(T) * element_count)); return data; } template void OrochiBuffer::download_data_async(void* out, oroStream_t stream) const { if (m_data_pointer == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to download data async from a non-allocated buffer!"); Utils::debugbreak(); return; } OROCHI_CHECK_ERROR(oroMemcpyAsync(out, m_data_pointer, m_element_count * sizeof(T), oroMemcpyDeviceToHost, stream)); } template void OrochiBuffer::upload_data(T* device_data_pointer, const std::vector& data_to_upload, size_t element_count) { OrochiBuffer::upload_data(device_data_pointer, data_to_upload.data(), element_count); } template void OrochiBuffer::upload_data(T* device_data_pointer, const T* data_to_upload, size_t element_count) { if (device_data_pointer && data_to_upload) OROCHI_CHECK_ERROR(oroMemcpy(reinterpret_cast(device_data_pointer), data_to_upload, sizeof(T) * element_count, oroMemcpyHostToDevice)); } template void OrochiBuffer::upload_data(const std::vector& data) { if (m_data_pointer) OROCHI_CHECK_ERROR(oroMemcpy(reinterpret_cast(m_data_pointer), data.data(), sizeof(T) * hippt::min(data.size(), m_element_count), oroMemcpyHostToDevice)); else g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to upload data to an OrochiBuffer that hasn't been allocated yet!"); } template void OrochiBuffer::upload_data(const T* data) { if (m_data_pointer) OROCHI_CHECK_ERROR(oroMemcpy(reinterpret_cast(m_data_pointer), data, sizeof(T) * m_element_count, oroMemcpyHostToDevice)); else g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to upload data to an OrochiBuffer that hasn't been allocated yet!"); } template inline void OrochiBuffer::upload_data_partial(int start_index, const T* data, size_t element_count) { if (start_index > m_element_count) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to upload partial data to an OrochiBuffer starting at in an index that is larger than the buffer's size!"); return; } if (m_data_pointer) OROCHI_CHECK_ERROR(oroMemcpy(reinterpret_cast(m_data_pointer + start_index), data, sizeof(T) * element_count, oroMemcpyHostToDevice)); else g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to upload partial data to an OrochiBuffer that hasn't been allocated yet!"); } template void OrochiBuffer::unpack_to_GL_texture(GLuint texture, GLint texture_unit, int width, int height, DisplayTextureType texture_type) { glActiveTexture(texture_unit); glBindTexture(GL_TEXTURE_2D, texture); // Downloading the Orochi buffer and then uploading it back to the GPU. // Isn't that great code? // // The proper solution would be to use OpenGL Interop to copy the Orochi buffer // to the underlying array of the OpenGL texture. But it seems like OpenGL interop can only // do that for RGBA textures. But we're not stricly using RGBA textures here. The template type // could be anything really and it at least doesn't work with float3 types because float3 are RGB, // not RGBA and again, OpenGL Interop throws an error at 'oroGraphicsGLRegisterImage' for RGB // textures. // // So to fix this, we could use an RGBA OpenGL texture in place of RGB. But then, in the case of // world-space normals buffer for example, we have to convert our float3 normals to float4 to upload // to the RGBA texture. And that conversion would be expensive (and require memory) // // We could also just use float4 data all the way for the normals. We wouldn't have any conversion to do. // But we would have a conversion to perform before denoising and so the issues would be the same // // So maybe there is another solution besides the RGBA OpenGL Interop but too lazy, this is an unlikely // code path in the application anyways std::vector data = download_data(); glTexImage2D(GL_TEXTURE_2D, 0, texture_type.get_gl_internal_format(), width, height, 0, texture_type.get_gl_format(), texture_type.get_gl_type(), data.data()); //oroGraphicsResource_t graphics_resource = nullptr; //OROCHI_CHECK_ERROR(oroGraphicsGLRegisterImage(&graphics_resource, texture, GL_TEXTURE_2D, oroGraphicsRegisterFlagsWriteDiscard)); //// Map the OpenGL texture for CUDA/HIP access //OROCHI_CHECK_ERROR(oroGraphicsMapResources(1, &graphics_resource, 0)); //// Access the CUDA/HIP array used by the OpenGL texture under the hood //oroArray_t array = nullptr; // OROCHI_CHECK_ERROR(oroGraphicsSubResourceGetMappedArray(&array, graphics_resource, 0, 0)); //// Copy data from the CUDA buffer to the CUDA array // OROCHI_CHECK_ERROR(oroMemcpy2DToArray(array, 0, 0, m_data_pointer, width * texture_type.sizeof_type(), width * texture_type.sizeof_type(), height, oroMemcpyDeviceToDevice)); //// Unmap the OpenGL texture //OROCHI_CHECK_ERROR(oroGraphicsUnmapResources(1, &graphics_resource, 0)); } template inline void OrochiBuffer::memcpy_from(const OrochiBuffer& other) { if (m_data_pointer == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to memcpy_from() into an OrochiBuffer that hasn't been allocated yet!"); return; } size_t size_to_copy = std::min(other.m_element_count, m_element_count); OROCHI_CHECK_ERROR(oroMemcpy(m_data_pointer, other.get_device_pointer(), size_to_copy * sizeof(T), oroMemcpyDeviceToDevice)); } template inline void OrochiBuffer::memcpy_from(T* data_source, size_t element_count_to_copy) { if (m_data_pointer == nullptr) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to memcpy_from() into an OrochiBuffer that hasn't been allocated yet!"); return; } OROCHI_CHECK_ERROR(oroMemcpy(m_data_pointer, data_source, element_count_to_copy * sizeof(T), oroMemcpyDeviceToDevice)); } template void OrochiBuffer::free() { if (m_data_pointer) { if (m_pinned_memory) OROCHI_CHECK_ERROR(oroHostFree(reinterpret_cast(m_data_pointer))); else OROCHI_CHECK_ERROR(oroFree(reinterpret_cast(m_data_pointer))); } else { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Freeing an OrochiBuffer buffer that hasn't been initialized (or has been freed already)!"); return; } m_element_count = 0; m_data_pointer = nullptr; m_pinned_memory = false; } #endif ================================================ FILE: src/HIPRT-Orochi/OrochiEnvmap.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "HIPRT-Orochi/OrochiEnvmap.h" #include "UI/ImGui/ImGuiLogger.h" extern ImGuiLogger g_imgui_logger; OrochiEnvmap::OrochiEnvmap(Image32Bit& image) : OrochiTexture(image) {} OrochiEnvmap::OrochiEnvmap(OrochiEnvmap&& other) noexcept : OrochiTexture(std::move(other)) { m_cdf = std::move(other.m_cdf); } void OrochiEnvmap::operator=(OrochiEnvmap&& other) noexcept { OrochiTexture::operator=(std::move(other)); m_cdf = std::move(other.m_cdf); } void OrochiEnvmap::init_from_image(const Image32Bit& image) { OrochiTexture::init_from_image(image); } void OrochiEnvmap::compute_cdf(const Image32Bit& image) { std::vector cdf = image.compute_cdf(); // When computing the CDF, the total sum is actually the last element. Handy. m_luminance_total_sum = cdf.back(); m_cdf.resize(width * height); m_cdf.upload_data(cdf.data()); } float* OrochiEnvmap::get_cdf_device_pointer() { if (m_cdf.size() == 0) g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to get the CDF of an OrochiEnvmap whose CDF wasn't computed in the first place..."); return m_cdf.get_device_pointer(); } void OrochiEnvmap::free_cdf() { m_cdf.free(); } void OrochiEnvmap::compute_alias_table(const Image32Bit& image) { std::vector probas; std::vector alias; image.compute_alias_table(probas, alias, &m_luminance_total_sum); m_alias_table_probas.resize(width * height); m_alias_table_alias.resize(width * height); m_alias_table_probas.upload_data(probas.data()); m_alias_table_alias.upload_data(alias.data()); } void OrochiEnvmap::get_alias_table_device_pointers(float*& probas, int*& aliases) { probas = m_alias_table_probas.get_device_pointer(); aliases = m_alias_table_alias.get_device_pointer(); } void OrochiEnvmap::free_alias_table() { m_alias_table_probas.free(); m_alias_table_alias.free(); } float OrochiEnvmap::get_luminance_total_sum() const { return m_luminance_total_sum; } ================================================ FILE: src/HIPRT-Orochi/OrochiEnvmap.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef OROCHI_ENVMAP_H #define OROCHI_ENVMAP_H #include "HIPRT-Orochi/OrochiTexture.h" class OrochiEnvmap : public OrochiTexture { public: OrochiEnvmap() : OrochiTexture() {} OrochiEnvmap(Image32Bit& image); OrochiEnvmap(const OrochiEnvmap& other) = delete; OrochiEnvmap(OrochiEnvmap&& other) noexcept; void operator=(const OrochiEnvmap other) = delete; void operator=(const OrochiEnvmap& other) = delete; void operator=(OrochiEnvmap&& other) noexcept; void init_from_image(const Image32Bit& image); void compute_cdf(const Image32Bit& image); float* get_cdf_device_pointer(); void free_cdf(); void compute_alias_table(const Image32Bit& image); void get_alias_table_device_pointers(float*& probas, int*& aliases); void free_alias_table(); /** * Returns the sum of the luminance of all the texels of the envmap. * This value is not computed by this function but is computed by compute_cdf() * and compute_alias_table() so one of these two functions must be * called before calling 'get_luminance_total_sum' or 'get_luminance_total_sum' * will return 0.0f */ float get_luminance_total_sum() const; private: float m_luminance_total_sum = 0.0f; OrochiBuffer m_cdf; OrochiBuffer m_alias_table_probas; OrochiBuffer m_alias_table_alias; }; #endif ================================================ FILE: src/HIPRT-Orochi/OrochiTexture.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "HIPRT-Orochi/OrochiTexture.h" #include OrochiTexture::OrochiTexture(const Image8Bit& image, hipTextureFilterMode filtering_mode, hipTextureAddressMode address_mode) { init_from_image(image, filtering_mode, address_mode); } OrochiTexture::OrochiTexture(const Image32Bit& image, hipTextureFilterMode filtering_mode, hipTextureAddressMode address_mode) { init_from_image(image, filtering_mode, address_mode); } OrochiTexture::OrochiTexture(OrochiTexture&& other) noexcept { m_texture_array = std::move(other.m_texture_array); m_texture = std::move(other.m_texture); other.m_texture = nullptr; other.m_texture_array = nullptr; } OrochiTexture::~OrochiTexture() { if (m_texture) oroDestroyTextureObject(m_texture); if (m_texture_array) oroFree(m_texture_array); } void OrochiTexture::operator=(OrochiTexture&& other) noexcept { m_texture_array = std::move(other.m_texture_array); m_texture = std::move(other.m_texture); other.m_texture = nullptr; other.m_texture_array = nullptr; } void create_texture_from_array_cuda(void* m_texture_array, void* m_texture, void* filtering_mode, void* address_mode, bool read_mode_float_normalized); void OrochiTexture::create_texture_from_array(hipTextureFilterMode filtering_mode, hipTextureAddressMode address_mode, bool read_mode_float_normalized) { #ifndef OROCHI_ENABLE_CUEW // Using native HIP here to access 'normalizedCoords' which isn't exposed by Orochi hipResourceDesc resource_descriptor = {}; resource_descriptor.resType = hipResourceTypeArray; resource_descriptor.res.array.array = m_texture_array; hipTextureDesc texture_descriptor = {}; texture_descriptor.addressMode[0] = address_mode; texture_descriptor.addressMode[1] = address_mode; texture_descriptor.addressMode[2] = address_mode; texture_descriptor.filterMode = filtering_mode; texture_descriptor.normalizedCoords = true; texture_descriptor.readMode = read_mode_float_normalized ? hipTextureReadMode::hipReadModeNormalizedFloat : hipTextureReadMode::hipReadModeElementType; texture_descriptor.sRGB = false; OROCHI_CHECK_ERROR(hipCreateTextureObject(&m_texture, &resource_descriptor, &texture_descriptor, nullptr)); #else // Using native CUDA here to access 'normalizedCoords' which isn't exposed by Orochi // Note that this function is defined in another compile unit because we need to include CUDA headers // and they conflict with HIP headers (structures redefinition, float2, float4, ...) it seems so we need to separate them create_texture_from_array_cuda(m_texture_array, &m_texture, &filtering_mode, &address_mode, read_mode_float_normalized); #endif } void OrochiTexture::init_from_image(const Image8Bit& image, hipTextureFilterMode filtering_mode, hipTextureAddressMode address_mode) { int channels = image.channels; if (channels == 3 || channels > 4) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "3-channels textures not supported on the GPU yet."); return; } width = image.width; height = image.height; if (width == 0 || height == 0) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Image given to OrochiTexture is 0 in width or height"); Utils::debugbreak(); } int bits_channel_x = (channels >= 1) ? 8 : 0; // First channel (e.g., Red) int bits_channel_y = (channels >= 2) ? 8 : 0; // Second channel (e.g., Green) int bits_channel_z = (channels >= 3) ? 8 : 0; // Third channel (e.g., Blue) int bits_channel_w = (channels == 4) ? 8 : 0; // Fourth channel (e.g., Alpha) oroChannelFormatDesc channel_descriptor = oroCreateChannelDesc(bits_channel_x, bits_channel_y, bits_channel_z, bits_channel_w, oroChannelFormatKindUnsigned); OROCHI_CHECK_ERROR(oroMallocArray(&m_texture_array, &channel_descriptor, image.width, image.height, oroArrayDefault)); OROCHI_CHECK_ERROR(oroMemcpy2DToArray(m_texture_array, 0, 0, image.data().data(), image.width * channels * sizeof(unsigned char), image.width * sizeof(unsigned char) * channels, image.height, oroMemcpyHostToDevice)); create_texture_from_array(filtering_mode, address_mode, true); } void OrochiTexture::init_from_image(const Image32Bit& image, hipTextureFilterMode filtering_mode, hipTextureAddressMode address_mode) { int channels = image.channels; if (channels == 3 || channels > 4) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "3-channels textures not supported on the GPU yet."); return; } width = image.width; height = image.height; if (width == 0 || height == 0) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Image given to OrochiTexture is 0 in width or height"); Utils::debugbreak(); } int bits_channel_x = (channels >= 1) ? 32 : 0; // First channel (e.g., Red) int bits_channel_y = (channels >= 2) ? 32 : 0; // Second channel (e.g., Green) int bits_channel_z = (channels >= 3) ? 32 : 0; // Third channel (e.g., Blue) int bits_channel_w = (channels == 4) ? 32 : 0; // Fourth channel (e.g., Alpha) oroChannelFormatDesc channel_descriptor = oroCreateChannelDesc(bits_channel_x, bits_channel_y, bits_channel_z, bits_channel_w, oroChannelFormatKindFloat); OROCHI_CHECK_ERROR(oroMallocArray(&m_texture_array, &channel_descriptor, image.width, image.height, oroArrayDefault)); OROCHI_CHECK_ERROR(oroMemcpy2DToArray(m_texture_array, 0, 0, image.data().data(), image.width * channels * sizeof(float), image.width * sizeof(float) * channels, image.height, oroMemcpyHostToDevice)); create_texture_from_array(filtering_mode, address_mode, false); } oroTextureObject_t OrochiTexture::get_device_texture() { return m_texture; } ================================================ FILE: src/HIPRT-Orochi/OrochiTexture.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef OROCHI_TEXTURE_H #define OROCHI_TEXTURE_H #include "HIPRT-Orochi/OrochiBuffer.h" #include "Image/Image.h" class OrochiTexture { public: OrochiTexture() {} OrochiTexture(const Image8Bit& image, hipTextureFilterMode filtering_mode = hipFilterModePoint, hipTextureAddressMode address_mode = hipAddressModeWrap); OrochiTexture(const Image32Bit& image, hipTextureFilterMode filtering_mode = hipFilterModePoint, hipTextureAddressMode address_mode = hipAddressModeWrap); OrochiTexture(const OrochiTexture& other) = delete; OrochiTexture(OrochiTexture&& other) noexcept; ~OrochiTexture(); void operator=(const OrochiTexture& other) = delete; void operator=(OrochiTexture&& other) noexcept; void init_from_image(const Image8Bit& image, hipTextureFilterMode filtering_mode = hipFilterModePoint, hipTextureAddressMode address_mode = hipAddressModeWrap); void init_from_image(const Image32Bit& image, hipTextureFilterMode filtering_mode = hipFilterModePoint, hipTextureAddressMode address_mode = hipAddressModeWrap); oroTextureObject_t get_device_texture(); unsigned int width = 0, height = 0; private: void create_texture_from_array(hipTextureFilterMode filtering_mode, hipTextureAddressMode address_mode, bool read_mode_float_normalized); oroArray_t m_texture_array = nullptr; oroTextureObject_t m_texture = nullptr; }; #endif ================================================ FILE: src/HIPRT-Orochi/OrochiTexture3D.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "HIPRT-Orochi/OrochiTexture3D.h" #include OrochiTexture3D::OrochiTexture3D(const std::vector& images, HIPfilter_mode filtering_mode, HIPaddress_mode address_mode) { init_from_images(images, filtering_mode, address_mode); } OrochiTexture3D::OrochiTexture3D(const std::vector& images, HIPfilter_mode filtering_mode, HIPaddress_mode address_mode) { init_from_images(images, filtering_mode, address_mode); } OrochiTexture3D::OrochiTexture3D(OrochiTexture3D&& other) noexcept { m_texture_array = std::move(other.m_texture_array); m_texture = std::move(other.m_texture); other.m_texture = nullptr; other.m_texture_array = nullptr; } OrochiTexture3D::~OrochiTexture3D() { if (m_texture) oroDestroyTextureObject(m_texture); if (m_texture_array) oroFree(m_texture_array); } void OrochiTexture3D::operator=(OrochiTexture3D&& other) noexcept { m_texture_array = std::move(other.m_texture_array); m_texture = std::move(other.m_texture); other.m_texture = nullptr; other.m_texture_array = nullptr; } void OrochiTexture3D::init_from_images(const std::vector& images, HIPfilter_mode filtering_mode, HIPaddress_mode address_mode) { int channels = images[0].channels; if (channels == 3 || channels > 4) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "3-channels textures not supported on the GPU yet."); return; } width = images[0].width; height = images[0].height; depth = images.size(); int bits_channel_x = (channels >= 1) ? 8 : 0; // First channel (e.g., Red) int bits_channel_y = (channels >= 2) ? 8 : 0; // Second channel (e.g., Green) int bits_channel_z = (channels >= 3) ? 8 : 0; // Third channel (e.g., Blue) int bits_channel_w = (channels == 4) ? 8 : 0; // Fourth channel (e.g., Alpha) oroChannelFormatDesc channel_descriptor = oroCreateChannelDesc(bits_channel_x, bits_channel_y, bits_channel_z, bits_channel_w, oroChannelFormatKindUnsigned); OROCHI_CHECK_ERROR(oroMalloc3DArray(&m_texture_array, &channel_descriptor, oroExtent{ width, height, depth }, oroArrayDefault)); // Because we'r ecopying to a CUDA/HIP array, we need the input data // to be in a single linear block of data std::vector linear_image_data(width * height * depth); for (int i = 0; i < images.size(); i++) std::copy(images[i].data().begin(), images[i].data().end(), linear_image_data.begin() + width * height * i); oroMemcpy3DParms copyParams = { 0 }; copyParams.dstArray = m_texture_array; copyParams.extent = { width, height, depth }; copyParams.kind = oroMemcpyHostToDevice; copyParams.srcPtr = oroPitchedPtr{ linear_image_data.data(), width, width, height }; OROCHI_CHECK_ERROR(oroMemcpy3D(©Params)); // Resource descriptor ORO_RESOURCE_DESC resource_descriptor; std::memset(&resource_descriptor, 0, sizeof(resource_descriptor)); resource_descriptor.resType = ORO_RESOURCE_TYPE_ARRAY; resource_descriptor.res.array.hArray = m_texture_array; ORO_TEXTURE_DESC texture_descriptor; std::memset(&texture_descriptor, 0, sizeof(texture_descriptor)); texture_descriptor.addressMode[0] = address_mode; texture_descriptor.addressMode[1] = address_mode; texture_descriptor.addressMode[2] = address_mode; texture_descriptor.filterMode = filtering_mode; OROCHI_CHECK_ERROR(oroTexObjectCreate(&m_texture, &resource_descriptor, &texture_descriptor, nullptr)); } void OrochiTexture3D::init_from_images(const std::vector& images, HIPfilter_mode filtering_mode, HIPaddress_mode address_mode) { int channels = images[0].channels; if (channels == 3 || channels > 4) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "3-channels textures not supported on the GPU yet."); return; } width = images[0].width; height = images[0].height; depth = images.size(); int bits_channel_x = (channels >= 1) ? 32 : 0; // First channel (e.g., Red) int bits_channel_y = (channels >= 2) ? 32 : 0; // Second channel (e.g., Green) int bits_channel_z = (channels >= 3) ? 32 : 0; // Third channel (e.g., Blue) int bits_channel_w = (channels == 4) ? 32 : 0; // Fourth channel (e.g., Alpha) oroChannelFormatDesc channel_descriptor = oroCreateChannelDesc(bits_channel_x, bits_channel_y, bits_channel_z, bits_channel_w, oroChannelFormatKindFloat); OROCHI_CHECK_ERROR(oroMalloc3DArray(&m_texture_array, &channel_descriptor, oroExtent{ width, height, depth }, oroArrayDefault)); // Because we'r ecopying to a CUDA/HIP array, we need the input data // to be in a single linear block of data std::vector linear_image_data(width * height * depth * channels); for (int i = 0; i < images.size(); i++) std::copy(images[i].data().begin(), images[i].data().end(), linear_image_data.begin() + width * height * i * channels); oroMemcpy3DParms copyParams = { 0 }; copyParams.srcPtr = oroPitchedPtr{ linear_image_data.data(), width * channels * sizeof(float), width * channels, height}; copyParams.dstArray = m_texture_array; copyParams.extent = { width, height, depth }; copyParams.kind = oroMemcpyHostToDevice; OROCHI_CHECK_ERROR(oroMemcpy3D(©Params)); // Resource descriptor ORO_RESOURCE_DESC resource_descriptor; std::memset(&resource_descriptor, 0, sizeof(resource_descriptor)); resource_descriptor.resType = ORO_RESOURCE_TYPE_ARRAY; resource_descriptor.res.array.hArray = m_texture_array; ORO_TEXTURE_DESC texture_descriptor; std::memset(&texture_descriptor, 0, sizeof(texture_descriptor)); texture_descriptor.addressMode[0] = address_mode; texture_descriptor.addressMode[1] = address_mode; texture_descriptor.addressMode[2] = address_mode; texture_descriptor.filterMode = filtering_mode; OROCHI_CHECK_ERROR(oroTexObjectCreate(&m_texture, &resource_descriptor, &texture_descriptor, nullptr)); } oroTextureObject_t OrochiTexture3D::get_device_texture() { return m_texture; } ================================================ FILE: src/HIPRT-Orochi/OrochiTexture3D.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef OROCHI_TEXTURE_3D_H #define OROCHI_TEXTURE_3D_H #include "HIPRT-Orochi/OrochiBuffer.h" #include "Image/Image.h" #include class OrochiTexture3D { public: OrochiTexture3D() {} OrochiTexture3D(const std::vector& image, HIPfilter_mode filtering_mode = ORO_TR_FILTER_MODE_POINT, HIPaddress_mode address_mode = ORO_TR_ADDRESS_MODE_WRAP); OrochiTexture3D(const std::vector& image, HIPfilter_mode filtering_mode = ORO_TR_FILTER_MODE_POINT, HIPaddress_mode address_mode = ORO_TR_ADDRESS_MODE_WRAP); OrochiTexture3D(const OrochiTexture3D& other) = delete; OrochiTexture3D(OrochiTexture3D&& other) noexcept; ~OrochiTexture3D(); void operator=(const OrochiTexture3D& other) = delete; void operator=(OrochiTexture3D&& other) noexcept; void init_from_images(const std::vector& images, HIPfilter_mode filtering_mode = ORO_TR_FILTER_MODE_POINT, HIPaddress_mode address_mode = ORO_TR_ADDRESS_MODE_WRAP); void init_from_images(const std::vector& images, HIPfilter_mode filtering_mode = ORO_TR_FILTER_MODE_POINT, HIPaddress_mode address_mode = ORO_TR_ADDRESS_MODE_WRAP); oroTextureObject_t get_device_texture(); unsigned int width = 0, height = 0, depth = 0; private: oroArray_t m_texture_array = nullptr; oroTextureObject_t m_texture = nullptr; }; #endif ================================================ FILE: src/HIPRT-Orochi/OrochiTextureCUDA.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifdef OROCHI_ENABLE_CUEW #include "cuda_runtime_api.h" #include "contrib/cuew/include/cuew.h" #include "Utils/Utils.h" void create_texture_from_array_cuda(void* m_texture_array, void* m_texture, void* filtering_mode, void* address_mode, bool read_mode_float_normalized) { // Resource descriptor cudaResourceDesc resource_descriptor = {}; resource_descriptor.resType = cudaResourceTypeArray; resource_descriptor.res.array.array = reinterpret_cast(m_texture_array); cudaTextureDesc texture_descriptor = {}; texture_descriptor.addressMode[0] = *reinterpret_cast(address_mode); texture_descriptor.addressMode[1] = *reinterpret_cast(address_mode); texture_descriptor.addressMode[2] = *reinterpret_cast(address_mode); texture_descriptor.filterMode = *reinterpret_cast(filtering_mode); texture_descriptor.normalizedCoords = true; texture_descriptor.readMode = read_mode_float_normalized ? cudaTextureReadMode::cudaReadModeNormalizedFloat : cudaTextureReadMode::cudaReadModeElementType; texture_descriptor.sRGB = false; cudaError_t error = cudaCreateTextureObject_oro(reinterpret_cast(m_texture), &resource_descriptor, &texture_descriptor, nullptr); if (error != cudaError::cudaSuccess) Utils::debugbreak(); } #endif ================================================ FILE: src/HostDeviceCommon/AtomicType.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_ATOMIC_TYPE_H #define HOST_DEVICE_COMMON_ATOMIC_TYPE_H #ifdef __KERNELCC__ template using AtomicType = T; #else #include template using AtomicType = std::atomic; #endif #endif ================================================ FILE: src/HostDeviceCommon/BSDFsData.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_BSDFS_DATA_H #define HOST_DEVICE_COMMON_BSDFS_DATA_H #include "HostDeviceCommon/MicrofacetRegularizationSettings.h" /** * What masking-shadowing term to use with the GGX NDF. * * 'HeightCorrelated' is a little be more precise and * corect than 'HeightUncorrelated' so it should basically * always be preferred. * * This is basically only for experimentation purposes */ enum GGXMaskingShadowingFlavor { HeightCorrelated, HeightUncorrelated }; struct BRDFsData { bool white_furnace_mode = false; bool white_furnace_mode_turn_off_emissives = true; // 32x32 texture containing the precomputed parameters of the LTC // fitted to approximate the SSGX sheen volumetric layer. // See SheenLTCFittedParameters.h void* sheen_ltc_parameters_texture = nullptr; // 2D texture for the precomputed directional albedo // for the GGX BRDFs used in the principled BSDF for energy compensation // of conductors void* GGX_conductor_directional_albedo = nullptr; // 3D texture for the precomputed directional albedo of the base layer // of the principled BSDF (specular GGX layer + diffuse below) void* glossy_dielectric_directional_albedo = nullptr; // 3D texture (cos_theta_o, roughness, relative_eta) for the precomputed // directional albedo used for energy compensation of glass objects when // entering a medium void* GGX_glass_directional_albedo = nullptr; // Table when leaving a medium void* GGX_glass_directional_albedo_inverse = nullptr; // Table for energy compesantion of thin walled glass // Fetching into this table should use the base roughness // of the material i.e. **not** the remapped thin-walled roughness void* GGX_thin_glass_directional_albedo = nullptr; // Whether or not to use the texture unit's hardware texel interpolation // when fetching the LUTs. It's faster but less precise. bool use_hardware_tex_interpolation = false; GGXMaskingShadowingFlavor GGX_masking_shadowing = GGXMaskingShadowingFlavor::HeightUncorrelated; float energy_compensation_roughness_threshold = 0.0f; // After hom many bounces to stop doing energy compensation to save performance? // // For example, 0 means that energy compensation will only be done on the first hit and // not later // // -1 to disable int glass_energy_compensation_max_bounce = -1; int metal_energy_compensation_max_bounce = -1; int clearcoat_energy_compensation_max_bounce = -1; int glossy_base_energy_compensation_max_bounce = -1; MicrofacetRegularizationSettings microfacet_regularization; }; #endif ================================================ FILE: src/HostDeviceCommon/Color.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_COLOR_H #define HOST_DEVICE_COMMON_COLOR_H #include "Device/includes/Hash.h" #include "HostDeviceCommon/Math.h" struct ColorRGBA32F { HIPRT_HOST_DEVICE ColorRGBA32F() { r = 0.0f; g = 0.0f; b = 0.0f; a = 1.0f; } HIPRT_HOST_DEVICE explicit ColorRGBA32F(float value) { r = value; g = value; b = value; a = 1.0f; } HIPRT_HOST_DEVICE ColorRGBA32F(float _r, float _g, float _b, float _a) { r = _r; g = _g; b = _b; a = _a; } HIPRT_HOST_DEVICE explicit ColorRGBA32F(float4 vec) { r = vec.x; g = vec.y; b = vec.z; a = vec.w; } HIPRT_HOST_DEVICE void operator+=(const ColorRGBA32F& other) { r += other.r; g += other.g; b += other.b; a += other.a; } HIPRT_HOST_DEVICE void operator-=(const ColorRGBA32F& other) { r -= other.r; g -= other.g; b -= other.b; a -= other.a; } HIPRT_HOST_DEVICE void operator*=(const ColorRGBA32F& other) { r *= other.r; g *= other.g; b *= other.b; a *= other.a; } HIPRT_HOST_DEVICE void operator*=(float k) { r *= k; g *= k; b *= k; a *= k; } HIPRT_HOST_DEVICE void operator/=(const ColorRGBA32F& other) { r /= other.r; g /= other.g; b /= other.b; a /= other.a; } HIPRT_HOST_DEVICE void operator/=(float k) { r /= k; g /= k; b /= k; a /= k; } HIPRT_HOST_DEVICE bool operator!=(const ColorRGBA32F& other) { return r != other.r || g != other.g || b != other.g || a != other.a; } HIPRT_HOST_DEVICE float length() const { return sqrtf(this->length2()); } HIPRT_HOST_DEVICE float length2() const { return r * r + g * g + b * b + a * a; } HIPRT_HOST_DEVICE float luminance() const { return 0.3086f * r + 0.6094f * g + 0.0820f * b; } HIPRT_HOST_DEVICE void clamp(float min, float max) { r = hippt::clamp(min, max, r); g = hippt::clamp(min, max, g); b = hippt::clamp(min, max, b); a = hippt::clamp(min, max, a); } HIPRT_HOST_DEVICE ColorRGBA32F clamped(float min, float max) { return ColorRGBA32F(hippt::clamp(min, max, r), g = hippt::clamp(min, max, g), b = hippt::clamp(min, max, b), a = hippt::clamp(min, max, a)); } HIPRT_HOST_DEVICE bool has_nan() const { return hippt::is_nan(r) || hippt::is_nan(g) || hippt::is_nan(b) || hippt::is_nan(a); } HIPRT_HOST_DEVICE bool has_inf() const { return hippt::is_inf(r) || hippt::is_inf(g) || hippt::is_inf(b) || hippt::is_inf(a); } HIPRT_HOST_DEVICE bool is_black() const { return !(r > 0.0f || g > 0.0f || b > 0.0f); } HIPRT_HOST_DEVICE bool is_white() const { return r == 1.0f && g == 1.0f && b == 1.0f; } HIPRT_HOST_DEVICE float max_component() const { return hippt::max(r, hippt::max(g, hippt::max(b, a))); } HIPRT_HOST_DEVICE float min_component() const { return hippt::min(r, hippt::min(g, hippt::min(b, a))); } HIPRT_HOST_DEVICE ColorRGBA32F normalized() const { float length = sqrtf(r * r + g * g + b * b); return ColorRGBA32F(r / length, g / length, b / length, /* not normalizing alpha */ a); } HIPRT_HOST_DEVICE ColorRGBA32F abs() { return ColorRGBA32F(hippt::abs(this->r), hippt::abs(this->g), hippt::abs(this->b), hippt::abs(this->a)); } HIPRT_HOST_DEVICE void max(const ColorRGBA32F& maxer) { this->r = hippt::max(this->r, maxer.r); this->g = hippt::max(this->g, maxer.g); this->b = hippt::max(this->b, maxer.b); this->a = hippt::max(this->a, maxer.a); } HIPRT_HOST_DEVICE static ColorRGBA32F max(const ColorRGBA32F& a, const ColorRGBA32F& b) { return ColorRGBA32F(hippt::max(a.r, b.r), hippt::max(a.g, b.g), hippt::max(a.b, b.b), hippt::max(a.a, b.a)); } HIPRT_HOST_DEVICE static ColorRGBA32F min(const ColorRGBA32F& a, const ColorRGBA32F& b) { return ColorRGBA32F(hippt::min(a.r, b.r), hippt::min(a.g, b.g), hippt::min(a.b, b.b), hippt::min(a.a, b.a)); } HIPRT_HOST_DEVICE float& operator[](int index) { return *(&r + index); } HIPRT_HOST_DEVICE float operator[](int index) const { return *(&r + index); } float r, g, b, a; }; HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F operator+ (const ColorRGBA32F& a, const ColorRGBA32F& b) { return ColorRGBA32F(a.r + b.r, a.g + b.g, a.b + b.b, a.a + b.a); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F operator- (const ColorRGBA32F& c) { return ColorRGBA32F(-c.r, -c.g, -c.b, c.a); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F operator- (const ColorRGBA32F& a, const ColorRGBA32F& b) { return ColorRGBA32F(a.r - b.r, a.g - b.g, a.b - b.b, a.a - b.a); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F operator* (const ColorRGBA32F& a, const ColorRGBA32F& b) { return ColorRGBA32F(a.r * b.r, a.g * b.g, a.b * b.b, a.a * b.a); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F operator* (const float k, const ColorRGBA32F& c) { return ColorRGBA32F(c.r * k, c.g * k, c.b * k, c.a * k); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F operator* (const ColorRGBA32F& c, const float k) { return ColorRGBA32F(c.r * k, c.g * k, c.b * k, c.a * k); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F operator/ (const ColorRGBA32F& a, const ColorRGBA32F& b) { return ColorRGBA32F(a.r / b.r, a.g / b.g, a.b / b.b, a.a / b.a); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F operator/ (const float k, const ColorRGBA32F& c) { return ColorRGBA32F(k / c.r, k / c.g, k / c.b, k / c.a); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F operator/ (const ColorRGBA32F& c, const float k) { return ColorRGBA32F(c.r / k, c.g / k, c.b / k, c.a / k); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F sqrt(const ColorRGBA32F& col) { return ColorRGBA32F(sqrtf(col.r), sqrtf(col.g), sqrtf(col.b), sqrtf(col.a)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F exp(const ColorRGBA32F& col) { return ColorRGBA32F(expf(col.r), expf(col.g), expf(col.b), expf(col.a)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F exp2(const ColorRGBA32F& col) { return ColorRGBA32F(exp2f(col.r), exp2f(col.g), exp2f(col.b), exp2f(col.a)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F log(const ColorRGBA32F& col) { return ColorRGBA32F(logf(col.r), logf(col.g), logf(col.b), logf(col.a)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F pow(const ColorRGBA32F& col, float k) { return ColorRGBA32F(powf(col.r, k), powf(col.g, k), powf(col.b, k), powf(col.a, k)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGBA32F intrin_pow(ColorRGBA32F x, float y) { return ColorRGBA32F(hippt::intrin_pow(x.r, y), hippt::intrin_pow(x.g, y), hippt::intrin_pow(x.b, y), hippt::intrin_pow(x.a, y)); } struct ColorRGB32F { HIPRT_HOST_DEVICE ColorRGB32F() { r = 0.0f; g = 0.0f; b = 0.0f; } HIPRT_HOST_DEVICE explicit ColorRGB32F(float value) { r = value; g = value; b = value; } HIPRT_HOST_DEVICE ColorRGB32F(float _r, float _g, float _b) { r = _r; g = _g; b = _b; } HIPRT_HOST_DEVICE explicit ColorRGB32F(float3 vec) { r = vec.x; g = vec.y; b = vec.z; } // W component of float4 is dropped HIPRT_HOST_DEVICE explicit ColorRGB32F(float4 vec) { r = vec.x; g = vec.y; b = vec.z; } // This constructor drops the alpha channel HIPRT_HOST_DEVICE explicit ColorRGB32F(const ColorRGBA32F& rgba) { r = rgba.r; g = rgba.g; b = rgba.b; } HIPRT_HOST_DEVICE void operator+=(const ColorRGB32F& other) { r += other.r; g += other.g; b += other.b; } HIPRT_HOST_DEVICE void operator-=(const ColorRGB32F& other) { r -= other.r; g -= other.g; b -= other.b; } HIPRT_HOST_DEVICE void operator*=(const ColorRGB32F& other) { r *= other.r; g *= other.g; b *= other.b; } HIPRT_HOST_DEVICE void operator*=(float k) { r *= k; g *= k; b *= k; } HIPRT_HOST_DEVICE void operator/=(const ColorRGB32F& other) { r /= other.r; g /= other.g; b /= other.b; } HIPRT_HOST_DEVICE void operator/=(float k) { r /= k; g /= k; b /= k; } HIPRT_HOST_DEVICE bool operator!=(const ColorRGB32F& other) { return r != other.r || g != other.g || b != other.g; } HIPRT_HOST_DEVICE float length() const { return sqrtf(this->length2()); } HIPRT_HOST_DEVICE float length2() const { return r * r + g * g + b * b; } HIPRT_HOST_DEVICE float luminance() const { return 0.3086f * r + 0.6094f * g + 0.0820f * b; } HIPRT_HOST_DEVICE void clamp(float min, float max) { r = hippt::clamp(min, max, r); g = hippt::clamp(min, max, g); b = hippt::clamp(min, max, b); } HIPRT_HOST_DEVICE ColorRGB32F clamped(float min, float max) { return ColorRGB32F(hippt::clamp(min, max, r), g = hippt::clamp(min, max, g), b = hippt::clamp(min, max, b)); } HIPRT_HOST_DEVICE bool has_nan() const { return hippt::is_nan(r) || hippt::is_nan(g) || hippt::is_nan(b); } HIPRT_HOST_DEVICE bool has_inf() const { return hippt::is_inf(r) || hippt::is_inf(g) || hippt::is_inf(b); } HIPRT_HOST_DEVICE bool is_black() const { return !(r > 0.0f || g > 0.0f || b > 0.0f); } HIPRT_HOST_DEVICE bool is_white() const { return r == 1.0f && g == 1.0f && b == 1.0f; } HIPRT_HOST_DEVICE float max_component() const { return hippt::max(r, hippt::max(g, b)); } HIPRT_HOST_DEVICE float min_component() const { return hippt::min(r, hippt::min(g, b)); } HIPRT_HOST_DEVICE ColorRGB32F normalized() const { float length = sqrtf(r * r + g * g + b * b); return ColorRGB32F(r / length, g / length, b / length); } HIPRT_HOST_DEVICE ColorRGB32F abs() { return ColorRGB32F(hippt::abs(this->r), hippt::abs(this->g), hippt::abs(this->b)); } HIPRT_HOST_DEVICE void max(const ColorRGB32F& maxer) { this->r = hippt::max(this->r, maxer.r); this->g = hippt::max(this->g, maxer.g); this->b = hippt::max(this->b, maxer.b); } HIPRT_HOST_DEVICE static ColorRGB32F max(const ColorRGB32F& a, const ColorRGB32F& b) { return ColorRGB32F(hippt::max(a.r, b.r), hippt::max(a.g, b.g), hippt::max(a.b, b.b)); } HIPRT_HOST_DEVICE static ColorRGB32F min(const ColorRGB32F& a, const ColorRGB32F& b) { return ColorRGB32F(hippt::min(a.r, b.r), hippt::min(a.g, b.g), hippt::min(a.b, b.b)); } HIPRT_HOST_DEVICE float& operator[](int index) { return *(&r + index); } HIPRT_HOST_DEVICE float operator[](int index) const { return *(&r + index); } HIPRT_HOST_DEVICE static ColorRGB32F random_color(unsigned int seed) { constexpr unsigned int UNSIGNED_INT_MAX = 0xffffffff; unsigned int seed1 = wang_hash(seed); unsigned int seed2 = wang_hash(seed1); unsigned int seed3 = wang_hash(seed2); return ColorRGB32F(seed1 / static_cast(UNSIGNED_INT_MAX), seed2 / static_cast(UNSIGNED_INT_MAX), seed3 / static_cast(UNSIGNED_INT_MAX)); } float r, g, b; }; HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F operator+ (const ColorRGB32F& a, const ColorRGB32F& b) { return ColorRGB32F(a.r + b.r, a.g + b.g, a.b + b.b); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F operator- (const ColorRGB32F& c) { return ColorRGB32F(-c.r, -c.g, -c.b); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F operator- (const ColorRGB32F& a, const ColorRGB32F& b) { return ColorRGB32F(a.r - b.r, a.g - b.g, a.b - b.b); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F operator* (const ColorRGB32F& a, const ColorRGB32F& b) { return ColorRGB32F(a.r * b.r, a.g * b.g, a.b * b.b); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F operator* (const float k, const ColorRGB32F& c) { return ColorRGB32F(c.r * k, c.g * k, c.b * k); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F operator* (const ColorRGB32F& c, const float k) { return ColorRGB32F(c.r * k, c.g * k, c.b * k); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F operator/ (const ColorRGB32F& a, const ColorRGB32F& b) { return ColorRGB32F(a.r / b.r, a.g / b.g, a.b / b.b); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F operator/ (const float k, const ColorRGB32F& c) { return ColorRGB32F(k / c.r, k / c.g, k / c.b); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F operator/ (const ColorRGB32F& c, const float k) { return ColorRGB32F(c.r / k, c.g / k, c.b / k); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F sqrt(const ColorRGB32F& col) { return ColorRGB32F(sqrtf(col.r), sqrtf(col.g), sqrtf(col.b)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F exp(const ColorRGB32F& col) { return ColorRGB32F(expf(col.r), expf(col.g), expf(col.b)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F exp2(const ColorRGB32F& col) { return ColorRGB32F(exp2f(col.r), exp2f(col.g), exp2f(col.b)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F log(const ColorRGB32F& col) { return ColorRGB32F(logf(col.r), logf(col.g), logf(col.b)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F pow(const ColorRGB32F& col, float k) { return ColorRGB32F(powf(col.r, k), powf(col.g, k), powf(col.b, k)); } HIPRT_HOST_DEVICE HIPRT_INLINE ColorRGB32F intrin_pow(ColorRGB32F x, float y) { return ColorRGB32F(hippt::intrin_pow(x.r, y), hippt::intrin_pow(x.g, y), hippt::intrin_pow(x.b, y)); } #ifndef __KERNELCC__ inline std::ostream& operator <<(std::ostream& os, const ColorRGB32F& color) { os << color.r << ", " << color.g << ", " << color.b; return os; } inline std::ostream& operator <<(std::ostream& os, const ColorRGBA32F& color) { os << color.r << ", " << color.g << ", " << color.b << ", " << color.a; return os; } #endif #endif ================================================ FILE: src/HostDeviceCommon/HIPRTCamera.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_HIPRT_CAMERA_H #define HOST_DEVICE_COMMON_HIPRT_CAMERA_H #include "HostDeviceCommon/Math.h" #include // for hiprtRay /** * Simplified camera class passed to the shader */ struct HIPRTCamera { float4x4 inverse_view; float4x4 inverse_projection; float4x4 view_projection; float3 position; float vertical_fov; int sensor_width, sensor_height; bool do_jittering = true; /** * Returns a camera ray for pixel (x, y) and the given render solution */ HIPRT_HOST_DEVICE hiprtRay get_camera_ray(float x, float y, int2 res) { float x_ndc_space = x / res.x * 2 - 1; float y_ndc_space = y / res.y * 2 - 1; float3 ray_origin_view_space = { 0.0f, 0.0f, 0.0f }; float3 ray_origin = matrix_X_point(inverse_view, ray_origin_view_space); // Point on the near plane float3 ray_point_dir_ndc_homog = { x_ndc_space, y_ndc_space, -1.0f }; float3 ray_point_dir_vs_homog = matrix_X_point(inverse_projection, ray_point_dir_ndc_homog); float3 ray_point_dir_vs = ray_point_dir_vs_homog; float3 ray_point_dir_ws = matrix_X_point(inverse_view, ray_point_dir_vs); float3 ray_direction = hippt::normalize(ray_point_dir_ws - ray_origin); hiprtRay ray; ray.origin = ray_origin; ray.direction = ray_direction; return ray; } }; #endif ================================================ FILE: src/HostDeviceCommon/HitInfo.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_HIT_INFO_H #define HOST_DEVICE_COMMON_HIT_INFO_H #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Math.h" struct HitInfo { float3 inter_point = { 0, 0, 0 }; float3 shading_normal = { 0, 0, 0 }; float3 geometric_normal = { 0, 0, 0 }; // TODO is texcoords useful? This may actually be returned by the intersection function and used only for reading textures but then we don't need it anymore when evaluating the bSDF and comp�ting the main path tracing stuff so let's save some registers float2 texcoords = { 0, 0 }; // Distance along ray float t = -1.0f; int primitive_index = -1; }; /** * Information returned by a shadow ray cast from a BSDF sample. * * This structure is filled by the 'evaluate_bsdf_light_sample_ray()' * function that is usually called for testing if a BSDF ray * (used by MIS) sees some emissive geometry or not. */ struct BSDFLightSampleRayHitInfo { // TODO do we use this only for the area of the light? In which case we can just store the area of the light int hit_prim_index; // TODO is this used? int hit_material_index; float hit_distance; float2 hit_interpolated_texcoords; float3 hit_shading_normal; float3 hit_geometric_normal; ColorRGB32F hit_emission; }; #endif ================================================ FILE: src/HostDeviceCommon/KernelOptions/Common.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_KERNEL_OPTIONS_COMMON_H #define HOST_DEVICE_COMMON_KERNEL_OPTIONS_COMMON_H #define KERNEL_OPTION_FALSE 0 #define KERNEL_OPTION_TRUE 1 #endif ================================================ FILE: src/HostDeviceCommon/KernelOptions/DirectLightSamplingOptions.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_DIRECT_LIGHT_SAMPLING_OPTIONS_H #define HOST_DEVICE_COMMON_DIRECT_LIGHT_SAMPLING_OPTIONS_H #include "HostDeviceCommon/KernelOptions/Common.h" #define LSS_NO_DIRECT_LIGHT_SAMPLING 0 #define LSS_ONE_LIGHT 1 #define LSS_BSDF 2 #define LSS_MIS_LIGHT_BSDF 3 #define LSS_RIS_BSDF_AND_LIGHT 4 #define LSS_RESTIR_DI 5 #define LSS_BASE_UNIFORM 0 #define LSS_BASE_POWER 1 #define LSS_BASE_REGIR 2 // This block is a security to make sure that we have everything defined otherwise this can lead // to weird behavior because of the compiler not knowing about some macros #ifndef KERNEL_OPTION_TRUE #error "KERNEL_OPTION_TRUE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #else #ifndef KERNEL_OPTION_FALSE #error "KERNEL_OPTION_FALSE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #endif #endif /** * Options are defined in a #ifndef __KERNELCC__ block because: * - If they were not, the would be defined on the GPU side. However, the -D = compiler option * cannot override a #define statement. This means that if the #define statement are encountered by the compiler, * we cannot modify the value of the macros anymore with the -D option which means no run-time switching / experimenting :( * - The CPU still needs the options to be able to compile the code so here they are, in a CPU-only block */ #ifndef __KERNELCC__ /** * What direct lighting sampling strategy to use. * * Possible values (the prefix LSS stands for "Light Sampling strategy"): * * - LSS_NO_DIRECT_LIGHT_SAMPLING * No direct light sampling. Emission is only gathered if rays happen to bounce into the lights. * * - LSS_ONE_LIGHT * Samples one random light in the scene without MIS. * Efficient as long as there are not too many lights in the scene and no glossy surfaces * * - LSS_BSDF * Samples lights only using a BSDF sample * Efficient as long as light sources in the scene are large * * - LSS_MIS_LIGHT_BSDF * Samples one random light in the scene with MIS (Multiple Importance Sampling): light sample + BRDF sample * * - LSS_RIS_BSDF_AND_LIGHT * Samples lights in the scene with Resampled Importance Sampling * * - LSS_RESTIR_DI * Uses ReSTIR DI to sample direct lighting at the first bounce in the scene. * Later bounces use the strategy given by ReSTIR_DI_LaterBouncesSamplingStrategy */ #define DirectLightSamplingStrategy LSS_RIS_BSDF_AND_LIGHT /** * How to sample lights in the scene. * This directly affects the 'DirectLightSamplingStrategy' strategies that sample lights * * - LSS_BASE_UNIFORM * Lights are sampled uniformly * * - LSS_BASE_POWER * Lights are sampled proportionally to their power * * - LSS_BASE_REGIR * Uses ReGIR to sample lights * Implementation of [Rendering many lights with grid-based reservoirs, Boksansky, 2021] */ #define DirectLightSamplingBaseStrategy LSS_BASE_POWER /** * How many light samples to take and shade per each vertex of the * ray's path. * * Said otherwise, we're going to run next-event estimation that many * times per each intersection point along the ray. * * This is good because this amortizes camera rays and bounce rays i.e. * we get better shading quality for as many camera rays and bounce rays * * This is not supported by ReSTIR DI because this would require recomputing * a new reservoir = full re-run of ReSTIR = too expensive. * It does apply to the secondary bounces shading when using ReSTIR DI for the * primary bounce though. */ #define DirectLightSamplingNEESampleCount 1 /** * If this is true, light sampling with NEE (emissive geometry & envmap) will not even * be attempted on perfectly smooth materials (smooth glass, smooth metals, ...) * * This is because these materials are delta distributions and light sampling * has no chance to give any contribution. * * There is no point in disabling that option, this is basically only for * performance comparisons */ #define DirectLightSamplingDeltaDistributionOptimization KERNEL_OPTION_TRUE /** * Whether or not to allow backfacing lights during NEE evaluation. * * For most scenes, this is going to have no impact on visuals as lights are generally * watertight meshes, meaning that backfacing emissive triangles of those meshes are not visible from * the outside. There will thus be no visual difference but a non negligeable boost in * performance/sampling quality as backfacing lights will not be sampled anymore (depending on the sampling strategy) */ #define DirectLightSamplingAllowBackfacingLights KERNEL_OPTION_FALSE #endif // #ifndef __KERNELCC__ #endif ================================================ FILE: src/HostDeviceCommon/KernelOptions/GMoNOptions.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_KERNEL_OPTIONS_GMON_OPTIONS_H #define HOST_DEVICE_COMMON_KERNEL_OPTIONS_GMON_OPTIONS_H #include "HostDeviceCommon/KernelOptions/Common.h" /** * Kernel options for the implementation of GMoN * * Reference: * [1] [Firefly removal in Monte Carlo rendering with adaptive Median of meaNs, Buisine et al., 2021] */ // This block is a security to make sure that we have everything defined otherwise this can lead // to weird behavior because of the compiler not knowing about some macros #ifndef KERNEL_OPTION_TRUE #error "KERNEL_OPTION_TRUE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #else #ifndef KERNEL_OPTION_FALSE #error "KERNEL_OPTION_FALSE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #endif #endif /** * Options are defined in a #ifndef __KERNELCC__ block because: * - If they were not, the would be defined on the GPU side. However, the -D = compiler option * cannot override a #define statement. This means that if the #define statement are encountered by the compiler, * we cannot modify the value of the macros anymore with the -D option which means no run-time switching / experimenting :( * - The CPU still needs the options to be able to compile the code so here they are, in a CPU-only block */ #ifndef __KERNELCC__ /** * How many sets to use for GMoN. M variable in the paper */ #define GMoNMSetsCount 11 #endif // #ifndef __KERNELCC__ // The options below are not in the "#ifndef __KERNELCC__" guard because they cannot change at runtime // so we're not passing them as options to the compiler with -D so they need to be know in the // source file at compile time /** * Thread block size dispatched when computing the G-Median of Means per each pixel */ #define GMoNComputeMeansKernelThreadBlockSize 8 /** * How many bits to use to sort the means with a radix sort */ #define GMoNKeysNbDigitsForRadixSort 32 /** * What radix is used for the radix sort of the means */ #define GMoNSortRadixSize 2 #endif // #ifndef HOST_DEVICE_COMMON_KERNEL_OPTIONS_GMON_OPTIONS_H ================================================ FILE: src/HostDeviceCommon/KernelOptions/KernelOptions.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_KERNEL_OPTIONS_H #define HOST_DEVICE_COMMON_KERNEL_OPTIONS_H #include "HostDeviceCommon/KernelOptions/DirectLightSamplingOptions.h" #include "HostDeviceCommon/KernelOptions/GMoNOptions.h" #include "HostDeviceCommon/KernelOptions/NEEPlusPlusOptions.h" #include "HostDeviceCommon/KernelOptions/PrincipledBSDFKernelOptions.h" #include "HostDeviceCommon/KernelOptions/ReGIROptions.h" #include "HostDeviceCommon/KernelOptions/ReSTIRDIOptions.h" #include "HostDeviceCommon/KernelOptions/ReSTIRGIOptions.h" /** * This file references the path tracer options that can be passed to HIPCC using the -D = option. * These path tracer options allow "compile-time" branching to enable/disable a variety * of functionalities in the path tracer. * * For example, you can decide, at kernel compile-time, what envmap sampling strategy to use * - "CDF + Binary search" * - "Alias table" * by passing the "-D EnvmapSamplingStrategy=1" or "-D EnvmapSamplingStrategy=2" option string during * the compilation of the kernel (for "CDF" and "alias table" respectively). * * If you wish to change one of the option used by the path tracer at runtime (by interacting with * ImGui for example), you will have to recompile the kernel with the correct set of options * passed to the kernel compiler. * * The advantage of recompiling the entire kernel over branching with a simple if() condition on * a variable (that would be passed in RenderData for example) is that the recompiling approach * does not incur an additional register cost that would harm the occupancy potential of the kernel * (whereas registers may be allocated for the block {} of the if() conditions since the compiler * has no way to know which branch of the if is going to be taken at runtime). */ /** * Those are simple defines to give names to the option values. * This allows the use of LSS_ONE_RANDOM_LIGHT_MIS (for example) instead of a hardcoded '2' */ #define BSDF_NONE 0 #define BSDF_LAMBERTIAN 1 #define BSDF_OREN_NAYAR 2 #define BSDF_PRINCIPLED 3 #define NESTED_DIELECTRICS_STACK_SIZE 4 #define TRIANGLE_POINT_SAMPLING_TURK_1990 0 #define TRIANGLE_POINT_SAMPLING_HEITZ_2019 1 #define ESS_NO_SAMPLING 0 #define ESS_BINARY_SEARCH 1 #define ESS_ALIAS_TABLE 2 #define PSS_BSDF 0 #define PSS_RESTIR_GI 1 /** * Options are defined in a #ifndef __KERNELCC__ block because: * - If they were not, the would be defined on the GPU side. However, the -D = compiler option * cannot override a #define statement. This means that if the #define statement are encountered by the compiler, * we cannot modify the value of the macros anymore with the -D option which means no run-time switching / experimenting :( * - The CPU still needs the options to be able to compile the code so here they are, in a CPU-only block */ #ifndef __KERNELCC__ /** * Whether or not to use shared memory and a global buffer for BVH traversal of global rays (no maximum distance). * * This improves performance at the cost of a higher VRAM usage (because of the global buffer needed) */ #define UseSharedStackBVHTraversal KERNEL_OPTION_TRUE /** * Size of the thread blocks for all kernels dispatched by this renderer */ #define KernelBlockWidthHeight 8 /** * Size of the thread blocks used when dispatching the kernels. * This value is used for allocating the shared memory stack for traversal */ #define KernelWorkgroupThreadCount (KernelBlockWidthHeight * KernelBlockWidthHeight) /** * Size of the shared memory stack for BVH traversal of "global" rays * (rays that search for the closest hit with no maximum distance) */ #define SharedStackBVHTraversalSize 16 /** * Partial and (very) experimental implementation of [Generate Coherent Rays Directly, Liu et al., 2024] * for reuse sampled directions on the first hit accross the threads of warps */ #define DoFirstBounceWarpDirectionReuse KERNEL_OPTION_FALSE /** * Allows the overriding of the BRDF/BSDF used by the path tracer. When an override is used, * the material retains its properties (color, roughness, ...) but only the parameters relevant * to the overriden BSDF are used. * * - BSDF_NONE * Materials will use their default BRDF/BSDF, no override * * - BSDF_LAMBERTIAN * All materials will use a lambertian BRDF * * - BSDF_OREN_NAYAR * All materials will use the Oren Nayar diffuse BRDF * * - BSDF_PRINCIPLED * All materials will use the Principled BSDF */ #define BSDFOverride BSDF_NONE /** * The stack size for handling nested dielectrics */ #define NestedDielectricsStackSize NESTED_DIELECTRICS_STACK_SIZE /** * How to randomly sample a point on a triangle * * - TRIANGLE_POINT_SAMPLING_TURK_1990 * Common way of warping from a square to a triangle using square roots: * V = (1.0f - sqrt(u1)) * V1 + sqrt(u1) * (s2 * V2 + (1.0f - s2) * V3) * * - TRIANGLE_POINT_SAMPLING_HEITZ_2019 * Implementation of [A Low-Distortion Map Between Triangle and Square, Heitz, 2019] * It is faster than Turk method's and better perserves the stratification of the random * number samplers */ #define TrianglePointSamplingStrategy TRIANGLE_POINT_SAMPLING_HEITZ_2019 /** * What envmap sampling strategy to use * * Possible values (the prefix ESS stands for "Envmap Sampling Strategy"): * * - ESS_NO_SAMPLING * No importance sampling of the envmap * * - ESS_BINARY_SEARCH * Importance samples a texel of the environment map proportionally to its * luminance using a binary search on the CDF distributions of the envmap luminance. * Good convergence. * * - ESS_ALIAS_TABLE * Importance samples a texel of the environment map proportionally to its * luminance using an alias table for constant time sampling * Good convergence and faster than ESS_BINARY_SEARCH */ #define EnvmapSamplingStrategy ESS_ALIAS_TABLE /** * Whether or not to do Muliple Importance Sampling between the envmap sample and a BSDF * sample when importance sampling direct lighting contribution from the envmap */ #define EnvmapSamplingDoBSDFMIS KERNEL_OPTION_TRUE /** * Whether or not to do bilinear filtering when sampling the envmap. * * This is mostly useful when the camera is looking straigth at the envmap and we don't * have camera ray jittering on: in this case, bilinear filtering will hide the * pixelated look of the envmap. */ #define EnvmapSamplingDoBilinearFiltering KERNEL_OPTION_FALSE /** * What sampling strategy to use for sampling the bounces during path tracing. * * - PSS_BSDF * The classical technique: importance samples the BSDF and bounces in that direction * * - PSS_RESTIR_GI * Uses ReSTIR GI for resampling a path for the pixel. * * The implementation is based on * [ReSTIR GI: Path Resampling for Real-Time Path Tracing] https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing * but is adapted almost full unbiasedness (full unbiasedness while resampling full path trees as in ReSTIR GI paper isn't supported * by the GRIS theory. Fully unbiased path resampling with the current RIS theory can only be achieved by resampling "paths" and not full "path trees" as proposed * in the ReSTIR GI paper and as implemented here) * * The original ReSTIR GI paper indeed only is unbiased for a Lambertian BRDF */ #define PathSamplingStrategy PSS_BSDF /** * Whether or not to use a visiblity term in the target function whose PDF we're * approximating with RIS. * Only applies for pure RIS direct lighting strategy (i.e. not RIS used by ReSTIR * on the initial candidates pass for example) * * - KERNEL_OPTION_TRUE or KERNEL_OPTION_FALSE values are accepted. Self-explanatory */ #define RISUseVisiblityTargetFunction KERNEL_OPTION_FALSE /** * Debug option which, if enabled, only outputs the sample 'render_settings.output_debug_sample_N' * to the framebuffer. * * Useful for debugging features that may take effect after the first sample and we only want to see what * the second sample looks like without the accumulation */ #define DisplayOnlySampleN KERNEL_OPTION_FALSE #endif // #ifndef __KERNELCC__ /** * This is a handy macro that tells us whether or not we have any other kernel option * that overrides the color of the framebuffer */ #define ViewportColorOverriden ( \ (NEEPlusPlusDebugMode != NEE_PLUS_PLUS_DEBUG_MODE_NO_DEBUG || DirectLightNEEPlusPlusDisplayShadowRaysDiscarded == KERNEL_OPTION_TRUE) || \ (DirectLightSamplingBaseStrategy == LSS_BASE_REGIR && ReGIR_DebugMode != REGIR_DEBUG_MODE_NO_DEBUG)) #endif ================================================ FILE: src/HostDeviceCommon/KernelOptions/NEEPlusPlusOptions.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_NEE_PLUS_PLUS_OPTIONS_H #define HOST_DEVICE_COMMON_NEE_PLUS_PLUS_OPTIONS_H #include "HostDeviceCommon/KernelOptions/Common.h" // This block is a security to make sure that we have everything defined otherwise this can lead // to weird behavior because of the compiler not knowing about some macros #ifndef KERNEL_OPTION_TRUE #error "KERNEL_OPTION_TRUE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #else #ifndef KERNEL_OPTION_FALSE #error "KERNEL_OPTION_FALSE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #endif #endif #define NEE_PLUS_PLUS_DEBUG_MODE_NO_DEBUG 0 #define NEE_PLUS_PLUS_DEBUG_MODE_GRID_CELLS 1 /** * The resolution downscale factor to apply for the ReGIR grid prepopulation. * * The lower the downscale, the more effective the prepoluation but also the more costly */ #define NEEPlusPlus_GridPrepoluationResolutionDownscale 2 /** * Options are defined in a #ifndef __KERNELCC__ block because: * - If they were not, the would be defined on the GPU side. However, the -D = compiler option * cannot override a #define statement. This means that if the #define statement are encountered by the compiler, * we cannot modify the value of the macros anymore with the -D option which means no run-time switching / experimenting :( * - The CPU still needs the options to be able to compile the code so here they are, in a CPU-only block */ #ifndef __KERNELCC__ /** * Whether or not to use NEE++ features at all */ #define DirectLightUseNEEPlusPlus KERNEL_OPTION_FALSE /** * Whether or not to use russian roulette to avoid tracing shadow rays based on the visibility * information of NEE++ */ #define DirectLightUseNEEPlusPlusRR KERNEL_OPTION_FALSE /** * This a debug option to visualize shadow rays discarded by the NEE++ russian roulette */ #define DirectLightNEEPlusPlusDisplayShadowRaysDiscarded KERNEL_OPTION_FALSE /** * When using the 'DirectLightNEEPlusPlusDisplayShadowRaysDiscarded' kernel options * for displaying in the viewport where shadow rays were discarded, this parameter is used * to determine at what bounce in the scene we should display the shadow ray discarded or not * * 0 is the first hit */ #define DirectLightNEEPlusPlusDisplayShadowRaysDiscardedBounce 0 /** * Maximum number of steps for the linear probing of the NEE++ hash grid */ #define NEEPlusPlus_LinearProbingSteps 4 /** * Debug mode for displaying some debug infos about NEE++ */ #define NEEPlusPlusDebugMode NEE_PLUS_PLUS_DEBUG_MODE_NO_DEBUG #endif #endif ================================================ FILE: src/HostDeviceCommon/KernelOptions/PrincipledBSDFKernelOptions.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_PRINCIPLED_BSDF_KERNEL_OPTIONS_H #define HOST_DEVICE_COMMON_PRINCIPLED_BSDF_KERNEL_OPTIONS_H #include "HostDeviceCommon/KernelOptions/Common.h" /** * This file references the path tracer options that can be passed to HIPCC/NVCC using the -D = option. * These path tracer options allow "compile-time" branching to enable/disable a variety * of functionalities in the path tracer. * * For example, you can decide, at kernel compile-time, what envmap sampling strategy to use * - "CDF + Binary search" * - "Alias table" * by passing the "-D EnvmapSamplingStrategy=1" or "-D EnvmapSamplingStrategy=2" option string during * the compilation of the kernel (for "CDF" and "alias table" respectively). * * If you wish to change one of the option used by the path tracer at runtime (by interacting with * ImGui for example), you will have to recompile the kernel with the correct set of options * passed to the kernel compiler. * * The advantage of recompiling the entire kernel over branching with a simple if() condition on * a variable (that would be passed in RenderData for example) is that the recompiling approach * does not incur an additional register cost that would harm the occupancy potential of the kernel * (whereas registers may be allocated for the block {} of the if() conditions since the compiler * has no way to know which branch of the if is going to be taken at runtime). */ /** * Those are simple defines to give names to the option values. * This allows the use of LSS_ONE_RANDOM_LIGHT_MIS (for example) instead of a hardcoded '2' */ #define PRINCIPLED_DIFFUSE_LOBE_LAMBERTIAN 0 #define PRINCIPLED_DIFFUSE_LOBE_OREN_NAYAR 1 #define GGX_VNDF_SAMPLING 0 #define GGX_VNDF_SPHERICAL_CAPS 1 #define GGX_VNDF_BOUNDED 2 // This block is a security to make sure that we have everything defined otherwise this can lead // to weird behavior because of the compiler not knowing about some macros #ifndef KERNEL_OPTION_TRUE #error "KERNEL_OPTION_TRUE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #else #ifndef KERNEL_OPTION_FALSE #error "KERNEL_OPTION_FALSE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #endif #endif /** * Options are defined in a #ifndef __KERNELCC__ block because: * - If they were not, the would be defined on the GPU side. However, the -D = compiler option * cannot override a #define statement. This means that if the #define statement are encountered by the compiler, * we cannot modify the value of the macros anymore with the -D option which means no run-time switching / experimenting :( * - The CPU still needs the options to be able to compile the code so here they are, in a CPU-only block */ #ifndef __KERNELCC__ /** * What diffuse lobe to use in the principled BSDF. * * - PRINCIPLED_DIFFUSE_LOBE_LAMBERTIAN * Use a lambertian BRDF for the diffuse lobe * * - PRINCIPLED_DIFFUSE_LOBE_OREN_NAYAR * Use an Oren-Nayar BRDF for the diffuse lobe */ #define PrincipledBSDFDiffuseLobe PRINCIPLED_DIFFUSE_LOBE_LAMBERTIAN /** * What sampling strategy to use for the GGX NDF * * - GGX_NO_VNDF [Not Yet Implemented] * Not sampling the visible distribution of normals. * Just classic GGX sampling * * - GGX_VNDF_SAMPLING * Sample the distribution of visible normals as proposed * in [Sampling the GGX Distribution of Visible Normals, Heitz, 2018] * * - GGX_VNDF_SPHERICAL_CAPS * Sample the distribution of visible normals using spherical * caps as proposed in [Sampling Visible GGX Normals with Spherical Caps, Dupuy & Benyoub, 2023] * * - GGX_VNDF_BOUNDED [Not Yet Implemented] * Sample the distribution of visible normals with a bounded VNDF * sampling range as proposed in [Bounded VNDF Sampling for Smith-GGX Reflections, Eto & Tokuyoshi, 2023] * */ #define PrincipledBSDFAnisotropicGGXSampleFunction GGX_VNDF_SAMPLING /** * Whether or not to use multiple scattering to conserve energy when evaluating * GGX BRDF lobes in the Principled BSDF * * This is implemented by following * [Practical multiple scattering compensation for microfacet models, Turquin, 2019] * * Possible options are KERNEL_OPTION_TRUE and KERNEL_OPTION_FALSE. Self explanatory. */ #define PrincipledBSDFDoEnergyCompensation KERNEL_OPTION_TRUE /** * Whether or not to perform energy compensation for the glass layer of the Principled BSDF */ #define PrincipledBSDFDoGlassEnergyCompensation KERNEL_OPTION_TRUE /** * Whether or not to perform energy compensation (it's an approximation for the clearcoat * layer, it's not perfect but very good in most cases) for the clearcoat layer of the Principled BSDF */ #define PrincipledBSDFDoClearcoatEnergyCompensation KERNEL_OPTION_TRUE /** * Whether or not to perform energy compensation for the metallic layer of the Principled BSDF */ #define PrincipledBSDFDoMetallicEnergyCompensation KERNEL_OPTION_TRUE /** * Whether or not to use multiple scattering to conserve energy and use a * Fresnel compensation term i.e. account for Fresnel when light scatters multiple * times on the microsurface. This increases saturation and has a noticeable impact. * Only applies to conductors. This term always is implicitely used for dielectrics * * This is implemented by following * [Practical multiple scattering compensation for microfacet models, Turquin, 2019] * * Possible options are KERNEL_OPTION_TRUE and KERNEL_OPTION_FALSE. Self explanatory. */ #define PrincipledBSDFDoMetallicFresnelEnergyCompensation KERNEL_OPTION_TRUE /** * Whether or not to perform energy compensation for the specular/diffuse layer of the Principled BSDF */ #define PrincipledBSDFDoSpecularEnergyCompensation KERNEL_OPTION_TRUE /** * If this is true, then delta distribution lobes of the principled BSDF will not be evaluated * if the incident light direction used for the evaluation doesn't come from sampling the * delta distribution lobe itself * * Some more details in BSDFIncidentLightInfo.h */ #define PrincipledBSDFDeltaDistributionEvaluationOptimization KERNEL_OPTION_TRUE /** * Whether or not to sample the glossy/diffuse base layer of the BSDF based on the fresnel or not. * * This means that the diffuse layer will be sampled more often at normal incidence since this is where * the specular layer reflects close to no light. * * At grazing angle however, where the specular layer reflects the most light (and so the diffuse layer * below isn't reached by that light that is reflected by the specular layer), it is the specular layer * that will be sampled more often. */ #define PrincipledBSDFSampleGlossyBasedOnFresnel KERNEL_OPTION_FALSE /** * Same PrincipledBSDFSampleGlossyBasedOnFresnel but for the coat layer */ #define PrincipledBSDFSampleCoatBasedOnFresnel KERNEL_OPTION_FALSE /** * Implementation of [Microfacet Model Regularization for Robust Light Transport, Jendersie et al. 2019] * for regularizing (roughening) microfacet materials and help with caustics rendering */ #define PrincipledBSDFDoMicrofacetRegularization KERNEL_OPTION_TRUE /** * For microfacet model regularization, whether or not the use the consistent parametertization for tau_0 as * given by equation 16 of the paper */ #define PrincipledBSDFDoMicrofacetRegularizationConsistentParameterization KERNEL_OPTION_TRUE /** * Whether or not to take the path's roughness into account when regularizing the BSDFs */ #define PrincipledBSDFMicrofacetRegularizationDiffusionHeuristic KERNEL_OPTION_TRUE #endif // #ifndef __KERNELCC__ #endif ================================================ FILE: src/HostDeviceCommon/KernelOptions/ReGIROptions.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_REGIR_OPTIONS_H #define HOST_DEVICE_COMMON_REGIR_OPTIONS_H #include "HostDeviceCommon/KernelOptions/DirectLightSamplingOptions.h" #define REGIR_DEBUG_MODE_NO_DEBUG 0 #define REGIR_DEBUG_MODE_GRID_CELLS 1 #define REGIR_DEBUG_MODE_AVERAGE_CELL_NON_CANONICAL_RESERVOIR_CONTRIBUTION 2 #define REGIR_DEBUG_MODE_AVERAGE_CELL_CANONICAL_RESERVOIR_CONTRIBUTION 3 #define REGIR_DEBUG_MODE_REPRESENTATIVE_POINTS 4 #define REGIR_DEBUG_MODE_REPRESENTATIVE_NORMALS 5 #define REGIR_DEBUG_MODE_SAMPLING_FALLBACK 6 #define REGIR_HASH_GRID_COLLISION_RESOLUTION_MODE_LINEAR_PROBING 0 #define REGIR_HASH_GRID_COLLISION_RESOLUTION_MODE_REHASHING 1 // This block is a security to make sure that we have everything defined otherwise this can lead // to weird behavior because of the compiler not knowing about some macros #ifndef KERNEL_OPTION_TRUE #error "KERNEL_OPTION_TRUE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #else #ifndef KERNEL_OPTION_FALSE #error "KERNEL_OPTION_FALSE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #endif #endif /** * The resolution downscale factor to apply for the ReGIR grid prepopulation. * * The lower the downscale, the more effective the prepoluation but also the more costly */ #define ReGIR_GridPrepopulationResolutionDownscale 1 /** * Options are defined in a #ifndef __KERNELCC__ block because: * - If they were not, the would be defined on the GPU side. However, the -D = compiler option * cannot override a #define statement. This means that if the #define statement are encountered by the compiler, * we cannot modify the value of the macros anymore with the -D option which means no run-time switching / experimenting :( * - The CPU still needs the options to be able to compile the code so here they are, in a CPU-only block */ #ifndef __KERNELCC__ /** * How to sample lights in the scene for filling the ReGIR grid. * * - LSS_BASE_UNIFORM * Lights are sampled uniformly * * - LSS_BASE_POWER * Lights are sampled proportionally to their power */ #define ReGIR_GridFillLightSamplingBaseStrategy LSS_BASE_POWER /** * Whether or not to use a visibility term in the target function used to resample the reservoirs of the grid cells. * * Probably too expensive to be efficient. */ #define ReGIR_GridFillTargetFunctionVisibility KERNEL_OPTION_FALSE /** * Whether or not to use a the cosine term between the direction to the light sample and the * representative normal of the grid cell in the target function used to resample the reservoirs of the grid cells. * * This has no effect is representative points are not being used */ #define ReGIR_GridFillTargetFunctionCosineTerm KERNEL_OPTION_TRUE /** * Takes the cosine term at the light source (i.e. the cosine term of the geometry term) into account when * evaluating the target function during grid fill */ #define ReGIR_GridFillTargetFunctionCosineTermLightSource KERNEL_OPTION_TRUE /** * Whether or not to include the BSDF in the target function used for the resampling of the initial candidates * for the grid fill. * * Helps a lot on glossy surfaces. * * This option applies to primary hits only and should generally be set to true for better sampling. */ #define ReGIR_GridFillPrimaryHitsTargetFunctionBSDF KERNEL_OPTION_TRUE /** * Same as 'ReGIR_GridFillPrimaryHitsTargetFunctionBSDF' but only applies to secondary hits * * This option should be set to false in general as we cannot guess in advance what the view direction is going * to be at secondary hits (since they can come from anywhere when the rays bounce around the scene) and thus we * cannot properly evaluate the BRDF for sampling lights. */ #define ReGIR_GridFillSecondaryHitsTargetFunctionBSDF KERNEL_OPTION_FALSE /** * Whether or not to estimate the visibility probability of samples with NEE++ during the grid fill. */ #define ReGIR_GridFillTargetFunctionNeePlusPlusVisibilityEstimation KERNEL_OPTION_TRUE /** * This option must be set to true and a grid fill + spatial reuse kernels compiled with this option set * to true for those passes to accumulate the RIS integral of the reservoirs (for use in MIS) */ #define ReGIR_GridFillSpatialReuse_AccumulatePreIntegration KERNEL_OPTION_FALSE /** * Whether or not to enable light presampling to improve grid fill performance * on scenes with many many lights */ #define ReGIR_GridFillDoLightPresampling KERNEL_OPTION_TRUE /** * Whether or not to use a shadow ray in the target function when shading a point at path tracing time. * This reduces visibility noise */ #define ReGIR_ShadingResamplingTargetFunctionVisibility KERNEL_OPTION_FALSE /** * Whether or not to use NEE++ to estimate the visibility probability of the reservoir being resampled during * shading such that reservoirs that are likely to be occluded will have a lower resampling probability * * This option is exclusive with ReGIR_ShadingResamplingTargetFunctionVisibility, the latter taking precedence. */ #define ReGIR_ShadingResamplingTargetFunctionNeePlusPlusVisibility KERNEL_OPTION_TRUE /** * Whether or not to jitter canonical candidates during the shading resampling. * This reduces grid artifacts but increases variance */ #define ReGIR_ShadingResamplingJitterCanonicalCandidates KERNEL_OPTION_FALSE /** * Whether or not to incorporate BSDF samples with MIS during shading resampling. */ #define ReGIR_ShadingResamplingDoBSDFMIS KERNEL_OPTION_TRUE /** * Whether or not to use Pairwise MIS weights for weighting the different samples at shading-resampling time. * * If this is false, 1/Z MIS weights will be used instead which are potentially faster but definitely have more variance. */ #define ReGIR_ShadingResamplingDoMISPairwiseMIS KERNEL_OPTION_TRUE /** * If true, all samples resampled will be shaded instead of shading only the reservoir result of the resampling. * * This massively improves quality at the cost of performance and is very likely to be worth it for scenes that are not * too hard to trace (where shadow rays are expensive). */ #define ReGIR_ShadingResamplingShadeAllSamples KERNEL_OPTION_TRUE /** * Light sampling technique used in case the position that we are shading is falling outside of the ReGIR grid * * All LSS_BASE_XXX strategies are allowed except LSS_BASE_REGIR */ #define ReGIR_FallbackLightSamplingStrategy LSS_BASE_POWER /** * Whether or not to increase the hash grid precision on surfaces that have a lower roughness * such that the BRDF term in the target function of the grid fill (if used at all) has a higher * precision and gives better results */ #define ReGIR_HashGridAdaptiveRoughnessGridPrecision KERNEL_OPTION_TRUE /** * Whether or not to use constant grid cell size for the hash grid. * * If this is false, the grid cell size will increase (cells gets bigger) the further away * from the camera. This can help with performance and the number of resident cells * in the hash grid but it tends to hurt quality because of the reduced grid cell resolution */ #define ReGIR_HashGridConstantGridCellSize KERNEL_OPTION_FALSE /** * How to resolve a collision found in the hash grid: * * - REGIR_HASH_GRID_COLLISION_RESOLUTION_LINEAR_PROBING: If a collision is found, look up the next index in the hash * table and see if that location is empty. If not empty, continue looking at the next location * up to 'ReGIR_HashGridCollisionResolutionMaxSteps' times * * - REGIR_HASH_GRID_COLLISION_RESOLUTION_REHASHING: If a collision is found, hash the current cell index to get the * new candidate location. Continue doing so until an empty location is found or 'ReGIR_HashGridCollisionResolutionMaxSteps' * steps is exceeded */ #define ReGIR_HashGridCollisionResolutionMode REGIR_HASH_GRID_COLLISION_RESOLUTION_MODE_LINEAR_PROBING /** * Maximum number of steps for the linear probing in the hash table to resolve collisions */ #define ReGIR_HashGridCollisionResolutionMaxSteps 32 /** * Whether or not to use the surface normal in the hash function of the hash grid */ #define ReGIR_HashGridHashSurfaceNormal KERNEL_OPTION_TRUE /** * The number of discretization steps used to hash the surface normal * The higher the number, the better the hash grid resolution but the higher the * memory cost of the grid and the computational cost of the grid fill */ #define ReGIR_HashGridHashSurfaceNormalResolutionPrimaryHits 4 /** * Same as above but for the secondary hits only. A lower setting here is usually enough and saves * on perf and VRAM */ #define ReGIR_HashGridHashSurfaceNormalResolutionSecondaryHits 2 /** * If using jittering, how many tries to perform to find a good neighbor at shading time? * * This is because with jittering, our jittered position may end up outside of the grid * or in an empty cell, in which case we want to retry with a differently jittered position * to try and find a good neighbor */ #define ReGIR_ShadingJitterTries 2 /** * Debug option to color the scene with the grid cells */ #define ReGIR_DebugMode REGIR_DEBUG_MODE_NO_DEBUG #endif // #ifndef __KERNELCC__ #endif ================================================ FILE: src/HostDeviceCommon/KernelOptions/ReSTIRDIOptions.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_RESTIR_DI_OPTIONS_H #define HOST_DEVICE_COMMON_RESTIR_DI_OPTIONS_H #define RESTIR_DI_BIAS_CORRECTION_1_OVER_M 0 #define RESTIR_DI_BIAS_CORRECTION_1_OVER_Z 1 #define RESTIR_DI_BIAS_CORRECTION_MIS_LIKE 2 #define RESTIR_DI_BIAS_CORRECTION_MIS_GBH 3 #define RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS 4 #define RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE 5 #define RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO 6 #define RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO 7 #define RESTIR_DI_LATER_BOUNCES_UNIFORM_ONE_LIGHT 0 #define RESTIR_DI_LATER_BOUNCES_BSDF 1 #define RESTIR_DI_LATER_BOUNCES_MIS_LIGHT_BSDF 2 #define RESTIR_DI_LATER_BOUNCES_RIS_BSDF_AND_LIGHT 3 #define RESTIR_DI_SPATIAL_DIRECTIONAL_REUSE_BIT_COUNT 64 // CHANGE THIS ONE TO MODIFY THE NUMBER OF BITS. // This block is a security to make sure that we have everything defined otherwise this can lead // to weird behavior because of the compiler not knowing about some macros #ifndef KERNEL_OPTION_TRUE #error "KERNEL_OPTION_TRUE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #else #ifndef KERNEL_OPTION_FALSE #error "KERNEL_OPTION_FALSE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #endif #endif /** * Options are defined in a #ifndef __KERNELCC__ block because: * - If they were not, the would be defined on the GPU side. However, the -D = compiler option * cannot override a #define statement. This means that if the #define statement are encountered by the compiler, * we cannot modify the value of the macros anymore with the -D option which means no run-time switching / experimenting :( * - The CPU still needs the options to be able to compile the code so here they are, in a CPU-only block */ #ifndef __KERNELCC__ /** * Whether or not to use a visibility term in the target function when resampling * initial candidates in ReSTIR DI. * * * - KERNEL_OPTION_TRUE or KERNEL_OPTION_FALSE values are accepted. Self-explanatory */ #define ReSTIR_DI_InitialTargetFunctionVisibility KERNEL_OPTION_FALSE /** * Whether or not to use a visibility term in the target function when resampling * samples in ReSTIR DI. This applies to the spatial reuse pass only. * This option can have a good impact on quality and be worth it in terms of cost. * * - KERNEL_OPTION_TRUE or KERNEL_OPTION_FALSE values are accepted. Self-explanatory */ #define ReSTIR_DI_SpatialTargetFunctionVisibility KERNEL_OPTION_FALSE /** * Whether or not to do a visibility check at the end of the initial candidates sampling. * This discards reservoirs (by setting their UCW to 0.0f) whose samples are occluded. * This allows following ReSTIR passes (temporal and spatial) to only resample on samples * that are not occluded which improves quality quite a bit. * * - KERNEL_OPTION_TRUE or KERNEL_OPTION_FALSE values are accepted. Self-explanatory */ #define ReSTIR_DI_DoVisibilityReuse KERNEL_OPTION_TRUE /** * Whether or not to use a visibility term in the MIS weights (MIS-like weights, * generalized balance heuristic, pairwise MIS, ...) used to remove bias when * resampling neighbors. An additional visibility ray will be traced for MIS-weight * evaluated. This effectively means for each neighbor resamples or (for each neighbor resampled)^2 * if using the generalized balance heuristics (without pairwise-MIS) * * To guarantee unbiasedness, this needs to be true. A small amount of energy loss * may be observed if this value is KERNEL_OPTION_FALSE but the performance cost of the spatial * reuse will be reduced noticeably * * - KERNEL_OPTION_TRUE or KERNEL_OPTION_FALSE values are accepted. Self-explanatory */ #define ReSTIR_DI_BiasCorrectionUseVisibility KERNEL_OPTION_TRUE /** * What bias correction weights to use when resampling neighbors (temporal / spatial) * * - RESTIR_DI_BIAS_CORRECTION_1_OVER_M * Very simple biased weights as described in the 2020 paper (Eq. 6). * Those weights are biased because they do not account for cases where * we resample a sample that couldn't have been produced by some neighbors. * The bias shows up as darkening, mostly at object boundaries. In GRIS vocabulary, * this type of weights can be seen as confidence weights alone c_i / sum(c_j) * * - RESTIR_DI_BIAS_CORRECTION_1_OVER_Z * Simple unbiased weights as described in the 2020 paper (Eq. 16 and Section 4.3) * Those weights are unbiased but can have **extremely** bad variance when a neighbor being resampled * has a very low target function (when the neighbor is a glossy surface for example). * See Fig. 7 of the 2020 paper. * * - RESTIR_DI_BIAS_CORRECTION_MIS_LIKE * Unbiased weights as proposed by Eq. 22 of the paper. Way better than 1/Z in terms of variance * and still unbiased. * * - RESTIR_DI_BIAS_CORRECTION_MIS_GBH * Unbiased MIS weights that use the generalized balance heuristic. Very good variance reduction but O(N^2) complexity, N being the number of neighbors resampled. * Eq. 36 of the 2022 Generalized Resampled Importance Sampling paper. * * - RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS (and the defensive version RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE) * Similar variance reduction to the generalized balance heuristic and only O(N) computational cost. * Section 7.1.3 of "A Gentle Introduction to ReSTIR", 2023 * * - RESTIR_DI_BIAS_CORRECTION_SYMMETRIC_RATIO (and the defensive version RESTIR_DI_BIAS_CORRECTION_ASYMMETRIC_RATIO) * A bit more variance than pairwise MIS but way more robust to temporal correlations * * Implementation of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, Pan et al., 2024] */ #define ReSTIR_DI_BiasCorrectionWeights RESTIR_DI_BIAS_CORRECTION_PAIRWISE_MIS /** * What direct lighting sampling strategy to use for secondary bounces when ReSTIR DI is used for sampling the first bounce * * Possible values (the prefix LSS stands for "Light Sampling strategy"): * * - RESTIR_DI_LATER_BOUNCES_UNIFORM_ONE_LIGHT * Samples one random light in the scene without MIS * * - RESTIR_DI_LATER_BOUNCES_MIS_LIGHT_BSDF * Samples one random light in the scene with MIS (Multiple Importance Sampling): light sample + BRDF sample * * - RESTIR_DI_LATER_BOUNCES_BSDF * Samples a light using a BSDF sample. * Efficient as long as the light sources in the scene are large. * * - RESTIR_DI_LATER_BOUNCES_RIS_BSDF_AND_LIGHT * Samples lights in the scene with Resampled Importance Sampling */ #define ReSTIR_DI_LaterBouncesSamplingStrategy RESTIR_DI_LATER_BOUNCES_RIS_BSDF_AND_LIGHT /** * If true, lights are presampled in a pre-process pass as described in * [Rearchitecting Spatiotemporal Resampling for Production, Wyman, Panteleev, 2021] * https://research.nvidia.com/publication/2021-07_rearchitecting-spatiotemporal-resampling-production. * * This improves performance in scenes with dozens of thousands / millions of * lights by avoiding cache trashing because of the memory random walk that * light sampling becomes with that many lights */ #define ReSTIR_DI_DoLightPresampling KERNEL_OPTION_FALSE /** * What light sampling strategy to use to presample lights * * - LSS_BASE_UNIFORM * Lights are sampled uniformly * * - LSS_BASE_POWER * Lights are sampled proportionally to their power */ #define ReSTIR_DI_LightPresamplingStrategy LSS_BASE_POWER /** * How many bits to use for the directional reuse masks * * More bits use more VRAM but increase the precision of the directional reuse */ #define ReSTIR_DI_SpatialDirectionalReuseBitCount (RESTIR_DI_SPATIAL_DIRECTIONAL_REUSE_BIT_COUNT > 64 ? 64 : RESTIR_DI_SPATIAL_DIRECTIONAL_REUSE_BIT_COUNT) /** * Technique presented in [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, Pan et al., 2024] * * Helps with the pepper noise introduced by not using visibility in the spatial resampling target function */ #define ReSTIR_DI_DoOptimalVisibilitySampling KERNEL_OPTION_FALSE #endif // #ifndef __KERNELCC__ #endif ================================================ FILE: src/HostDeviceCommon/KernelOptions/ReSTIRGIOptions.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_RESTIR_GI_OPTIONS_H #define HOST_DEVICE_COMMON_RESTIR_GI_OPTIONS_H #define RESTIR_GI_BIAS_CORRECTION_1_OVER_M 0 #define RESTIR_GI_BIAS_CORRECTION_1_OVER_Z 1 #define RESTIR_GI_BIAS_CORRECTION_MIS_LIKE 2 #define RESTIR_GI_BIAS_CORRECTION_MIS_GBH 3 #define RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS 4 #define RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE 5 #define RESTIR_GI_BIAS_CORRECTION_SYMMETRIC_RATIO 6 #define RESTIR_GI_BIAS_CORRECTION_ASYMMETRIC_RATIO 7 #define RESTIR_GI_SPATIAL_DIRECTIONAL_REUSE_BIT_COUNT 64 // CHANGE THIS ONE TO MODIFY THE NUMBER OF BITS. // This block is a security to make sure that we have everything defined otherwise this can lead // to weird behavior because of the compiler not knowing about some macros #ifndef KERNEL_OPTION_TRUE #error "KERNEL_OPTION_TRUE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #else #ifndef KERNEL_OPTION_FALSE #error "KERNEL_OPTION_FALSE not defined, include 'HostDeviceCommon/KernelOptions/Common.h'" #endif #endif /** * Options are defined in a #ifndef __KERNELCC__ block because: * - If they were not, the would be defined on the GPU side. However, the -D = compiler option * cannot override a #define statement. This means that if the #define statement are encountered by the compiler, * we cannot modify the value of the macros anymore with the -D option which means no run-time switching / experimenting :( * - The CPU still needs the options to be able to compile the code so here they are, in a CPU-only block */ #ifndef __KERNELCC__ /** * Whether or not to use a visibility term in the target function when resampling * samples in ReSTIR GI. This applies to the spatial reuse pass only. * * - KERNEL_OPTION_TRUE or KERNEL_OPTION_FALSE values are accepted. Self-explanatory */ #define ReSTIR_GI_SpatialTargetFunctionVisibility KERNEL_OPTION_FALSE /** * Whether or not to use a visibility term in the MIS weights (MIS-like weights, * generalized balance heuristic, pairwise MIS, ...) used to remove bias when * resampling neighbors. An additional visibility ray will be traced for MIS-weight * evaluated. This effectively means for each neighbor resampled or (for each neighbor resampled)^2 * if using the generalized balance heuristics (without pairwise-MIS) * * To guarantee unbiasedness, this needs to be true. A small amount of energy loss * may be observed if this value is KERNEL_OPTION_FALSE but the performance cost of the spatial * reuse will be reduced noticeably * * - KERNEL_OPTION_TRUE or KERNEL_OPTION_FALSE values are accepted. Self-explanatory */ #define ReSTIR_GI_BiasCorrectionUseVisibility KERNEL_OPTION_TRUE /** * What bias correction weights to use when resampling neighbors (temporal / spatial) * * - RESTIR_GI_BIAS_CORRECTION_1_OVER_M * Very simple biased weights as described in the 2020 paper (Eq. 6). * Those weights are biased because they do not account for cases where * we resample a sample that couldn't have been produced by some neighbors. * The bias shows up as darkening, mostly at object boundaries. In GRIS vocabulary, * this type of weights can be seen as confidence weights alone c_i / sum(c_j) * * - RESTIR_GI_BIAS_CORRECTION_1_OVER_Z * Simple unbiased weights as described in the 2020 paper (Eq. 16 and Section 4.3) * Those weights are unbiased but can have **extremely** bad variance when a neighbor being resampled * has a very low target function (when the neighbor is a glossy surface for example). * See Fig. 7 of the 2020 paper. * * - RESTIR_GI_BIAS_CORRECTION_MIS_LIKE * Unbiased weights as proposed by Eq. 22 of the paper. Way better than 1/Z in terms of variance * and still unbiased. * * - RESTIR_GI_BIAS_CORRECTION_MIS_GBH * Unbiased MIS weights that use the generalized balance heuristic. Very good variance reduction but O(N^2) complexity, N being the number of neighbors resampled. * Eq. 36 of the 2022 Generalized Resampled Importance Sampling paper. * * - RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS (and the defensive version RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE) * Similar variance reduction to the generalized balance heuristic and only O(N) computational cost. * Section 7.1.3 of "A Gentle Introduction to ReSTIR", 2023 * * * - RESTIR_GI_BIAS_CORRECTION_SYMMETRIC_RATIO (and the defensive version RESTIR_GI_BIAS_CORRECTION_ASYMMETRIC_RATIO) * A bit more variance than pairwise MIS but way more robust to temporal correlations * * Implementation of [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, Pan et al., 2024] */ #define ReSTIR_GI_BiasCorrectionWeights RESTIR_GI_BIAS_CORRECTION_PAIRWISE_MIS_DEFENSIVE /** * How many bits to use for the directional reuse masks * * More bits use more VRAM but increase the precision of the directional reuse */ #define ReSTIR_GI_SpatialDirectionalReuseBitCount (RESTIR_GI_SPATIAL_DIRECTIONAL_REUSE_BIT_COUNT > 64 ? 64 : RESTIR_GI_SPATIAL_DIRECTIONAL_REUSE_BIT_COUNT) /** * Technique presented in [Enhancing Spatiotemporal Resampling with a Novel MIS Weight, Pan et al., 2024] * * Helps with the pepper noise introduced by not using visibility in the spatial resampling target function */ #define ReSTIR_GI_DoOptimalVisibilitySampling KERNEL_OPTION_FALSE #endif // #ifndef __KERNELCC__ #endif ================================================ FILE: src/HostDeviceCommon/LightSampleInformation.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_LIGHT_SAMPLE_INFORMATION_H #define HOST_DEVICE_COMMON_LIGHT_SAMPLE_INFORMATION_H #include "Device/includes/BSDFs/BSDFIncidentLightInfo.h" #include "Device/includes/LightSampling/PDFConversion.h" #include "HostDeviceCommon/Color.h" struct LightSampleInformation { // Index of the triangle in the whole scene (not just in the emissive triangles buffer) int emissive_triangle_index = -1; float3 light_source_normal = { 0.0f, 1.0f, 0.0f }; float light_area = 1.0f; ColorRGB32F emission; float3 point_on_light = make_float3(0.0f, 0.0f, 0.0f); float area_measure_pdf = 0.0f; // The light sample may come from BSDF sampling (with ReGIR mostly) and so we may have // information about the lobe that was sampled. BSDFIncidentLightInfo incident_light_info = BSDFIncidentLightInfo::NO_INFO; }; #endif ================================================ FILE: src/HostDeviceCommon/Material/MaterialCPU.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_MATERIAL_CPU_H #define HOST_DEVICE_COMMON_MATERIAL_CPU_H #include "Device/includes/NestedDielectrics.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Material/MaterialPacked.h" #include "HostDeviceCommon/Material/MaterialUtils.h" // Adding this guard to make sure that we never use the CPU materials in the GPU code #ifndef __KERNELCC__ // This material structure is only used on the CPU. // The reason why we have a CPU and a GPU material is because // we may want to precompute some properties on the CPU before sending them // to the GPU. This means that the CPU material stores both the precomputed values // and the values needed for the precomputation itself. // // But the GPU only cares about the precomputed values itself, not the ingredients // to the precomputation so that's why we have separate structures struct CPUMaterial { /** * Function that transforms/packs this material to the version that the GPU is going to use */ DevicePackedTexturedMaterial pack_to_GPU() const { DevicePackedTexturedMaterial mat; mat.set_normal_map_texture_index(this->normal_map_texture_index); mat.set_emission_texture_index(this->emission_texture_index); mat.set_base_color_texture_index(this->base_color_texture_index); mat.set_roughness_metallic_texture_index(this->roughness_metallic_texture_index); mat.set_roughness_texture_index(this->roughness_texture_index); mat.set_metallic_texture_index(this->metallic_texture_index); mat.set_anisotropic_texture_index(this->anisotropic_texture_index); mat.set_specular_texture_index(this->specular_texture_index); mat.set_coat_texture_index(this->coat_texture_index); mat.set_sheen_texture_index(this->sheen_texture_index); mat.set_specular_transmission_texture_index(this->specular_transmission_texture_index); mat.set_emission(emission * emission_strength * global_emissive_factor); mat.set_emissive_texture_used(emissive_texture_used); mat.set_base_color(base_color); mat.set_roughness(roughness); mat.set_oren_nayar_sigma(oren_nayar_sigma); // Parameters for Adobe 2023 F82-tint model mat.set_metallic(metallic); mat.set_metallic_F90_falloff_exponent(metallic_F90_falloff_exponent); // F0 is not here as it uses the 'base_color' of the material mat.set_metallic_F82(metallic_F82); mat.set_metallic_F90(metallic_F90); mat.set_anisotropy(anisotropy); mat.set_anisotropy_rotation(anisotropy_rotation); mat.set_second_roughness_weight(second_roughness_weight); mat.set_second_roughness(second_roughness); mat.set_metallic_energy_compensation(do_metallic_energy_compensation); // Specular intensity mat.set_specular(specular); // Specular tint intensity. // Specular will be white if 0.0f and will be 'specular_color' if 1.0f mat.set_specular_tint(specular_tint); mat.set_specular_color(specular_color); // Same as coat darkening but for total internal reflection inside the specular layer // that sits on top of the diffuse base // // Disabled by default for artistic "expectations" mat.set_specular_darkening(specular_darkening); mat.set_specular_energy_compensation(do_specular_energy_compensation); mat.set_coat(coat); mat.set_coat_medium_absorption(coat_medium_absorption); // The coat thickness influences the amount of absorption (given by 'coat_medium_absorption') // that will happen inside the coat mat.set_coat_medium_thickness(coat_medium_thickness); mat.set_coat_roughness(coat_roughness); // Physical accuracy requires that a rough clearcoat also roughens what's underneath it // i.e. the specular/metallic/transmission layers. // // The option is however given here to artistically disable // that behavior by using coat roughening = 0.0f. mat.set_coat_roughening(coat_roughening); // Because of the total internal reflection that can happen inside the coat layer (i.e. // light bouncing between the coat/BSDF and air/coat interfaces), the BSDF below the // clearcoat will appear will increased saturation. mat.set_coat_darkening(coat_darkening); mat.set_coat_anisotropy(coat_anisotropy); mat.set_coat_anisotropy_rotation(coat_anisotropy_rotation); mat.set_coat_ior(coat_ior); mat.set_coat_energy_compensation(do_coat_energy_compensation); mat.set_sheen(sheen); // Sheen strength mat.set_sheen_roughness(sheen_roughness); mat.set_sheen_color(sheen_color); mat.set_ior(ior); mat.set_specular_transmission(specular_transmission); mat.set_diffuse_transmission(diffuse_transmission); // At what distance is the light absorbed to the given absorption_color mat.set_absorption_at_distance(absorption_at_distance); // Color of the light absorption when traveling through the medium mat.set_absorption_color(absorption_color); mat.set_dispersion_scale(dispersion_scale); mat.set_dispersion_abbe_number(dispersion_abbe_number); mat.set_thin_walled(thin_walled); mat.set_glass_energy_compensation(do_glass_energy_compensation); mat.set_thin_film(thin_film); mat.set_thin_film_ior(thin_film_ior); mat.set_thin_film_thickness(thin_film_thickness); mat.set_thin_film_kappa_3(thin_film_kappa_3); // Sending the hue film in [0, 1] to the GPU mat.set_thin_film_hue_shift_degrees(thin_film_hue_shift_degrees / 360.0f); mat.set_thin_film_base_ior_override(thin_film_base_ior_override); mat.set_thin_film_do_ior_override(thin_film_do_ior_override); // 1.0f makes the material completely opaque // 0.0f completely transparent (becomes invisible) mat.set_alpha_opacity(alpha_opacity); // Nested dielectric parameter mat.set_dielectric_priority(dielectric_priority); return mat; } HIPRT_HOST_DEVICE bool is_emissive() const { return !hippt::is_zero(emission.r) || !hippt::is_zero(emission.g) || !hippt::is_zero(emission.b) || emissive_texture_used; } /* * Clamps some of the parameters of the material to avoid edge cases like NaNs * during rendering (i.e. numerical instabilities) */ HIPRT_HOST_DEVICE void make_safe() { // The values are going to be packed before being sent to the GPU // Packing limits the range of values (most of them in [0, 1] because // they are not expected to go higher) so we're clamping the values // to avoid out-of-range-packing base_color.clamp(0.0f, 1.0f); metallic = hippt::clamp(0.0f, 1.0f, metallic); metallic_F82.clamp(0.0f, 1.0f); metallic_F90.clamp(0.0f, 1.0f); anisotropy = hippt::clamp(0.0f, 1.0f, anisotropy); anisotropy_rotation = hippt::clamp(0.0f, 1.0f, anisotropy_rotation); second_roughness_weight = hippt::clamp(0.0f, 1.0f, second_roughness_weight); second_roughness = hippt::clamp(0.0f, 1.0f, second_roughness); specular = hippt::clamp(0.0f, 1.0f, specular); specular_tint = hippt::clamp(0.0f, 1.0f, specular_tint); specular_color.clamp(0.0f, 1.0f); specular_darkening = hippt::clamp(0.0f, 1.0f, specular_darkening); coat = hippt::clamp(0.0f, 1.0f, coat); coat_medium_absorption.clamp(0.0f, 1.0f); coat_roughness = hippt::clamp(MaterialConstants::ROUGHNESS_CLAMP, 1.0f, coat_roughness); coat_roughening = hippt::clamp(0.0f, 1.0f, coat_roughening); coat_darkening = hippt::clamp(0.0f, 1.0f, coat_darkening); coat_anisotropy = hippt::clamp(0.0f, 1.0f, coat_anisotropy); coat_anisotropy_rotation = hippt::clamp(0.0f, 1.0f, coat_anisotropy_rotation); sheen = hippt::clamp(0.0f, 1.0f, sheen); sheen_roughness = hippt::clamp(MaterialConstants::ROUGHNESS_CLAMP, 1.0f, sheen_roughness); sheen_color.clamp(0.0f, 1.0f); specular_transmission = hippt::clamp(0.0f, 1.0f, specular_transmission); // Avoiding zero absorption_at_distance = hippt::max(absorption_at_distance, 1.0e-4f); absorption_color = ColorRGB32F::max(absorption_color, ColorRGB32F(1.0f / 512.0f)); absorption_color.clamp(0.0f, 1.0f); dispersion_abbe_number = hippt::max(1.0e-5f, dispersion_abbe_number); dispersion_scale = hippt::clamp(0.0f, 1.0f, dispersion_scale); thin_film = hippt::clamp(0.0f, 1.0f, thin_film); thin_film_hue_shift_degrees = hippt::clamp(0.0f, 360.0f, thin_film_hue_shift_degrees); thin_film_ior = hippt::max(1.0005f, thin_film_ior); alpha_opacity = hippt::clamp(0.0f, 1.0f, alpha_opacity); dielectric_priority = hippt::clamp(0, (int)StackPriorityEntry::PRIORITY_BIT_MASK >> StackPriorityEntry::PRIORITY_BIT_SHIFT, dielectric_priority); // Clamping to avoid negative emission emission = ColorRGB32F::max(ColorRGB32F(0.0f), emission); if (specular_transmission == 0.0f && diffuse_transmission == 0.0f) // No transmission means that we should never skip this boundary --> max priority dielectric_priority = (1 << StackPriorityEntry::PRIORITY_MAXIMUM) - 1; } ColorRGB32F emission = ColorRGB32F{ 0.0f, 0.0f, 0.0f }; float emission_strength = 1.0f; // This factor is baked into 'emission' before being sent to the GPU float global_emissive_factor = 1.0f; // This factor is baked into 'emission' before being sent to the GPU bool emissive_texture_used = false; ColorRGB32F base_color = ColorRGB32F(1.0f); float roughness = 0.3f; float oren_nayar_sigma = 0.34906585039886591538f; // 20 degrees standard deviation in radian // Parameters for Adobe 2023 F82-tint model float metallic = 0.0f; float metallic_F90_falloff_exponent = 5.0f; // F0 is not here as it uses the 'base_color' of the material ColorRGB32F metallic_F82 = ColorRGB32F(1.0f); ColorRGB32F metallic_F90 = ColorRGB32F(1.0f); float anisotropy = 0.0f; float anisotropy_rotation = 0.0f; float second_roughness_weight = 0.0f; float second_roughness = 0.5f; // Whether or not to do energy compensation of the metallic layer // for that material bool do_metallic_energy_compensation = true; // Specular intensity float specular = 1.0f; // Specular tint intensity. // Specular will be white if 0.0f and will be 'specular_color' if 1.0f float specular_tint = 1.0f; ColorRGB32F specular_color = ColorRGB32F(1.0f); // Same as coat darkening but for total internal reflection inside the specular layer // that sits on top of the diffuse base // // Disabled by default for "artistic expectations" but this is not physically accurate float specular_darkening = 0.0f; // Whether or not to do energy compensation of the specular/diffuse layer // for that material bool do_specular_energy_compensation = true; float coat = 0.0f; ColorRGB32F coat_medium_absorption = ColorRGB32F{ 1.0f, 1.0f, 1.0f }; // The coat thickness influences the amount of absorption (given by 'coat_medium_absorption') // that will happen inside the coat float coat_medium_thickness = 5.0f; float coat_roughness = 0.0f; // Physical accuracy requires that a rough clearcoat also roughens what's underneath it // i.e. the specular/metallic/transmission layers. // // The option is however given here to artistically disable // that behavior by using coat roughening = 0.0f. float coat_roughening = 1.0f; // Because of the total internal reflection that can happen inside the coat layer (i.e. // light bouncing between the coat/BSDF and air/coat interfaces), the BSDF below the // clearcoat will appear will increased saturation. float coat_darkening = 1.0f; float coat_anisotropy = 0.0f; float coat_anisotropy_rotation = 0.0f; float coat_ior = 1.5f; // Whether or not to do energy compensation of the clearcoat layer // for that material bool do_coat_energy_compensation = true; float sheen = 0.0f; // Sheen strength float sheen_roughness = 0.5f; ColorRGB32F sheen_color = ColorRGB32F(1.0f); float ior = 1.40f; float specular_transmission = 0.0f; float diffuse_transmission = 0.0f; // At what distance is the light absorbed to the given absorption_color float absorption_at_distance = 1.0f; // Color of the light absorption when traveling through the medium ColorRGB32F absorption_color = ColorRGB32F(1.0f); float dispersion_scale = 0.0f; float dispersion_abbe_number = 20.0f; bool thin_walled = false; // Whether or not to do energy compensation of the glass layer // for that material bool do_glass_energy_compensation = true; float thin_film = 0.0f; float thin_film_ior = 1.3f; float thin_film_thickness = 500.0f; float thin_film_kappa_3 = 0.0f; float thin_film_hue_shift_degrees = 0.0f; float thin_film_base_ior_override = 1.0f; bool thin_film_do_ior_override = false; // 1.0f makes the material completely opaque // 0.0f completely transparent (becomes invisible) float alpha_opacity = 1.0f; // Nested dielectric parameter int dielectric_priority = 0; int normal_map_texture_index = MaterialConstants::NO_TEXTURE; int emission_texture_index = MaterialConstants::NO_TEXTURE; int base_color_texture_index = MaterialConstants::NO_TEXTURE; // If not MaterialConstants::NO_TEXTURE, there is only one texture for the metallic and the roughness parameters in which. // case the green channel is the roughness and the blue channel is the metalness int roughness_metallic_texture_index = MaterialConstants::NO_TEXTURE; int roughness_texture_index = MaterialConstants::NO_TEXTURE; int metallic_texture_index = MaterialConstants::NO_TEXTURE; int anisotropic_texture_index = MaterialConstants::NO_TEXTURE; int specular_texture_index = MaterialConstants::NO_TEXTURE; int coat_texture_index = MaterialConstants::NO_TEXTURE; int sheen_texture_index = MaterialConstants::NO_TEXTURE; int specular_transmission_texture_index = MaterialConstants::NO_TEXTURE; }; #endif // #ifndef __KERNELCC__ #endif ================================================ FILE: src/HostDeviceCommon/Material/MaterialConstants.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_MATERIAL_CONSTANTS_H #define HOST_DEVICE_COMMON_MATERIAL_CONSTANTS_H struct MaterialConstants { static constexpr int NO_TEXTURE = 65535; // When an emissive texture is read and is determine to be // constant, no emissive texture will be used. Instead, // we'll just set the emission of the material to that constant emission value // and the emissive texture index of the material will be replaced by // CONSTANT_EMISSIVE_TEXTURE static constexpr int CONSTANT_EMISSIVE_TEXTURE = 65534; // Maximum number of different textures per scene static constexpr int MAX_TEXTURE_COUNT = 65533; static constexpr float ROUGHNESS_CLAMP = 1.0e-4f; static constexpr float PERFECTLY_SMOOTH_ROUGHNESS_THRESHOLD = 1.0e-2f; static constexpr float DELTA_DISTRIBUTION_HIGH_VALUE = 1.0e9f; static constexpr float DELTA_DISTRIBUTION_ALIGNEMENT_THRESHOLD = 0.999999f; }; #endif ================================================ FILE: src/HostDeviceCommon/Material/MaterialPacked.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_MATERIAL_PACKED_H #define HOST_DEVICE_COMMON_MATERIAL_PACKED_H #include "HostDeviceCommon/Packing.h" #include "HostDeviceCommon/Material/MaterialConstants.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" /** * Packed material for use in the shaders */ struct DevicePackedEffectiveMaterial { enum PackedFlagsIndices : unsigned char { PACKED_THIN_WALLED = 0, PACKED_EMISSIVE_TEXTURE_USED = 1, PACKED_THIN_FILM_DO_IOR_OVERRIDE = 2, GLASS_ENERGY_COMPENSATION = 3, CLEARCOAT_ENERGY_COMPENSATION = 4, METALLIC_ENERGY_COMPENSATION = 5, SPECULAR_ENERGY_COMPENSATION = 6, PACKED_ENFORCE_STRONG_ENERGY_CONSERVATION = 7, }; enum PackedAnisotropyGroupIndices : unsigned char { PACKED_ANISOTROPY = 0, PACKED_ANISOTROPY_ROTATION = 1, PACKED_SECOND_ROUGHNESS_WEIGHT = 2, PACKED_SECOND_ROUGHNESS = 3, }; enum PackedSpecularGroupIndices : unsigned char { PACKED_SPECULAR = 0, PACKED_SPECULAR_DARKENING = 1, PACKED_COAT_ROUGHNESS = 2 }; enum PackedCoatGroupIndices : unsigned char { PACKED_COAT_ROUGHENING = 0, PACKED_COAT_DARKENING = 1, PACKED_COAT_ANISOTROPY = 2, PACKED_COAT_ANISOTROPY_ROTATION = 3, }; enum PackedSheenRoughnessGroupIndices : unsigned char { PACKED_SHEEN_ROUGHNESS = 0, PACKED_SPECULAR_TRANSMISSION = 1, PACKED_DISPERSION_SCALE = 2, PACKED_THIN_FILM = 3, }; enum PackedAlphaOpacityGroupIndices : unsigned char { PACKED_ALPHA_OPACITY = 0, PACKED_THIN_FILM_HUE_SHIFT = 1, PACKED_DIELECTRIC_PRIORITY = 0, PACKED_ENERGY_PRESERVATION_SAMPLES = 1, }; HIPRT_HOST_DEVICE bool is_emissive() const { return !hippt::is_zero(emission.r) || !hippt::is_zero(emission.g) || !hippt::is_zero(emission.b) || get_emissive_texture_used(); } /** * This function packs an UnpackedEffectiveMaterial into its packed version. * * This is used in the shaders when a material is read after hitting some geometry: * the texture of the material will be evaluated, transforming a * DeviceUnpackedTexturedMaterial into a DeviceUnpackedEffectiveMaterial. * * That DeviceUnpackedEffectiveMaterial will then be packed (using the pack() function below) * before being written to the G-buffer */ HIPRT_HOST_DEVICE static DevicePackedEffectiveMaterial pack(const DeviceUnpackedEffectiveMaterial& unpacked) { DevicePackedEffectiveMaterial packed; packed.set_emission(unpacked.emission); packed.set_emissive_texture_used(unpacked.emissive_texture_used); packed.set_base_color(unpacked.base_color); packed.set_roughness(unpacked.roughness); packed.set_oren_nayar_sigma(unpacked.oren_nayar_sigma); packed.set_metallic(unpacked.metallic); packed.set_metallic_F90_falloff_exponent(unpacked.metallic_F90_falloff_exponent); packed.set_metallic_F82(unpacked.metallic_F82); packed.set_metallic_F90(unpacked.metallic_F90); packed.set_anisotropy(unpacked.anisotropy); packed.set_anisotropy_rotation(unpacked.anisotropy_rotation); packed.set_second_roughness_weight(unpacked.second_roughness_weight); packed.set_second_roughness(unpacked.second_roughness); packed.set_metallic_energy_compensation(unpacked.do_metallic_energy_compensation); packed.set_specular(unpacked.specular); packed.set_specular_tint(unpacked.specular_tint); packed.set_specular_color(unpacked.specular_color); packed.set_specular_darkening(unpacked.specular_darkening); packed.set_specular_energy_compensation(unpacked.do_specular_energy_compensation); packed.set_coat(unpacked.coat); packed.set_coat_medium_absorption(unpacked.coat_medium_absorption); packed.set_coat_medium_thickness(unpacked.coat_medium_thickness); packed.set_coat_roughness(unpacked.coat_roughness); packed.set_coat_roughening(unpacked.coat_roughening); packed.set_coat_darkening(unpacked.coat_darkening); packed.set_coat_anisotropy(unpacked.coat_anisotropy); packed.set_coat_anisotropy_rotation(unpacked.coat_anisotropy_rotation); packed.set_coat_ior(unpacked.coat_ior); packed.set_coat_energy_compensation(unpacked.do_coat_energy_compensation); packed.set_sheen(unpacked.sheen); packed.set_sheen_roughness(unpacked.sheen_roughness); packed.set_sheen_color(unpacked.sheen_color); packed.set_ior(unpacked.ior); packed.set_specular_transmission(unpacked.specular_transmission); packed.set_diffuse_transmission(unpacked.diffuse_transmission); packed.set_absorption_at_distance(unpacked.absorption_at_distance); packed.set_absorption_color(unpacked.absorption_color); packed.set_dispersion_scale(unpacked.dispersion_scale); packed.set_dispersion_abbe_number(unpacked.dispersion_abbe_number); packed.set_thin_walled(unpacked.thin_walled); packed.set_glass_energy_compensation(unpacked.do_glass_energy_compensation); packed.set_thin_film(unpacked.thin_film); packed.set_thin_film_ior(unpacked.thin_film_ior); packed.set_thin_film_thickness(unpacked.thin_film_thickness); packed.set_thin_film_kappa_3(unpacked.thin_film_kappa_3); packed.set_thin_film_hue_shift_degrees(unpacked.thin_film_hue_shift_degrees); packed.set_thin_film_base_ior_override(unpacked.thin_film_base_ior_override); packed.set_thin_film_do_ior_override(unpacked.thin_film_do_ior_override); packed.set_alpha_opacity(unpacked.alpha_opacity); packed.set_dielectric_priority(unpacked.get_dielectric_priority()); packed.set_energy_preservation_monte_carlo_samples(unpacked.energy_preservation_monte_carlo_samples); packed.set_enforce_strong_energy_conservation(unpacked.enforce_strong_energy_conservation); return packed; } HIPRT_HOST_DEVICE DeviceUnpackedEffectiveMaterial unpack() const { DeviceUnpackedEffectiveMaterial unpacked; unpacked.emission = this->get_emission(); unpacked.emissive_texture_used = this->get_emissive_texture_used(); unpacked.base_color = this->get_base_color(); unpacked.roughness = this->get_roughness(); unpacked.oren_nayar_sigma = this->get_oren_nayar_sigma(); unpacked.metallic = this->get_metallic(); unpacked.metallic_F90_falloff_exponent = this->get_metallic_F90_falloff_exponent(); unpacked.metallic_F82 = this->get_metallic_F82(); unpacked.metallic_F90 = this->get_metallic_F90(); unpacked.anisotropy = this->get_anisotropy(); unpacked.anisotropy_rotation = this->get_anisotropy_rotation(); unpacked.second_roughness_weight = this->get_second_roughness_weight(); unpacked.second_roughness = this->get_second_roughness(); unpacked.do_metallic_energy_compensation = this->get_do_metallic_energy_compensation(); unpacked.specular = this->get_specular(); unpacked.specular_tint = this->get_specular_tint(); unpacked.specular_color = this->get_specular_color(); unpacked.specular_darkening = this->get_specular_darkening(); unpacked.do_specular_energy_compensation = this->get_do_specular_energy_compensation(); unpacked.coat = this->get_coat(); unpacked.coat_medium_absorption = this->get_coat_medium_absorption(); unpacked.coat_medium_thickness = this->get_coat_medium_thickness(); unpacked.coat_roughness = this->get_coat_roughness(); unpacked.coat_roughening = this->get_coat_roughening(); unpacked.coat_darkening = this->get_coat_darkening(); unpacked.coat_anisotropy = this->get_coat_anisotropy(); unpacked.coat_anisotropy_rotation = this->get_coat_anisotropy_rotation(); unpacked.coat_ior = this->get_coat_ior(); unpacked.do_coat_energy_compensation = this->get_do_coat_energy_compensation(); unpacked.sheen = this->get_sheen(); unpacked.sheen_roughness = this->get_sheen_roughness(); unpacked.sheen_color = this->get_sheen_color(); unpacked.ior = this->get_ior(); unpacked.specular_transmission = this->get_specular_transmission(); unpacked.diffuse_transmission = this->get_diffuse_transmission(); unpacked.absorption_at_distance = this->get_absorption_at_distance(); unpacked.absorption_color = this->get_absorption_color(); unpacked.dispersion_scale = this->get_dispersion_scale(); unpacked.dispersion_abbe_number = this->get_dispersion_abbe_number(); unpacked.thin_walled = this->get_thin_walled(); unpacked.do_glass_energy_compensation = this->get_do_glass_energy_compensation(); unpacked.thin_film = this->get_thin_film(); unpacked.thin_film_ior = this->get_thin_film_ior(); unpacked.thin_film_thickness = this->get_thin_film_thickness(); unpacked.thin_film_kappa_3 = this->get_thin_film_kappa_3(); unpacked.thin_film_hue_shift_degrees = this->get_thin_film_hue_shift_degrees(); unpacked.thin_film_base_ior_override = this->get_thin_film_base_ior_override(); unpacked.thin_film_do_ior_override = this->get_thin_film_do_ior_override(); unpacked.alpha_opacity = this->get_alpha_opacity(); unpacked.set_dielectric_priority(this->get_dielectric_priority()); unpacked.energy_preservation_monte_carlo_samples = this->get_energy_preservation_monte_carlo_samples(); unpacked.enforce_strong_energy_conservation = this->get_enforce_strong_energy_conservation(); return unpacked; } HIPRT_HOST_DEVICE ColorRGB32F get_emission() const { return this->emission; } HIPRT_HOST_DEVICE bool get_emissive_texture_used() const { return flags.get_bool(); } HIPRT_HOST_DEVICE ColorRGB32F get_base_color() const { return base_color_roughness.get_color(); } HIPRT_HOST_DEVICE float get_roughness() const { return base_color_roughness.get_float(); } HIPRT_HOST_DEVICE float get_oren_nayar_sigma() const { return this->oren_nayar_sigma; } HIPRT_HOST_DEVICE float get_metallic() const { return metallic_F90_and_metallic.get_float(); } HIPRT_HOST_DEVICE float get_metallic_F90_falloff_exponent() const { return this->metallic_F90_falloff_exponent; } HIPRT_HOST_DEVICE ColorRGB32F get_metallic_F82() const { return metallic_F82_packed_and_diffuse_transmission.get_color(); } HIPRT_HOST_DEVICE ColorRGB32F get_metallic_F90() const { return metallic_F90_and_metallic.get_color(); } HIPRT_HOST_DEVICE float get_anisotropy() const { return anisotropy_and_rotation_and_second_roughness.get_float(); } HIPRT_HOST_DEVICE float get_anisotropy_rotation() const { return anisotropy_and_rotation_and_second_roughness.get_float(); } HIPRT_HOST_DEVICE float get_second_roughness_weight() const { return anisotropy_and_rotation_and_second_roughness.get_float(); } HIPRT_HOST_DEVICE float get_second_roughness() const { return anisotropy_and_rotation_and_second_roughness.get_float(); } HIPRT_HOST_DEVICE bool get_do_metallic_energy_compensation() const { return flags.get_bool(); } HIPRT_HOST_DEVICE float get_specular() const { return specular_and_darkening_and_coat_roughness.get_float(); } HIPRT_HOST_DEVICE float get_specular_tint() const { return specular_color_and_tint_factor.get_float(); } HIPRT_HOST_DEVICE ColorRGB32F get_specular_color() const { return specular_color_and_tint_factor.get_color(); } HIPRT_HOST_DEVICE float get_specular_darkening() const { return specular_and_darkening_and_coat_roughness.get_float(); } HIPRT_HOST_DEVICE bool get_do_specular_energy_compensation() const { return flags.get_bool(); } HIPRT_HOST_DEVICE float get_coat() const { return coat_and_medium_absorption.get_float(); } HIPRT_HOST_DEVICE ColorRGB32F get_coat_medium_absorption() const { return coat_and_medium_absorption.get_color(); } HIPRT_HOST_DEVICE float get_coat_medium_thickness() const { return this->coat_medium_thickness; } HIPRT_HOST_DEVICE float get_coat_roughness() const { return specular_and_darkening_and_coat_roughness.get_float(); } HIPRT_HOST_DEVICE float get_coat_roughening() const { return coat_roughening_darkening_anisotropy_and_rotation.get_float(); } HIPRT_HOST_DEVICE float get_coat_darkening() const { return coat_roughening_darkening_anisotropy_and_rotation.get_float(); } HIPRT_HOST_DEVICE float get_coat_anisotropy() const { return coat_roughening_darkening_anisotropy_and_rotation.get_float(); } HIPRT_HOST_DEVICE float get_coat_anisotropy_rotation() const { return coat_roughening_darkening_anisotropy_and_rotation.get_float(); } HIPRT_HOST_DEVICE float get_coat_ior() const { return this->coat_ior; } HIPRT_HOST_DEVICE bool get_do_coat_energy_compensation() const { return flags.get_bool(); } HIPRT_HOST_DEVICE float get_sheen() const { return sheen_and_color.get_float(); } HIPRT_HOST_DEVICE float get_sheen_roughness() const { return sheen_roughness_transmission_dispersion_thin_film.get_float(); } HIPRT_HOST_DEVICE ColorRGB32F get_sheen_color() const { return sheen_and_color.get_color(); } HIPRT_HOST_DEVICE float get_ior() const { return this->ior; } HIPRT_HOST_DEVICE float get_specular_transmission() const { return sheen_roughness_transmission_dispersion_thin_film.get_float(); } HIPRT_HOST_DEVICE float get_diffuse_transmission() const { return metallic_F82_packed_and_diffuse_transmission.get_float(); } HIPRT_HOST_DEVICE float get_absorption_at_distance() const { return this->absorption_at_distance; } HIPRT_HOST_DEVICE ColorRGB32F get_absorption_color() const { return absorption_color_packed.get_color(); } HIPRT_HOST_DEVICE float get_dispersion_scale() const { return sheen_roughness_transmission_dispersion_thin_film.get_float(); } HIPRT_HOST_DEVICE float get_dispersion_abbe_number() const { return this->dispersion_abbe_number; } HIPRT_HOST_DEVICE bool get_thin_walled() const { return flags.get_bool(); } HIPRT_HOST_DEVICE bool get_do_glass_energy_compensation() const { return flags.get_bool(); } HIPRT_HOST_DEVICE float get_thin_film() const { return sheen_roughness_transmission_dispersion_thin_film.get_float(); } HIPRT_HOST_DEVICE float get_thin_film_ior() const { return this->thin_film_ior; } HIPRT_HOST_DEVICE float get_thin_film_thickness() const { return this->thin_film_thickness; } HIPRT_HOST_DEVICE float get_thin_film_kappa_3() const { return this->thin_film_kappa_3; } HIPRT_HOST_DEVICE float get_thin_film_hue_shift_degrees() const { return alpha_thin_film_hue_dielectric_priority.get_float(); } HIPRT_HOST_DEVICE float get_thin_film_base_ior_override() const { return this->thin_film_base_ior_override; } HIPRT_HOST_DEVICE bool get_thin_film_do_ior_override() const { return flags.get_bool(); } HIPRT_HOST_DEVICE float get_alpha_opacity() const { return alpha_thin_film_hue_dielectric_priority.get_float(); } HIPRT_HOST_DEVICE unsigned char get_dielectric_priority() const { return alpha_thin_film_hue_dielectric_priority.get_uchar(); } HIPRT_HOST_DEVICE unsigned char get_energy_preservation_monte_carlo_samples() const { return alpha_thin_film_hue_dielectric_priority.get_uchar(); } HIPRT_HOST_DEVICE bool get_enforce_strong_energy_conservation() const { return flags.get_bool(); } HIPRT_HOST_DEVICE void set_emission(ColorRGB32F emission_) { this->emission = emission_; } HIPRT_HOST_DEVICE void set_emissive_texture_used(bool emissive_texture_used) { flags.set_bool(emissive_texture_used); } HIPRT_HOST_DEVICE void set_base_color(ColorRGB32F base_color) { base_color_roughness.set_color(base_color); } HIPRT_HOST_DEVICE void set_roughness(float roughness) { base_color_roughness.set_float(roughness); } HIPRT_HOST_DEVICE void set_oren_nayar_sigma(float oren_nayar_sigma_) { this->oren_nayar_sigma = oren_nayar_sigma_; } HIPRT_HOST_DEVICE void set_metallic(float metallic) { metallic_F90_and_metallic.set_float(metallic); } HIPRT_HOST_DEVICE void set_metallic_F90_falloff_exponent(float metallic_F90_falloff_exponent_) { this->metallic_F90_falloff_exponent = metallic_F90_falloff_exponent_; } HIPRT_HOST_DEVICE void set_metallic_F82(ColorRGB32F metallic_F82) { metallic_F82_packed_and_diffuse_transmission.set_color(metallic_F82); } HIPRT_HOST_DEVICE void set_metallic_F90(ColorRGB32F metallic_F90) { metallic_F90_and_metallic.set_color(metallic_F90); } HIPRT_HOST_DEVICE void set_anisotropy(float anisotropy) { anisotropy_and_rotation_and_second_roughness.set_float(anisotropy); } HIPRT_HOST_DEVICE void set_anisotropy_rotation(float anisotropy_rotation) { anisotropy_and_rotation_and_second_roughness.set_float(anisotropy_rotation); } HIPRT_HOST_DEVICE void set_second_roughness_weight(float second_roughness_weight) { anisotropy_and_rotation_and_second_roughness.set_float(second_roughness_weight); } HIPRT_HOST_DEVICE void set_second_roughness(float second_roughness) { anisotropy_and_rotation_and_second_roughness.set_float(second_roughness); } HIPRT_HOST_DEVICE void set_metallic_energy_compensation(bool do_metallic_energy_compensation) { flags.set_bool(do_metallic_energy_compensation); } HIPRT_HOST_DEVICE void set_specular(float specular) { specular_and_darkening_and_coat_roughness.set_float(specular); } HIPRT_HOST_DEVICE void set_specular_tint(float specular_tint) { specular_color_and_tint_factor.set_float(specular_tint); } HIPRT_HOST_DEVICE void set_specular_color(ColorRGB32F specular_color) { specular_color_and_tint_factor.set_color(specular_color); } HIPRT_HOST_DEVICE void set_specular_darkening(float specular_darkening) { specular_and_darkening_and_coat_roughness.set_float(specular_darkening); } HIPRT_HOST_DEVICE void set_specular_energy_compensation(bool do_specular_energy_compensation) { flags.set_bool(do_specular_energy_compensation); } HIPRT_HOST_DEVICE void set_coat(float coat) { coat_and_medium_absorption.set_float(coat); } HIPRT_HOST_DEVICE void set_coat_medium_absorption(ColorRGB32F coat_medium_absorption) { coat_and_medium_absorption.set_color(coat_medium_absorption); } HIPRT_HOST_DEVICE void set_coat_medium_thickness(float coat_medium_thickness_) { this->coat_medium_thickness = coat_medium_thickness_; } HIPRT_HOST_DEVICE void set_coat_roughness(float coat_roughness) { specular_and_darkening_and_coat_roughness.set_float(coat_roughness); } HIPRT_HOST_DEVICE void set_coat_roughening(float coat_roughening) { coat_roughening_darkening_anisotropy_and_rotation.set_float(coat_roughening); } HIPRT_HOST_DEVICE void set_coat_darkening(float coat_darkening) { coat_roughening_darkening_anisotropy_and_rotation.set_float(coat_darkening); } HIPRT_HOST_DEVICE void set_coat_anisotropy(float coat_anisotropy) { coat_roughening_darkening_anisotropy_and_rotation.set_float(coat_anisotropy); } HIPRT_HOST_DEVICE void set_coat_anisotropy_rotation(float coat_anisotropy_rotation) { coat_roughening_darkening_anisotropy_and_rotation.set_float(coat_anisotropy_rotation); } HIPRT_HOST_DEVICE void set_coat_ior(float coat_ior_) { this->coat_ior = coat_ior_; } HIPRT_HOST_DEVICE void set_coat_energy_compensation(bool do_coat_energy_compensation) { flags.set_bool(do_coat_energy_compensation); } HIPRT_HOST_DEVICE void set_sheen(float sheen) { sheen_and_color.set_float(sheen); } HIPRT_HOST_DEVICE void set_sheen_roughness(float sheen_roughness) { sheen_roughness_transmission_dispersion_thin_film.set_float(sheen_roughness); } HIPRT_HOST_DEVICE void set_sheen_color(ColorRGB32F sheen_color) { sheen_and_color.set_color(sheen_color); } HIPRT_HOST_DEVICE void set_ior(float ior_) { this->ior = ior_; } HIPRT_HOST_DEVICE void set_specular_transmission(float specular_transmission) { sheen_roughness_transmission_dispersion_thin_film.set_float(specular_transmission); } HIPRT_HOST_DEVICE void set_diffuse_transmission(float diffuse_transmission) { metallic_F82_packed_and_diffuse_transmission.set_float(diffuse_transmission); } HIPRT_HOST_DEVICE void set_absorption_at_distance(float absorption_at_distance_) { this->absorption_at_distance = absorption_at_distance_; } HIPRT_HOST_DEVICE void set_absorption_color(ColorRGB32F absorption_color) { absorption_color_packed.set_color(absorption_color); } HIPRT_HOST_DEVICE void set_dispersion_scale(float dispersion_scale) { sheen_roughness_transmission_dispersion_thin_film.set_float(dispersion_scale); } HIPRT_HOST_DEVICE void set_dispersion_abbe_number(float dispersion_abbe_number_) { this->dispersion_abbe_number = dispersion_abbe_number_; } HIPRT_HOST_DEVICE void set_thin_walled(bool thin_walled) { flags.set_bool(thin_walled); } HIPRT_HOST_DEVICE void set_glass_energy_compensation(bool do_glass_energy_compensation) { flags.set_bool(do_glass_energy_compensation); } HIPRT_HOST_DEVICE void set_thin_film(float thin_film) { sheen_roughness_transmission_dispersion_thin_film.set_float(thin_film); } HIPRT_HOST_DEVICE void set_thin_film_ior(float thin_film_ior_) { this->thin_film_ior = thin_film_ior_; } HIPRT_HOST_DEVICE void set_thin_film_thickness(float thin_film_thickness_) { this->thin_film_thickness = thin_film_thickness_; } HIPRT_HOST_DEVICE void set_thin_film_kappa_3(float thin_film_kappa_3_) { this->thin_film_kappa_3 = thin_film_kappa_3_; } HIPRT_HOST_DEVICE void set_thin_film_hue_shift_degrees(float thin_film_hue_shift_degrees) { alpha_thin_film_hue_dielectric_priority.set_float(thin_film_hue_shift_degrees); } HIPRT_HOST_DEVICE void set_thin_film_base_ior_override(bool thin_film_base_ior_override_) { this->thin_film_base_ior_override = thin_film_base_ior_override_; } HIPRT_HOST_DEVICE void set_thin_film_do_ior_override(bool thin_film_do_ior_override) { flags.set_bool(thin_film_do_ior_override); } HIPRT_HOST_DEVICE void set_alpha_opacity(float alpha_opacity) { alpha_thin_film_hue_dielectric_priority.set_float(alpha_opacity); } HIPRT_HOST_DEVICE void set_dielectric_priority(unsigned char dielectric_priority) { alpha_thin_film_hue_dielectric_priority.set_uchar(dielectric_priority); } HIPRT_HOST_DEVICE void set_energy_preservation_monte_carlo_samples(unsigned char energy_preservation_monte_carlo_samples) { alpha_thin_film_hue_dielectric_priority.set_uchar(energy_preservation_monte_carlo_samples); } HIPRT_HOST_DEVICE void set_enforce_strong_energy_conservation(bool enforce_strong_energy_conservation) { flags.set_bool(enforce_strong_energy_conservation); } private: friend class DevicePackedTexturedMaterialSoAGPUData; friend class DevicePackedTexturedMaterialSoACPUData; // Packed flags of the material: // - thin_walled // Is the material thin walled? i.e. it doesn't have an interior and light doesn't // bend as it goes through // // - emissive_texture_used // Does the material use an emissive texture? // // - thin_film_do_ior_override // Whether or not to override the IORs used for the base material on top of which // the thin film sits. // // - enforce_strong_energy_conservation // If true, 'energy_preservation_monte_carlo_samples' will be used // to compute the directional albedo of this material. // This computed directional albedo is then used to ensure perfect energy conservation // and preservation. // // This is however very expensive. // This is usually only needed on clearcoated materials (but even then, the energy loss due to the absence of multiple scattering between // the clearcoat layer and the BSDF below may be acceptable). // // Non-clearcoated materials can already ensure perfect (modulo implementation quality) energy // conservation/preservation with the precomputed LUTs [Turquin, 2019]. // // See PrincipledBSDFDoEnergyCompensation in this codebase. // // Values from the 'PackedFlagsIndices' enum should be used // to retrieve/set from the packed flags UChar8BoolsPacked flags; // Full range emission ColorRGB32F emission = ColorRGB32F{ 0.0f, 0.0f, 0.0f }; // Base color RGB 3x8 bits + roughness uchar [float in [0,1] packed in 8 bit] ColorRGB24bFloat0_1Packed base_color_roughness; float oren_nayar_sigma = 0.34906585039886591538f; // 20 degrees standard deviation in radian // Parameters for Adobe 2023 F82-tint model // Packs the SDR F90 color and the metalness parameter ColorRGB24bFloat0_1Packed metallic_F90_and_metallic; ColorRGB24bFloat0_1Packed metallic_F82_packed_and_diffuse_transmission; float metallic_F90_falloff_exponent = 5.0f; Float4xPacked anisotropy_and_rotation_and_second_roughness; // Packed specular color and the intensity of the tint // // Specular tint intensity: Specular will be white if 0.0f and will be 'specular_color' if 1.0f ColorRGB24bFloat0_1Packed specular_color_and_tint_factor; // Packed: // - specular_darkening // Same as coat darkening but for total internal reflection inside the specular layer // that sits on top of the diffuse base // // Disabled by default for artistic "expectations" // // - Specular // Specular intensity // // - Coat roughness // Roughness of the coat // TODO: PACKED 1 FLOAT IS UNUSED IN HERE Float4xPacked specular_and_darkening_and_coat_roughness; float coat_medium_thickness = 5.0f; // Packed: // - Coat // Intensity of the coat. 0.0f disables the coating // // - Coat medium absorption color ColorRGB24bFloat0_1Packed coat_and_medium_absorption; // Packed: // - Coat roughening // Physical accuracy requires that a rough clearcoat also roughens what's underneath it // i.e. the specular/metallic/transmission layers. // // The option is however given here to artistically disable // that behavior by using coat roughening = 0.0f. // // - Coat darkening // Because of the total internal reflection that can happen inside the coat layer (i.e. // light bouncing between the coat/BSDF and air/coat interfaces), the BSDF below the // clearcoat will appear will increased saturation. // // - Coat anisotropy // - Coat anisotropy rotation Float4xPacked coat_roughening_darkening_anisotropy_and_rotation; float coat_ior = 1.5f; // Packed: // - Sheen intensity. 0.0f disables the sheen effect // // - Sheen color ColorRGB24bFloat0_1Packed sheen_and_color; // IOR of the base material float ior = 1.40f; // Packed: // - Absorption color // Color of the light absorption when traveling through the medium // TODO: PACKED FLOAT IS UNUSED IN HERE ColorRGB24bFloat0_1Packed absorption_color_packed; float absorption_at_distance = 5.0f; // Packed: // - Sheen roughness // // - Specular transmission // How much light is transmitted through the material. This essentially controls the glass lobe // // - Dispersion scale // Intensity of the dispersion effect in glass objects // // - Thin film // Intensity of the thin-film effect Float4xPacked sheen_roughness_transmission_dispersion_thin_film; float dispersion_abbe_number = 20.0f; float thin_film_ior = 1.3f; float thin_film_thickness = 500.0f; float thin_film_kappa_3 = 0.0f; float thin_film_base_ior_override = 1.0f; // Packed: // - Alpha opacity // 1.0f makes the material completely opaque // 0.0f completely transparent (becomes invisible) // // - Thin film hue shift in degrees // // - Dielectric priority // Nested dielectric with priority parameter // // - Energy preservation samples // How many samples will be computed for the integration of the directional // when the strong energy preservation/conservation of the material is enabled Float2xUChar2xPacked alpha_thin_film_hue_dielectric_priority; }; struct DevicePackedTexturedMaterial : public DevicePackedEffectiveMaterial { enum NormalMapEmissionIndices : unsigned char { NORMAL_MAP_INDEX = 0, EMISSION_INDEX = 1, }; enum BaseColorRoughnessMetallicIndices : unsigned char { BASE_COLOR_INDEX = 0, ROUGHNESS_METALLIC_INDEX = 1, }; enum RoughnessAndMetallicIndices : unsigned char { ROUGHNESS_INDEX = 0, METALLIC_INDEX = 1, }; enum AnisotropicSpecularIndices : unsigned char { ANISOTROPIC_INDEX = 0, SPECULAR_INDEX = 1, }; enum CoatSheenIndices : unsigned char { COAT_INDEX = 0, SHEEN_INDEX = 1, }; enum SpecularTransmissionIndex : unsigned char { SPECULAR_TRANSMISSION_INDEX = 0, }; HIPRT_HOST_DEVICE DeviceUnpackedTexturedMaterial unpack() { DeviceUnpackedTexturedMaterial out; out.normal_map_texture_index = this->get_normal_map_texture_index(); out.emission_texture_index = this->get_emission_texture_index(); out.base_color_texture_index = this->get_base_color_texture_index(); out.roughness_metallic_texture_index = this->get_roughness_metallic_texture_index(); out.roughness_texture_index = this->get_roughness_texture_index(); out.metallic_texture_index = this->get_metallic_texture_index(); out.anisotropic_texture_index = this->get_anisotropic_texture_index(); out.specular_texture_index = this->get_specular_texture_index(); out.coat_texture_index = this->get_coat_texture_index(); out.sheen_texture_index = this->get_sheen_texture_index(); out.specular_transmission_texture_index = this->get_specular_transmission_texture_index(); out.emissive_texture_used = this->get_emissive_texture_used(); if (!out.emissive_texture_used) out.emission = this->get_emission(); if (out.base_color_texture_index == MaterialConstants::NO_TEXTURE) out.base_color = this->get_base_color(); out.roughness = this->get_roughness(); out.oren_nayar_sigma = this->get_oren_nayar_sigma(); // Parameters for Adobe 2023 F82-tint model out.metallic = this->get_metallic(); if (out.metallic > 0.0f || out.metallic_texture_index != MaterialConstants::NO_TEXTURE || out.roughness_metallic_texture_index != MaterialConstants::NO_TEXTURE) { // We only need to unpack all of this if we actually have a metallic lobe out.metallic_F90_falloff_exponent = this->get_metallic_F90_falloff_exponent(); // F0 is not here as it uses the 'base_color' of the material out.metallic_F82 = this->get_metallic_F82(); out.metallic_F90 = this->get_metallic_F90(); out.second_roughness_weight = this->get_second_roughness_weight(); out.second_roughness = this->get_second_roughness(); #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoMetallicEnergyCompensation == KERNEL_OPTION_TRUE out.do_metallic_energy_compensation = this->get_do_metallic_energy_compensation(); #endif } out.anisotropy = this->get_anisotropy(); out.anisotropy_rotation = this->get_anisotropy_rotation(); // Specular intensity out.specular = this->get_specular(); if (out.specular > 0.0f || out.specular_texture_index != MaterialConstants::NO_TEXTURE) { // Specular tint intensity. // Specular will be white if 0.0f and will be 'specular_color' if 1.0f out.specular_tint = this->get_specular_tint(); out.specular_color = this->get_specular_color(); // Same as coat darkening but for total internal reflection inside the specular layer // that sits on top of the diffuse base // // Disabled by default for artistic "expectations" out.specular_darkening = this->get_specular_darkening(); #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoSpecularEnergyCompensation == KERNEL_OPTION_TRUE out.do_specular_energy_compensation = this->get_do_specular_energy_compensation(); #endif } out.coat = this->get_coat(); if (out.coat > 0.0f || out.coat_texture_index != MaterialConstants::NO_TEXTURE) { out.coat_medium_absorption = this->get_coat_medium_absorption(); // The coat thickness influences the amount of absorption (given by 'coat_medium_absorption') // that will happen inside the coat out.coat_medium_thickness = this->get_coat_medium_thickness(); out.coat_roughness = this->get_coat_roughness(); // Physical accuracy requires that a rough clearcoat also roughens what's underneath it // i.e. the specular/metallic/transmission layers. // // The option is however given here to artistically disable // that behavior by using coat roughening = 0.0f. out.coat_roughening = this->get_coat_roughening(); // Because of the total internal reflection that can happen inside the coat layer (i.e. // light bouncing between the coat/BSDF and air/coat interfaces), the BSDF below the // clearcoat will appear will increased saturation. out.coat_darkening = this->get_coat_darkening(); out.coat_anisotropy = this->get_coat_anisotropy(); out.coat_anisotropy_rotation = this->get_coat_anisotropy_rotation(); out.coat_ior = this->get_coat_ior(); #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoClearcoatEnergyCompensation == KERNEL_OPTION_TRUE out.do_coat_energy_compensation = this->get_do_coat_energy_compensation(); #endif } out.sheen = this->get_sheen(); // Sheen strength if (out.sheen > 0.0f || out.sheen_texture_index != MaterialConstants::NO_TEXTURE) { out.sheen_roughness = this->get_sheen_roughness(); out.sheen_color = this->get_sheen_color(); } out.ior = this->get_ior(); out.diffuse_transmission = this->get_diffuse_transmission(); out.specular_transmission = this->get_specular_transmission(); if (out.specular_transmission > 0.0f || out.specular_transmission_texture_index != MaterialConstants::NO_TEXTURE) { // Specular transmission specific out.dispersion_scale = this->get_dispersion_scale(); out.dispersion_abbe_number = this->get_dispersion_abbe_number(); out.thin_walled = this->get_thin_walled(); #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoGlassEnergyCompensation == KERNEL_OPTION_TRUE out.do_glass_energy_compensation = this->get_do_glass_energy_compensation(); #endif } if (out.specular_transmission > 0.0f || out.diffuse_transmission > 0.0f || out.specular_transmission_texture_index != MaterialConstants::NO_TEXTURE) { // Also enabled by diffuse transmission as well as specular transmission // At what distance is the light absorbed to the given absorption_color out.absorption_at_distance = this->get_absorption_at_distance(); // Color of the light absorption when traveling through the medium out.absorption_color = this->get_absorption_color(); } out.thin_film = this->get_thin_film(); if (out.thin_film > 0.0f) { out.thin_film_ior = this->get_thin_film_ior(); out.thin_film_thickness = this->get_thin_film_thickness(); out.thin_film_kappa_3 = this->get_thin_film_kappa_3(); // Sending the hue film in [0, 1] to the GPU out.thin_film_hue_shift_degrees = this->get_thin_film_hue_shift_degrees(); out.thin_film_base_ior_override = this->get_thin_film_base_ior_override(); out.thin_film_do_ior_override = this->get_thin_film_do_ior_override(); } // 1.0f makes the material completely opaque // 0.0f completely transparent (becomes invisible) out.alpha_opacity = this->get_alpha_opacity(); // Nested dielectric parameter out.set_dielectric_priority(this->get_dielectric_priority()); // If true, 'energy_preservation_monte_carlo_samples' will be used // to compute the directional albedo of this material. // This computed directional albedo is then used to ensure perfect energy conservation // and preservation. // // This is however very expensive. // This is usually only needed on clearcoated materials (but even then, the energy loss due to the absence of multiple scattering between // the clearcoat layer and the BSDF below may be acceptable). // // Non-clearcoated materials can already ensure perfect (modulo implementation quality) energy // conservation/preservation with the precomputed LUTs [Turquin, 2019]. // // See PrincipledBSDFDoEnergyCompensation in this codebase. out.enforce_strong_energy_conservation = this->get_enforce_strong_energy_conservation(); if (out.enforce_strong_energy_conservation) out.energy_preservation_monte_carlo_samples = this->get_energy_preservation_monte_carlo_samples(); return out; } HIPRT_HOST_DEVICE unsigned short int get_normal_map_texture_index() const { return normal_map_emission_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_emission_texture_index() const { return normal_map_emission_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_base_color_texture_index() const { return base_color_roughness_metallic_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_roughness_metallic_texture_index() const { return base_color_roughness_metallic_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_roughness_texture_index() const { return roughness_and_metallic_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_metallic_texture_index() const { return roughness_and_metallic_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_anisotropic_texture_index() const { return roughness_and_metallic_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_specular_texture_index() const { return anisotropic_specular_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_coat_texture_index() const { return coat_sheen_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_sheen_texture_index() const { return coat_sheen_index.get_value(); } HIPRT_HOST_DEVICE unsigned short int get_specular_transmission_texture_index() const { return specular_transmission_index.get_value(); } HIPRT_HOST_DEVICE void set_normal_map_texture_index(unsigned short normal_map_index) { normal_map_emission_index.set_value(normal_map_index); } HIPRT_HOST_DEVICE void set_emission_texture_index(unsigned short emission_index) { normal_map_emission_index.set_value(emission_index); } HIPRT_HOST_DEVICE void set_base_color_texture_index(unsigned short base_color_index) { base_color_roughness_metallic_index.set_value(base_color_index); } HIPRT_HOST_DEVICE void set_roughness_metallic_texture_index(unsigned short roughness_metallic_index) { base_color_roughness_metallic_index.set_value(roughness_metallic_index); } HIPRT_HOST_DEVICE void set_roughness_texture_index(unsigned short roughness_index) { roughness_and_metallic_index.set_value(roughness_index); } HIPRT_HOST_DEVICE void set_metallic_texture_index(unsigned short metallic_index) { roughness_and_metallic_index.set_value(metallic_index); } HIPRT_HOST_DEVICE void set_anisotropic_texture_index(unsigned short anisotropic_index) { roughness_and_metallic_index.set_value(anisotropic_index); } HIPRT_HOST_DEVICE void set_specular_texture_index(unsigned short specular_index) { anisotropic_specular_index.set_value(specular_index); } HIPRT_HOST_DEVICE void set_coat_texture_index(unsigned short coat_index) { coat_sheen_index.set_value(coat_index); } HIPRT_HOST_DEVICE void set_sheen_texture_index(unsigned short sheen_index) { coat_sheen_index.set_value(sheen_index); } HIPRT_HOST_DEVICE void set_specular_transmission_texture_index(unsigned short _specular_transmission_index) { specular_transmission_index.set_value(_specular_transmission_index); } private: friend class DevicePackedTexturedMaterialSoAGPUData; friend class DevicePackedTexturedMaterialSoACPUData; Uint2xPacked normal_map_emission_index; // If the roughness_metallic texture index is not MaterialConstants::NO_TEXTURE, // then there is only one texture for the metallic and the roughness parameters in which. // case the green channel is the roughness and the blue channel is the metalness. Uint2xPacked base_color_roughness_metallic_index; Uint2xPacked roughness_and_metallic_index; Uint2xPacked anisotropic_specular_index; Uint2xPacked coat_sheen_index; // TODO: 1 PACKED UINT IS UNUSED IN HERE Uint2xPacked specular_transmission_index; }; #endif ================================================ FILE: src/HostDeviceCommon/Material/MaterialPackedSoA.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_MATERIAL_PACKED_SOA_H #define HOST_DEVICE_COMMON_MATERIAL_PACKED_SOA_H #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Material/MaterialPacked.h" #include "HostDeviceCommon/Material/MaterialUtils.h" struct DevicePackedEffectiveMaterialSoA { HIPRT_DEVICE ColorRGB32F get_emission(int material_index) const { return this->emission[material_index]; } HIPRT_DEVICE bool get_emissive_texture_used(int material_index) const { return flags[material_index].get_bool(); } HIPRT_DEVICE ColorRGB32F get_base_color(int material_index) const { return base_color_roughness[material_index].get_color(); } HIPRT_DEVICE float get_roughness(int material_index) const { return base_color_roughness[material_index].get_float(); } HIPRT_DEVICE float get_oren_nayar_sigma(int material_index) const { return this->oren_nayar_sigma[material_index]; } HIPRT_DEVICE float get_metallic(int material_index) const { return metallic_F90_and_metallic[material_index].get_float(); } HIPRT_DEVICE float get_metallic_F90_falloff_exponent(int material_index) const { return this->metallic_F90_falloff_exponent[material_index]; } HIPRT_DEVICE ColorRGB32F get_metallic_F82(int material_index) const { return metallic_F82_packed_and_diffuse_transmission[material_index].get_color(); } HIPRT_DEVICE ColorRGB32F get_metallic_F90(int material_index) const { return metallic_F90_and_metallic[material_index].get_color(); } HIPRT_DEVICE float get_anisotropy(int material_index) const { return anisotropy_and_rotation_and_second_roughness[material_index].get_float(); } HIPRT_DEVICE float get_anisotropy_rotation(int material_index) const { return anisotropy_and_rotation_and_second_roughness[material_index].get_float(); } HIPRT_DEVICE float get_second_roughness_weight(int material_index) const { return anisotropy_and_rotation_and_second_roughness[material_index].get_float(); } HIPRT_DEVICE float get_second_roughness(int material_index) const { return anisotropy_and_rotation_and_second_roughness[material_index].get_float(); } HIPRT_DEVICE bool get_do_metallic_energy_compensation(int material_index) const { return flags[material_index].get_bool(); } HIPRT_DEVICE float get_specular(int material_index) const { return specular_and_darkening_and_coat_roughness[material_index].get_float(); } HIPRT_DEVICE float get_specular_tint(int material_index) const { return specular_color_and_tint_factor[material_index].get_float(); } HIPRT_DEVICE ColorRGB32F get_specular_color(int material_index) const { return specular_color_and_tint_factor[material_index].get_color(); } HIPRT_DEVICE float get_specular_darkening(int material_index) const { return specular_and_darkening_and_coat_roughness[material_index].get_float(); } HIPRT_DEVICE bool get_do_specular_energy_compensation(int material_index) const { return flags[material_index].get_bool(); } HIPRT_DEVICE float get_coat(int material_index) const { return coat_and_medium_absorption[material_index].get_float(); } HIPRT_DEVICE ColorRGB32F get_coat_medium_absorption(int material_index) const { return coat_and_medium_absorption[material_index].get_color(); } HIPRT_DEVICE float get_coat_medium_thickness(int material_index) const { return this->coat_medium_thickness[material_index]; } HIPRT_DEVICE float get_coat_roughness(int material_index) const { return specular_and_darkening_and_coat_roughness[material_index].get_float(); } HIPRT_DEVICE float get_coat_roughening(int material_index) const { return coat_roughening_darkening_anisotropy_and_rotation[material_index].get_float(); } HIPRT_DEVICE float get_coat_darkening(int material_index) const { return coat_roughening_darkening_anisotropy_and_rotation[material_index].get_float(); } HIPRT_DEVICE float get_coat_anisotropy(int material_index) const { return coat_roughening_darkening_anisotropy_and_rotation[material_index].get_float(); } HIPRT_DEVICE float get_coat_anisotropy_rotation(int material_index) const { return coat_roughening_darkening_anisotropy_and_rotation[material_index].get_float(); } HIPRT_DEVICE float get_coat_ior(int material_index) const { return this->coat_ior[material_index]; } HIPRT_DEVICE bool get_do_coat_energy_compensation(int material_index) const { return flags[material_index].get_bool(); } HIPRT_DEVICE float get_sheen(int material_index) const { return sheen_and_color[material_index].get_float(); } HIPRT_DEVICE float get_sheen_roughness(int material_index) const { return sheen_roughness_transmission_dispersion_thin_film[material_index].get_float(); } HIPRT_DEVICE ColorRGB32F get_sheen_color(int material_index) const { return sheen_and_color[material_index].get_color(); } HIPRT_DEVICE float get_ior(int material_index) const { return this->ior[material_index]; } HIPRT_DEVICE float get_specular_transmission(int material_index) const { return sheen_roughness_transmission_dispersion_thin_film[material_index].get_float(); } HIPRT_DEVICE float get_diffuse_transmission(int material_index) const { return metallic_F82_packed_and_diffuse_transmission[material_index].get_float(); } HIPRT_DEVICE float get_absorption_at_distance(int material_index) const { return this->absorption_at_distance[material_index]; } HIPRT_DEVICE ColorRGB32F get_absorption_color(int material_index) const { return absorption_color_packed[material_index].get_color(); } HIPRT_DEVICE float get_dispersion_scale(int material_index) const { return sheen_roughness_transmission_dispersion_thin_film[material_index].get_float(); } HIPRT_DEVICE float get_dispersion_abbe_number(int material_index) const { return this->dispersion_abbe_number[material_index]; } HIPRT_DEVICE bool get_thin_walled(int material_index) const { return flags[material_index].get_bool(); } HIPRT_DEVICE bool get_do_glass_energy_compensation(int material_index) const { return flags[material_index].get_bool(); } HIPRT_DEVICE float get_thin_film(int material_index) const { return sheen_roughness_transmission_dispersion_thin_film[material_index].get_float(); } HIPRT_DEVICE float get_thin_film_ior(int material_index) const { return this->thin_film_ior[material_index]; } HIPRT_DEVICE float get_thin_film_thickness(int material_index) const { return this->thin_film_thickness[material_index]; } HIPRT_DEVICE float get_thin_film_kappa_3(int material_index) const { return this->thin_film_kappa_3[material_index]; } HIPRT_DEVICE float get_thin_film_hue_shift_degrees(int material_index) const { return alpha_thin_film_hue_dielectric_priority[material_index].get_float(); } HIPRT_DEVICE float get_thin_film_base_ior_override(int material_index) const { return this->thin_film_base_ior_override[material_index]; } HIPRT_DEVICE bool get_thin_film_do_ior_override(int material_index) const { return flags[material_index].get_bool(); } HIPRT_DEVICE float get_alpha_opacity(int material_index) const { return alpha_thin_film_hue_dielectric_priority[material_index].get_float(); } HIPRT_DEVICE unsigned char get_dielectric_priority(int material_index) const { return alpha_thin_film_hue_dielectric_priority[material_index].get_uchar(); } HIPRT_DEVICE unsigned char get_energy_preservation_monte_carlo_samples(int material_index) const { return alpha_thin_film_hue_dielectric_priority[material_index].get_uchar(); } HIPRT_DEVICE bool get_enforce_strong_energy_conservation(int material_index) const { return flags[material_index].get_bool(); } // Packed flags of the material: // - thin_walled // Is the material thin walled? i.e. it doesn't have an interior and light doesn't // bend as it goes through // // - emissive_texture_used // Does the material use an emissive texture? // // - thin_film_do_ior_override // Whether or not to override the IORs used for the base material on top of which // the thin film sits. // // - enforce_strong_energy_conservation // If true, 'energy_preservation_monte_carlo_samples' will be used // to compute the directional albedo of this material. // This computed directional albedo is then used to ensure perfect energy conservation // and preservation. // // This is however very expensive. // This is usually only needed on clearcoated materials (but even then, the energy loss due to the absence of multiple scattering between // the clearcoat layer and the BSDF below may be acceptable). // // Non-clearcoated materials can already ensure perfect (modulo implementation quality) energy // conservation/preservation with the precomputed LUTs [Turquin, 2019]. // // See PrincipledBSDFDoEnergyCompensation in this codebase. // // Values from the 'PackedFlagsIndices' enum should be used // to retrieve/set from the packed flags UChar8BoolsPacked* flags = nullptr; // Full range emission ColorRGB32F* emission = nullptr; // Base color RGB 3x8 bits + roughness uchar [float in [0,1] packed in 8 bit] ColorRGB24bFloat0_1Packed* base_color_roughness = nullptr; float* oren_nayar_sigma = nullptr; // Parameters for Adobe 2023 F82-tint model // Packs the SDR F90 color and the metalness parameter ColorRGB24bFloat0_1Packed* metallic_F90_and_metallic; ColorRGB24bFloat0_1Packed* metallic_F82_packed_and_diffuse_transmission; float* metallic_F90_falloff_exponent = nullptr; Float4xPacked* anisotropy_and_rotation_and_second_roughness = nullptr; // Packed specular color and the intensity of the tint // // Specular tint intensity: Specular will be white if 0.0f and will be 'specular_color' if 1.0f ColorRGB24bFloat0_1Packed* specular_color_and_tint_factor = nullptr; // Packed: // - specular_darkening // Same as coat darkening but for total internal reflection inside the specular layer // that sits on top of the diffuse base // // Disabled by default for artistic "expectations" // // - Specular // Specular intensity // // - Coat roughness // Roughness of the coat // TODO: PACKED 1 FLOAT IS UNUSED IN HERE Float4xPacked* specular_and_darkening_and_coat_roughness = nullptr; float* coat_medium_thickness = nullptr; // Packed: // - Coat // Intensity of the coat. 0.0f disables the coating // // - Coat medium absorption color ColorRGB24bFloat0_1Packed* coat_and_medium_absorption = nullptr; // Packed: // - Coat roughening // Physical accuracy requires that a rough clearcoat also roughens what's underneath it // i.e. the specular/metallic/transmission layers. // // The option is however given here to artistically disable // that behavior by using coat roughening = 0.0f. // // - Coat darkening // Because of the total internal reflection that can happen inside the coat layer (i.e. // light bouncing between the coat/BSDF and air/coat interfaces), the BSDF below the // clearcoat will appear will increased saturation. // // - Coat anisotropy // - Coat anisotropy rotation Float4xPacked* coat_roughening_darkening_anisotropy_and_rotation = nullptr; float* coat_ior = nullptr; // Packed: // - Sheen intensity. 0.0f disables the sheen effect // // - Sheen color ColorRGB24bFloat0_1Packed* sheen_and_color = nullptr; // IOR of the base material float* ior = nullptr; // Packed: // - Absorption color // Color of the light absorption when traveling through the medium // TODO: PACKED FLOAT IS UNUSED IN HERE ColorRGB24bFloat0_1Packed* absorption_color_packed = nullptr; float* absorption_at_distance = nullptr; // Packed: // - Sheen roughness // // - Specular transmission // How much light is transmitted through the material. This essentially controls the glass lobe // // - Dispersion scale // Intensity of the dispersion effect in glass objects // // - Thin film // Intensity of the thin-film effect Float4xPacked* sheen_roughness_transmission_dispersion_thin_film = nullptr; float* dispersion_abbe_number = nullptr; float* thin_film_ior = nullptr; float* thin_film_thickness = nullptr; float* thin_film_kappa_3 = nullptr; float* thin_film_base_ior_override = nullptr; // Packed: // - Alpha opacity // 1.0f makes the material completely opaque // 0.0f completely transparent (becomes invisible) // // - Thin film hue shift in degrees // // - Dielectric priority // Nested dielectric with priority parameter // // - Energy preservation samples // How many samples will be computed for the integration of the directional // when the strong energy preservation/conservation of the material is enabled Float2xUChar2xPacked* alpha_thin_film_hue_dielectric_priority = nullptr; }; struct DevicePackedTexturedMaterialSoA : public DevicePackedEffectiveMaterialSoA { HIPRT_DEVICE unsigned short int get_normal_map_texture_index(int material_index) const { return normal_map_emission_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_emission_texture_index(int material_index) const { return normal_map_emission_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_base_color_texture_index(int material_index) const { return base_color_roughness_metallic_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_roughness_metallic_texture_index(int material_index) const { return base_color_roughness_metallic_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_roughness_texture_index(int material_index) const { return roughness_and_metallic_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_metallic_texture_index(int material_index) const { return roughness_and_metallic_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_anisotropic_texture_index(int material_index) const { return roughness_and_metallic_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_specular_texture_index(int material_index) const { return anisotropic_specular_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_coat_texture_index(int material_index) const { return coat_sheen_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_sheen_texture_index(int material_index) const { return coat_sheen_index[material_index].get_value(); } HIPRT_DEVICE unsigned short int get_specular_transmission_texture_index(int material_index) const { return specular_transmission_index[material_index].get_value(); } /** * The 'DevicePackedTexturedMaterial' returned contains all the data read * in all the arrays the SoA material struct for the given 'material_index' * * Note that, for example, even if the material has its 'coat' parameter at 0.0f * (i.e. it doesn't use coat at all), all the coat parameters (coat thickness, roughness, anisotropy, .....) * will be loaded from global memory and this will be useless global memory accesses * * A more performant alternative of this function is 'read_partial_material(int material_index)' * * This function is fully commented out because it's actually never used */ //HIPRT_DEVICE DevicePackedTexturedMaterial read_full_textured_material(int material_index) const //{ // DevicePackedTexturedMaterial out; // out.set_normal_map_texture_index(this->get_normal_map_texture_index(material_index)); // out.set_emission_texture_index(this->get_emission_texture_index(material_index)); // out.set_base_color_texture_index(this->get_base_color_texture_index(material_index)); // out.set_roughness_metallic_texture_index(this->get_roughness_metallic_texture_index(material_index)); // out.set_roughness_texture_index(this->get_roughness_texture_index(material_index)); // out.set_metallic_texture_index(this->get_metallic_texture_index(material_index)); // out.set_anisotropic_texture_index(this->get_anisotropic_texture_index(material_index)); // out.set_specular_texture_index(this->get_specular_texture_index(material_index)); // out.set_coat_texture_index(this->get_coat_texture_index(material_index)); // out.set_sheen_texture_index(this->get_sheen_texture_index(material_index)); // out.set_specular_transmission_texture_index(this->get_specular_transmission_texture_index(material_index)); // out.set_emission(this->get_emission(material_index)); // out.set_emissive_texture_used(this->get_emissive_texture_used(material_index)); // out.set_base_color(this->get_base_color(material_index)); // out.set_roughness(this->get_roughness(material_index)); // out.set_oren_nayar_sigma(this->get_oren_nayar_sigma(material_index)); // // Parameters for Adobe 2023 F82-tint model // out.set_metallic(this->get_metallic(material_index)); // out.set_metallic_F90_falloff_exponent(this->get_metallic_F90_falloff_exponent(material_index)); // // F0 is not here as it uses the 'base_color' of the material // out.set_metallic_F82(this->get_metallic_F82(material_index)); // out.set_metallic_F90(this->get_metallic_F90(material_index)); // out.set_anisotropy(this->get_anisotropy(material_index)); // out.set_anisotropy_rotation(this->get_anisotropy_rotation(material_index)); // out.set_second_roughness_weight(this->get_second_roughness_weight(material_index)); // out.set_second_roughness(this->get_second_roughness(material_index)); // out.set_metallic_energy_compensation(this->get_do_metallic_energy_compensation(material_index)); // // Specular intensity // out.set_specular(this->get_specular(material_index)); // // Specular tint intensity. // // Specular will be white if 0.0f and will be 'specular_color' if 1.0f // out.set_specular_tint(this->get_specular_tint(material_index)); // out.set_specular_color(this->get_specular_color(material_index)); // // Same as coat darkening but for total internal reflection inside the specular layer // // that sits on top of the diffuse base // // // // Disabled by default for artistic "expectations" // out.set_specular_darkening(this->get_specular_darkening(material_index)); // out.set_specular_energy_compensation(this->get_do_specular_energy_compensation(material_index)); // out.set_coat(this->get_coat(material_index)); // out.set_coat_medium_absorption(this->get_coat_medium_absorption(material_index)); // // The coat thickness influences the amount of absorption (given by 'coat_medium_absorption') // // that will happen inside the coat // out.set_coat_medium_thickness(this->get_coat_medium_thickness(material_index)); // out.set_coat_roughness(this->get_coat_roughness(material_index)); // // Physical accuracy requires that a rough clearcoat also roughens what's underneath it // // i.e. the specular/metallic/transmission layers. // // // // The option is however given here to artistically disable // // that behavior by using coat roughening = 0.0f. // out.set_coat_roughening(this->get_coat_roughening(material_index)); // // Because of the total internal reflection that can happen inside the coat layer (i.e. // // light bouncing between the coat/BSDF and air/coat interfaces), the BSDF below the // // clearcoat will appear will increased saturation. // out.set_coat_darkening(this->get_coat_darkening(material_index)); // out.set_coat_anisotropy(this->get_coat_anisotropy(material_index)); // out.set_coat_anisotropy_rotation(this->get_coat_anisotropy_rotation(material_index)); // out.set_coat_ior(this->get_coat_ior(material_index)); // out.set_coat_energy_compensation(this->get_do_coat_energy_compensation(material_index)); // out.set_sheen(this->get_sheen(material_index)); // Sheen strength // out.set_sheen_roughness(this->get_sheen_roughness(material_index)); // out.set_sheen_color(this->get_sheen_color(material_index)); // out.set_ior(this->get_ior(material_index)); // out.set_specular_transmission(this->get_specular_transmission(material_index)); // // At what distance is the light absorbed to the given absorption_color // out.set_absorption_at_distance(this->get_absorption_at_distance(material_index)); // // Color of the light absorption when traveling through the medium // out.set_absorption_color(this->get_absorption_color(material_index)); // out.set_dispersion_scale(this->get_dispersion_scale(material_index)); // out.set_dispersion_abbe_number(this->get_dispersion_abbe_number(material_index)); // out.set_thin_walled(this->get_thin_walled(material_index)); // out.set_glass_energy_compensation(this->get_do_glass_energy_compensation(material_index)); // out.set_thin_film(this->get_thin_film(material_index)); // out.set_thin_film_ior(this->get_thin_film_ior(material_index)); // out.set_thin_film_thickness(this->get_thin_film_thickness(material_index)); // out.set_thin_film_kappa_3(this->get_thin_film_kappa_3(material_index)); // // Sending the hue film in [0, 1] to the GPU // out.set_thin_film_hue_shift_degrees(get_thin_film_hue_shift_degrees(material_index)); // out.set_thin_film_base_ior_override(this->get_thin_film_base_ior_override(material_index)); // out.set_thin_film_do_ior_override(this->get_thin_film_do_ior_override(material_index)); // // 1.0f makes the material completely opaque // // 0.0f completely transparent (becomes invisible) // out.set_alpha_opacity(this->get_alpha_opacity(material_index)); // // Nested dielectric parameter // out.set_dielectric_priority(this->get_dielectric_priority(material_index)); // out.set_energy_preservation_monte_carlo_samples(this->get_energy_preservation_monte_carlo_samples(material_index)); // // If true, 'energy_preservation_monte_carlo_samples' will be used // // to compute the directional albedo of this material. // // This computed directional albedo is then used to ensure perfect energy conservation // // and preservation. // // // // This is however very expensive. // // This is usually only needed on clearcoated materials (but even then, the energy loss due to the absence of multiple scattering between // // the clearcoat layer and the BSDF below may be acceptable). // // // // Non-clearcoated materials can already ensure perfect (modulo implementation quality) energy // // conservation/preservation with the precomputed LUTs [Turquin, 2019]. // // // // See PrincipledBSDFDoEnergyCompensation in this codebase. // out.set_enforce_strong_energy_conservation(this->get_enforce_strong_energy_conservation(material_index)); // return out; //} /** * Only reads the relevant parameters of the material based on what parameters this material is using. * For example, if the 'coat' parameter of the material is 0.0f (i.e. the coat isn't used), none of the * coat parameters will be read from global memory which saves on memory traffic */ HIPRT_DEVICE DevicePackedTexturedMaterial read_partial_material(int material_index) const { DevicePackedTexturedMaterial out; out.set_normal_map_texture_index(this->get_normal_map_texture_index(material_index)); out.set_emission_texture_index(this->get_emission_texture_index(material_index)); out.set_base_color_texture_index(this->get_base_color_texture_index(material_index)); out.set_roughness_metallic_texture_index(this->get_roughness_metallic_texture_index(material_index)); out.set_roughness_texture_index(this->get_roughness_texture_index(material_index)); out.set_metallic_texture_index(this->get_metallic_texture_index(material_index)); out.set_anisotropic_texture_index(this->get_anisotropic_texture_index(material_index)); out.set_specular_texture_index(this->get_specular_texture_index(material_index)); out.set_coat_texture_index(this->get_coat_texture_index(material_index)); out.set_sheen_texture_index(this->get_sheen_texture_index(material_index)); out.set_specular_transmission_texture_index(this->get_specular_transmission_texture_index(material_index)); out.set_emissive_texture_used(this->get_emissive_texture_used(material_index)); if (!out.get_emissive_texture_used()) // Only loading the emission if no emissive texture is used out.set_emission(this->get_emission(material_index)); if (out.get_base_color_texture_index() == MaterialConstants::NO_TEXTURE) // Only reading the base color if no base color texture is used // (because if we have a base color texture, it's going to override // the base color parameter anyway) out.set_base_color(this->get_base_color(material_index)); if (out.get_roughness_texture_index() == MaterialConstants::NO_TEXTURE && out.get_roughness_metallic_texture_index() == MaterialConstants::NO_TEXTURE) // Same for the roughness out.set_roughness(this->get_roughness(material_index)); out.set_oren_nayar_sigma(this->get_oren_nayar_sigma(material_index)); // Parameters for Adobe 2023 F82-tint model // Only reading the metallic if no metallic texture is used // (because if we have a metallic texture, it's going to override // the metallic parameter anyway) out.set_metallic(this->get_metallic(material_index)); if (out.get_metallic() > 0.0f || out.get_metallic_texture_index() != MaterialConstants::NO_TEXTURE || out.get_roughness_metallic_texture_index() != MaterialConstants::NO_TEXTURE) { // If the metallic parameter isn't 0.0f, i.e. the material does have a metallic lobe, // then and only then do we need to load the metallic parameters out.set_metallic_F90_falloff_exponent(this->get_metallic_F90_falloff_exponent(material_index)); // F0 is not here as it uses the 'base_color' of the material out.set_metallic_F82(this->get_metallic_F82(material_index)); out.set_metallic_F90(this->get_metallic_F90(material_index)); out.set_second_roughness_weight(this->get_second_roughness_weight(material_index)); out.set_second_roughness(this->get_second_roughness(material_index)); #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoMetallicEnergyCompensation == KERNEL_OPTION_TRUE out.set_metallic_energy_compensation(this->get_do_metallic_energy_compensation(material_index)); #endif } if(out.get_anisotropic_texture_index() == MaterialConstants::NO_TEXTURE) out.set_anisotropy(this->get_anisotropy(material_index)); out.set_anisotropy_rotation(this->get_anisotropy_rotation(material_index)); // Specular intensity out.set_specular(this->get_specular(material_index)); if (out.get_specular() > 0.0f || out.get_specular_texture_index() != MaterialConstants::NO_TEXTURE) { // We only need to read the various specular parameters if the material actually has a specular lobe // Specular tint intensity. out.set_specular_tint(this->get_specular_tint(material_index)); // Specular will be white if 0.0f and will be 'specular_color' if 1.0f out.set_specular_color(this->get_specular_color(material_index)); // Same as coat darkening but for total internal reflection inside the specular layer // that sits on top of the diffuse base // // Disabled by default for artistic "expectations" out.set_specular_darkening(this->get_specular_darkening(material_index)); #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoSpecularEnergyCompensation == KERNEL_OPTION_TRUE out.set_specular_energy_compensation(this->get_do_specular_energy_compensation(material_index)); #endif } out.set_coat(this->get_coat(material_index)); if (out.get_coat() > 0.0f || out.get_coat_texture_index() != MaterialConstants::NO_TEXTURE) { // We only need to read the coat parameters if the material has a coat lobe // (which is when out.get_coat() > 0.0f) out.set_coat_medium_absorption(this->get_coat_medium_absorption(material_index)); // The coat thickness influences the amount of absorption (given by 'coat_medium_absorption') // that will happen inside the coat out.set_coat_medium_thickness(this->get_coat_medium_thickness(material_index)); out.set_coat_roughness(this->get_coat_roughness(material_index)); // Physical accuracy requires that a rough clearcoat also roughens what's underneath it // i.e. the specular/metallic/transmission layers. // // The option is however given here to artistically disable // that behavior by using coat roughening = 0.0f. out.set_coat_roughening(this->get_coat_roughening(material_index)); // Because of the total internal reflection that can happen inside the coat layer (i.e. // light bouncing between the coat/BSDF and air/coat interfaces), the BSDF below the // clearcoat will appear will increased saturation. out.set_coat_darkening(this->get_coat_darkening(material_index)); out.set_coat_anisotropy(this->get_coat_anisotropy(material_index)); out.set_coat_anisotropy_rotation(this->get_coat_anisotropy_rotation(material_index)); out.set_coat_ior(this->get_coat_ior(material_index)); #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoClearcoatEnergyCompensation == KERNEL_OPTION_TRUE out.set_coat_energy_compensation(this->get_do_coat_energy_compensation(material_index)); #endif } out.set_sheen(this->get_sheen(material_index)); // Sheen strength if (out.get_sheen() > 0.0f || out.get_sheen_texture_index() != MaterialConstants::NO_TEXTURE) { out.set_sheen_roughness(this->get_sheen_roughness(material_index)); out.set_sheen_color(this->get_sheen_color(material_index)); } out.set_ior(this->get_ior(material_index)); out.set_diffuse_transmission(this->get_diffuse_transmission(material_index)); out.set_specular_transmission(this->get_specular_transmission(material_index)); if (out.get_specular_transmission() > 0.0f || out.get_specular_transmission_texture_index() != MaterialConstants::NO_TEXTURE) { // This is all specific to specular transmission out.set_dispersion_scale(this->get_dispersion_scale(material_index)); out.set_dispersion_abbe_number(this->get_dispersion_abbe_number(material_index)); out.set_thin_walled(this->get_thin_walled(material_index)); #if PrincipledBSDFDoEnergyCompensation == KERNEL_OPTION_TRUE && PrincipledBSDFDoGlassEnergyCompensation == KERNEL_OPTION_TRUE out.set_glass_energy_compensation(this->get_do_glass_energy_compensation(material_index)); #endif } if (out.get_specular_transmission() > 0.0f || out.get_diffuse_transmission() > 0.0f || out.get_specular_transmission_texture_index() != MaterialConstants::NO_TEXTURE) { // This is also applicable to diffuse transmission // At what distance is the light absorbed to the given absorption_color out.set_absorption_at_distance(this->get_absorption_at_distance(material_index)); // Color of the light absorption when traveling through the medium out.set_absorption_color(this->get_absorption_color(material_index)); } out.set_thin_film(this->get_thin_film(material_index)); if (out.get_thin_film() > 0.0f) { out.set_thin_film_ior(this->get_thin_film_ior(material_index)); out.set_thin_film_thickness(this->get_thin_film_thickness(material_index)); out.set_thin_film_kappa_3(this->get_thin_film_kappa_3(material_index)); // Sending the hue film in [0, 1] to the GPU out.set_thin_film_hue_shift_degrees(get_thin_film_hue_shift_degrees(material_index)); out.set_thin_film_base_ior_override(this->get_thin_film_base_ior_override(material_index)); out.set_thin_film_do_ior_override(this->get_thin_film_do_ior_override(material_index)); } // 1.0f makes the material completely opaque // 0.0f completely transparent (becomes invisible) out.set_alpha_opacity(this->get_alpha_opacity(material_index)); // Nested dielectric parameter out.set_dielectric_priority(this->get_dielectric_priority(material_index)); return out; } Uint2xPacked* normal_map_emission_index = nullptr; // If the roughness_metallic texture index is not MaterialConstants::NO_TEXTURE, // then there is only one texture for the metallic and the roughness parameters in which. // case the green channel is the roughness and the blue channel is the metalness. Uint2xPacked* base_color_roughness_metallic_index = nullptr; Uint2xPacked* roughness_and_metallic_index = nullptr; Uint2xPacked* anisotropic_specular_index = nullptr; Uint2xPacked* coat_sheen_index = nullptr; // TODO: 1 PACKED UINT IS UNUSED IN HERE Uint2xPacked* specular_transmission_index = nullptr; }; #endif ================================================ FILE: src/HostDeviceCommon/Material/MaterialUnpacked.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_MATERIAL_UNPACKED_H #define HOST_DEVICE_COMMON_MATERIAL_UNPACKED_H //#include "HostDeviceCommon/Material/MaterialUtils.h" /** * How to add a material property: * * 1) MaterialUnpacked.h * * Add the unpacked device material structure what the GPU is going to need in the shaders * For most parameters, this is just the parameters itself * * For some other parameters, some stuff can be precomputed on the CPU and the GPU * can then use only the precomputed stuff. In these cases, you need to add the precomputed * stuff in here. The data needed for the precomputation is only going to be stored * in the CPUMaterial, step 2). * * An example of a precomputed parameter is the emission. On the GPU, the emission is a simple * color but on the CPU the emission is a color + emission strength. * The emission strength is precomputed (multiplied/factored in) into the emission when * packed into the material that the GPU uses * * 2) CPUMaterial.h * * Add the parameter to the CPUMaterial structure. Read step 1) for precomputed parameters. * * If the parameter needs clamping to avoid NaNs/singularities/numerical imprecisions, the clamping * must be done in CPUMaterial::make_safe() * * You also need to add a line in CPUMaterial::pack_to_gpu() to define how the GPUMaterial (whose data is packed). * This is most likely just a .set() call like all the other parameters. That setter will be defined in step 3). * If you have some precomputation to do (such as with the emission), it can be done in there (look at the .set_emission() call) * * 3) MaterialPacked.h * * Add the parameter to the MaterialPacked structure in MaterialPacked.h (only the parameters that * the GPU is going to use. So, if there is any precomputation to be done (most parameters do not have precomputation), add only what * holds the precomputed result that the GPU is directly going to use, not the data needed * for the precomputation (that's only in CPUMaterial). * * The parameter will need to be packed. It can be added to a member that doesn't have all its "fields" filled yet (look for // TODO) * or a new member needs to be created for the new parameter. * * The new parameter can also be full-range, i.e. not packed if precision is important or packing is impractical * * After the parameter has been added to a packed member, write the getter and setter in the same class (structure) * * The function DevicePackedEffectiveMaterial::pack() needs to be completed (follow what is done for the other parameters). * This is the function that will be called when packing the GPUMaterial into the GBuffer * This looks quite a bit like pack_to_gpu() from before but there is no precomputations to be done here because the precomputation * has already been done before in pack_to_gpu() * * The function DevicePackedEffectiveMaterial::unpack() needs to be completed (follow what is done for the other parameters). * This is the function that will be called when unpacking the material from the G-Buffer * * The function DevicePackedTexturedMaterial::unpack() needs to be completed (follow what is done for the other parameters). * This is the function that will be called when unpacking the material from the materials buffer (when reading the material of the geometry a ray just hit). * The unpacked textured material will then be used to read the textures of the material at the hit point and the whole * will result in a DevicePackedEffectiveMaterial that will be used in the rest of the shaders (or packed into the G-Buffer) * * 4) MaterialPackedSoA.h * * Add a getter for the parameter to DevicePackedEffectiveMaterialSoA. * This is to read the parameter from the structure of arrays (one buffer per each parameter packed) given a material index * * Add the parameter reading in DevicePackedEffectiveMaterialSoA::read_partial_material(). This function is just a handy function * to produce a DevicePackedTexturedMaterial by reading all the arrays of the structure of arrays * * Note that memory traffic can be saved in some case. Let's you're adding a bunch of parameters for a "super metallic" lobe: * - super metallic strength * - super roughness * - super anisotropy * - super flakes * - super fresnel F82 color * * The 'super metallic strength' parameter controls the overall strength of the super metallic lobe. * If it is 0, then the super metallic lobe is disabled from the BSDF. This is a case where it is not * needed to read any of the other parameters (super roughness, super anisotropy, ...) because they * will not be used anyways since the super metallic lobe is disabled. * * This logic to save memory traffic has already been applied to most of the other lobes (coat, glass, ...) * * 5) Add controls to ImGuiObjectsWindow (and the global material overrider) */ /** * Unpacked material for use in the shaders */ struct DeviceUnpackedEffectiveMaterial { HIPRT_HOST_DEVICE bool is_emissive() const { return !hippt::is_zero(emission.r) || !hippt::is_zero(emission.g) || !hippt::is_zero(emission.b) || emissive_texture_used; } ColorRGB32F emission = ColorRGB32F{ 0.0f, 0.0f, 0.0f }; ColorRGB32F base_color = ColorRGB32F(1.0f); float roughness = 0.3f; float oren_nayar_sigma = 0.34906585039886591538f; // 20 degrees standard deviation in radian // Parameters for Adobe 2023 F82-tint model float metallic = 0.0f; float metallic_F90_falloff_exponent = 5.0f; // F0 is not here as it uses the 'base_color' of the material ColorRGB32F metallic_F82 = ColorRGB32F(1.0f); ColorRGB32F metallic_F90 = ColorRGB32F(1.0f); float anisotropy = 0.0f; float anisotropy_rotation = 0.0f; float second_roughness_weight = 0.0f; float second_roughness = 0.5f; // Specular intensity float specular = 1.0f; // Specular tint intensity. // Specular will be white if 0.0f and will be 'specular_color' if 1.0f float specular_tint = 1.0f; ColorRGB32F specular_color = ColorRGB32F(1.0f); // Same as coat darkening but for total internal reflection inside the specular layer // that sits on top of the diffuse base // // Disabled by default for artistic "expectations" float specular_darkening = 0.0f; float coat = 0.0f; ColorRGB32F coat_medium_absorption = ColorRGB32F{ 1.0f, 1.0f, 1.0f }; // The coat thickness influences the amount of absorption (given by 'coat_medium_absorption') // that will happen inside the coat float coat_medium_thickness = 5.0f; float coat_roughness = 0.0f; // Physical accuracy requires that a rough clearcoat also roughens what's underneath it // i.e. the specular/metallic/transmission layers. // // The option is however given here to artistically disable // that behavior by using coat roughening = 0.0f. float coat_roughening = 1.0f; // Because of the total internal reflection that can happen inside the coat layer (i.e. // light bouncing between the coat/BSDF and air/coat interfaces), the BSDF below the // clearcoat will appear will increased saturation. float coat_darkening = 1.0f; float coat_anisotropy = 0.0f; float coat_anisotropy_rotation = 0.0f; float coat_ior = 1.5f; float sheen = 0.0f; // Sheen strength float sheen_roughness = 0.5f; ColorRGB32F sheen_color = ColorRGB32F(1.0f); float ior = 1.40f; float specular_transmission = 0.0f; float diffuse_transmission = 0.0f; // At what distance is the light absorbed to the given absorption_color float absorption_at_distance = 1.0f; // Color of the light absorption when traveling through the medium ColorRGB32F absorption_color = ColorRGB32F(1.0f); float dispersion_scale = 0.0f; float dispersion_abbe_number = 20.0f; float thin_film = 0.0f; float thin_film_ior = 1.3f; float thin_film_thickness = 500.0f; float thin_film_kappa_3 = 0.0f; float thin_film_hue_shift_degrees = 0.0f; float thin_film_base_ior_override = 1.0f; // 1.0f makes the material completely opaque // 0.0f completely transparent (becomes invisible) float alpha_opacity = 1.0f; unsigned char energy_preservation_monte_carlo_samples = 12; /** * The booleans are moved to the end of the structure to avoid too much structure packing */ // Whether or not to do energy compensation of the metallic layer // for that material bool do_metallic_energy_compensation = true; // Whether or not to do energy compensation of the specular/diffuse layer // for that material bool do_specular_energy_compensation = true; // Whether or not to do energy compensation of the clearcoat layer // for that material bool do_coat_energy_compensation = true; bool thin_walled = false; // Whether or not to do energy compensation of the glass layer // for that material bool do_glass_energy_compensation = true; bool thin_film_do_ior_override = false; // If true, 'energy_preservation_monte_carlo_samples' will be used // to compute the directional albedo of this material. // This computed directional albedo is then used to ensure perfect energy conservation // and preservation. // // This is however very expensive. // This is usually only needed on clearcoated materials (but even then, the energy loss due to the absence of multiple scattering between // the clearcoat layer and the BSDF below may be acceptable). // // Non-clearcoated materials can already ensure perfect (modulo implementation quality) energy // conservation/preservation with the precomputed LUTs [Turquin, 2019]. // // See PrincipledBSDFDoEnergyCompensation in this codebase. bool enforce_strong_energy_conservation = false; // This member is only ever set to true on the GPU when we have the simplified material // that doesn't have texture indices anymore. Then we can manually fetch the emissive texture // index of the material and sample the emissive texture bool emissive_texture_used = false; HIPRT_HOST_DEVICE void set_dielectric_priority(unsigned char priority) { dielectric_priority = priority; } HIPRT_HOST_DEVICE unsigned char get_dielectric_priority() const { #if BSDFOverride == BSDF_LAMBERTIAN || BSDFOverride == BSDF_OREN_NAYAR // These BSDFs do not support tranmission so every material // should have the same priority return 0; #else return dielectric_priority; #endif } private: // Nested dielectric parameter // Private because this may be different depending on the BRDF override // being used so we want to control this with getters/setters unsigned char dielectric_priority = 0; }; struct DeviceUnpackedTexturedMaterial : public DeviceUnpackedEffectiveMaterial { int normal_map_texture_index = 65535; int emission_texture_index = 65535; int base_color_texture_index = 65535; // If not 65535, there is only one texture for the metallic and the roughness parameters in which. // case the green channel is the roughness and the blue channel is the metalness int roughness_metallic_texture_index = 65535; int roughness_texture_index = 65535; int metallic_texture_index = 65535; int anisotropic_texture_index = 65535; int specular_texture_index = 65535; int coat_texture_index = 65535; int sheen_texture_index = 65535; int specular_transmission_texture_index = 65535; }; #endif ================================================ FILE: src/HostDeviceCommon/Material/MaterialUtils.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_MATERIAL_UTILS_H #define HOST_DEVICE_COMMON_MATERIAL_UTILS_H #include "Device/includes/BSDFs/BSDFIncidentLightInfo.h" #include "HostDeviceCommon/Material/MaterialConstants.h" #include "HostDeviceCommon/Material/MaterialPacked.h" #include "HostDeviceCommon/Material/MaterialUnpacked.h" #include "HostDeviceCommon/KernelOptions/PrincipledBSDFKernelOptions.h" struct MaterialUtils { HIPRT_HOST_DEVICE static void get_oren_nayar_AB(float sigma, float& out_oren_A, float& out_oren_B) { float sigma2 = sigma * sigma; out_oren_A = 1.0f - sigma2 / (2.0f * (sigma2 + 0.33f)); out_oren_B = 0.45f * sigma2 / (sigma2 + 0.09f); } HIPRT_HOST_DEVICE static void get_alphas(float roughness, float anisotropy, float& out_alpha_x, float& out_alpha_y) { float aspect = sqrtf(1.0f - 0.9f * anisotropy); out_alpha_x = hippt::max(MaterialConstants::ROUGHNESS_CLAMP, roughness * roughness / aspect); out_alpha_y = hippt::max(MaterialConstants::ROUGHNESS_CLAMP, roughness * roughness * aspect); } HIPRT_HOST_DEVICE static float get_thin_walled_roughness(bool thin_walled, float base_roughness, float relative_eta) { if (!thin_walled) return base_roughness; /* * Roughness remapping so that a thin walled interface matches better a * properly modeled double interface model. Said otherwise: roughness remapping * so that the thin walled approximation matches the non thin walled physically correct equivalent * * Reference: * [Revisiting Physically Based Shading at Imageworks, Christopher Kulla & Alejandro Conty, 2017] * * https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf */ float remapped = base_roughness * sqrt(3.7f * (relative_eta - 1.0f) * hippt::square(relative_eta - 0.5f) / hippt::pow_3(relative_eta)); // Remapped roughness starts going above 1.0f starting at relative eta around 1.9f // and ends up at 1.39f at relative eta 3.5f // // Because we don't expect the user to input higher IOR values than that, // we remap that remapped roughness from [0.0f, 1.39f] to [0.0f, 1.0f] // and if the user inputs higher IOR values than 3.5f, we clamp to 1.0f roughness // anyways return hippt::clamp(0.0f, 1.0f, remapped / 1.39f); } HIPRT_HOST_DEVICE static bool is_perfectly_smooth(float roughness, float roughness_threshold = MaterialConstants::PERFECTLY_SMOOTH_ROUGHNESS_THRESHOLD) { return roughness <= roughness_threshold; } /** * Whether or not it makes sense to even try light sampling with NEE on that material * * Perfectly smooth materials for example cannot do light sampling because no given light * direction is going to align with the delta distribution peak of the BRDF so we can save * some performance by not even attempting light sampling in the first place */ HIPRT_HOST_DEVICE static bool can_do_light_sampling(float material_roughness, float material_metallic, float material_specular_transmission, float material_coat, float material_coat_roughness, float material_second_roughness, float material_second_roughness_weight, float roughness_threshold) { #if DirectLightSamplingDeltaDistributionOptimization == KERNEL_OPTION_FALSE return true; #elif PrincipledBSDFDoMicrofacetRegularization == KERNEL_OPTION_TRUE // If we have BSDF regularization, everything can do light sampling now return true; #endif #if BSDFOverride == BSDF_LAMBERTIAN || BSDFOverride == BSDF_OREN_NAYAR // We can always do light sampling on these BSDFs return true; #endif bool smooth_base_layer = MaterialUtils::is_perfectly_smooth(material_roughness, roughness_threshold) && (material_metallic == 1.0f || material_specular_transmission == 1.0f); bool smooth_coat = material_coat == 0.0f || (material_coat > 0.0f && MaterialUtils::is_perfectly_smooth(material_coat_roughness, roughness_threshold)); bool second_roughness_smooth = MaterialUtils::is_perfectly_smooth(material_second_roughness, roughness_threshold) || material_second_roughness_weight == 0.0f; if (smooth_base_layer && smooth_coat && second_roughness_smooth) // Everything is smooth, cannot do light sampling return false; return true; } HIPRT_HOST_DEVICE static bool can_do_light_sampling(const DeviceUnpackedEffectiveMaterial& material, float roughness_threshold = MaterialConstants::PERFECTLY_SMOOTH_ROUGHNESS_THRESHOLD) { return can_do_light_sampling(material.roughness, material.metallic, material.specular_transmission, material.coat, material.coat_roughness, material.second_roughness, material.second_roughness_weight, roughness_threshold); } HIPRT_HOST_DEVICE static bool can_do_light_sampling(const DevicePackedEffectiveMaterial& material, float roughness_threshold = MaterialConstants::PERFECTLY_SMOOTH_ROUGHNESS_THRESHOLD) { return can_do_light_sampling(material.get_roughness(), material.get_metallic(), material.get_specular_transmission(), material.get_coat(), material.get_coat_roughness(), material.get_second_roughness(), material.get_second_roughness_weight(), roughness_threshold); } /** * Returns the minimum roughness of the material looking at all the active lobes */ HIPRT_HOST_DEVICE static float minimum_roughness(const DeviceUnpackedEffectiveMaterial& material) { float coat_roughness = material.coat > 0.0f ? material.coat_roughness : 1.0f; float specular_roughness = material.specular > 0.0f ? material.roughness : 1.0f; float glass_roughness = material.specular_transmission > 0.0f ? material.roughness : 1.0f; float metallic_roughness = (material.metallic > 0.0f && material.second_roughness_weight < 1.0f) ? material.roughness : 1.0f; float metallic_2_roughness = (material.metallic > 0.0f && material.second_roughness_weight > 0.0f) ? material.second_roughness : 1.0f; return hippt::min(coat_roughness, hippt::min(specular_roughness, hippt::min(glass_roughness, hippt::min(metallic_roughness, metallic_2_roughness)))); } enum SpecularDeltaReflectionSampled : int { NOT_SPECULAR = -1, SPECULAR_PEAK_NOT_SAMPLED = 0, SPECULAR_PEAK_SAMPLED = 1, }; /** * Determines whether a perfectly smooth lobe has any chance of evaluating to non-0. * * This is only relevant for perfectly smooth materials/lobe where we don't want to evaluate the specular BRDF * with anything other than a direction that was sampled directly from that specular BRDF. * * The 'delta_distribution_oughness' and 'delta_distribution_anisotropy' parameters here describe the BRDF lobe * that is being evaluated. * * 'incident_light_info' is some additional information about the incident light direction used for * evaluating the current lobe * * Returns 1 only if the specular distribution is worth evaluating, 0 if there's no point because it's going to * evaluate to 0 anyways * * Returns -1 if the distribution given isn't specular in the first place (delta_distribution_roughness isn't very close to 0) */ HIPRT_HOST_DEVICE static SpecularDeltaReflectionSampled is_specular_delta_reflection_sampled(const DeviceUnpackedEffectiveMaterial& material, float delta_distribution_roughness, float delta_distribution_anisotropy, BSDFIncidentLightInfo incident_light_info) { if (!MaterialUtils::is_perfectly_smooth(delta_distribution_roughness)) return SpecularDeltaReflectionSampled::NOT_SPECULAR; // For the glass lobe sampled direction to match, we only need it to be a reflection // and we need the glass lobe to be perfectly smooth bool matching_base_substrate_anisotropy = hippt::abs(delta_distribution_anisotropy - material.anisotropy) < 1.0e-3f; bool sampled_from_glass = incident_light_info == BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_GLASS_REFLECT_LOBE && MaterialUtils::is_perfectly_smooth(material.roughness) && matching_base_substrate_anisotropy; if (sampled_from_glass) // We can stop here return SpecularDeltaReflectionSampled::SPECULAR_PEAK_SAMPLED; // Same for the metal lobe (except that it's alawys a reflection, so it's easy there) bool sampled_from_first_metal = incident_light_info == BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_FIRST_METAL_LOBE && MaterialUtils::is_perfectly_smooth(material.roughness) && matching_base_substrate_anisotropy; bool sampled_from_second_metal = incident_light_info == BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_SECOND_METAL_LOBE && MaterialUtils::is_perfectly_smooth(material.second_roughness) && matching_base_substrate_anisotropy; if (sampled_from_first_metal || sampled_from_second_metal) // We can stop here return SpecularDeltaReflectionSampled::SPECULAR_PEAK_SAMPLED; // Same for the coat bool matching_coat_anisotropy = hippt::abs(delta_distribution_anisotropy - material.coat_anisotropy) < 1.0e-3f; bool sampled_from_coat = incident_light_info == BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_COAT_LOBE && matching_coat_anisotropy && MaterialUtils::is_perfectly_smooth(material.coat_roughness); if (sampled_from_coat) // We can stop here return SpecularDeltaReflectionSampled::SPECULAR_PEAK_SAMPLED; // Same for the specular layer bool sampled_from_specular = incident_light_info == BSDFIncidentLightInfo::LIGHT_DIRECTION_SAMPLED_FROM_SPECULAR_LOBE && MaterialUtils::is_perfectly_smooth(material.roughness) && matching_base_substrate_anisotropy; if (sampled_from_specular) // We can stop here return SpecularDeltaReflectionSampled::SPECULAR_PEAK_SAMPLED; return SpecularDeltaReflectionSampled::SPECULAR_PEAK_NOT_SAMPLED; } }; #endif ================================================ FILE: src/HostDeviceCommon/Math.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_MATH_H #define HOST_DEVICE_COMMON_MATH_H #if defined( __KERNELCC__ ) #include #else #include #endif #define int2 hiprtInt2 #define int3 hiprtInt3 #define int4 hiprtInt4 #define uint2 hiprtUint2 #define float2 hiprtFloat2 #define float3 hiprtFloat3 #define float4 hiprtFloat4 #define make_int2 make_hiprtInt2 #define make_int3 make_hiprtInt3 #define make_int4 make_hiprtInt4 #define make_uint2 make_hiprtUint2 #define make_float2 make_hiprtFloat2 #define make_float3 make_hiprtFloat3 #define make_float4 make_hiprtFloat4 #if !defined(__KERNELCC__) || defined(HIPRT_BITCODE_LINKING) #include // For std::atomic in hippt:: #include // For std::bit_cast in hippt:: #include #endif #include "HostDeviceCommon/AtomicType.h" struct float4x4 { float m[4][4] = { {0.0f, 0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f, 0.0f} }; }; struct float3x3 { float m[3][3] = { {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f} }; }; // Here we're defining aliases for common functions used in shader code. // // Because the same shader code can be used both on the CPU and the GPU, // both code have to compile either through the classical C++ compiler or // through the GPU shader compiler. This means that we have to use functions // that were meant to be used on the CPU or on the GPU (depending on the case). // // For example, we're using glm as the math library on the CPU, so 'normalize' // will actually be aliased to glm::normalize for the CPU // but 'normalize' will be aliased to hiprt::normalize on the GPU because // glm isn't meant to be used on the GPU namespace hippt { #ifdef __KERNELCC__ #define M_PI hiprt::Pi #define M_TWO_PI 6.28318530717958647693f // 2.0f * M_PI #define M_FOUR_PI 12.5663706143591729539f // 4.0f * M_PI #define M_INV_PI 0.31830988618379067154f // 1.0f / M_PI #define M_INV_2_PI 0.15915494309189533577f // 1.0f / (2.0f * M_PI) #define M_TWO_PI_SQUARED 19.73920880217871723767f #define NEAR_ZERO 1.0e-10f /** * Returns the 'warpSize' runtime constant of the GPU */ __device__ int warp_size() { return warpSize; } __device__ int thread_idx_x() { return threadIdx.x + blockIdx.x * blockDim.x; } __device__ int thread_idx_y() { return threadIdx.y + blockIdx.y * blockDim.y; } __device__ int thread_idx_global() { return hippt::thread_idx_x() + hippt::thread_idx_y() * blockDim.x * gridDim.x; } __device__ bool is_pixel_index(int x, int y) { return hippt::thread_idx_x() == x && hippt::thread_idx_y() == y; } __device__ int current_warp_lane() { return (threadIdx.x + threadIdx.y * blockDim.x) % hippt::warp_size(); } template __device__ T ldg_load(T* address) { return __ldg(address); } __device__ float3 cross(float3 u, float3 v) { return hiprt::cross(u, v); } __device__ float dot(float3 u, float3 v) { return hiprt::dot(u, v); } __device__ float length(float3 u) { return sqrt(hiprt::dot(u, u)); } __device__ float length2(float3 u) { return hiprt::dot(u, u); } __device__ float3 abs(float3 u) { return make_float3(fabsf(u.x), fabsf(u.y), fabsf(u.z)); } __device__ float abs(float a) { return fabsf(a); } template __device__ T max(T a, T b) { return a > b ? a : b; } /** * Component-wise max of float3 and int3 */ template <> __device__ float3 max(float3 a, float3 b) { return make_float3(hiprt::max(a.x, b.x), hiprt::max(a.y, b.y), hiprt::max(a.z, b.z)); } template <> __device__ int3 max(int3 a, int3 b) { return make_int3(hiprt::max(a.x, b.x), hiprt::max(a.y, b.y), hiprt::max(a.z, b.z)); } template __device__ T min(T a, T b) { return a < b ? a : b; } /** * Component-wise min of float3 and int3 */ template <> __device__ float3 min(float3 a, float3 b) { return make_float3(hiprt::min(a.x, b.x), hiprt::min(a.y, b.y), hiprt::min(a.z, b.z)); } template <> __device__ int3 min(int3 a, int3 b) { return make_int3(hiprt::min(a.x, b.x), hiprt::min(a.y, b.y), hiprt::min(a.z, b.z)); } /** * Minimum of each component of the float3 against x */ __device__ float3 min(float3 a, float x) { return make_float3(hiprt::min(a.x, x), hiprt::min(a.y, x), hiprt::min(a.z, x)); } __device__ float3 min(float x, float3 a) { return hippt::min(a, x); } template __device__ T clamp(T min_val, T max_val, T val) { return hiprt::min(max_val, hiprt::max(min_val, val)); } __device__ float max(float a, float b) { return a > b ? a : b; } __device__ float min(float a, float b) { return a < b ? a : b; } __device__ float clamp(float min_val, float max_val, float val) { return hiprt::clamp(val, min_val, max_val); } __device__ float3 cos(float3 x) { return make_float3(cosf(x.x), cosf(x.y), cosf(x.z)); } __device__ float2 cos(float2 x) { return make_float2(cosf(x.x), cosf(x.y)); } __device__ float intrin_cosf(float x) { return cosf(x); } // Not using the intrinsic for now because of a compiler bug __device__ float3 sin(float3 x) { return make_float3(sinf(x.x), sinf(x.y), sinf(x.z)); } __device__ float2 sin(float2 x) { return make_float2(sinf(x.x), sinf(x.y)); } __device__ float intrin_sinf(float x) { return __sinf(x); } __device__ float3 atan2(float3 y, float3 x) { return make_float3(atan2f(y.x, x.x), atan2f(y.y, x.y), atan2f(y.z, x.z)); } __device__ float2 exp(float2 x) { return make_float2(expf(x.x), expf(x.y)); } __device__ float3 exp(float3 x) { return make_float3(expf(x.x), expf(x.y), expf(x.z)); } __device__ float3 ldexp(float3 x, int exp) { return make_float3(ldexpf(x.x, exp), ldexpf(x.y, exp), ldexpf(x.z, exp)); } template __device__ T square(T x) { return x * x; } __device__ float2 sqrt(float2 uv) { return make_float2(sqrtf(uv.x), sqrtf(uv.y)); } __device__ float3 sqrt(float3 uvw) { return make_float3(sqrtf(uvw.x), sqrtf(uvw.y), sqrtf(uvw.z)); } __device__ float pow_1_4(float x) { return sqrtf(sqrtf(x)); } __device__ constexpr float pow_3(float x) { return x * x * x; } __device__ constexpr float pow_4(float x) { float x2 = x * x; return x2 * x2; } __device__ constexpr float pow_5(float x) { float x2 = x * x; float x4 = x2 * x2; return x4 * x; } __device__ constexpr float pow_6(float x) { float x2 = x * x; float x4 = x2 * x2; return x4 * x2; } __device__ float intrin_pow(float x, float y) { return __powf(x, y); } __device__ float3 normalize(float3 u) { return hiprt::normalize(u); } template __device__ bool is_nan(const T& v) { return isnan(v); } template __device__ bool is_inf(const T& v) { return isinf(v); } __device__ bool is_zero(float x) { return x < NEAR_ZERO && x > -NEAR_ZERO; } __device__ unsigned int float_as_uint(float float_num) { return __float_as_uint(float_num); } /** * Reads the 32-bit or 64-bit word old located at the address 'address' * in global or shared memory and stores 'value' to memory at the same address. * * These two operations are performed in one atomic transaction. * The function returns old. */ template __device__ T atomic_exchange(T* address, T value) { return atomicExch(address, value); } /** * Reads the 32-bit or 64-bit word 'old' located at 'address' in global or shared memory, * computes the maximum of 'old' and 'value', and stores the result back to memory at the * same address. * * The function returns 'old' */ template __device__ T atomic_max(T* address, T value) { return atomicMax(address, value); } /** * Reads the 32-bit or 64-bit word 'old' located at 'address' in global or shared memory, * computes the minimum of 'old' and 'value', and stores the result back to memory at the * same address. * * The function returns 'old' */ template __device__ T atomic_min(T* address, T value) { return atomicMin(address, value); } /** * The function returns the value at 'address' because the increment */ template __device__ T atomic_fetch_add(T* address, T increment) { return atomicAdd(address, increment); } template __device__ T atomic_load(T* address) { return *address; } /** * Reads the 16/32/64 bit word at the 'address' in global or shared memory, * computes(*address == expected ? new_value : *address), and stores the result * back to memory at the same address. * * These three operations are performed in one atomic transaction. * The function returns old (Compare And Swap). */ template __device__ T atomic_compare_exchange(T* address, T expected, T new_value) { return atomicCAS(address, expected, new_value); } template <> __device__ float atomic_compare_exchange(float* p, float cmp, float val) { return __int_as_float(atomicCAS((int*)p, __float_as_int(cmp), __float_as_int(val))); } /** * For t=0, returns a */ template __device__ T lerp(T a, T b, float t) { return (1.0f - t) * a + t * b; } /** * For a 'value' between 'a' and 'b', returns 't' such that * (1.0f - t) * a + t * b = value * * For 'value' == 'a', returns 0.0f * For 'value' == 'b', returns 1.0f */ template __device__ float inverse_lerp(T value, T a, T b) { // Clamping value = hippt::max(a, hippt::min(value, b)); return (value - a) / (b - a); } /** * Reference: https://registry.khronos.org/OpenGL-Refpages/gl4/html/smoothstep.xhtml * * For t == min, returns 0.0f * For t == max, returns 1.0f * Smoothstep interpolation in between */ template __device__ T smoothstep(T min, T max, float x) { float t = hippt::clamp(0.0f, 1.0f, (x - min) / (max - min)); return t * t * (3.0f - 2.0f * t); } __device__ float fract(float a) { return a - floorf(a); } __device__ float asfloat(unsigned int x) { return __uint_as_float(x); } __device__ unsigned int asuint(float x) { return __float_as_uint(x); } template __device__ int popc(T bitmask) { return 0; } template <> __device__ int popc(unsigned int bitmask) { return __popc(bitmask); } template <> __device__ int popc(unsigned long long int bitmask) { return __popcll(bitmask); } /** * Finds the position of least signigicant bit set to 1 in a 32 bit unsigned integer. * Returs a value between 0 and 32 inclusive. * * Returns 0 if all bits are zero */ __device__ unsigned int ffs(unsigned int bitmask) { return __ffs(bitmask); } // TODO these functions require __sync on modern NVIDIA GPUs. We should check that with __CUDACC__ __device__ bool warp_any(unsigned int thread_mask, bool predicate) { return __any(predicate); } /** * Returns a bit mask whose bits are set to 1 for threads that evaluated the predicate to true. */ __device__ unsigned long long int warp_ballot(unsigned int thread_mask, bool predicate) { return __ballot(predicate); } __device__ unsigned int warp_activemask() { return hippt::warp_ballot(0xFFFFFFFF, true); } /** * T can be a 32-bit integer type, 64-bit integer type or a single precision or double precision floating point type. * * The warp shuffle functions exchange values between threads within a warp. * * The optional width argument specifies subgroups, in which the warp can be * divided to share the variables. It has to be a power of two smaller than * or equal to warpSize. If it is smaller than warpSize, the warp is grouped * into separate groups, that are each indexed from 0 to width as if it was * its own entity, and only the lanes within that subgroup participate in the shuffle. * The lane indices in the subgroup are given by laneIdx % width. * * 'warp_shfl': The thread reads the value from the lane specified in srcLane */ template __device__ T warp_shfl(T var, int src_lane, int width = warpSize) { #ifdef __CUDACC__ return __shfl_sync(0xFFFFFFFF, var, src_lane, width); #else return __shfl(var, src_lane, width); #endif } /** * Returns the index within its warp (not group) of the calling thread */ __device__ unsigned int warp_2D_thread_index() { // warpSize assuming to be a power of 2 so the '&' operation // here is a modulo return (threadIdx.x + threadIdx.y * blockDim.x) & warpSize; } #else #undef M_PI #define M_PI 3.14159265358979323846f #define M_TWO_PI 6.28318530717958647693f // 2.0f * M_PI #define M_FOUR_PI 12.5663706143591729539f // 4.0f * M_PI #define M_INV_PI 0.31830988618379067154f // 1.0f / M_PI #define M_INV_2_PI 0.15915494309189533577f // 1.0f / (2.0f * M_PI) #define M_TWO_PI_SQUARED 19.73920880217871723767f // 2.0f * pi^2 #define NEAR_ZERO 1.0e-10f /** * Returns the 'warpSize' runtime constant of the GPU */ inline int warp_size() { return 1; } inline int thread_idx_x() { return 0; } inline int thread_idx_y() { return 0; } inline int thread_idx_global() { return 0; } inline bool is_pixel_index(int x, int y) { return false; } inline int current_warp_lane() { return 0; } template inline T ldg_load(T* address) { return *address; } inline float3 cross(float3 u, float3 v) { return hiprt::cross(u, v); } inline float dot(float3 u, float3 v) { return hiprt::dot(u, v); } inline float length(float3 u) { return sqrtf(dot(u, u)); } inline float length2(float3 u) { return dot(u, u); } inline float3 abs(float3 u) { return make_float3(std::abs(u.x), std::abs(u.y), std::abs(u.z)); } inline float abs(float a) { return std::abs(a); } template inline T max(T a, T b) { return a > b ? a : b; } /** * Component-wise max of float3 and int3 */ template <> inline float3 max(float3 a, float3 b) { return make_float3(hiprt::max(a.x, b.x), hiprt::max(a.y, b.y), hiprt::max(a.z, b.z)); } template <> inline int3 max(int3 a, int3 b) { return make_int3(hiprt::max(a.x, b.x), hiprt::max(a.y, b.y), hiprt::max(a.z, b.z)); } template inline T min(T a, T b) { return a < b ? a : b; } /** * Component-wise min of float3 and int3 */ template <> inline float3 min(float3 a, float3 b) { return make_float3(hiprt::min(a.x, b.x), hiprt::min(a.y, b.y), hiprt::min(a.z, b.z)); } template <> inline int3 min(int3 a, int3 b) { return make_int3(hiprt::min(a.x, b.x), hiprt::min(a.y, b.y), hiprt::min(a.z, b.z)); } /** * Minimum of each component of the float3 against x */ inline float3 min(float3 a, float x) { return make_float3(hiprt::min(a.x, x), hiprt::min(a.y, x), hiprt::min(a.z, x)); } inline float3 min(float x, float3 a) { return hippt::min(a, x); } template inline T clamp(T min_val, T max_val, T val) { return hiprt::min(max_val, hiprt::max(min_val, val)); } inline float2 cos(float2 x) { return make_float2(std::cos(x.x), std::cos(x.y)); } inline float3 cos(float3 x) { return make_float3(std::cos(x.x), std::cos(x.y), std::cos(x.z)); } inline float intrin_cosf(float x) { return std::cos(x); } inline float2 sin(float2 x) { return make_float2(std::sin(x.x), std::sin(x.y)); } inline float3 sin(float3 x) { return make_float3(std::sin(x.x), std::sin(x.y), std::sin(x.z)); } inline float intrin_sinf(float x) { return std::sin(x); } inline float3 atan2(float3 y, float3 x) { return make_float3(atan2f(y.x, x.x), atan2f(y.y, x.y), atan2f(y.z, x.z)); } inline float2 exp(float2 x) { return make_float2(expf(x.x), expf(x.y)); } inline float3 exp(float3 x) { return make_float3(expf(x.x), expf(x.y), expf(x.z)); } inline float3 ldexp(float3 x, int exp) { return make_float3(std::ldexp(x.x, exp), std::ldexp(x.y, exp), std::ldexp(x.z, exp)); } template inline T square(T x) { return x * x; } inline float2 sqrt(float2 uv) { return make_float2(sqrtf(uv.x), sqrtf(uv.y)); } inline float3 sqrt(float3 uvw) { return make_float3(sqrtf(uvw.x), sqrtf(uvw.y), sqrtf(uvw.z)); } inline float pow_1_4(float x) { return sqrtf(sqrtf(x)); } inline constexpr float pow_3(float x) { return x * x * x; } inline constexpr float pow_4(float x) { float x2 = x * x; return x2 * x2; } inline constexpr float pow_5(float x) { float x2 = x * x; float x4 = x2 * x2; return x4 * x; } inline constexpr float pow_6(float x) { float x2 = x * x; float x4 = x2 * x2; return x4 * x2; } inline float intrin_pow(float x, float y) { return powf(x, y); } inline float3 normalize(float3 u) { return hiprt::normalize(u); } template inline bool is_nan(const T& v) { return std::isnan(v); } template inline bool is_inf(const T& v) { return std::isinf(v); } inline bool is_zero(float x) { return x < NEAR_ZERO && x > -NEAR_ZERO; } inline unsigned int float_as_uint(float float_num) { return *reinterpret_cast(&float_num); } /** * Reads the 32-bit or 64-bit word old located at the address 'address' * in global or shared memory and stores 'value' to memory at the same address. * * These two operations are performed in one atomic transaction. * * The function returns old. */ template T atomic_exchange(std::atomic* address, T value) { return address->exchange(value); } /** * Reads the 32-bit or 64-bit word 'old' located at 'address' in global or shared memory, * computes the maximum of 'old' and 'value', and stores the result back to memory at the * same address. * * The function returns 'old' */ template T atomic_max(std::atomic* address, T value) { T prev_value = *address; while (prev_value < value && !address->compare_exchange_weak(prev_value, value)) {} return prev_value; } /** * Reads the 32-bit or 64-bit word 'old' located at 'address' in global or shared memory, * computes the minimum of 'old' and 'value', and stores the result back to memory at the * same address. * * The function returns 'old' */ template T atomic_min(std::atomic* address, T value) { T prev_value = *address; while (prev_value > value && !address->compare_exchange_weak(prev_value, value)) {} return prev_value; } /** * The function returns the value at 'address' because the increment */ template T atomic_fetch_add(std::atomic* atomic_address, T increment) { return atomic_address->fetch_add(increment); } template T atomic_load(std::atomic* atomic_address) { return atomic_address->load(); } /** * Reads the 16/32/64 bit word at the 'address' in global or shared memory, * computes(*address == expected ? new_value : *address), and stores the result * back to memory at the same address. * * These three operations are performed in one atomic transaction. * * The function returns old (Compare And Swap). */ template T atomic_compare_exchange(std::atomic* atomic_address, T expected, T new_value) { // Overriding the semantic here so that it behaves the same as in CUDA i.e. returns the old value // instead of returning true or false (stdlib default behavior) T old = *atomic_address; atomic_address->compare_exchange_strong(expected, new_value); return old; } /** * For t=0, returns a */ template inline T lerp(T a, T b, float t) { return (1.0f - t) * a + t * b; } /** * For a 'value' between 'a' and 'b', returns 't' such that * (1.0f - t) * a + t * b = value * * For 'value' == 'a', returns 0.0f * For 'value' == 'b', returns 1.0f */ template inline float inverse_lerp(T value, T a, T b) { // Clamping value = hippt::max(a, hippt::min(value, b)); return (value - a) / (b - a); } /** * Reference: https://registry.khronos.org/OpenGL-Refpages/gl4/html/smoothstep.xhtml * * For t == min, returns 0.0f * For t == max, returns 1.0f * Smoothstep interpolation in between */ template inline T smoothstep(T min, T max, float x) { float t = hippt::clamp(0.0f, 1.0f, (x - min) / (max - min)); return t * t * (3.0f - 2.0f * t); } inline float fract(float a) { return a - floorf(a); } inline float asfloat(unsigned int x) { return std::bit_cast(x); } inline unsigned int asuint(float x) { return std::bit_cast(x); } template inline int popc(T bitmask) { return std::popcount(bitmask); } /** * Finds the position of least signigicant bit set to 1 in a 32 bit unsigned integer. * Returs a value between 0 and 32 inclusive. * * Returns 0 if all bits are zero */ inline unsigned int ffs(unsigned int bitmask) { for (int i = 0; i < sizeof(unsigned int) * 8; i++) if (bitmask & (1 << i)) return i; return 0; } inline bool warp_any(unsigned int thread_mask, bool predicate) { return predicate; } /** * Returns a bit mask whose bits are set to 1 for threads that evaluated the predicate to true. */ inline unsigned long long int warp_ballot(unsigned int thread_mask, bool predicate) { return predicate ? 1 : 0; } inline unsigned int warp_activemask() { return 1; } /** * T can be a 32-bit integer type, 64-bit integer type or a single precision or double precision floating point type. * * The warp shuffle functions exchange values between threads within a warp. * * The optional width argument specifies subgroups, in which the warp can be * divided to share the variables. It has to be a power of two smaller than * or equal to warpSize. If it is smaller than warpSize, the warp is grouped * into separate groups, that are each indexed from 0 to width as if it was * its own entity, and only the lanes within that subgroup participate in the shuffle. * The lane indices in the subgroup are given by laneIdx % width. * * 'warp_shfl': The thread reads the value from the lane specified in srcLane */ template inline T warp_shfl(T var, int srcLane, int width = 1) { return var; } /** * Returns the index within its warp (not group) of the calling thread * * Warp sizes of 1 on the CPU */ HIPRT_HOST_DEVICE HIPRT_INLINE unsigned int warp_2D_thread_index() { return 1; } #endif } HIPRT_HOST_DEVICE HIPRT_INLINE float3 matrix_X_point(const float4x4& m, const float3& p) { float x = p.x; float y = p.y; float z = p.z; // Assuming w = 1.0f for the point p float xt = m.m[0][0] * x + m.m[0][1] * y + m.m[0][2] * z + m.m[0][3]; float yt = m.m[1][0] * x + m.m[1][1] * y + m.m[1][2] * z + m.m[1][3]; float zt = m.m[2][0] * x + m.m[2][1] * y + m.m[2][2] * z + m.m[2][3]; float wt = m.m[3][0] * x + m.m[3][1] * y + m.m[3][2] * z + m.m[3][3]; float inv_w = 1.0f; if (!hippt::is_zero(wt)) inv_w = 1.0f / wt; return make_float3(xt * inv_w, yt * inv_w, zt * inv_w); } HIPRT_HOST_DEVICE HIPRT_INLINE float3 matrix_X_vec(const float3x3& m, const float3& u) { float x = u.x; float y = u.y; float z = u.z; // Assuming w = 0.0f for the vector u float xt = m.m[0][0] * x + m.m[1][0] * y + m.m[2][0] * z; float yt = m.m[0][1] * x + m.m[1][1] * y + m.m[2][1] * z; float zt = m.m[0][2] * x + m.m[1][2] * y + m.m[2][2] * z; return make_float3(xt, yt, zt); } HIPRT_HOST_DEVICE HIPRT_INLINE float3 matrix_X_vec(const float4x4& m, const float3& u) { float x = u.x; float y = u.y; float z = u.z; // Assuming w = 0.0f for the vector u float xt = m.m[0][0] * x + m.m[1][0] * y + m.m[2][0] * z; float yt = m.m[0][1] * x + m.m[1][1] * y + m.m[2][1] * z; float zt = m.m[0][2] * x + m.m[1][2] * y + m.m[2][2] * z; float wt = m.m[0][3] * x + m.m[1][3] * y + m.m[2][3] * z; float inv_w = 1.0f; if (!hippt::is_zero(wt)) inv_w = 1.0f / wt; return make_float3(xt * inv_w, yt * inv_w, zt * inv_w); } #ifndef __KERNELCC__ #include static std::ostream& operator<<(std::ostream& os, float3 uvw) { os << uvw.x << ", " << uvw.y << ", " << uvw.z; return os; } #endif #endif ================================================ FILE: src/HostDeviceCommon/MicrofacetRegularizationSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_MICROFACET_REGULARIZATION_SETTINGS_H #define HOST_DEVICE_COMMON_MICROFACET_REGULARIZATION_SETTINGS_H struct MicrofacetRegularizationSettings { // Maximum value that the microfacet distribution is allowed to take // The regularized roughness will be derived from this value float tau_0 = 30.0f; // Minimum roughness. Useful when lights are so small that even camera ray jittering // causes variance float min_roughness = 0.0f; }; #endif ================================================ FILE: src/HostDeviceCommon/Packing.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_PACKING_H #define HOST_DEVICE_COMMON_PACKING_H #include "Device/includes/FixIntellisense.h" #include "HostDeviceCommon/Color.h" /** * Packs 88 bools into a uchar */ struct UChar8BoolsPacked { /** * Returns the bool packed at bit 'index'. 0 is LSB. * * 'index' is in [0, 7] */ template HIPRT_DEVICE bool get_bool() const { return m_packed & (1 << index); } /** * Sets the bool at bit 'index' in the packed data. 0 is LSB. * * 'index' is in [0, 7] */ template HIPRT_DEVICE void set_bool(bool value) { // Clear the bit m_packed &= ~(1 << index); // Sets m_packed |= (value ? 1 : 0) << index; } private: unsigned char m_packed = 0; }; /** * Packs a ColorRGB32F into 3x8 bit = 24 bits (this isn't a loss of precision * for colors that already were in SDR [0, 255]). * * A float in range [0, 1] can be packed in the remaining 8 bits. * This leaves us with a precision of 0.004 between values in [0, 1]. Which is probably * more than enough. Who picks up the difference between a roughness of 0.5 and 0.504 anyways? */ struct ColorRGB24bFloat0_1Packed { static constexpr float inv_255 = (1.0f / (255 << 0)); static constexpr float inv_255_shl_8 = (1.0f / (255 << 8)); HIPRT_DEVICE ColorRGB32F get_color() const { float r = static_cast(m_packed & 0x000000FF) * inv_255; float g = static_cast(m_packed & 0x0000FF00) * inv_255_shl_8; float b = static_cast((m_packed & 0x00FF0000) >> 16) * inv_255; return ColorRGB32F(r, g, b); } HIPRT_DEVICE float get_float() const { return static_cast((m_packed & 0xFF000000) >> 24) * inv_255; } HIPRT_DEVICE void set_color(const ColorRGB32F& color) { // Clear 24 lower bits m_packed &= 0xFF000000; // Set m_packed |= static_cast(color.r * 255.0f); m_packed |= static_cast(color.g * 255.0f) << 8; m_packed |= static_cast(color.b * 255.0f) << 16; } HIPRT_DEVICE void set_float(float float_in_0_1) { // Clear m_packed &= 0x00FFFFFF; // Set m_packed |= static_cast(float_in_0_1 * 255.0f) << 24; } private: unsigned int m_packed = 0; }; /** * 4 floats in [0, 1] all packed into a 32 bit unsigned int. * * This gives 8 bits for each float in [0, 1] --> precision of 0.004 */ struct Float4xPacked { static constexpr float inv_255 = 0.00392156862745098039f; /** * Returns the float at index 'index' * * 'index' must be in [0, 3] */ template HIPRT_DEVICE float get_float() const { return static_cast((m_packed & (0xFFu << (index * 8))) >> (index * 8)) * inv_255; } /** * Sets the float number 'index' of this 4x packed float * * 'index' must be in [0, 3] */ template HIPRT_DEVICE void set_float(float value) { // Clear m_packed &= ~(0xFFu << (index * 8)); // Set m_packed |= static_cast(value * 255.0f) << (index * 8); } private: unsigned int m_packed = 0; }; /** * 2 floats in [0, 1] and 2 unsigned chars all packed into a 32 bit unsigned int. * * This gives 8 bits for each float in [0, 1] --> precision of 0.004 */ struct Float2xUChar2xPacked { static constexpr float inv_255 = 0.00392156862745098039f; /** * Returns one of the float packed in this structure * * 'index' must be in [0, 1] */ template HIPRT_DEVICE float get_float() const { return ((m_packed & (0xFF << (index * 8))) >> (index * 8)) * inv_255; } /** * Returns one of the unsigned char packed in this structure * * 'index' must be in [0, 1] */ template HIPRT_DEVICE unsigned char get_uchar() const { return (m_packed & (0x00FF0000u << (index * 8))) >> (index * 8 + 16); } /** * 'index' must be in [0, 1] */ template HIPRT_DEVICE void set_float(float value) { // Clear m_packed &= ~(0xFFu << (index * 8)); // Set m_packed |= static_cast(value * 255.0f) << (index * 8); } /** * 'index' must be in [0, 1] */ template HIPRT_DEVICE void set_uchar(unsigned char value) { // Clear m_packed &= ~(0x00FF0000u << (index * 8)); // Set m_packed |= value << (index * 8 + 16); } private: // Floats are in the 16 LSB // Uchars are in the 16 MSB unsigned int m_packed = 0; }; /** * Packs two uints 16bits into one 32bit */ struct Uint2xPacked { /** * Index must be 0 or 1 */ template HIPRT_DEVICE unsigned short get_value() const { return (m_packed & (0xFFFFu << (index * 16))) >> (index * 16); } /** * Index must be 0 or 1 */ template HIPRT_DEVICE void set_value(unsigned short value) { // Clear m_packed &= ~(0xFFFFu << (index * 16)); // Set m_packed |= value << (index * 16); } private: unsigned int m_packed = 0; }; /** * Reference: * * [1] [Survey of Efficient Representations for Independent Unit Vectors, Cigolle et al., 2014] */ struct GPU_CPU_ALIGN(4) Octahedral24BitNormalPadded32b { public: HIPRT_DEVICE Octahedral24BitNormalPadded32b() {} HIPRT_DEVICE Octahedral24BitNormalPadded32b(float3 normal) { pack(normal); } HIPRT_DEVICE static Octahedral24BitNormalPadded32b pack_static(float3 normal) { Octahedral24BitNormalPadded32b packed; packed.pack(normal); return packed; } HIPRT_DEVICE void pack(float3 normal) { float2_to_Snorm12_2x_as_3UChar(octahedral_encode(normal), m_packed_x, m_packed_y, m_packed_z); } /** * Returns the normal that was packed in there * * The returned normal is normalized */ HIPRT_DEVICE float3 unpack() const { float2 v = Snorm12_2x_as_UChar_to_float2(m_packed_x, m_packed_y, m_packed_z); return final_decode(v.x, v.y); } private: HIPRT_DEVICE float pack_Snorm12_float(float f) { return roundf(hippt::clamp(0.0f, 2.0f, f + 1.0f) * 2047.0f); } HIPRT_DEVICE void Snorm12_2x_as_3Uchar(float2 s, unsigned char& out_x, unsigned char& out_y, unsigned char& out_z) { float3 u; u.x = s.x / 16.0f; float t = floorf(s.y / 256.0f); u.y = ((u.x - floorf(u.x)) * 256.0f) + t; u.z = s.y - (t * 256.0f); out_x = u.x; out_y = u.y; out_z = u.z; } HIPRT_DEVICE void float2_to_Snorm12_2x_as_3UChar(float2 v, unsigned char& out_x, unsigned char& out_y, unsigned char& out_z) { float2 s = make_float2(pack_Snorm12_float(v.x), pack_Snorm12_float(v.y)); Snorm12_2x_as_3Uchar(s, out_x, out_y, out_z); } HIPRT_DEVICE float2 octahedral_encode(float3 v) { float l1norm_inv = 1.0f / (abs(v.x) + abs(v.y) + abs(v.z)); float2 result = make_float2(v.x * l1norm_inv, v.y * l1norm_inv); if (v.z < 0.0f) result = (make_float2(1.0f) - make_float2(hippt::abs(result.y), hippt::abs(result.x))) * sign_not_zero(make_float2(result.x, result.y)); return result; } HIPRT_DEVICE float sign_not_zero(float k) const { return k >= 0.0f ? 1.0f : -1.0f; } HIPRT_DEVICE float2 sign_not_zero(float2 v) const { return make_float2(sign_not_zero(v.x), sign_not_zero(v.y)); } HIPRT_DEVICE float3 final_decode(float x, float y) const { float3 v = make_float3(x, y, 1.0f - abs(x) - abs(y)); if (v.z < 0.0f) { float2 temp = make_float2(v.x, v.y); v.x = (1.0f - hippt::abs(temp.y)) * sign_not_zero(temp.x); v.y = (1.0f - hippt::abs(temp.x)) * sign_not_zero(temp.y); } return hippt::normalize(v); } HIPRT_DEVICE float2 Snorm12_2x_as_Uchar_to_packed_float2(unsigned char x, unsigned char y, unsigned char z) const { float2 s; float temp = y / 16.0f; s.x = x * 16.0f + floorf(temp); s.y = (temp - floorf(temp)) * 256.0f * 16.0f + z; return s; } HIPRT_DEVICE float unpack_Snorm12(float f) const { return hippt::clamp(-1.0f, 1.0f, (f / 2047.0f) - 1.0f); } HIPRT_DEVICE float2 Snorm12_2x_as_UChar_to_float2(unsigned char x, unsigned char y, unsigned char z) const { float2 s = Snorm12_2x_as_Uchar_to_packed_float2(x, y, z); return make_float2(unpack_Snorm12(s.x), unpack_Snorm12(s.y)); } unsigned char m_packed_x = 0; unsigned char m_packed_y = 0; unsigned char m_packed_z = 0; // This padding here improves performance significantly on my machine for the megakernel. // Order of 60% faster, mainly due to a massive reduction in register pressure and we got 2 more wavefronts running // out of that. Tested with a lambertian BRDF // // Doesn't really make sense that we would get any register out of that but :shrug:. // This padding here is theoretically better anyways thanks to the 4 bytes alignment that it // provides instead of the 3-bytes alignement of the default packed struct (which is poor access pattern on the GPU) unsigned char padding = 0; }; /** * Packs a float3 into 8 bytes (saves 4 bytes) with very good precision * * This stores the length of the float3 and then normalizes it and then stores * a 10 bit quantized version of each normalized component of the float3 */ struct Float3xLengthUint10bPacked { HIPRT_DEVICE void pack(float3 data) { length = hippt::length(data); float3 normalized = data / length; // Bringing in [0, 1] from [-1, 1] normalized += make_float3(1.0f, 1.0f, 1.0f); normalized *= 0.5f; unsigned int quantized_x = roundf(normalized.x * 1023); unsigned int quantized_y = roundf(normalized.y * 1023); unsigned int quantized_z = roundf(normalized.z * 1023); quantized = 0; quantized |= quantized_x; quantized |= quantized_y << 10; quantized |= quantized_z << 20; } HIPRT_DEVICE void pack(ColorRGB32F data) { pack(make_float3(data.r, data.g, data.b)); } HIPRT_DEVICE static Float3xLengthUint10bPacked pack_static(float3 data) { Float3xLengthUint10bPacked packed; packed.pack(data); return packed; } HIPRT_DEVICE static Float3xLengthUint10bPacked pack_static(ColorRGB32F data) { Float3xLengthUint10bPacked packed; packed.pack(data); return packed; } HIPRT_DEVICE ColorRGB32F unpack_color3x32f() const { float3 unpacked = unpack_float3(); return ColorRGB32F(unpacked.x, unpacked.y, unpacked.z); } HIPRT_DEVICE float3 unpack_float3() const { unsigned int quantized_x = (quantized >> 00) & 0b1111111111; unsigned int quantized_y = (quantized >> 10) & 0b1111111111; unsigned int quantized_z = (quantized >> 20) & 0b1111111111; float3 normalized = make_float3(quantized_x / 1023.0f, quantized_y / 1023.0f, quantized_z / 1023.0f); // Back in [-1, 1] from [0, 1] float3 rescaled = normalized * 2.0f - 1.0f; float3 with_length = rescaled * length; return with_length; } private: float length = 0.0f; unsigned quantized = 0; }; /** * Reference: https://github.com/microsoft/DirectX-Graphics-Samples/blob/master/MiniEngine/Core/Shaders/PixelPacking_RGBE.hlsli */ struct RGBE9995Packed { // RGBE, aka R9G9B9E5_SHAREDEXP, is an unsigned float HDR pixel format where red, green, // and blue all share the same exponent. The color channels store a 9-bit value ranging // from [0/512, 511/512] which multiplies by 2^Exp and Exp ranges from [-15, 16]. // Floating point specials are not encoded. HIPRT_DEVICE void pack(ColorRGB32F rgb) { // To determine the shared exponent, we must clamp the channels to an expressible range const float kMaxVal = hippt::asfloat(0x477F8000); // 1.FF x 2^+15 const float kMinVal = hippt::asfloat(0x37800000); // 1.00 x 2^-16 // Non-negative and <= kMaxVal rgb.clamp(0.0f, kMaxVal); // From the maximum channel we will determine the exponent. We clamp to a min value // so that the exponent is within the valid 5-bit range. float MaxChannel = hippt::max(hippt::max(kMinVal, rgb.r), hippt::max(rgb.g, rgb.b)); // 'Bias' has to have the biggest exponent plus 15 (and nothing in the mantissa). When // added to the three channels, it shifts the explicit '1' and the 8 most significant // mantissa bits into the low 9 bits. IEEE rules of float addition will round rather // than truncate the discarded bits. Channels with smaller natural exponents will be // shifted further to the right (discarding more bits). float Bias = hippt::asfloat((hippt::asuint(MaxChannel) + 0x07804000) & 0x7F800000); // Shift bits into the right places unsigned int R, G, B; R = hippt::asuint(rgb.r + Bias); G = hippt::asuint(rgb.g + Bias); B = hippt::asuint(rgb.b + Bias); unsigned int E = (hippt::asuint(Bias) << 4) + 0x10000000; m_packed = E | B << 18 | G << 9 | (R & 0x1FF); } HIPRT_DEVICE ColorRGB32F unpack() const { float3 rgb = make_float3(m_packed & 0x1FF, (m_packed >> 9) & 0x1FF, (m_packed >> 18) & 0x1FF); return ColorRGB32F(hippt::ldexp(rgb, static_cast(m_packed >> 27) - 24)); } private: unsigned int m_packed; }; #endif ================================================ FILE: src/HostDeviceCommon/PathRussianRoulette.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_PATH_RUSSIAN_ROULETTE_H #define HOST_DEVICE_COMMON_PATH_RUSSIAN_ROULETTE_H enum PathRussianRoulette { MAX_THROUGHPUT, ARNOLD_2014 }; #endif ================================================ FILE: src/HostDeviceCommon/PrecomputedEmissiveTrianglesDataSoADevice.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_PRECOMPUTED_EMISSIVE_TRIANGLE_DATA_SOA_DEVICE_H #define HOST_DEVICE_COMMON_PRECOMPUTED_EMISSIVE_TRIANGLE_DATA_SOA_DEVICE_H struct PrecomputedEmissiveTrianglesDataSoADevice { float3* triangles_A = nullptr; float3* triangles_AB = nullptr; float3* triangles_AC = nullptr; }; #endif ================================================ FILE: src/HostDeviceCommon/RIS/RISSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_RIS_SETTINGS_H #define HOST_DEVICE_COMMON_RIS_SETTINGS_H struct RISSettings { // How many candidate lights to sample for RIS (Resampled Importance Sampling) int number_of_light_candidates = 4; // How many candidates samples from the BSDF to use in combination // with the light candidates for RIS int number_of_bsdf_candidates = 1; }; #endif ================================================ FILE: src/HostDeviceCommon/ReSTIR/ReSTIRCommonSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_RESTIR_COMMON_SETTINGS_H #define HOST_DEVICE_COMMON_RESTIR_COMMON_SETTINGS_H /** * Note that no default values are set here because they are all set in * the ReSTIR_XXX_DefaultSettings structure/header files */ struct ReSTIRCommonTemporalPassSettings { bool do_temporal_reuse_pass; // If true, the position of the canonical temporal neighbor will be shuffled to increase // variation between frames and make the render more denoiser friendly bool use_permutation_sampling; // Random bits used for all the pixels in the image for the permutation sampling int permutation_sampling_random_bits; // How many neighbors at most to check around the temporal back-projected pixel location // to find a valid neighbor int max_neighbor_search_count; // Radius around the temporal reprojected location of a pixel in which to look for an // acceptable temporal neighbor int neighbor_search_radius; // If set to true, the temporal buffers will be cleared by the camera // rays kernel bool temporal_buffer_clear_requested; }; struct ReSTIRCommonSpatialPassSettings { bool do_spatial_reuse_pass; // What spatial pass are we currently performing? // Takes values in [0, number_of_passes - 1] int spatial_pass_index; // How many spatial reuse pass to perform int number_of_passes; // The radius within which neighbor are going to be reused spatially int reuse_radius; // if true, the reuse radius will automatically be adjusted based on the render resolution bool auto_reuse_radius = true; // How many neighbors to reuse during the spatial pass int reuse_neighbor_count; // Whether or not to increase the number of spatially resampled neighbor // for disoccluded pixels (that have no temporal history) bool do_disocclusion_reuse_boost; // How many neighbors to spatially reuse when a disocclusion is detected. // This reduces the increased variance of disoccluded regions int disocclusion_reuse_count; // If true, reused neighbors will be hardcoded to always be 'reuse_radius' pixels to the right, // not in a circle around the center pixel. bool debug_neighbor_location; // If this is 0, the debug location will be horizontal // If this is 1, the debug location will be vertical // If this is 2, the debug location will be in diagonal int debug_neighbor_location_direction; // Whether or not to rotate the spatial neighbor locations generated. // Pretty much mandatory when using Hammersley points otherwise the neighbors // will always be the exact same bool do_neighbor_rotation; // This seed is used to generate the spatial neighbors positions if not using Hammersley unsigned int spatial_neighbors_rng_seed; // Reuses the same random numbers for all the pixels in the image for picking the spatial neighbors // such that memory accesses to surface data / reservoirs are coalesced bool coalesced_spatial_reuse; // If true, the best per-pixel spatial reuse radius to use as // well as the sectors in the spatial reuse disk (split in 32 sectors) that should be used for reuse // will be precomputed in a prepass // // This increases the spatial reuse "hit rate" (i.e. the number of neighbors that are not rejected by G-Buffer heuristics) // and thus increases convergence speed. bool use_adaptive_directional_spatial_reuse; /** * If you want to check whether you should use the features of the adaptive directional spatial * reuse, prefer using this function rather than directly checking the 'use_adaptive_directional_spatial_reuse' * member * * This is because the directional spatial reuse feature cannot be used in realtime mode so if you use the * 'use_adaptive_directional_spatial_reuse' member directly, you would also have to check for 'render_data.render_settings.accumulate' * everytime. * * This function does it all */ HIPRT_HOST_DEVICE bool do_adaptive_directional_spatial_reuse(bool render_data_render_settings_accumulate) const { return use_adaptive_directional_spatial_reuse && render_data_render_settings_accumulate; } // If true, neighboring pixels that have converged (if adaptive sampling is enabled) // won't be reused to reduce bias. // If false, even neighboring pixels that have converged can be reused by the spatial pass bool allow_converged_neighbors_reuse; // If we're allowing the spatial reuse of converged neighbors, we're doing so we're a given // probability instead of always/never. This helps trade performance for bias. float converged_neighbor_reuse_probability; // If true, the visibility in the target function will only be used on the last spatial reuse // pass (and also if visibility is wanted) bool do_visibility_only_last_pass; // Visibility term in the target function will only be used for the first // 'neighbor_visibility_count' neighbors, not all. int neighbor_visibility_count; unsigned int* per_pixel_spatial_reuse_directions_mask_u = nullptr; unsigned long long int* per_pixel_spatial_reuse_directions_mask_ull = nullptr; // Framebuffer that contains per-pixel spatial radius for use in the spatial reuse passes of ReSTIR. // This framebuffer is filled by the unsigned char* per_pixel_spatial_reuse_radius = nullptr; // The minimum radius that will be used per pixel when the optimal per - pixel spatial reuse // radius is computed by adaptive-directional spatial reuse int minimum_per_pixel_reuse_radius = 3; // This variable here is spatial because it is written to at the beginning of the spatial reuse pass. // The only goal of this variable is to be able to carry around the function the direction reuse mask // (i.e. which directions are allowed for reuse)of the pixel. // // This is purely to avoid passing yet another arguments to every function in the code... unsigned long long int current_pixel_directions_reuse_mask = 0; // Whether or not to gather statistics on the hit rate of the spatial reuse pass (i.e. how many // neighbors are rejected because of the G-Buffer heuristics vs. the maximum number of neighbors that can be reused) bool compute_spatial_reuse_hit_rate; // Counters for gathering the statistics on the spatial reuse hit rate AtomicType* spatial_reuse_hit_rate_hits = nullptr; AtomicType* spatial_reuse_hit_rate_total = nullptr; }; struct ReSTIRCommonNeighborSimiliaritySettings { bool use_normal_similarity_heuristic; // User-friendly (for ImGui) normal angle. When resampling a neighbor (temporal or spatial), // the normal of the neighbor being re-sampled must be similar to our normal. This angle gives the // "similarity threshold". Normals must be within 25 degrees of each other by default float normal_similarity_angle_degrees; // Precomputed cosine of the angle for use in the shader float normal_similarity_angle_precomp; // Normals must be within 25 degrees by default // If true, the geometric normals will be compared for the normal rejection heuristic. // If false, smooth vertex normals (or normal map normals) will be compared // // Geometric normals are prefered as they are not disturbed by high details normal maps bool reject_using_geometric_normals; bool use_plane_distance_heuristic; // Threshold used when determining whether a temporal neighbor is acceptable // for temporal reuse regarding the spatial proximity of the neighbor and the current // point. // This is a world space distance. float plane_distance_threshold; bool use_roughness_similarity_heuristic; // How close the roughness of the neighbor's surface must be to ours to resample that neighbor // If this value is 0.25f for example, then the roughnesses must be within 0.25f of each other. Simple. float roughness_similarity_threshold; }; struct ReSTIRCommonSettings { // Settings for the initial candidates generation pass ReSTIRCommonTemporalPassSettings common_temporal_pass; // Settings for the spatial reuse pass ReSTIRCommonSpatialPassSettings common_spatial_pass; ReSTIRCommonNeighborSimiliaritySettings neighbor_similarity_settings; // When finalizing the reservoir in the spatial reuse pass, what value // to cap the reservoirs's M value to. // // The point of this parameter is to avoid too much correlation between frames if using // a bias correction that uses confidence weights. Without M-capping, the M value of a reservoir // will keep growing exponentially through temporal and spatial reuse and when that exponentially // grown M value is used in confidence weights, it results in new samples being very unlikely // to be chosen which in turn results in non-convergence since always the same sample is evaluated // for a given pixel. // // A M-cap value between 5 - 30 is usually good // // 0 for infinite M-cap (don't...) int m_cap; // Whether or not to use confidence weights when resampling neighbors. bool use_confidence_weights; // Beta exponent to the difference function for symmetric and asymmetric ratio MIS weights float symmetric_ratio_mis_weights_beta_exponent = 2.0f; }; #endif ================================================ FILE: src/HostDeviceCommon/ReSTIR/ReSTIRDIDefaultSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_RESTIR_DI_DEFAULT_SETTINGS_H #define HOST_DEVICE_RESTIR_DI_DEFAULT_SETTINGS_H #include "HostDeviceCommon/ReSTIR/ReSTIRCommonSettings.h" #endif ================================================ FILE: src/HostDeviceCommon/ReSTIR/ReSTIRDISettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_RESTIR_DI_SETTINGS_H #define HOST_DEVICE_RESTIR_DI_SETTINGS_H #include "HostDeviceCommon/ReSTIR/ReSTIRDIDefaultSettings.h" struct ReSTIRDIReservoir; struct ReSTIRDIPresampledLight; struct ReSTIRDIInitialCandidatesSettings { // How many light candidates to resamples during the initial candidates sampling pass int number_of_initial_light_candidates = 4; // How many BSDF candidates to resamples during the initial candidates sampling pass int number_of_initial_bsdf_candidates = 1; // For each 'number_of_initial_light_candidates', the probability that this light sample // will sample the envmap instead of a light in the scene float envmap_candidate_probability = 0.5f; // Buffer that contains the reservoirs that will hold the reservoir // for the initial candidates generated ReSTIRDIReservoir* output_reservoirs = nullptr; }; struct ReSTIRDITemporalPassSettings { // The temporal reuse pass resamples the initial candidates as well as the last frame reservoirs which // are accessed through this pointer ReSTIRDIReservoir* input_reservoirs = nullptr; // Buffer that holds the output of the temporal reuse pass ReSTIRDIReservoir* output_reservoirs = nullptr; }; struct ReSTIRDISpatialPassSettings { // Buffer that contains the input reservoirs for the spatial reuse pass ReSTIRDIReservoir* input_reservoirs = nullptr; // Buffer that contains the output reservoir of the spatial reuse pass ReSTIRDIReservoir* output_reservoirs = nullptr; }; struct ReSTIRDILightPresamplingSettings { // From all the lights of the scene, how many subsets to presample int number_of_subsets = 128; // How many lights to presample in each subset int subset_size = 1024; // All threads in a tile_size * tile_size block of pixels will sample from the same subset of light samples int tile_size = 8; // Buffer for the presampled light samples ReSTIRDIPresampledLight* light_samples; }; struct ReSTIRDISettings : public ReSTIRCommonSettings { HIPRT_HOST_DEVICE ReSTIRDISettings() { common_temporal_pass.do_temporal_reuse_pass = true; common_temporal_pass.use_permutation_sampling = false; common_temporal_pass.permutation_sampling_random_bits = 42; common_temporal_pass.max_neighbor_search_count = 8; common_temporal_pass.neighbor_search_radius = 4; common_temporal_pass.temporal_buffer_clear_requested = false; common_spatial_pass.do_spatial_reuse_pass = true; common_spatial_pass.spatial_pass_index = 0; common_spatial_pass.number_of_passes = 1; common_spatial_pass.reuse_radius = 16; common_spatial_pass.reuse_neighbor_count = 5; common_spatial_pass.do_disocclusion_reuse_boost = false; common_spatial_pass.disocclusion_reuse_count = 5; common_spatial_pass.debug_neighbor_location = false; common_spatial_pass.debug_neighbor_location_direction = 0; common_spatial_pass.do_neighbor_rotation = false; common_spatial_pass.spatial_neighbors_rng_seed = 42; common_spatial_pass.coalesced_spatial_reuse = true; common_spatial_pass.use_adaptive_directional_spatial_reuse = false; common_spatial_pass.allow_converged_neighbors_reuse = false; common_spatial_pass.converged_neighbor_reuse_probability = 0.5f; common_spatial_pass.do_visibility_only_last_pass = true; common_spatial_pass.neighbor_visibility_count = common_spatial_pass.do_disocclusion_reuse_boost ? common_spatial_pass.disocclusion_reuse_count : common_spatial_pass.reuse_neighbor_count; common_spatial_pass.compute_spatial_reuse_hit_rate = false; neighbor_similarity_settings.use_normal_similarity_heuristic = true; neighbor_similarity_settings.normal_similarity_angle_degrees = 37.5f; neighbor_similarity_settings.normal_similarity_angle_precomp = 0.906307787f; neighbor_similarity_settings.reject_using_geometric_normals = true; neighbor_similarity_settings.use_plane_distance_heuristic = true; neighbor_similarity_settings.plane_distance_threshold = 0.1f; neighbor_similarity_settings.use_roughness_similarity_heuristic = false; neighbor_similarity_settings.roughness_similarity_threshold = 0.25f; m_cap = 3; use_confidence_weights = true; } // Settings for the initial candidates generation pass ReSTIRDIInitialCandidatesSettings initial_candidates; // Settings for the temporal reuse pass ReSTIRDITemporalPassSettings temporal_pass; // Settings for the spatial reuse pass ReSTIRDISpatialPassSettings spatial_pass; // Settings for the light presampling pass ReSTIRDILightPresamplingSettings light_presampling; // If true, the spatial and temporal pass will be fused into a single kernel call. // This avois a synchronization barrier between the temporal pass and the spatial pass // and increases performance. // Because the spatial must then resample without the output of the temporal pass, the spatial // pass only resamples on the temporal reservoir buffer, not the temporal + initial candidates reservoir // (which is the output of the temporal pass). This is usually imperceptible. bool do_fused_spatiotemporal = false; // Whether or not to trace a visibility ray when evaluating the final light sample produced by ReSTIR. // This is strongly biased but allows good performance. bool do_final_shading_visibility = true; // Pointer to the buffer that contains the output of all the passes of ReSTIR DI // This the buffer that should be used when evaluating direct lighting in the path tracer // // This buffer isn't allocated but is actually just a pointer // to the buffer that was last used as the output of the resampling // passes last frame. // For example if there was spatial reuse in last frame, this buffer // is going to be a pointer to the output of the spatial reuse pass // If there was only temporal reuse pass last frame, this buffer is going // to be a pointer to the output of the temporal reuse pass // // This is handy to know which buffer the temporal reuse pass is going to use // as input on the next frame ReSTIRDIReservoir* restir_output_reservoirs = nullptr; }; #endif ================================================ FILE: src/HostDeviceCommon/ReSTIR/ReSTIRGIDefaultSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_RESTIR_GI_DEFAULT_SETTINGS_H #define HOST_DEVICE_RESTIR_GI_DEFAULT_SETTINGS_H #include "HostDeviceCommon/ReSTIR/ReSTIRCommonSettings.h" #endif ================================================ FILE: src/HostDeviceCommon/ReSTIR/ReSTIRGISettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_RESTIR_GI_SETTINGS_H #define HOST_DEVICE_RESTIR_GI_SETTINGS_H #include "HostDeviceCommon/ReSTIR/ReSTIRGIDefaultSettings.h" struct ReSTIRGIReservoir; struct ReSTIRGIInitialCandidatesPassSettings { // Buffer that contains the reservoirs that will hold the reservoir // for the initial candidates generated ReSTIRGIReservoir* initial_candidates_buffer = nullptr; }; struct ReSTIRGITemporalPassSettings { // Buffer that contains the input reservoirs for the temporal reuse pass ReSTIRGIReservoir* input_reservoirs = nullptr; // Buffer that contains the output reservoir of the temporal reuse pass ReSTIRGIReservoir* output_reservoirs = nullptr; }; struct ReSTIRGISpatialPassSettings { // Buffer that contains the input reservoirs for the spatial reuse pass ReSTIRGIReservoir* input_reservoirs = nullptr; // Buffer that contains the output reservoir of the spatial reuse pass ReSTIRGIReservoir* output_reservoirs = nullptr; }; enum ReSTIRGIDebugView { NO_DEBUG = 0, FINAL_RESERVOIR_UCW = 1, TARGET_FUNCTION = 2, WEIGHT_SUM = 3, M_COUNT = 4, PER_PIXEL_REUSE_RADIUS = 5, PER_PIXEL_VALID_DIRECTIONS_PERCENTAGE = 6, }; struct ReSTIRGISettings : public ReSTIRCommonSettings { HIPRT_HOST_DEVICE ReSTIRGISettings() { common_temporal_pass.do_temporal_reuse_pass = true; common_temporal_pass.use_permutation_sampling = false; common_temporal_pass.permutation_sampling_random_bits = 42; common_temporal_pass.max_neighbor_search_count = 8; common_temporal_pass.neighbor_search_radius = 4; common_temporal_pass.temporal_buffer_clear_requested = false; common_spatial_pass.do_spatial_reuse_pass = true; common_spatial_pass.spatial_pass_index = 0; common_spatial_pass.number_of_passes = 2; common_spatial_pass.reuse_radius = 20; common_spatial_pass.reuse_neighbor_count = 5; common_spatial_pass.do_disocclusion_reuse_boost = false; common_spatial_pass.disocclusion_reuse_count = 5; common_spatial_pass.debug_neighbor_location = false; common_spatial_pass.debug_neighbor_location_direction = 0; common_spatial_pass.do_neighbor_rotation = false; common_spatial_pass.spatial_neighbors_rng_seed = 42; common_spatial_pass.coalesced_spatial_reuse = false; common_spatial_pass.use_adaptive_directional_spatial_reuse = true; common_spatial_pass.allow_converged_neighbors_reuse = false; common_spatial_pass.converged_neighbor_reuse_probability = 0.5f; common_spatial_pass.do_visibility_only_last_pass = true; common_spatial_pass.neighbor_visibility_count = common_spatial_pass.do_disocclusion_reuse_boost ? common_spatial_pass.disocclusion_reuse_count : common_spatial_pass.reuse_neighbor_count; common_spatial_pass.compute_spatial_reuse_hit_rate = false; neighbor_similarity_settings.use_normal_similarity_heuristic = true; neighbor_similarity_settings.normal_similarity_angle_degrees = 37.5f; neighbor_similarity_settings.normal_similarity_angle_precomp = 0.906307787f; neighbor_similarity_settings.reject_using_geometric_normals = true; neighbor_similarity_settings.use_plane_distance_heuristic = true; neighbor_similarity_settings.plane_distance_threshold = 0.1f; neighbor_similarity_settings.use_roughness_similarity_heuristic = false; neighbor_similarity_settings.roughness_similarity_threshold = 0.25f; use_jacobian_rejection_heuristic = true; jacobian_rejection_threshold = 15.0f; use_neighbor_sample_point_roughness_heuristic = true; neighbor_sample_point_roughness_threshold = 0.1f; // Very very small m-cap to avoid correlations m_cap = 1; use_confidence_weights = true; debug_view = ReSTIRGIDebugView::NO_DEBUG; debug_view_scale_factor = 1.0f; } ReSTIRGIInitialCandidatesPassSettings initial_candidates; ReSTIRGITemporalPassSettings temporal_pass; ReSTIRGISpatialPassSettings spatial_pass; ReSTIRGIReservoir* restir_output_reservoirs = nullptr; ReSTIRGIDebugView debug_view; float debug_view_scale_factor; // If a neighbor has its sample point on a glossy surface, we don't want to reuse // that sample with the reconnection shift if it is below a given roughness threshold because // the BSDF at the neighbor's glossy sample point is going to evaluate to 0 anyways if we change // its view direction bool use_neighbor_sample_point_roughness_heuristic; float neighbor_sample_point_roughness_threshold; bool use_jacobian_rejection_heuristic; HIPRT_HOST_DEVICE float get_jacobian_heuristic_threshold() const { if (use_jacobian_rejection_heuristic) return jacobian_rejection_threshold; else // Returning a super high threshold so that neighbors are basically // never rejected based on their jacobian return 1.0e20f; } /** * This function is used by ImGui to get a pointer to the private member */ HIPRT_HOST_DEVICE float* get_jacobian_heuristic_threshold_pointer() { return &jacobian_rejection_threshold; } HIPRT_HOST_DEVICE void set_jacobian_heuristic_threshold(float new_threshold) { jacobian_rejection_threshold = new_threshold; } private: float jacobian_rejection_threshold; }; #endif ================================================ FILE: src/HostDeviceCommon/ReSTIRSettingsHelper.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_RESTI_SETTINGS_HELPER_H #define HOST_DEVICE_COMMON_RESTI_SETTINGS_HELPER_H #include "Device/includes/ReSTIR/Surface.h" #include "HostDeviceCommon/RenderData.h" template struct ReSTIRSettingsTypeTemplate {}; template <> struct ReSTIRSettingsTypeTemplate { using Type = ReSTIRDISettings; }; template <> struct ReSTIRSettingsTypeTemplate { using Type = ReSTIRGISettings; }; template using ReSTIRSettingsType = typename ReSTIRSettingsTypeTemplate::Type; struct ReSTIRSettingsHelper { template HIPRT_HOST_DEVICE static ReSTIRSettingsType get_restir_settings(const HIPRTRenderData& render_data) { if constexpr (IsReSTIRGI) return render_data.render_settings.restir_gi_settings; else return render_data.render_settings.restir_di_settings; } template HIPRT_HOST_DEVICE static ReSTIRCommonSpatialPassSettings get_restir_spatial_pass_settings(const HIPRTRenderData& render_data) { if constexpr (IsReSTIRGI) return render_data.render_settings.restir_gi_settings.common_spatial_pass; else return render_data.render_settings.restir_di_settings.common_spatial_pass; } template HIPRT_HOST_DEVICE static ReSTIRCommonSpatialPassSettings& get_restir_spatial_pass_settings(HIPRTRenderData& render_data) { if constexpr (IsReSTIRGI) return render_data.render_settings.restir_gi_settings.common_spatial_pass; else return render_data.render_settings.restir_di_settings.common_spatial_pass; } template HIPRT_HOST_DEVICE static ReSTIRCommonTemporalPassSettings get_restir_temporal_pass_settings(const HIPRTRenderData& render_data) { if constexpr (IsReSTIRGI) return render_data.render_settings.restir_gi_settings.common_temporal_pass; else return render_data.render_settings.restir_di_settings.common_temporal_pass; } template HIPRT_HOST_DEVICE static ReSTIRCommonNeighborSimiliaritySettings get_restir_neighbor_similarity_settings(const HIPRTRenderData& render_data) { if constexpr (IsReSTIRGI) return render_data.render_settings.restir_gi_settings.neighbor_similarity_settings; else return render_data.render_settings.restir_di_settings.neighbor_similarity_settings; } /** * Returns the M value of a reservoir from the spatial pass input buffer given its pixel index * * The template argument can be used to select between ReSTIR DI and ReSTIR GI spatial buffers */ template HIPRT_HOST_DEVICE static int get_restir_spatial_pass_input_reservoir_M(const HIPRTRenderData& render_data, int pixel_index) { if constexpr (IsReSTIRGI) return render_data.render_settings.restir_gi_settings.spatial_pass.input_reservoirs[pixel_index].M; else return render_data.render_settings.restir_di_settings.spatial_pass.input_reservoirs[pixel_index].M; } template HIPRT_HOST_DEVICE static unsigned long long int get_spatial_reuse_direction_mask_ull(const HIPRTRenderData& render_data, int pixel_index) { if constexpr (IsReSTIRGI) { #if ReSTIR_GI_SpatialDirectionalReuseBitCount > 32 return render_data.render_settings.restir_gi_settings.common_spatial_pass.per_pixel_spatial_reuse_directions_mask_ull[pixel_index]; #else return render_data.render_settings.restir_gi_settings.common_spatial_pass.per_pixel_spatial_reuse_directions_mask_u[pixel_index]; #endif } else { #if ReSTIR_DI_SpatialDirectionalReuseBitCount > 32 return render_data.render_settings.restir_di_settings.common_spatial_pass.per_pixel_spatial_reuse_directions_mask_ull[pixel_index]; #else return render_data.render_settings.restir_di_settings.common_spatial_pass.per_pixel_spatial_reuse_directions_mask_u[pixel_index]; #endif } } /** * Returns the shading normal or geometric normal of the given surface depending on the rejection heuristics settings */ template HIPRT_HOST_DEVICE static float3 get_normal_for_rejection_heuristic(const HIPRTRenderData& render_data, const ReSTIRSurface& surface) { if constexpr (IsReSTIRGI) { if (render_data.render_settings.restir_gi_settings.neighbor_similarity_settings.reject_using_geometric_normals) return surface.geometric_normal; else return surface.shading_normal; } else { if (render_data.render_settings.restir_di_settings.neighbor_similarity_settings.reject_using_geometric_normals) return surface.geometric_normal; else return surface.shading_normal; } } }; #endif ================================================ FILE: src/HostDeviceCommon/RenderBuffers.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_RENDER_BUFFERS_H #define HOST_DEVICE_COMMON_RENDER_BUFFERS_H #include "Device/includes/AliasTable.h" #include "Device/includes/GMoN/GMoNDevice.h" #include "HostDeviceCommon/Material/MaterialPackedSoA.h" #include "HostDeviceCommon/PrecomputedEmissiveTrianglesDataSoADevice.h" struct RenderBuffers { // Sum of samples color per pixel. Should not be // pre-divided by the number of samples i.e. this buffer // contains pure accumulation of pixel colors ColorRGB32F* accumulated_ray_colors = nullptr; // Data for the GMoN estimator GMoNDevice gmon_estimator; // A device pointer to the buffer of triangles vertex indices // triangles_indices[0], triangles_indices[1] and triangles_indices[2] // represent the indices of the vertices of the first triangle for example int* triangles_indices = nullptr; // A device pointer to the buffer of triangle vertices positions float3* vertices_positions = nullptr; // A device pointer to a buffer filled with 0s and 1s that // indicates whether or not a vertex normal is available for // the given vertex index unsigned char* has_vertex_normals = nullptr; // The smooth normal at each vertex of the scene // Needs to be indexed by a vertex index float3* vertex_normals = nullptr; // Texture coordinates at each vertices float2* texcoords = nullptr; // Precomputed areas of all triangles of the scene float* triangles_areas = nullptr; // For each emissive triangle of the scene, this buffer contains the vertex A of the triangle // as well as AB and AC edges. This is usseful for sampling a point on a triangle without having // to go through the usual // // float3 vertex_A = vertices_positions[triangles_indices[triangle_index * 3 + 0]]; // float3 vertex_B = vertices_positions[triangles_indices[triangle_index * 3 + 1]]; // float3 vertex_C = vertices_positions[triangles_indices[triangle_index * 3 + 2]]; // // indirect fetch code which is expensive on the GPU because of the pointer chasing // // This is a remnant of some tests and it was actually more expensive than the indirect fetch code // above. // PrecomputedEmissiveTrianglesDataSoADevice precomputed_emissive_triangles_data; // Index of the material used by each triangle of the scene int* material_indices = nullptr; // Materials array to be indexed by an index retrieved from the // material_indices array DevicePackedTexturedMaterialSoA materials_buffer; // A buffer that can be indexed by a material_id. // // If indexing this buffer returns true, then the material is fully opaque // and there is no need to test alpha testing for it // // This is actually a buffer of bools but manipulating bools is annoying so this // is unsigned char. But the value of the unsigned char is either 0 or 1 unsigned char* material_opaque = nullptr; int emissive_triangles_count = 0; // A buffer that contains the primitive indices of the emissive triangles of the scene // Does not contains the indices of the emissive triangles that have emissive textures int* emissive_triangles_primitive_indices = nullptr; // Same as 'emissive_triangles_primitive_indices' but does contain the indices of the emissive triangles // that have emissive textures int* emissive_triangles_primitive_indices_and_emissive_textures = nullptr; // Alias table for sampling emissives lights according to power DeviceAliasTable emissives_power_alias_table; // A pointer either to an array of Image8Bit or to an array of // oroTextureObject_t whether if CPU or GPU rendering respectively // This pointer can be cast for the textures to be be retrieved. void* material_textures = nullptr; }; #endif ================================================ FILE: src/HostDeviceCommon/RenderData.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_RENDER_DATA_H #define HOST_DEVICE_COMMON_RENDER_DATA_H #include "Device/includes/GBufferDevice.h" #include "Device/includes/ReSTIR/DI/Reservoir.h" #include "Device/includes/NEE++/NEE++.h" #include "HostDeviceCommon/BSDFsData.h" #include "HostDeviceCommon/HIPRTCamera.h" #include "HostDeviceCommon/Math.h" #include "HostDeviceCommon/RenderBuffers.h" #include "HostDeviceCommon/RenderSettings.h" #include "HostDeviceCommon/WorldSettings.h" #include #include struct AuxiliaryBuffers { // Whether or not the pixel at a given index in the buffer is active or not. // // A pixel can be inactive when we're rendering at low resolution for example // (and so some pixels are not rendered) or when adaptive sampling has // judged that the pixel was converged enough and doesn't need more samples unsigned char* pixel_active = nullptr; // World space normals for the denoiser // These normals should already be divided by the number of samples float3* denoiser_normals = nullptr; // Albedo for the denoiser // The albedo should already be divided by the number of samples ColorRGB32F* denoiser_albedo = nullptr; // Per pixel sample count. Useful when doing adaptive sampling // where each pixel can have a different number of sample int* pixel_sample_count = nullptr; // Per pixel sum of squared luminance of samples. Used for adaptive sampling // This buffer should not be pre-divided by the number of samples float* pixel_squared_luminance = nullptr; // If a given pixel has converged, this buffer contains the number of samples // that were necessary for the convergence. // // If the pixel hasn't converged yet, the buffer contains the -1 value for that pixel int* pixel_converged_sample_count = nullptr; // A single boolean (contained in a buffer, hence the pointer) // to indicate whether at least one single ray is still active in the kernel. // This is an unsigned char instead of a boolean because std::vector.data() // isn't standard unsigned char* still_one_ray_active = nullptr; // If render_settings.stop_pixel_noise_threshold > 0.0f, this buffer // (consisting of a single unsigned int) counts how many pixels have reached the // noise threshold. If this value is equal to the number of pixels of the // framebuffer, then all pixels have converged according to the given // noise threshold. AtomicType* pixel_count_converged_so_far = nullptr; // Same for ReSTIR GI ReSTIRGIReservoir* restir_gi_reservoir_buffer_1 = nullptr; ReSTIRGIReservoir* restir_gi_reservoir_buffer_2 = nullptr; ReSTIRGIReservoir* restir_gi_reservoir_buffer_3 = nullptr; }; /** * The CPU and GPU use the same kernel code but the CPU still need some specific data * (the CPU BVH for example) which is stored in this structure */ class BVH; struct CPUData { // BVH built over all the triangles of the scene BVH* bvh = nullptr; // BVH built over the emissive triangles of the scene only BVH* light_bvh = nullptr; }; /* * A structure containing all the information about the scene * that the kernel is going to need for the render (vertices of the triangles, * vertices indices, skysphere data, ...) */ struct HIPRTRenderData { // Random number that is updated by the CPU and that can help generate a // random seed on the GPU for the random number generator to get started unsigned int random_number = 42; // HIPRT BVH built over all the triangles of the scene hiprtGeometry GPU_BVH = nullptr; // HIPRT BVH built over the emissive triangles of the scene only hiprtGeometry light_GPU_BVH = nullptr; // GPU Intersection functions (for alpha testing for example) hiprtFuncTable hiprt_function_table = nullptr; // Size of the *global* stack per thread. Default is 32. int global_traversal_stack_buffer_size = 32; hiprtGlobalStackBuffer global_traversal_stack_buffer = { 0, 0, nullptr }; RenderBuffers buffers; BRDFsData bsdfs_data; AuxiliaryBuffers aux_buffers; GBufferDevice g_buffer; GBufferDevice g_buffer_prev_frame; HIPRTRenderSettings render_settings; WorldSettings world_settings; // Data for NEE++ NEEPlusPlusDevice nee_plus_plus; // Camera for the current frame HIPRTCamera current_camera; // Camera of the last frame HIPRTCamera prev_camera; // Data only used by the CPU CPUData cpu_only; }; #endif ================================================ FILE: src/HostDeviceCommon/RenderSettings.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Compiler/GPUKernelCompilerOptions.h" #include "HostDeviceCommon/RenderSettings.h" #include "Renderer/GPURenderer.h" #ifndef __KERNELCC__ HIPRT_HOST bool HIPRTRenderSettings::use_prev_frame_g_buffer(GPURenderer* renderer) const { // If ReSTIR DI isn't used, we don't need the last frame's g-buffer // (as far as the codebase goes at the time of writing this function anyways) bool need_g_buffer = false; need_g_buffer |= renderer->get_ReSTIR_DI_render_pass()->is_render_pass_used() && restir_di_settings.common_temporal_pass.do_temporal_reuse_pass; need_g_buffer |= renderer->get_ReSTIR_GI_render_pass()->is_render_pass_used() && restir_gi_settings.common_temporal_pass.do_temporal_reuse_pass; return need_g_buffer; } #endif ================================================ FILE: src/HostDeviceCommon/RenderSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_RENDER_SETTINGS_H #define HOST_DEVICE_COMMON_RENDER_SETTINGS_H #include "Device/includes/ReSTIR/ReGIR/Settings.h" #include "Device/includes/ReSTIR/DI/Reservoir.h" #include "Device/includes/ReSTIR/GI/Reservoir.h" #include "HostDeviceCommon/PathRussianRoulette.h" #include "HostDeviceCommon/KernelOptions/KernelOptions.h" #include "HostDeviceCommon/RIS/RISSettings.h" #include "HostDeviceCommon/ReSTIR/ReSTIRCommonSettings.h" #include "HostDeviceCommon/ReSTIR/ReSTIRDISettings.h" #include "HostDeviceCommon/ReSTIR/ReSTIRGISettings.h" #include "HostDeviceCommon/Math.h" // Just used for initializing some structure members below #define local_min_macro(a, b) ((a) < (b) ? (a) : (b)) class GPURenderer; struct HIPRTRenderSettings { int2 render_resolution = make_int2(1280, 720); // If true, the camera ray kernel will reset all buffers to their default values. // This is mainly useful for the first frame of the render bool need_to_reset = true; // TODO DEBUG REMOVE THESE //////////////////////////////////////////////////// static constexpr bool DEBUG_DEV_GMON_BLEND_WEIGHTS = true; bool DEBUG_gmon_auto_blending_weights = false; float DEBUG_GMON_DIVIDER = 3.0f; int DEBUG_GMON_WINDOW_SIZE = 3; int DEBUG_REGIR_PRE_INTEGRATION_ITERATIONS = 4; int DEBUG_REGIR_PRE_INTEGRATION_SAMPLE_COUNT_PER_RESERVOIR = 32; bool enable_direct = true; AtomicType* DEBUG_SUM_COUNT = nullptr; AtomicType* DEBUG_SUM_TOTAL = nullptr; //////////////////////////////////////////////////// // If true, then the kernels are allowed to modify the status buffers (how many pixels have converged so far, ...) // // Why is this useful? // There is a "status" buffer that contains the number of pixels that have converged for a kernel launch. // It is a simple counter that threads of the kernel increment if the pixel corresponding to the thread has converged. // Because thread keep incrementing this counter, we need to reset it to 0 before each kernel launch. // // To simulate multiple samples per frame and reduce CPU overhead, we can launch multiple times the kernels per frame. // We would thus need to reset the status buffer before each kernel launch but this is a synchronous operation which then // slows down the UI. This means that we cannot reset the status buffer before each kernel launch, we can only reset it // at each frame before GPURenderer::render() is called. // // In the case where we have 5 samples per pixel for example, we would have each kernel launch increment the status // buffer and that would largely go above 100% of pixels converged (which doesn't make sense). // What we do instead is that we only allow the last kernel launch of the frame to increment the status buffers. // // This is the variable that enables / disables the increment of status buffers bool do_update_status_buffers = false; // Whether or not to accumulate each frame to allow progressive rendering. If false, // each frame will be displayed on screen and discarded on the next frame without accumulation bool accumulate = true; // How many samples were accumulated in the denoiser's AOV buffers (albedo & normals) // This is used mainly for the normals AOVs because we want a way to accumulate the normals. // However, we still want to feed the normalized normals to the denoiser. // This means that we need to store normalized normals in the normals AOV GPU buffer. // But if we also want to accumulate, we also need to get the normals back from "normalized" // to their "accumulated" value. We can then add the normal of the first hit of our current // frame to that "accumulated" value and then normalize again. // // We need denoiser_AOV_accumulation_counter to multiply the normalized normals of the buffer with // and get that "accumulated" normals value. int denoiser_AOV_accumulation_counter = 0; // Number of samples rendered so far before (before means that this counter starts at 0) the kernel call // // This is the sum of samples_per_frame for all frames that have been rendered. unsigned int sample_number = 0; // See the DisplayOnlySampleN kernel option int output_debug_sample_N = 1; // How many samples to compute per pixel per frame // Higher values reduce CPU overhead since the GPU spends // more time computing per frame but reduces interactivity int samples_per_frame = 1; // Maximum number of bounces of rays in the scene. // 1 is direct light only. int nb_bounces = 5; bool do_russian_roulette = true; // After how many bounces can russian roulette kick in? // 0 means that the camera ray hits, and then the next bounce // is already susceptible to being terminated by russian roulette int russian_roulette_min_depth = local_min_macro(5, nb_bounces / 2); // After applying russian roulette(dividing by the continuation probability) // the energy added to the ray throughput is clamped to this maximum value. // // This is biased and darkens the image the lower the threshold but it helps // reduce variance and fireflies introduced by the russian roulette --> faster // convergence. // // 0 for no clamping. float russian_roulette_throughput_clamp = 10.0f; // What Russian roulette method to use to determine the path termination // probability PathRussianRoulette path_russian_roulette_method = PathRussianRoulette::MAX_THROUGHPUT; // Whether or not to "freeze" random number generation so that each frame uses // exactly the same random number. This allows every ray to follow the exact // same path every frame, allowing for more stable benchmarking. int freeze_random = false; // If true, NaNs encountered during rendering will be rendered as very bright pink. // Useful for debugging only. bool display_NaNs = true; // If true, then rendering at low resolution will be performed if 'wants_render_low_resolution' // is also true. // This boolean basically is an additional condition for rendering at low resolution: // - If we're interacting with the camera, we *want* to render at low resolution // but if rendering at low resolution is not allowed (this boolean), then we will still // not render at low resolution // This boolean is controlled by the user in Imgui bool allow_render_low_resolution = false; // If true, this means that the user is moving the camera and we want to // render the image at a much lower resolution to allow for smoother // interaction. Having this flag at true isn't sufficient for rendering at low // resolution. The user must also *allow* rendering at low resolution // with the 'allow_render_low_resolution' flag bool wants_render_low_resolution = false; // How to divide the render resolution by when rendering at low resolution // (when interacting with the camera) int render_low_resolution_scaling = 2; bool enable_adaptive_sampling = false; // How many samples before the adaptive sampling actually kicks in. // This is useful mainly for the per-pixel adaptive sampling method // where you want to be sure that each pixel in the image has had enough // chance find a path to a potentially int adaptive_sampling_min_samples = 96; // Adaptive sampling noise threshold float adaptive_sampling_noise_threshold = 0.075f; // If true, the rendering will stop after a certain proportion (defined by 'stop_pixel_percentage_converged') // of pixels of the image have converged. "converged" here is defined according to the adaptive sampling if // enabled or according to 'stop_pixel_noise_threshold' if adaptive sampling is not enabled. // // If false, the render will not stop until all pixels have converged bool use_pixel_stop_noise_threshold = true; // A percentage in [0, 100] that dictates the proportion of pixels that must // have reached the given noise threshold (stop_pixel_noise_threshold // variable) before we stop rendering. // // For example, if this variable is 90, we will stop rendering when 90% of all // pixels have reached the stop_pixel_noise_threshold float stop_pixel_percentage_converged = 70.0f; // Noise threshold for use with the stop_pixel_percentage_converged stopping // condition float stop_pixel_noise_threshold = 0.075f; // Clamp direct lighting contribution to reduce fireflies float direct_contribution_clamp = 0.0f; // Clamp envmap contribution to reduce fireflies float envmap_contribution_clamp = 0.0f; // Clamp indirect lighting contribution to reduce fireflies float indirect_contribution_clamp = 0.0f; // If a selected light (for direct lighting estimation) contributes at a given // point less than this 'minimum_light_contribution' value then the light sample is discarded // 0.0f to disable float minimum_light_contribution = 0.0f; // Whether or not to do alpha testing for geometry with transparent base color textures bool do_alpha_testing = true; // At what bounce to stop doing alpha testing // // A value of 0 means that alpha testing isn't done at bounce 0 which means that even camera // rays do not do alpha testing --> alpha testing is disable // // A value of 1 means that camera rays do alpha testing but the next bounce rays do not do alpha // testing // // Shadow rays for NEE are also affected by this setting int alpha_testing_indirect_bounce = nb_bounces + 1; // Whether or not to do normal mapping at all // If false, geometric normals will always be used bool do_normal_mapping = true; // Settings for RIS (direct light sampling) RISSettings ris_settings; // Settings for ReSTIR DI ReSTIRDISettings restir_di_settings; // Settings for ReSTIR GI ReSTIRGISettings restir_gi_settings; ReGIRSettings regir_settings; /** * Returns true if the current frame should be renderer at low resolution, false otherwise. * * This function is a simple helper that combines a few flags to make sure that we * actually want to render at low resolution */ HIPRT_HOST_DEVICE bool do_render_low_resolution() const { return wants_render_low_resolution && allow_render_low_resolution && accumulate; } /** * Returns true if the adaptive sampling buffers are ready for use, false otherwise. * * Adaptive sampling buffers are "ready for use" if the adaptive sampling is enabled or * if the pixel stop noise threshold is enabled. Otherwise, the adaptive sampling buffers * are freed to save VRAM so they cannot be used. */ HIPRT_HOST_DEVICE bool has_access_to_adaptive_sampling_buffers() const { bool has_access = false; has_access |= stop_pixel_noise_threshold > 0.0f; has_access |= enable_adaptive_sampling; // Cannot use adaptive sampling without accumulation has_access &= accumulate; return has_access; } /** * Returns true if the renderer needs the G-buffer of the previous frame. * * The boolean parameter is some additional condition that must be satisfied * for the G-buffer to be needed * * We need two overrides of this function: one for use in the shaders and one * for use in the C++ CPU side code. * * This is because to determine whether or not we need the g-buffer of last * frame, we need to check if ReSTIR DI is being used or not. On the CPP side, this * can be done with the GPURenderer instance by checking the path tracer * options and check if the DirectLightSamplingStrategy is equal to * LSS_RESTIR_DI. On the device however, we don't have access to the * GPURenderer instance but instead, we can check directly using the * DirectLightSamplingStrategy macro (and we don't want the GPURenderer parameter * because that doesn't exist on the device). */ HIPRT_DEVICE bool use_prev_frame_g_buffer() const { // If ReSTIR DI isn't used, we don't need the last frame's g-buffer // (as far as the codebase goes at the time of writing this function anyways) bool need_g_buffer = false; need_g_buffer |= DirectLightSamplingStrategy == LSS_RESTIR_DI && restir_di_settings.common_temporal_pass.do_temporal_reuse_pass; need_g_buffer |= PathSamplingStrategy == PSS_RESTIR_GI && restir_gi_settings.common_temporal_pass.do_temporal_reuse_pass; return need_g_buffer; } // Only need this one on the host #ifndef __KERNELCC__ HIPRT_HOST bool use_prev_frame_g_buffer(GPURenderer* renderer) const; #endif }; #endif ================================================ FILE: src/HostDeviceCommon/WorldSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_WORLD_SETTINGS_H #define HOST_DEVICE_COMMON_WORLD_SETTINGS_H #include "Device/includes/AliasTable.h" #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Packing.h" enum AmbientLightType { NONE, UNIFORM, ENVMAP }; struct WorldSettings { AmbientLightType ambient_light_type = AmbientLightType::UNIFORM; ColorRGB32F uniform_light_color = ColorRGB32F(0.0f); // Width and height in pixels. Both in the range [1, XXX] unsigned int envmap_width = 0, envmap_height = 0; // Simple scale multiplier on the envmap color read from the envmap texture // in the shader float envmap_intensity = 1.0f; // If true, the background of the scene (where rays directly miss any geometry // and we directly see the skysphere) will scale with the envmap_intensity coefficient. // This can be visually unpleasing because the background will most likely // become completely white and blown out. int envmap_scale_background_intensity = false; // Packed RGBE 9/9/9/5 envmap texels RGBE9995Packed* envmap; // Luminance sum of all the texels of the envmap float envmap_total_sum = 0.0f; // Cumulative distribution function. 1D float array of length width * height for // importance sampling the envmap with a binary search strategy float* envmap_cdf = nullptr; // Probabilities and aliases for sampling the envmap with the alias table strategy DeviceAliasTable envmap_alias_table; // Rotation matrix for rotating the envmap around in the current frame float3x3 envmap_to_world_matrix = float3x3{ { {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f} } }; float3x3 world_to_envmap_matrix = float3x3{ { {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f} } }; }; #endif ================================================ FILE: src/HostDeviceCommon/Xorshift.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_XORSHIFT_H #define HOST_DEVICE_COMMON_XORSHIFT_H #include #include "HostDeviceCommon/Math.h" struct Xorshift32State { unsigned int seed = 42; }; struct Xorshift32Generator { static const unsigned int XORSHIFT_MAX = 0xffffffff; HIPRT_DEVICE Xorshift32Generator() { m_state.seed = 42; } HIPRT_DEVICE Xorshift32Generator(unsigned int seed) { m_state.seed = seed; } /* * Returns a uniform random number between 0 and * array_size - 1 (included) */ HIPRT_DEVICE int random_index(int array_size) { int random_num = xorshift32() / static_cast(XORSHIFT_MAX) * array_size; return hippt::min(random_num, array_size - 1); } /* * Returns a float int [0, 1.0 - 1.0e-9f] */ HIPRT_DEVICE float operator()() { //Float in [0, 1[ float a = xorshift32() / static_cast(XORSHIFT_MAX); return hippt::min(a, 1.0f - 1.0e-7f); } /** * Returns a random uint */ HIPRT_DEVICE unsigned int xorshift32() { /* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */ unsigned int x = m_state.seed; x ^= x << 13; x ^= x >> 17; x ^= x << 5; return m_state.seed = x; } Xorshift32State m_state; }; #endif ================================================ FILE: src/Image/EnvmapRGBE9995.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DEVICE_RGBE9995_ENVMAP_H #define DEVICE_RGBE9995_ENVMAP_H #include "HostDeviceCommon/Packing.h" #include "Image/Image.h" #include "HIPRT-Orochi/OrochiBuffer.h" /** * If GPU is true, then functions of this class will be templated such * that they compute / return data that can be used on the GPU */ template class RGBE9995Envmap { public: HIPRT_HOST void pack_from(const Image32Bit& image) { packed_data_CPU.resize(image.width * image.height); #pragma omp parallel for for (int y = 0; y < image.height; y++) { for (int x = 0; x < image.width; x++) { int index = x + y * image.width; packed_data_CPU[index].pack(image.get_pixel_ColorRGB32F(index)); } } if (GPU) { // If the data is for the GPU, upload the data to the GPU buffer and then discard the CPU data packed_data_GPU.resize(image.width * image.height); packed_data_GPU.upload_data(packed_data_CPU); // Clearing the CPU data packed_data_CPU = std::vector(); } } HIPRT_HOST RGBE9995Packed* get_data_pointer() { if (GPU) return packed_data_GPU.get_device_pointer(); else return packed_data_CPU.data(); } private: // Linear array for the packed data of the envmap OrochiBuffer packed_data_GPU; std::vector packed_data_CPU; }; #endif ================================================ FILE: src/Image/Image.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Image/Image.h" #include "UI/ImGui/ImGuiLogger.h" #include "Utils/Utils.h" extern ImGuiLogger g_imgui_logger; // This CPP file is used to define the STBI implementation once and for all #define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image.h" #include "stb_image_write.h" #include "tinyexr.cc" #include Image8Bit::Image8Bit(int width, int height, int channels) : Image8Bit(std::vector(width * height * channels, 0), width, height, channels) {} Image8Bit::Image8Bit(const unsigned char* data, int width, int height, int channels) : width(width), height(height), channels(channels) { m_pixel_data = std::vector(); m_pixel_data.insert(m_pixel_data.end(), &data[0], &data[width * height * channels]); } Image8Bit::Image8Bit(const std::vector& data, int width, int height, int channels) : width(width), height(height), channels(channels), m_pixel_data(data) {} Image8Bit Image8Bit::read_image(const std::string& filepath, int output_channels, bool flipY) { stbi_set_flip_vertically_on_load_thread(flipY); int width, height, read_channels; unsigned char* pixels = stbi_load(filepath.c_str(), &width, &height, &read_channels, output_channels); if (!pixels) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Error reading image %s: %s", filepath.c_str(), stbi_failure_reason()); return Image8Bit(); } Image8Bit output_image(width, height, output_channels); #pragma omp parallel for for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = x + y * width; for (int i = 0; i < output_channels; i++) output_image[index * output_channels + i] = pixels[index * output_channels + i]; } } stbi_image_free(pixels); return output_image; } Image8Bit Image8Bit::read_image_hdr(const std::string& filepath, int output_channels, bool flipY) { stbi_set_flip_vertically_on_load(flipY); int width, height, read_channels; float* pixels = stbi_loadf(filepath.c_str(), &width, &height, &read_channels, output_channels); if (!pixels) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Error reading image %s: %s", filepath.c_str(), stbi_failure_reason()); return Image8Bit(); } std::vector converted_data(width * height * output_channels); #pragma omp parallel for for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = x + y * width; for (int i = 0; i < output_channels; i++) { converted_data[index * output_channels + i] = static_cast(pixels[index * output_channels + i]); } } } stbi_image_free(pixels); return Image8Bit(converted_data, width, height, output_channels); } bool Image8Bit::write_image_png(const char* filename, const bool flipY) const { if (byte_size() == 0) return false; std::vector tmp(width * height * channels); for (unsigned i = 0; i < width * height; i++) for (int j = 0; j < channels; j++) tmp[i * channels + j] = hippt::clamp(static_cast(0), static_cast(255), m_pixel_data[i * channels + j]); stbi_flip_vertically_on_write(flipY); return stbi_write_png(filename, width, height, channels, tmp.data(), width * channels) != 0; } bool Image8Bit::write_image_hdr(const char* filename, const bool flipY) const { if (byte_size() == 0) return false; std::vector tmp(width * height * channels); for (unsigned i = 0; i < width * height; i++) for (int j = 0; j < channels; j++) tmp[i * channels + j] = m_pixel_data[i * channels + j] / 255.0f; stbi_flip_vertically_on_write(flipY); return stbi_write_hdr(filename, width, height, channels, reinterpret_cast(m_pixel_data.data())) != 0; } float Image8Bit::luminance_of_pixel(int x, int y) const { int start_pixel = (x + y * width) * channels; // Computing the luminance with a *maximum* of 3 components. // // If the texture only has one component (i.e. only red), the following // loop will only loop through the red component with the right weight. // // If the image has more than 1 components, 3 for example, then we'll loop through // the 3 components and apply the weights. // // If the image has 4 components, we will still only take RGB into account for the // luminance computation but not alpha float luminance = 0.0f; float weights[3] = { 0.3086f, 0.6094f, 0.0820f }; for (int i = 0; i < hippt::min(channels, 3); i++) luminance += m_pixel_data[start_pixel + i] * weights[i]; return luminance; } float Image8Bit::luminance_of_area(int start_x, int start_y, int stop_x, int stop_y) const { float luminance = 0.0f; for (int x = start_x; x < stop_x; x++) for (int y = start_y; y < stop_y; y++) luminance += luminance_of_pixel(x, y); return luminance; } float Image8Bit::luminance_of_area(const ImageBin& area) const { return luminance_of_area(area.x0, area.y0, area.x1, area.y1); } ColorRGBA32F Image8Bit::sample_rgba32f(float2 uv) const { // Sampling in repeat mode so we're just keeping the fractional part float u = uv.x; if (u != 1.0f) // Only doing that if u != 1.0f because if we actually have // uv.x == 1.0f, then subtracting static_cast(uv.x) will // give us 0.0f even though we actually want 1.0f (which is correct). // // Basically, 1.0f gets transformed into 0.0f even though 1.0f is a correct // U coordinate which needs not to be wrapped u -= static_cast(uv.x); float v = uv.y; if (v != 1.0f) // Same for v v -= static_cast(uv.y); // For negative UVs, we also want to repeat and we want, for example, // -0.1f to behave as 0.9f u = u < 0 ? 1.0f + u : u; v = v < 0 ? 1.0f + v : v; // Sampling with [0, 0] bottom-left convention v = 1.0f - v; int x = (u * (width - 1)); int y = (v * (height - 1)); ColorRGBA32F out_color; for (int i = 0; i < channels; i++) out_color[i] = m_pixel_data[(x + y * width) * channels + i] / 255.0f; return out_color; } void Image8Bit::set_data(const std::vector& data) { m_pixel_data = data; } const std::vector& Image8Bit::data() const { return m_pixel_data; } std::vector& Image8Bit::data() { return m_pixel_data; } const unsigned char& Image8Bit::operator[](int index) const { return m_pixel_data[index]; } unsigned char& Image8Bit::operator[](int index) { return m_pixel_data[index]; } std::vector Image8Bit::compute_cdf() const { std::vector out_cdf; out_cdf.resize(height * width); out_cdf[0] = 0.0f; float max_radiance = 0.0f; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = y * width + x; out_cdf[index] = out_cdf[std::max(index - 1, 0)] + luminance_of_pixel(x, y); for (int i = 0; i < hippt::min(3, channels); i++) max_radiance = hippt::max(max_radiance, static_cast(m_pixel_data[(x + y * width) * channels + i])); } } return out_cdf; } size_t Image8Bit::byte_size() const { return width * height * sizeof(unsigned char); } bool Image8Bit::is_constant_color(int threshold) const { if (width == 0 || height == 0) // Incorrect image return false; std::vector first_pixel_color(channels); for (int i = 0; i < channels; i++) first_pixel_color[i] = m_pixel_data[i]; // Comparing the first pixel to all pixels of the texture and returning as soon as we find one // that is not within the threshold for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) for (int i = 0; i < channels; i++) if (std::abs(first_pixel_color[i] - m_pixel_data[(y * width + x) * channels + i]) > threshold) return false; return true; } bool Image8Bit::is_fully_opaque() const { if (width == 0 || height == 0) // Incorrect image return false; if (channels < 4) // No alpha channel so this is fully opaque return true; // Comparing the first pixel to all pixels of the texture and returning as soon as we find one // that is not within the threshold for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // Checking the alpha channel // Assuming 4 channels if we're here unsigned char alpha_channel = m_pixel_data[(y * width + x) * 4 + 3]; if (alpha_channel != 255) return false; } } return true; } void Image8Bit::free() { m_pixel_data.clear(); width = 0; height = 0; channels = 0; } Image32Bit::Image32Bit(int width, int height, int channels) : Image32Bit(std::vector(width * height * channels, 0), width, height, channels) {} Image32Bit::Image32Bit(const float* data, int width, int height, int channels) : width(width), height(height), channels(channels) { m_pixel_data = std::vector(); m_pixel_data.insert(m_pixel_data.end(), &data[0], &data[width * height * channels]); } Image32Bit::Image32Bit(const std::vector& data, int width, int height, int channels) : width(width), height(height), channels(channels), m_pixel_data(data) {} Image32Bit::Image32Bit(Image8Bit image, int channels) { int input_channels = image.channels; int output_channels = channels == -1 ? image.channels : channels; m_pixel_data.resize(image.width * image.height * output_channels); for (int y = 0; y < image.height; y++) { for (int x = 0; x < image.width; x++) { int index = x + y * image.width; for (int i = 0; i < output_channels; i++) m_pixel_data[index * output_channels + i] = static_cast(image[index * input_channels + i]) / 255.0f; } } width = image.width; height = image.height; this->channels = output_channels; } Image32Bit Image32Bit::read_image(const std::string& filepath, int output_channels, bool flipY) { stbi_set_flip_vertically_on_load(flipY); int width, height, read_channels; unsigned char* pixels = stbi_load(filepath.c_str(), &width, &height, &read_channels, output_channels); if (!pixels) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Error reading image %s: %s", filepath.c_str(), stbi_failure_reason()); return Image32Bit(); } Image32Bit output_image(width, height, output_channels); #pragma omp parallel for for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = x + y * width; for (int i = 0; i < output_channels; i++) output_image[index * output_channels + i] = pixels[index * output_channels + i] / 255.0f; } } stbi_image_free(pixels); return output_image; } Image32Bit Image32Bit::read_image_hdr(const std::string& filepath, int output_channels, bool flipY) { stbi_set_flip_vertically_on_load_thread(flipY); int width, height, read_channels; float* pixels = stbi_loadf(filepath.c_str(), &width, &height, &read_channels, output_channels); if (!pixels) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Error reading image %s: %s", filepath.c_str(), stbi_failure_reason()); return Image32Bit(); } std::vector converted_data(width * height * output_channels); #pragma omp parallel for for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = x + y * width; for (int i = 0; i < output_channels; i++) converted_data[index * output_channels + i] = pixels[index * output_channels + i]; } } stbi_image_free(pixels); return Image32Bit(converted_data, width, height, output_channels); } Image32Bit Image32Bit::read_image_exr(const std::string& filepath, bool flipY) { float* out; int width; int height; const char* err = nullptr; int ret = LoadEXR(&out, &width, &height, filepath.c_str(), &err); if (ret != TINYEXR_SUCCESS) { if (err) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Error reading EXR image: %s", err); FreeEXRErrorMessage(err); // release memory of error message. } return Image32Bit(); } else { if (!flipY) { std::vector vector_data(out, out + width * height * 4); std::free(out); // release memory of image data return Image32Bit(vector_data, width, height, 4); } else { std::vector vector_data(width * height * 4); for (int y = height - 1; y >= 0; y--) { for (int x = 0; x < width; x++) { int index_y_flipped = x + (height - 1 - y) * width; int index = x + y * width; index *= 4; // for RGBA index_y_flipped *= 4; // for RGBA vector_data[index + 0] = out[index_y_flipped + 0]; vector_data[index + 1] = out[index_y_flipped + 1]; vector_data[index + 2] = out[index_y_flipped + 2]; vector_data[index + 3] = out[index_y_flipped + 3]; } } std::free(out); // release memory of image data return Image32Bit(vector_data, width, height, 4); } } } bool Image32Bit::write_image_png(const char* filename, const bool flipY) const { if (byte_size() == 0) return false; std::vector tmp(width * height * channels); for (unsigned i = 0; i < width * height * channels; i++) tmp[i] = hippt::clamp(0.0f, 255.0f, m_pixel_data[i] * 255.0f); stbi_flip_vertically_on_write(flipY); return stbi_write_png(filename, width, height, channels, tmp.data(), width * channels) != 0; } bool Image32Bit::write_image_hdr(const char* filename, const bool flipY) const { if (byte_size() == 0) return false; std::vector tmp(width * height * channels); for (unsigned i = 0; i < width * height; i++) for (int j = 0; j < channels; j++) tmp[i * channels + j] = m_pixel_data[i * channels + j]; stbi_flip_vertically_on_write(flipY); return stbi_write_hdr(filename, width, height, channels, reinterpret_cast(m_pixel_data.data())) != 0; } Image32Bit Image32Bit::to_linear_rgb() const { Image32Bit out(width, height, channels); #pragma omp parallel for for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int start_pixel = (x + y * width) * channels; int out_start_pixel = (x + y * width) * channels; for (int i = 0; i < channels; i++) { float color_component = m_pixel_data[start_pixel + i]; // Assuming gamma 2.2 out.m_pixel_data[out_start_pixel + i] = powf(color_component, 2.2f); } } } return out; } float Image32Bit::luminance_of_pixel(int x, int y) const { int start_pixel = (x + y * width) * channels; // Computing the luminance with a *maximum* of 3 components. // // If the texture only has one component (i.e. only red), the following // loop will only loop through the red component with the right weight. // // If the image has more than 1 components, 3 for example, then we'll loop through // the 3 components and apply the weights. // // If the image has 4 components, we will still only take RGB into account for the // luminance computation but not alpha float luminance = 0.0f; float weights[3] = { 0.3086f, 0.6094f, 0.0820f }; for (int i = 0; i < hippt::min(channels, 3); i++) luminance += m_pixel_data[start_pixel + i] * weights[i]; return luminance; } float Image32Bit::luminance_of_area(int start_x, int start_y, int stop_x, int stop_y) const { float luminance = 0.0f; for (int x = start_x; x < stop_x; x++) for (int y = start_y; y < stop_y; y++) luminance += luminance_of_pixel(x, y); return luminance; } float Image32Bit::luminance_of_area(const ImageBin& area) const { return luminance_of_area(area.x0, area.y0, area.x1, area.y1); } ColorRGBA32F Image32Bit::sample_rgba32f(float2 uv) const { // Sampling in repeat mode so we're just keeping the fractional part float u = uv.x; if (u != 1.0f) // Only doing that if u != 1.0f because if we actually have // uv.x == 1.0f, then subtracting static_cast(uv.x) will // give us 0.0f even though we actually want 1.0f (which is correct). // // Basically, 1.0f gets transformed into 0.0f even though 1.0f is a correct // U coordinate which needs not to be wrapped u -= static_cast(uv.x); float v = uv.y; if (v != 1.0f) // Same for v v -= static_cast(uv.y); // For negative UVs, we also want to repeat and we want, for example, // -0.1f to behave as 0.9f u = u < 0 ? 1.0f + u : u; v = v < 0 ? 1.0f + v : v; // Sampling with [0, 0] bottom-left convention v = 1.0f - v; int x = (u * (width - 1)); int y = (v * (height - 1)); ColorRGBA32F out_color; for (int i = 0; i < channels; i++) out_color[i] = m_pixel_data[(x + y * width) * channels + i]; return out_color; } void Image32Bit::set_data(const std::vector& data) { m_pixel_data = data; } const std::vector& Image32Bit::data() const { return m_pixel_data; } std::vector& Image32Bit::data() { return m_pixel_data; } const float& Image32Bit::operator[](int index) const { return m_pixel_data[index]; } float& Image32Bit::operator[](int index) { return m_pixel_data[index]; } std::vector Image32Bit::compute_cdf() const { std::vector out_cdf; out_cdf.resize(height * width); out_cdf[0] = 0.0f; float max_radiance = 0.0f; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = y * width + x; out_cdf[index] = out_cdf[std::max(index - 1, 0)] + luminance_of_pixel(x, y); for (int i = 0; i < hippt::min(3, channels); i++) max_radiance = hippt::max(max_radiance, m_pixel_data[(x + y * width) * channels + i]); } } return out_cdf; } /** * Reference: Vose's Alias Method [https://www.keithschwarz.com/darts-dice-coins/] */ void Image32Bit::compute_alias_table(std::vector& out_probas, std::vector& out_alias, float* out_luminance_total_sum) const { float luminance_sum = 0.0f; std::vector texel_luminance(width * height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float luminance = luminance_of_pixel(x, y); luminance_sum += luminance; texel_luminance[x + y * width] = luminance; } } if (out_luminance_total_sum) *out_luminance_total_sum = luminance_sum; Utils::compute_alias_table(texel_luminance, luminance_sum, out_probas, out_alias); } float Image32Bit::compute_luminance_sum() const { float sum = 0.0f; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) sum += luminance_of_pixel(x, y); return sum; } size_t Image32Bit::byte_size() const { return width * height * sizeof(unsigned char); } bool Image32Bit::is_constant_color(float threshold) const { if (width == 0 || height == 0) // Incorrect image return false; std::vector first_pixel_color(channels); for (int i = 0; i < channels; i++) first_pixel_color[i] = m_pixel_data[i]; std::atomic different_pixel_found = false; // Comparing the first pixel to all pixels of the texture and returning as soon as we find one // that is not within the threshold for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) for (int i = 0; i < channels; i++) if (std::abs(first_pixel_color[i] - m_pixel_data[(y * width + x) * channels + i]) > threshold) return false; return true; } ColorRGB32F* Image32Bit::get_data_as_ColorRGB32F() { return reinterpret_cast(m_pixel_data.data()); } ColorRGB32F Image32Bit::get_pixel_ColorRGB32F(int pixel_index) const { return ColorRGB32F(m_pixel_data[pixel_index * channels + 0], m_pixel_data[pixel_index * channels + 1], m_pixel_data[pixel_index * channels + 2]); } ColorRGBA32F* Image32Bit::get_data_as_ColorRGBA32F() { return reinterpret_cast(m_pixel_data.data()); } ColorRGBA32F Image32Bit::get_pixel_ColorRGBA32F(int pixel_index) const { return ColorRGBA32F(m_pixel_data[pixel_index * channels + 0], m_pixel_data[pixel_index * channels + 1], m_pixel_data[pixel_index * channels + 2], m_pixel_data[pixel_index * channels + 3]); } void Image32Bit::free() { m_pixel_data.clear(); width = 0; height = 0; channels = 0; } Image32Bit3D::Image32Bit3D() { width = 0; height = 0; depth = 0; channels = 0; } Image32Bit3D::Image32Bit3D(const std::vector images) { m_images = images; width = images[0].width; height = images[0].height; depth = images.size(); channels = images[0].channels; } ColorRGBA32F Image32Bit3D::sample_rgba32f(float3 uvw) const { // Sampling in repeat mode so we're just keeping the fractional part float u = uvw.x; if (u != 1.0f) // Only doing that if u != 1.0f because if we actually have // uv.x == 1.0f, then subtracting static_cast(uv.x) will // give us 0.0f even though we actually want 1.0f (which is correct). // // Basically, 1.0f gets transformed into 0.0f even though 1.0f is a correct // U coordinate which needs not to be wrapped u -= static_cast(uvw.x); float v = uvw.y; if (v != 1.0f) // Same for v v -= static_cast(uvw.y); float w = uvw.z; if (w != 1.0f) // Same for w w -= static_cast(uvw.z); // For negative UVs, we also want to repeat and we want, for example, // -0.1f to behave as 0.9f u = u < 0 ? 1.0f + u : u; v = v < 0 ? 1.0f + v : v; w = w < 0 ? 1.0f + w : w; // Sampling with [0, 0] bottom-left convention v = 1.0f - v; int x = (u * (width - 1)); int y = (v * (height - 1)); int z = (w * (depth - 1)); ColorRGBA32F out_color; for (int i = 0; i < channels; i++) out_color[i] = m_images[z][(x + y * width) * channels + i]; return out_color; } ================================================ FILE: src/Image/Image.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef IMAGE_H #define IMAGE_H #include "HostDeviceCommon/Color.h" #include "stb_image.h" #include "stb_image_write.h" #include #include struct ImageBin { int x0, x1; int y0, y1; }; class Image8Bit { public: Image8Bit() : width(0), height(0), channels(0) {} Image8Bit(int width, int height, int channels); Image8Bit(const unsigned char* data, int width, int height, int channels); Image8Bit(const std::vector& data, int width, int height, int channels); static Image8Bit read_image(const std::string& filepath, int output_channels, bool flipY); static Image8Bit read_image_hdr(const std::string& filepath, int output_channels, bool flipY); bool write_image_png(const char* filename, const bool flipY = true) const; bool write_image_hdr(const char* filename, const bool flipY = true) const; float luminance_of_pixel(int x, int y) const; float luminance_of_area(int start_x, int start_y, int stop_x, int stop_y) const; float luminance_of_area(const ImageBin& area) const; ColorRGBA32F sample_rgba32f(float2 uv) const; void set_data(const std::vector& data); const std::vector& data() const; std::vector& data(); const unsigned char& operator[](int index) const; unsigned char& operator[](int index); std::vector compute_cdf() const; size_t byte_size() const; /** * Returns true if all the pixels of the texture are the same color * False otherwise * * A threshold can be given to assume that a color is equal to another * if the R, G and B channels of the two colors are each within 'threshold' * distance */ bool is_constant_color(int threshold = 0) const; /** * Returns true if all pixels of the image have 1.0f alpha channel. * Returns true if the texture has less than 4 channels * * Returns false otherwise */ bool is_fully_opaque() const; /** * Frees the data of this image and sets its width, height and channels back to 0 */ void free(); int width, height, channels; protected: std::vector m_pixel_data; }; class Image32Bit { public: Image32Bit() {} Image32Bit(int width, int height, int channels); Image32Bit(const float* data, int width, int height, int channels); Image32Bit(const std::vector& data, int width, int height, int channels); Image32Bit(Image8Bit image, int channels = -1); static Image32Bit read_image(const std::string& filepath, int output_channels, bool flipY); static Image32Bit read_image_hdr(const std::string& filepath, int output_channels, bool flipY); static Image32Bit read_image_exr(const std::string& filepath, bool flipY); bool write_image_png(const char* filename, const bool flipY = true) const; bool write_image_hdr(const char* filename, const bool flipY = true) const; Image32Bit to_linear_rgb() const; float luminance_of_pixel(int x, int y) const; float luminance_of_area(int start_x, int start_y, int stop_x, int stop_y) const; float luminance_of_area(const ImageBin& area) const; ColorRGB32F* get_data_as_ColorRGB32F(); ColorRGB32F get_pixel_ColorRGB32F(int pixel_index) const; ColorRGBA32F* get_data_as_ColorRGBA32F(); ColorRGBA32F get_pixel_ColorRGBA32F(int pixel_index) const; ColorRGBA32F sample_rgba32f(float2 uv) const; void set_data(const std::vector& data); const std::vector& data() const; std::vector& data(); const float& operator[](int index) const; float& operator[](int index); std::vector compute_cdf() const; void compute_alias_table(std::vector& out_probas, std::vector& out_alias, float* out_luminance_total_sum = nullptr) const; float compute_luminance_sum() const; size_t byte_size() const; /** * Returns true if all the pixels of the texture are the same color * False otherwise * * A threshold can be given to assume that a color is equal to another * if the R, G and B channels of the two colors are each within 'threshold' * distance */ bool is_constant_color(float threshold) const; /** * Frees the data of this image and sets its width, height and channels back to 0 */ void free(); int width = 0, height = 0, channels = 0; protected: std::vector m_pixel_data; }; class Image32Bit3D { public: Image32Bit3D(); Image32Bit3D(const std::vector images); ColorRGBA32F sample_rgba32f(float3 uvw) const; int width, height, depth, channels; private: std::vector m_images; }; #endif ================================================ FILE: src/OpenGL/OpenGLInteropBuffer.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef OPENGL_INTEROP_BUFFER_H #define OPENGL_INTEROP_BUFFER_H #include "HIPRT-Orochi/HIPRTOrochiUtils.h" #include "UI/DisplayView/DisplayTextureType.h" #include "UI/ImGui/ImGuiLogger.h" #include "Utils/Utils.h" #include "GL/glew.h" #include "GLFW/glfw3.h" #include "Orochi/Orochi.h" extern ImGuiLogger g_imgui_logger; template class OpenGLInteropBuffer { public: OpenGLInteropBuffer() {} OpenGLInteropBuffer(int element_count); ~OpenGLInteropBuffer(); GLuint get_opengl_buffer(); void resize(int new_element_count); size_t size() const; size_t get_byte_size() const; /** * Makes the buffer accesible to HIP/CUDA and returns the HIP/CUDA pointer * to that buffer */ T* map(); /** * Makes the buffer accessible by OpenGL */ void unmap(); /** * Copies the buffer data to an OpenGL texture */ void unpack_to_GL_texture(GLuint texture, GLint texture_unit, int width, int height, DisplayTextureType texture_type); void free(); private: bool m_initialized = false; bool m_mapped = false; T* m_mapped_pointer = nullptr; size_t m_element_count = 0; GLuint m_buffer_name = -1; oroGraphicsResource_t m_buffer_resource = nullptr; }; template OpenGLInteropBuffer::OpenGLInteropBuffer(int element_count) { glCreateBuffers(1, &m_buffer_name); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m_buffer_name); glBufferData(GL_PIXEL_UNPACK_BUFFER, element_count * sizeof(T), nullptr, GL_DYNAMIC_DRAW); OROCHI_CHECK_ERROR(oroGraphicsGLRegisterBuffer(&m_buffer_resource, m_buffer_name, oroGraphicsRegisterFlagsNone)); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); m_initialized = true; m_mapped = false; m_element_count = element_count; } template OpenGLInteropBuffer::~OpenGLInteropBuffer() { if (m_initialized) free(); } template GLuint OpenGLInteropBuffer::get_opengl_buffer() { return m_buffer_name; } template void OpenGLInteropBuffer::resize(int new_element_count) { if (m_mapped) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Trying to resize interop buffer while it is mapped! This is undefined behavior"); return; } if (m_initialized) { oroGraphicsUnregisterResource(m_buffer_resource); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m_buffer_name); glBufferData(GL_PIXEL_UNPACK_BUFFER, new_element_count * sizeof(T), nullptr, GL_DYNAMIC_DRAW); } else { glCreateBuffers(1, &m_buffer_name); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m_buffer_name); glBufferData(GL_PIXEL_UNPACK_BUFFER, new_element_count * sizeof(T), nullptr, GL_DYNAMIC_DRAW); } #ifndef OROCHI_ENABLE_CUEW // TODO hipGLGetDevices here is required for hipGraphicsGLRegisterBuffer to work. This is very scuffed. unsigned int count = 0; std::vector devices(16); hipGLGetDevices(&count, devices.data(), 16, hipGLDeviceListAll); #endif oroGraphicsGLRegisterBuffer(&m_buffer_resource, m_buffer_name, oroGraphicsRegisterFlagsNone); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); m_initialized = true; m_element_count = new_element_count; } template size_t OpenGLInteropBuffer::size() const { return m_element_count; } template size_t OpenGLInteropBuffer::get_byte_size() const { return m_element_count * sizeof(T); } template T* OpenGLInteropBuffer::map() { if (!m_initialized) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Mapping a buffer that hasn't been initialized!"); Utils::debugbreak(); return nullptr; } if (m_mapped) // Already mapped return m_mapped_pointer; size_t byte_size; OROCHI_CHECK_ERROR(oroGraphicsMapResources(1, reinterpret_cast(&m_buffer_resource), 0)); OROCHI_CHECK_ERROR(oroGraphicsResourceGetMappedPointer((void**)(&m_mapped_pointer), &byte_size, reinterpret_cast(m_buffer_resource))); m_mapped = true; return m_mapped_pointer; } template void OpenGLInteropBuffer::unmap() { if (!m_mapped) // Already unmapped return; OROCHI_CHECK_ERROR(oroGraphicsUnmapResources(1, reinterpret_cast(&m_buffer_resource), 0)); m_mapped = false; m_mapped_pointer = nullptr; } template void OpenGLInteropBuffer::unpack_to_GL_texture(GLuint texture, GLint texture_unit, int width, int height, DisplayTextureType texture_type) { GLenum format = texture_type.get_gl_format(); GLenum type = texture_type.get_gl_type(); glActiveTexture(texture_unit); glBindTexture(GL_TEXTURE_2D, texture); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, get_opengl_buffer()); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, format, type, 0); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); } template void OpenGLInteropBuffer::free() { if (m_initialized) { glDeleteBuffers(1, &m_buffer_name); if (m_mapped) unmap(); OROCHI_CHECK_ERROR(oroGraphicsUnregisterResource(reinterpret_cast(m_buffer_resource))); } else { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "Freeing an OpenGLInterop buffer that hasn't been initialized (or has been freed already)!"); return; } m_element_count = 0; m_initialized = false; } #endif ================================================ FILE: src/OpenGL/OpenGLProgram.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "OpenGL/OpenGLProgram.h" #include "UI/ImGui/ImGuiLogger.h" extern ImGuiLogger g_imgui_logger; OpenGLProgram::OpenGLProgram(const OpenGLShader& compiled_vertex) { m_program = glCreateProgram(); glAttachShader(m_program, compiled_vertex.get_shader()); glLinkProgram(m_program); } OpenGLProgram::OpenGLProgram(const OpenGLShader& compiled_vertex, const OpenGLShader& compiled_fragment) { m_program = glCreateProgram(); glAttachShader(m_program, compiled_vertex.get_shader()); glAttachShader(m_program, compiled_fragment.get_shader()); glLinkProgram(m_program); } OpenGLProgram::~OpenGLProgram() { glDeleteProgram(m_program); } void OpenGLProgram::attach(const OpenGLShader& compiled_shader) { if (m_program == (unsigned int)(-1)) m_program = glCreateProgram(); glAttachShader(m_program, compiled_shader.get_shader()); if (compiled_shader.get_shader_type() == OpenGLShader::COMPUTE_SHADER) m_is_compute = true; } void OpenGLProgram::link() { glLinkProgram(m_program); if (m_is_compute) glGetProgramiv(m_program, GL_COMPUTE_WORK_GROUP_SIZE, m_compute_threads); } void OpenGLProgram::use() { glUseProgram(m_program); } void OpenGLProgram::get_compute_threads(GLint threads[3]) { if (!m_is_compute) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "This program isn't a compute shader"); return; } threads[0] = m_compute_threads[0]; threads[1] = m_compute_threads[1]; threads[2] = m_compute_threads[2]; } void OpenGLProgram::set_uniform(const char* name, int value) { glUniform1i(glGetUniformLocation(m_program, name), value); } void OpenGLProgram::set_uniform(const char* name, float value) { glUniform1f(glGetUniformLocation(m_program, name), value); } void OpenGLProgram::set_uniform(const char* name, const float2& value) { glUniform2f(glGetUniformLocation(m_program, name), value.x, value.y); } void OpenGLProgram::set_uniform(const char* name, const float3& value) { glUniform3f(glGetUniformLocation(m_program, name), value.x, value.y, value.z); } void OpenGLProgram::set_uniform(const char* name, int count, const float* values) { glUniform3fv(glGetUniformLocation(m_program, name), count, values); } void OpenGLProgram::set_uniform(const char* name, const float4& value) { glUniform4f(glGetUniformLocation(m_program, name), value.x, value.y, value.z, value.w); } ================================================ FILE: src/OpenGL/OpenGLProgram.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef OPENGL_PROGRAM_H #define OPENGL_PROGRAM_H #include "GL/glew.h" #include "HostDeviceCommon/Math.h" #include "OpenGL/OpenGLShader.h" class OpenGLProgram { public: OpenGLProgram() : m_program(-1) { } OpenGLProgram(OpenGLProgram& other) = delete; OpenGLProgram(const OpenGLShader& vertex); OpenGLProgram(const OpenGLShader& compiled_vertex, const OpenGLShader& compiled_fragment); ~OpenGLProgram(); void attach(const OpenGLShader& compiled_shader); void link(); void use(); void get_compute_threads(GLint threads[3]); void set_uniform(const char* name, int value); void set_uniform(const char* name, float value); void set_uniform(const char* name, const float2& value); void set_uniform(const char* name, const float3& value); void set_uniform(const char* name, int count, const float* values); void set_uniform(const char* name, const float4& value); private: bool m_is_compute = false; GLuint m_program = -1; GLint m_compute_threads[3] = { 0, 0, 0 }; }; #endif ================================================ FILE: src/OpenGL/OpenGLShader.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "OpenGL/OpenGLShader.h" #include "UI/ImGui/ImGuiLogger.h" #include "Utils/Utils.h" extern ImGuiLogger g_imgui_logger; OpenGLShader::OpenGLShader(const std::string& source_code, ShaderType type, const std::vector& macros) { m_shader_type = type; set_source(source_code); compile(macros); } OpenGLShader::OpenGLShader(const char* filepath, ShaderType type, const std::vector& macros) { m_shader_type = type; set_source_from_file(filepath); compile(macros); } std::string& OpenGLShader::get_source() { return m_source_code; } const std::string& OpenGLShader::get_source() const { return m_source_code; } bool OpenGLShader::has_filepath() const { return m_filepath.length() > 0; } std::string& OpenGLShader::get_path() { return m_filepath; } const std::string& OpenGLShader::get_path() const { return m_filepath; } void OpenGLShader::set_source(const std::string& source_code) { m_source_code = source_code; } void OpenGLShader::set_source_from_file(const char* filepath) { m_source_code = Utils::file_to_string(filepath); } GLuint OpenGLShader::get_shader() const { return m_compiled_shader; } OpenGLShader::ShaderType OpenGLShader::get_shader_type() const { return m_shader_type; } void OpenGLShader::compile(const std::vector& macros /* = std::vector() */) { std::string source_code = add_macros_to_source(macros); const char* shader_text = source_code.c_str(); m_compiled_shader = glCreateShader(m_shader_type); glShaderSource(m_compiled_shader, 1, &shader_text, NULL); glCompileShader(m_compiled_shader); if (!print_shader_compile_error(m_compiled_shader)) { if (has_filepath()) throw new std::runtime_error("Unable to compile shader given at this path: " + get_path()); else throw new std::runtime_error("Unable to compile shader"); } } bool OpenGLShader::print_shader_compile_error(GLuint shader) { GLint isCompiled = 0; glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled); if (isCompiled == GL_FALSE) { GLint maxLength = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength); // The maxLength includes the NULL character std::vector errorLog(maxLength); glGetShaderInfoLog(shader, maxLength, &maxLength, &errorLog[0]); g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_ERROR, "%s", errorLog.data()); // Provide the infolog in whatever manor you deem best. // Exit with failure. glDeleteShader(shader); // Don't leak the shader. return false; } return true; } std::string OpenGLShader::add_macros_to_source(const std::vector& macros) { size_t version_pos = m_source_code.find("#version"); if (version_pos != std::string::npos) { size_t line_return_pos = m_source_code.find('\n', version_pos); size_t after_return = line_return_pos + 1; std::string modified_source = m_source_code; for (const std::string& macro : macros) modified_source = modified_source.insert(after_return, macro + "\n"); return modified_source; } else throw new std::runtime_error("No #version directive found in shader..."); } ================================================ FILE: src/OpenGL/OpenGLShader.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef OPENGL_SHADER_H #define OPENGL_SHADER_H #include "GL/glew.h" #include #include class OpenGLShader { public: enum ShaderType { UNDEFINED, VERTEX_SHADER = GL_VERTEX_SHADER, FRAGMENT_SHADER = GL_FRAGMENT_SHADER, COMPUTE_SHADER = GL_COMPUTE_SHADER }; OpenGLShader() : m_compiled_shader(-1), m_shader_type(ShaderType::UNDEFINED) {} /** * If given, macros should be entire strings (with the '#') like "#define MY_MACRO", #if X, #endif, ... */ OpenGLShader(const std::string& source_code, ShaderType type, const std::vector& macros = std::vector()); OpenGLShader(const char* filepath, ShaderType type, const std::vector& macros = std::vector()); std::string& get_source(); const std::string& get_source() const; bool has_filepath() const; std::string& get_path(); const std::string& get_path() const; void set_source(const std::string& source_code); void set_source_from_file(const char* filepath); GLuint get_shader() const; ShaderType get_shader_type() const; void compile(const std::vector& macros); std::string add_macros_to_source(const std::vector& macros); static bool print_shader_compile_error(GLuint shader); private: std::string m_filepath; std::string m_source_code; ShaderType m_shader_type; GLuint m_compiled_shader; }; #endif ================================================ FILE: src/Renderer/BVH.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include #include #include #include "Renderer/BVH.h" const float3 BoundingVolume::PLANE_NORMALS[BVHConstants::PLANES_COUNT] = { make_float3(1, 0, 0), make_float3(0, 1, 0), make_float3(0, 0, 1), make_float3(std::sqrt(3.0f) / 3, std::sqrt(3.0f) / 3, std::sqrt(3.0f) / 3), make_float3(-std::sqrt(3.0f) / 3, std::sqrt(3.0f) / 3, std::sqrt(3.0f) / 3), make_float3(-std::sqrt(3.0f) / 3, -std::sqrt(3.0f) / 3, std::sqrt(3.0f) / 3), make_float3(std::sqrt(3.0f) / 3, -std::sqrt(3.0f) / 3, std::sqrt(3.0f) / 3), }; BVH::BVH() : m_root(nullptr), m_triangles(nullptr) {} BVH::BVH(std::vector* triangles, int max_depth, int leaf_max_obj_count) : m_triangles(triangles) { BoundingVolume volume; float3 minimum = make_float3(INFINITY, INFINITY, INFINITY); float3 maximum = make_float3(-INFINITY, -INFINITY, -INFINITY); for (const Triangle& triangle : *triangles) { volume.extend_volume(triangle); for (int i = 0; i < 3; i++) { minimum = hippt::min(minimum, triangle[i]); maximum = hippt::max(maximum, triangle[i]); } } //We now have a bounding volume to work with build_bvh(max_depth, leaf_max_obj_count, minimum, maximum, volume); } BVH::~BVH() { delete m_root; } void BVH::operator=(BVH&& bvh) { m_triangles = bvh.m_triangles; m_root = bvh.m_root; bvh.m_root = nullptr; } void BVH::build_bvh(int max_depth, int leaf_max_obj_count, float3 min, float3 max, const BoundingVolume& volume) { m_root = new OctreeNode(min, max); for (int triangle_id = 0; triangle_id < m_triangles->size(); triangle_id++) m_root->insert(*m_triangles, triangle_id, 0, max_depth, leaf_max_obj_count); m_root->compute_volume(*m_triangles); } bool BVH::intersect(const hiprtRay& ray, hiprtHit& hit_info, void* filter_function_payload) const { return m_root->intersect(*m_triangles, ray, hit_info, filter_function_payload); } ================================================ FILE: src/Renderer/BVH.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef BVH_H #define BVH_H #include "Device/functions/FilterFunction.h" #include "Renderer/BoundingVolume.h" #include "Renderer/BVHConstants.h" #include "Renderer/Triangle.h" #include #include #include #include #include #include #include // for hiprtRay class BVH { public: struct OctreeNode { struct QueueElement { QueueElement(const BVH::OctreeNode* node, float t_near) : m_node(node), _t_near(t_near) {} bool operator > (const QueueElement& a) const { return _t_near > a._t_near; } const OctreeNode* m_node;//Reference on the node float _t_near;//Intersection distance used to order the elements in the priority queue used //by the OctreeNode to compute the intersection with a ray }; OctreeNode(float3 min, float3 max) : m_min(min), m_max(max) {} ~OctreeNode() { if (m_is_leaf) return; else { for (int i = 0; i < 8; i++) delete m_children[i]; } } /* * Once the objects have been inserted in the hierarchy, this function computes * the bounding volume of all the node in the hierarchy */ BoundingVolume compute_volume(const std::vector& triangles_geometry) { if (m_is_leaf) for (int triangle_id : m_triangles) m_bounding_volume.extend_volume(triangles_geometry[triangle_id]); else for (int i = 0; i < 8; i++) m_bounding_volume.extend_volume(m_children[i]->compute_volume(triangles_geometry)); return m_bounding_volume; } void create_children(int max_depth, int leaf_max_obj_count) { float middle_x = (m_min.x + m_max.x) / 2; float middle_y = (m_min.y + m_max.y) / 2; float middle_z = (m_min.z + m_max.z) / 2; m_children[0] = new OctreeNode(m_min, make_float3(middle_x, middle_y, middle_z)); m_children[1] = new OctreeNode(make_float3(middle_x, m_min.y, m_min.z), make_float3(m_max.x, middle_y, middle_z)); m_children[2] = new OctreeNode(m_min + make_float3(0, middle_y, 0), make_float3(middle_x, m_max.y, middle_z)); m_children[3] = new OctreeNode(make_float3(middle_x, middle_y, m_min.z), make_float3(m_max.x, m_max.y, middle_z)); m_children[4] = new OctreeNode(m_min + make_float3(0, 0, middle_z), make_float3(middle_x, middle_y, m_max.z)); m_children[5] = new OctreeNode(make_float3(middle_x, m_min.y, middle_z), make_float3(m_max.x, middle_y, m_max.z)); m_children[6] = new OctreeNode(m_min + make_float3(0, middle_y, middle_z), make_float3(middle_x, m_max.y, m_max.z)); m_children[7] = new OctreeNode(make_float3(middle_x, middle_y, middle_z), make_float3(m_max.x, m_max.y, m_max.z)); } void insert(const std::vector& triangles_geometry, int triangle_id_to_insert, int current_depth, int max_depth, int leaf_max_obj_count) { bool depth_exceeded = max_depth != -1 && current_depth == max_depth; if (m_is_leaf || depth_exceeded) { m_triangles.push_back(triangle_id_to_insert); if (m_triangles.size() > leaf_max_obj_count && !depth_exceeded) { m_is_leaf = false;//This node isn't a leaf anymore create_children(max_depth, leaf_max_obj_count); for (int triangle_id : m_triangles) insert_to_children(triangles_geometry, triangle_id, current_depth, max_depth, leaf_max_obj_count); m_triangles.clear(); m_triangles.shrink_to_fit(); } } else insert_to_children(triangles_geometry, triangle_id_to_insert, current_depth, max_depth, leaf_max_obj_count); } void insert_to_children(const std::vector& triangles_geometry, int triangle_id_to_insert, int current_depth, int max_depth, int leaf_max_obj_count) { const Triangle& triangle = triangles_geometry[triangle_id_to_insert]; float3 bbox_centroid = triangle.bbox_centroid(); float middle_x = (m_min.x + m_max.x) / 2; float middle_y = (m_min.y + m_max.y) / 2; float middle_z = (m_min.z + m_max.z) / 2; int octant_index = 0; if (bbox_centroid.x > middle_x) octant_index += 1; if (bbox_centroid.y > middle_y) octant_index += 2; if (bbox_centroid.z > middle_z) octant_index += 4; m_children[octant_index]->insert(triangles_geometry, triangle_id_to_insert, current_depth + 1, max_depth, leaf_max_obj_count); } bool intersect(const std::vector& triangles_geometry, const hiprtRay& ray, hiprtHit& hit_info, void* filter_function_payload) const { float trash; float denoms[BVHConstants::PLANES_COUNT]; float numers[BVHConstants::PLANES_COUNT]; for (int i = 0; i < BVHConstants::PLANES_COUNT; i++) { denoms[i] = hippt::dot(BoundingVolume::PLANE_NORMALS[i], ray.direction); numers[i] = hippt::dot(BoundingVolume::PLANE_NORMALS[i], float3(ray.origin)); } return intersect(triangles_geometry, ray, hit_info, trash, denoms, numers, filter_function_payload); } bool intersect(const std::vector& triangles_geometry, const hiprtRay& ray, hiprtHit& hit_info, float& t_near, float* denoms, float* numers, void* filter_function_payload) const { float t_far, trash; if (!m_bounding_volume.intersect(trash, t_far, denoms, numers)) return false; if (m_is_leaf) { for (int triangle_id : m_triangles) { const Triangle& triangle = triangles_geometry[triangle_id]; hiprtHit localHit; if (triangle.intersect(ray, localHit)) { localHit.primID = triangle_id; if (localHit.t < ray.minT) // minT test not passed continue; if (filter_function(ray, nullptr, filter_function_payload, localHit)) // Hit is filtered continue; if (localHit.t < hit_info.t || hit_info.t == -1) hit_info = localHit; } } t_near = hit_info.t; return t_near > 0; } std::priority_queue, std::greater> intersection_queue; for (int i = 0; i < 8; i++) { float inter_distance; if (m_children[i]->m_bounding_volume.intersect(inter_distance, t_far, denoms, numers)) intersection_queue.emplace(QueueElement(m_children[i], inter_distance)); } bool intersection_found = false; float closest_inter = 100000000, inter_distance = 100000000; while (!intersection_queue.empty()) { QueueElement top_element = intersection_queue.top(); intersection_queue.pop(); if (top_element.m_node->intersect(triangles_geometry, ray, hit_info, inter_distance, denoms, numers, filter_function_payload)) { closest_inter = std::min(closest_inter, inter_distance); intersection_found = true; //If we found an intersection that is closer than //the next element in the queue, we can stop intersecting further if (intersection_queue.empty() || closest_inter < intersection_queue.top()._t_near) { t_near = closest_inter; return true; } } } if (!intersection_found) return false; else { t_near = closest_inter; return true; } } //If this node has been subdivided (and thus cannot accept any triangles), //this boolean will be set to false bool m_is_leaf = true; std::vector m_triangles; std::array m_children = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; float3 m_min, m_max; BoundingVolume m_bounding_volume; }; public: BVH(); BVH(std::vector* triangles, int max_depth = 32, int leaf_max_obj_count = 8); ~BVH(); void operator=(BVH&& bvh); bool intersect(const hiprtRay& ray, hiprtHit& hit_info, void* filter_function_payload) const; private: void build_bvh(int max_depth, int leaf_max_obj_count, float3 min, float3 max, const BoundingVolume& volume); public: OctreeNode* m_root; std::vector* m_triangles; }; #endif ================================================ FILE: src/Renderer/BVHConstants.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef BVH_CONSTANTS_H #define BVH_CONSTANTS_H struct BVHConstants { static constexpr int FLATTENED_BVH_MAX_STACK_SIZE = 100000; static constexpr int PLANES_COUNT = 7; static constexpr int MAX_TRIANGLES_PER_LEAF = 8; }; #endif ================================================ FILE: src/Renderer/Baker/GGXConductorDirectionalAlbedoSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GGX_CONDUCTOR_DIRECTIONAL_ALBEDO_SETTINGS_H #define GGX_CONDUCTOR_DIRECTIONAL_ALBEDO_SETTINGS_H #include "Renderer/Baker/GPUBakerConstants.h" struct GGXConductorDirectionalAlbedoSettings { int texture_size_cos_theta = GPUBakerConstants::GGX_CONDUCTOR_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O; int texture_size_roughness = GPUBakerConstants::GGX_CONDUCTOR_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS; GGXMaskingShadowingFlavor masking_shadowing_term; int integration_sample_count = 262144; }; #endif ================================================ FILE: src/Renderer/Baker/GGXFresnelDirectionalAlbedoSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GGX_FRESNEL_DIRECTIONAL_ALBEDO_SETTINGS_H #define GGX_FRESNEL_DIRECTIONAL_ALBEDO_SETTINGS_H #include "Renderer/Baker/GPUBakerConstants.h" struct GGXFresnelDirectionalAlbedoSettings { int texture_size_cos_theta = GPUBakerConstants::GGX_FRESNEL_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O; int texture_size_roughness = GPUBakerConstants::GGX_FRESNEL_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS; int texture_size_ior = GPUBakerConstants::GGX_FRESNEL_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR; GGXMaskingShadowingFlavor masking_shadowing_term; int integration_sample_count = 65536; }; #endif ================================================ FILE: src/Renderer/Baker/GGXGlassDirectionalAlbedoSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GGX_GLASS_DIRECTIONAL_ALBEDO_SETTINGS_H #define GGX_GLASS_DIRECTIONAL_ALBEDO_SETTINGS_H #include "Renderer/Baker/GPUBakerConstants.h" struct GGXGlassDirectionalAlbedoSettings { int texture_size_cos_theta_o = GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O; int texture_size_roughness = GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS; int texture_size_ior = GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR; GGXMaskingShadowingFlavor masking_shadowing_term; int integration_sample_count = 65536; }; #endif ================================================ FILE: src/Renderer/Baker/GGXThinGlassDirectionalAlbedoSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_SETTINGS_H #define GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_SETTINGS_H #include "Renderer/Baker/GPUBakerConstants.h" struct GGXThinGlassDirectionalAlbedoSettings { int texture_size_cos_theta_o = GPUBakerConstants::GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O; int texture_size_roughness = GPUBakerConstants::GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS; int texture_size_ior = GPUBakerConstants::GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR; GGXMaskingShadowingFlavor masking_shadowing_term; int integration_sample_count = 65536; }; #endif ================================================ FILE: src/Renderer/Baker/GPUBaker.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Renderer/GPURenderer.h" #include "Renderer/Baker/GPUBaker.h" #include "Threads/ThreadManager.h" extern ImGuiLogger g_imgui_logger; GPUBaker::GPUBaker(std::shared_ptr renderer) : m_renderer(renderer) { OROCHI_CHECK_ERROR(oroStreamCreate(&m_bake_stream)); m_compiler_priority_mutex = std::make_shared(); m_ggx_conductor_directional_albedo_bake_kernel = GPUBakerKernel(m_renderer, m_bake_stream, m_compiler_priority_mutex, DEVICE_KERNELS_DIRECTORY "/Baking/GGXConductorDirectionalAlbedo.h", "GGXConductorDirectionalAlbedoBake", "GGX conductor directional albedo"); m_ggx_fresnel_directional_albedo_bake_kernel = GPUBakerKernel(m_renderer, m_bake_stream, m_compiler_priority_mutex, DEVICE_KERNELS_DIRECTORY "/Baking/GGXFresnelDirectionalAlbedo.h", "GGXFresnelDirectionalAlbedoBake", "GGX fresnel directional albedo"); m_glossy_dielectric_directional_albedo_bake_kernel = GPUBakerKernel(m_renderer, m_bake_stream, m_compiler_priority_mutex, DEVICE_KERNELS_DIRECTORY "/Baking/GlossyDielectricDirectionalAlbedo.h", "GlossyDielectricDirectionalAlbedoBake", "dielectric directional albedo"); m_ggx_glass_entering_directional_albedo_bake_kernel = GPUBakerKernel(m_renderer, m_bake_stream, m_compiler_priority_mutex, DEVICE_KERNELS_DIRECTORY "/Baking/GGXGlassDirectionalAlbedo.h", "GGXGlassDirectionalAlbedoBakeEntering", "GGX glass directional albedo 1/2"); m_ggx_glass_exiting_directional_albedo_bake_kernel = GPUBakerKernel(m_renderer, m_bake_stream, m_compiler_priority_mutex, DEVICE_KERNELS_DIRECTORY "/Baking/GGXGlassDirectionalAlbedo.h", "GGXGlassDirectionalAlbedoBakeExiting", "GGX glass directional albedo 2/2"); m_ggx_thin_glass_directional_albedo_bake_kernel = GPUBakerKernel(m_renderer, m_bake_stream, m_compiler_priority_mutex, DEVICE_KERNELS_DIRECTORY "/Baking/GGXThinGlassDirectionalAlbedo.h", "GGXThinGlassDirectionalAlbedoBake", "GGX thin glass directional albedo"); } void GPUBaker::bake_ggx_conductor_directional_albedo(const GGXConductorDirectionalAlbedoSettings& bake_settings, const std::string& output_filename) { m_ggx_conductor_directional_albedo_bake_kernel.bake_internal( make_int3(bake_settings.texture_size_cos_theta, bake_settings.texture_size_roughness, 1), &bake_settings, bake_settings.integration_sample_count, output_filename); } bool GPUBaker::is_ggx_conductor_directional_albedo_bake_complete() const { return m_ggx_conductor_directional_albedo_bake_kernel.is_complete(); } void GPUBaker::bake_ggx_fresnel_directional_albedo(const GGXFresnelDirectionalAlbedoSettings& bake_settings, const std::string& output_filename) { m_ggx_fresnel_directional_albedo_bake_kernel.bake_internal( make_int3(bake_settings.texture_size_cos_theta, bake_settings.texture_size_roughness, bake_settings.texture_size_ior), &bake_settings, bake_settings.integration_sample_count, output_filename); } bool GPUBaker::is_ggx_fresnel_directional_albedo_bake_complete() const { return m_ggx_fresnel_directional_albedo_bake_kernel.is_complete(); } void GPUBaker::bake_glossy_dielectric_directional_albedo(const GlossyDielectricDirectionalAlbedoSettings& bake_settings, const std::string& output_filename) { m_glossy_dielectric_directional_albedo_bake_kernel.bake_internal( make_int3(bake_settings.texture_size_cos_theta_o, bake_settings.texture_size_roughness, bake_settings.texture_size_ior), &bake_settings, bake_settings.integration_sample_count, output_filename); } bool GPUBaker::is_glossy_dielectric_directional_albedo_bake_complete() const { return m_glossy_dielectric_directional_albedo_bake_kernel.is_complete(); } void GPUBaker::bake_ggx_glass_directional_albedo(const GGXGlassDirectionalAlbedoSettings& bake_settings, const std::string& output_filename) { m_ggx_glass_entering_directional_albedo_bake_kernel.bake_internal( make_int3(bake_settings.texture_size_cos_theta_o, bake_settings.texture_size_roughness, bake_settings.texture_size_ior), &bake_settings, bake_settings.integration_sample_count, output_filename); m_ggx_glass_exiting_directional_albedo_bake_kernel.bake_internal( make_int3(bake_settings.texture_size_cos_theta_o, bake_settings.texture_size_roughness, bake_settings.texture_size_ior), &bake_settings, bake_settings.integration_sample_count, "inv_" + output_filename); } bool GPUBaker::is_ggx_glass_directional_albedo_bake_complete() const { return m_ggx_glass_entering_directional_albedo_bake_kernel.is_complete() && m_ggx_glass_exiting_directional_albedo_bake_kernel.is_complete(); } void GPUBaker::bake_ggx_thin_glass_directional_albedo(const GGXThinGlassDirectionalAlbedoSettings& bake_settings, const std::string& output_filename) { m_ggx_thin_glass_directional_albedo_bake_kernel.bake_internal( make_int3(bake_settings.texture_size_cos_theta_o, bake_settings.texture_size_roughness, bake_settings.texture_size_ior), &bake_settings, bake_settings.integration_sample_count, output_filename); } bool GPUBaker::is_ggx_thin_glass_directional_albedo_bake_complete() const { return m_ggx_thin_glass_directional_albedo_bake_kernel.is_complete(); } ================================================ FILE: src/Renderer/Baker/GPUBaker.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GPU_BAKER_H #define GPU_BAKER_H #include "Compiler/GPUKernel.h" #include "HIPRT-Orochi/OrochiBuffer.h" #include "Image/Image.h" #include "Renderer/Baker/GlossyDielectricDirectionalAlbedoSettings.h" #include "Renderer/Baker/GGXConductorDirectionalAlbedoSettings.h" #include "Renderer/Baker/GGXFresnelDirectionalAlbedoSettings.h" #include "Renderer/Baker/GGXGlassDirectionalAlbedoSettings.h" #include "Renderer/Baker/GGXThinGlassDirectionalAlbedoSettings.h" #include "Renderer/Baker/GPUBakerKernel.h" #include "Renderer/GPURenderer.h" #include class GPUBaker { public: GPUBaker(std::shared_ptr renderer); void bake_ggx_conductor_directional_albedo(const GGXConductorDirectionalAlbedoSettings& bake_settings, const std::string& output_filename); bool is_ggx_conductor_directional_albedo_bake_complete() const; void bake_ggx_fresnel_directional_albedo(const GGXFresnelDirectionalAlbedoSettings& bake_settings, const std::string& output_filename); bool is_ggx_fresnel_directional_albedo_bake_complete() const; void bake_glossy_dielectric_directional_albedo(const GlossyDielectricDirectionalAlbedoSettings& bake_settings, const std::string& output_filename); bool is_glossy_dielectric_directional_albedo_bake_complete() const; void bake_ggx_glass_directional_albedo(const GGXGlassDirectionalAlbedoSettings& bake_settings, const std::string& output_filename); bool is_ggx_glass_directional_albedo_bake_complete() const; void bake_ggx_thin_glass_directional_albedo(const GGXThinGlassDirectionalAlbedoSettings& bake_settings, const std::string& output_filename); bool is_ggx_thin_glass_directional_albedo_bake_complete() const; private: std::shared_ptr m_renderer = nullptr; oroStream_t m_bake_stream; // Mutex so that if we're baking multiple textures at the same time, // we don't run into issue with the compilers wanting to take the priority // (over background compiling kerneks) at the same time std::shared_ptr m_compiler_priority_mutex; GPUBakerKernel m_ggx_conductor_directional_albedo_bake_kernel; GPUBakerKernel m_ggx_fresnel_directional_albedo_bake_kernel; GPUBakerKernel m_glossy_dielectric_directional_albedo_bake_kernel; GPUBakerKernel m_ggx_glass_entering_directional_albedo_bake_kernel; GPUBakerKernel m_ggx_glass_exiting_directional_albedo_bake_kernel; GPUBakerKernel m_ggx_thin_glass_directional_albedo_bake_kernel; }; #endif ================================================ FILE: src/Renderer/Baker/GPUBakerConstants.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GPU_BAKER_CONSTANTS_H #define GPU_BAKER_CONSTANTS_H #ifndef __KERNELCC__ #include #endif #include "HostDeviceCommon/BSDFsData.h" struct GPUBakerConstants { static const int GGX_CONDUCTOR_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O = 128; static const int GGX_CONDUCTOR_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS = 128; static const int GGX_FRESNEL_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O = 256; static const int GGX_FRESNEL_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS = 256; static const int GGX_FRESNEL_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR = 256; static const int GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O = 256; static const int GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS = 16; static const int GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR = 128; static const int GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O = 32; static const int GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS = 32; static const int GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR = 96; static const int GLOSSY_DIELECTRIC_TEXTURE_SIZE_COS_THETA_O = 128; static const int GLOSSY_DIELECTRIC_TEXTURE_SIZE_ROUGHNESS = 64; static const int GLOSSY_DIELECTRIC_TEXTURE_SIZE_IOR = 128; // Arbitrary number to limit how much computation we do per bake kernel launch. // This is to avoid driver timeouts static const int COMPUTE_ELEMENT_PER_BAKE_KERNEL_LAUNCH = 100000000; #ifndef __KERNELCC__ // Not using these on the GPU since they are std::string types: unavailable on the GPU // and besides, we don't these paths on the GPU, only the texture sizes static std::string get_GGX_conductor_directional_albedo_texture_filename(GGXMaskingShadowingFlavor masking_shadowing_term, int texture_size_cos_theta = GPUBakerConstants::GGX_CONDUCTOR_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O, int texture_size_roughness = GPUBakerConstants::GGX_CONDUCTOR_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS) { std::string flavor_string = masking_shadowing_term == GGXMaskingShadowingFlavor::HeightCorrelated ? "Correlated_" : "Uncorrelated_"; return "GGX_Conductor_" + flavor_string + std::to_string(texture_size_cos_theta) + "x" + std::to_string(texture_size_roughness) + ".hdr"; } static std::string get_GGX_fresnel_directional_albedo_texture_filename(GGXMaskingShadowingFlavor masking_shadowing_term, int texture_size_cos_theta = GPUBakerConstants::GGX_FRESNEL_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O, int texture_size_roughness = GPUBakerConstants::GGX_FRESNEL_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS, int texture_size_ior = GPUBakerConstants::GGX_FRESNEL_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR) { std::string flavor_string = masking_shadowing_term == GGXMaskingShadowingFlavor::HeightCorrelated ? "Correlated_" : "Uncorrelated_"; return "GGX_Fresnel_" + flavor_string + std::to_string(texture_size_cos_theta) + "x" + std::to_string(texture_size_roughness) + "x" + std::to_string(texture_size_ior) + ".hdr"; } static std::string get_glossy_dielectric_directional_albedo_texture_filename(GGXMaskingShadowingFlavor masking_shadowing_term, int texture_size_cos_theta = GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_COS_THETA_O, int texture_size_roughness = GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_ROUGHNESS, int texture_size_ior = GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_IOR) { std::string flavor_string = masking_shadowing_term == GGXMaskingShadowingFlavor::HeightCorrelated ? "Correlated_" : "Uncorrelated_"; return "Glossy_Ess_" + flavor_string + std::to_string(texture_size_cos_theta) + "x" + std::to_string(texture_size_roughness) + "x" + std::to_string(texture_size_ior) + ".hdr"; } static std::string get_GGX_glass_directional_albedo_texture_filename(GGXMaskingShadowingFlavor masking_shadowing_term, int texture_size_cos_theta = GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O, int texture_size_roughness = GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS, int texture_size_ior = GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR) { std::string flavor_string = masking_shadowing_term == GGXMaskingShadowingFlavor::HeightCorrelated ? "Correlated_" : "Uncorrelated_"; return "GGX_Glass_Ess_" + flavor_string + std::to_string(texture_size_cos_theta) + "x" + std::to_string(texture_size_roughness) + "x" + std::to_string(texture_size_ior) + ".hdr"; } static std::string get_GGX_thin_glass_directional_albedo_texture_filename(GGXMaskingShadowingFlavor masking_shadowing_term, int texture_size_cos_theta = GPUBakerConstants::GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O, int texture_size_roughness = GPUBakerConstants::GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS, int texture_size_ior = GPUBakerConstants::GGX_THIN_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR) { std::string flavor_string = masking_shadowing_term == GGXMaskingShadowingFlavor::HeightCorrelated ? "Correlated_" : "Uncorrelated_"; return "GGX_Thin_Glass_Ess_" + flavor_string + std::to_string(texture_size_cos_theta) + "x" + std::to_string(texture_size_roughness) + "x" + std::to_string(texture_size_ior) + ".hdr"; } static std::string get_GGX_glass_directional_albedo_inv_texture_filename(GGXMaskingShadowingFlavor masking_shadowing_term, int texture_size_cos_theta = GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_COS_THETA_O, int texture_size_roughness = GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_ROUGHNESS, int texture_size_ior = GPUBakerConstants::GGX_GLASS_DIRECTIONAL_ALBEDO_TEXTURE_SIZE_IOR) { std::string flavor_string = masking_shadowing_term == GGXMaskingShadowingFlavor::HeightCorrelated ? "Correlated_" : "Uncorrelated_"; return "inv_GGX_Glass_Ess_" + flavor_string + std::to_string(texture_size_cos_theta) + "x" + std::to_string(texture_size_roughness) + "x" + std::to_string(texture_size_ior) + ".hdr"; } #endif }; #endif ================================================ FILE: src/Renderer/Baker/GPUBakerKernel.cpp ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #include "Renderer/Baker/GPUBakerKernel.h" #include "Renderer/Baker/GPUBakerConstants.h" #include "Threads/ThreadManager.h" GPUBakerKernel::GPUBakerKernel(std::shared_ptr renderer, oroStream_t bake_stream, std::shared_ptr compiler_priority_mutex, const std::string& kernel_filepath, const std::string& kernel_function, const std::string& kernel_title) { m_renderer = renderer; m_bake_stream = bake_stream; m_kernel_filepath = kernel_filepath; m_kernel_function = kernel_function; m_kernel_title = kernel_title; } void GPUBakerKernel::bake_internal(int3 bake_resolution, const void* bake_settings_pointer, int nb_kernel_iterations, std::string output_filename) { m_bake_complete = false; struct ThreadData { int3 bake_resolution; const void* bake_settings_pointer; std::string output_filename; }; // Allocating that on the heap so that it stays alive for the thread even // when we return from this function std::shared_ptr thread_data = std::make_shared(); thread_data->bake_resolution = bake_resolution; thread_data->bake_settings_pointer = bake_settings_pointer; thread_data->output_filename = output_filename; // Starting everything on a thread to avoid blocking to UI (during the compilation // of the kernel mainly) ThreadManager::start_thread("kernel_bake_" + m_kernel_title, [this, thread_data, nb_kernel_iterations] { OROCHI_CHECK_ERROR(oroCtxSetCurrent(m_renderer->get_hiprt_orochi_ctx()->orochi_ctx)); int3& bake_resolution = thread_data->bake_resolution; const void* bake_settings_pointer = thread_data->bake_settings_pointer; std::string& output_filename = thread_data->output_filename; if (!m_bake_kernel.has_been_compiled()) { g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_INFO, "%s", ("Compiling " + m_kernel_title + " kernel...").c_str()); m_bake_kernel = GPUKernel(m_kernel_filepath, m_kernel_function); m_bake_kernel.compile(m_renderer->get_hiprt_orochi_ctx()); } m_bake_buffer.resize(bake_resolution.x * bake_resolution.y * bake_resolution.z); g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_INFO, "%s", ("Launching " + m_kernel_title + " baking...").c_str()); int3 tile_size; if (bake_resolution.z > 1) // 3D launch tile_size = make_int3(4, 4, 4); else // 2D launch tile_size = make_int3(8, 8, 1); float kernel_duration; oroEvent_t start, stop; OROCHI_CHECK_ERROR(oroEventCreate(&start)); OROCHI_CHECK_ERROR(oroEventCreate(&stop)); OROCHI_CHECK_ERROR(oroEventRecord(start, m_bake_stream)); // Zeroing the buffer that we're going to accumulate the bake data into m_bake_buffer.memset_whole_buffer(0); // Launching many "small" kernels to avoid driver timeouts int iterations_per_kernel = floor(hippt::max(1.0f, (float)GPUBakerConstants::COMPUTE_ELEMENT_PER_BAKE_KERNEL_LAUNCH / (bake_resolution.x * bake_resolution.y * bake_resolution.z))); int nb_kernel_launch = ceil(nb_kernel_iterations / (float)iterations_per_kernel); void* non_const_setting = const_cast(bake_settings_pointer); for (int i = 0; i < nb_kernel_launch; i++) { // The current iteration variable is used in the kernel to shuffle the random // so that we get different random numbers per each kernel launch int current_iteration = i + 1; float* device_buffer = m_bake_buffer.get_device_pointer(); void* bake_kernel_launch_args[] = { &iterations_per_kernel, ¤t_iteration, non_const_setting, &device_buffer }; m_bake_kernel.launch_asynchronous_3D( tile_size.x, tile_size.y, tile_size.z, bake_resolution.x, bake_resolution.y, bake_resolution.z, bake_kernel_launch_args, m_bake_stream); } OROCHI_CHECK_ERROR(oroEventRecord(stop, m_bake_stream)); OROCHI_CHECK_ERROR(oroStreamSynchronize(m_bake_stream)); OROCHI_CHECK_ERROR(oroEventElapsedTime(&kernel_duration, start, stop)); OROCHI_CHECK_ERROR(oroEventDestroy(start)); OROCHI_CHECK_ERROR(oroEventDestroy(stop)); std::string unit_suffix = kernel_duration < 1000.0f ? "ms!" : "s!"; kernel_duration = kernel_duration > 1000.0f ? kernel_duration / 1000.0f : kernel_duration; g_imgui_logger.add_line(ImGuiLoggerSeverity::IMGUI_LOGGER_INFO, "%s", (m_kernel_title + " completed in " + std::to_string(kernel_duration) + unit_suffix).c_str()); if (bake_resolution.z > 1) { // 3D texture std::vector baked_data = m_bake_buffer.download_data(); for (int i = 0; i < bake_resolution.z; i++) { Image32Bit image = Image32Bit(baked_data.data() + i * bake_resolution.x * bake_resolution.y, bake_resolution.x, bake_resolution.y, /* nb channels */ 1); std::string final_filename = std::to_string(i) + output_filename; image.write_image_hdr(final_filename.c_str(), false); } } else { // A single 2D image std::vector baked_data = m_bake_buffer.download_data(); Image32Bit image = Image32Bit(baked_data, bake_resolution.x, bake_resolution.y, 1); image.write_image_hdr(output_filename.c_str(), false); } m_bake_buffer.free(); m_bake_complete = true; }); ThreadManager::detach_threads("kernel_bake_" + m_kernel_title); } bool GPUBakerKernel::is_complete() const { return m_bake_complete; } ================================================ FILE: src/Renderer/Baker/GPUBakerKernel.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef GPU_BAKER_KERNEL_H #define GPU_BAKER_KERNEL_H #include "Renderer/GPURenderer.h" #include class GPUBakerKernel { public: GPUBakerKernel() {} GPUBakerKernel(std::shared_ptr renderer, oroStream_t bake_stream, std::shared_ptr compiler_priority_mutex, const std::string& kernel_filepath, const std::string& kernel_function, const std::string& kernel_title); /** * Starts the baking process */ void bake_internal(int3 bake_resolution, const void* bake_settings_pointer, int nb_kernel_iterations, std::string output_filename); /** * Is the baking process complete? */ bool is_complete() const; private: bool m_bake_complete = true; std::shared_ptr m_renderer = nullptr; oroStream_t m_bake_stream = nullptr; // Filepath and function within this file that will be launched // when the baking of the kernel starts std::string m_kernel_filepath = ""; std::string m_kernel_function = ""; // String used for logging infos std::string m_kernel_title = ""; // State for baking GGX conductors directional albedo GPUKernel m_bake_kernel; // Buffer for holding the baked data OrochiBuffer m_bake_buffer; }; #endif ================================================ FILE: src/Renderer/Baker/GlossyDielectricDirectionalAlbedoSettings.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef DIELECTRIC_FRESNEL_DIRECTIONAL_ALBEDO_SETTINGS_H #define DIELECTRIC_FRESNEL_DIRECTIONAL_ALBEDO_SETTINGS_H #include "Renderer/Baker/GPUBakerConstants.h" struct GlossyDielectricDirectionalAlbedoSettings { int texture_size_cos_theta_o = GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_COS_THETA_O; int texture_size_roughness = GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_ROUGHNESS; int texture_size_ior = GPUBakerConstants::GLOSSY_DIELECTRIC_TEXTURE_SIZE_IOR; GGXMaskingShadowingFlavor masking_shadowing_term; int integration_sample_count = 131072; }; #endif ================================================ FILE: src/Renderer/BoundingVolume.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef BOUNDING_VOLUME_H #define BOUNDING_VOLUME_H #include "BVHConstants.h" #include "Renderer/Triangle.h" #include struct BoundingVolume { static const float3 PLANE_NORMALS[BVHConstants::PLANES_COUNT]; std::array _d_near; std::array _d_far; BoundingVolume() { for (int i = 0; i < BVHConstants::PLANES_COUNT; i++) { _d_near[i] = INFINITY; _d_far[i] = -INFINITY; } } static void triangle_volume(const Triangle& triangle, std::array& d_near, std::array& d_far) { for (int i = 0; i < BVHConstants::PLANES_COUNT; i++) { for (int j = 0; j < 3; j++) { float dist = hippt::dot(BoundingVolume::PLANE_NORMALS[i], float3(triangle[j])); d_near[i] = hippt::min(d_near[i], dist); d_far[i] = hippt::max(d_far[i], dist); } } } void extend_volume(const std::array& d_near, const std::array& d_far) { for (int i = 0; i < BVHConstants::PLANES_COUNT; i++) { _d_near[i] = hippt::min(_d_near[i], d_near[i]); _d_far[i] = hippt::max(_d_far[i], d_far[i]); } } void extend_volume(const BoundingVolume& volume) { extend_volume(volume._d_near, volume._d_far); } void extend_volume(const Triangle& triangle) { std::array d_near; std::array d_far; for (int i = 0; i < BVHConstants::PLANES_COUNT; i++) { d_near[i] = INFINITY; d_far[i] = -INFINITY; } triangle_volume(triangle, d_near, d_far); extend_volume(d_near, d_far); } static bool intersect(const std::array& d_near, const std::array& d_far, const std::array& denoms, const std::array& numers) { float t_near = -INFINITY; float t_far = INFINITY; for (int i = 0; i < BVHConstants::PLANES_COUNT; i++) { float denom = denoms[i]; if (denom == 0.0f) continue; //inverse denom to avoid division float d_near_i = (d_near[i] - numers[i]) / denom; float d_far_i = (d_far[i] - numers[i]) / denom; if (denom < 0) std::swap(d_near_i, d_far_i); t_near = hippt::max(t_near, d_near_i); t_far = hippt::min(t_far, d_far_i); if (t_far < t_near) return false; } return true; } /** * @params denoms Precomputed denominators */ bool intersect(float& t_near, float& t_far, float* denoms, float* numers) const { t_near = -INFINITY; t_far = INFINITY; for (int i = 0; i < BVHConstants::PLANES_COUNT; i++) { float denom = denoms[i]; if (denom == 0.0f) continue; //inverse denom to avoid division float d_near_i = (_d_near[i] - numers[i]) / denom; float d_far_i = (_d_far[i] - numers[i]) / denom; if (denom < 0.0f) std::swap(d_near_i, d_far_i); t_near = hippt::max(t_near, d_near_i); t_far = hippt::min(t_far, d_far_i); if (t_far < t_near) return false; } return true; } }; #endif ================================================ FILE: src/Renderer/CPUDataStructures/GBufferCPUData.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef G_BUFFER_CPU_RENDERER_H #define G_BUFFER_CPU_RENDERER_H #include "Device/includes/RayVolumeState.h" #include "HostDeviceCommon/Material/MaterialPacked.h" #include // GBuffer that stores information about the current frame first hit data struct GBufferCPUData { void resize(unsigned int new_element_count) { materials.resize(new_element_count); geometric_normals.resize(new_element_count); shading_normals.resize(new_element_count); primary_hit_position.resize(new_element_count); first_hit_prim_index.resize(new_element_count); cameray_ray_hit.resize(new_element_count); ray_volume_states.resize(new_element_count); } std::vector materials; std::vector geometric_normals; std::vector shading_normals; std::vector primary_hit_position; std::vector first_hit_prim_index; std::vector cameray_ray_hit; std::vector ray_volume_states; }; #endif ================================================ FILE: src/Renderer/CPUDataStructures/GMoNCPUData.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef RENDERER_GMON_CPU_DATA_H #define RENDERER_GMON_CPU_DATA_H #include "Renderer/CPUGPUCommonDataStructures/GMoNCPUGPUCommonData.h" /** * CPU-side data structure for the implementation of GMoN on the CPU * * Reference: * [1] [Firefly removal in Monte Carlo rendering with adaptive Median of meaNs, Buisine et al., 2021] */ struct GMoNCPUData : public GMoNCPUGPUCommonData { void resize(unsigned int render_width, unsigned int render_height) { sets.resize(render_width * render_height * number_of_sets); result_framebuffer = Image32Bit(render_width, render_height, /* channels */ 3); } // This is one very big buffer that contains all the sets we accumulate into for GMoN // // For example, if GMoNMSets == 5 and a render resolution of 1280x720, // this is going to be a buffer that is 1280*720*5 elements long std::vector sets; // This is the buffer that contains the G-median of means result of each pixel and this is going // to be displayed in the viewport instead of the regular framebuffer if GMoN is being used Image32Bit result_framebuffer; unsigned int number_of_sets = GMoNMSetsCount; }; #endif ================================================ FILE: src/Renderer/CPUDataStructures/MaterialPackedSoACPUData.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef HOST_DEVICE_COMMON_MATERIAL_PACKED_SOA_CPU_DATA_H #define HOST_DEVICE_COMMON_MATERIAL_PACKED_SOA_CPU_DATA_H #include "HostDeviceCommon/Color.h" #include "HostDeviceCommon/Material/MaterialPackedSoA.h" #include "Renderer/CPUGPUCommonDataStructures/DevicePackedMaterialSoACPUGPUCommonData.h" #define DECLARE_ALL_MEMBERS_STD_TIE \ auto all_members = std::tie( \ normal_map_emission_index, base_color_roughness_metallic_index, \ roughness_and_metallic_index, anisotropic_specular_index, \ coat_sheen_index, specular_transmission_index, \ \ flags, \ \ emission, \ \ base_color_roughness, \ \ oren_nayar_sigma, \ \ metallic_F90_and_metallic, metallic_F82_packed_and_diffuse_transmission, \ metallic_F90_falloff_exponent, \ anisotropy_and_rotation_and_second_roughness, \ \ specular_color_and_tint_factor, \ specular_and_darkening_and_coat_roughness, coat_medium_thickness, \ coat_and_medium_absorption, \ coat_roughening_darkening_anisotropy_and_rotation, coat_ior, \ \ sheen_and_color, \ \ ior, absorption_color_packed, absorption_at_distance, \ \ sheen_roughness_transmission_dispersion_thin_film, \ \ dispersion_abbe_number, thin_film_ior, thin_film_thickness, \ thin_film_kappa_3, thin_film_base_ior_override, \ alpha_thin_film_hue_dielectric_priority); /** * These two structures here are just there to hold all the buffers created on the CPU * * The device pointers of these buffers are then set on to the RenderData of the CPU * * For a documentation of what's packed into the members ('specular_and_darkening_and_coat_roughness' for example), * see the 'DevicePackedEffectiveMaterialSoA' class */ struct DevicePackedEffectiveMaterialSoACPUData : public DevicePackedMaterialSoACPUGPUCommonData { std::vector flags; std::vector emission; std::vector base_color_roughness; std::vector oren_nayar_sigma; std::vector metallic_F90_and_metallic; std::vector metallic_F82_packed_and_diffuse_transmission; std::vector metallic_F90_falloff_exponent; std::vector anisotropy_and_rotation_and_second_roughness; std::vector specular_color_and_tint_factor; std::vector specular_and_darkening_and_coat_roughness; std::vector coat_medium_thickness; std::vector coat_and_medium_absorption; std::vector coat_roughening_darkening_anisotropy_and_rotation; std::vector coat_ior; std::vector sheen_and_color; std::vector ior; std::vector absorption_color_packed; std::vector absorption_at_distance; std::vector sheen_roughness_transmission_dispersion_thin_film; std::vector dispersion_abbe_number; std::vector thin_film_ior; std::vector thin_film_thickness; std::vector thin_film_kappa_3; std::vector thin_film_base_ior_override; std::vector alpha_thin_film_hue_dielectric_priority; }; struct DevicePackedTexturedMaterialSoACPUData : public DevicePackedEffectiveMaterialSoACPUData { std::vector normal_map_emission_index; std::vector base_color_roughness_metallic_index; std::vector roughness_and_metallic_index; std::vector anisotropic_specular_index; std::vector coat_sheen_index; std::vector specular_transmission_index; // Resize function using the generic for_each_member void resize(size_t new_element_count) { m_element_count = new_element_count; // This declares a std::tie of all the buffers DECLARE_ALL_MEMBERS_STD_TIE; // Function that will be applied to all the buffers to resize them auto resize_lambda_function = [new_element_count](auto& buffer) { buffer.resize(new_element_count); }; // Applying the resize function to all the buffers std::apply([&](auto&... args) { (resize_lambda_function(args), ...); }, all_members); } void upload_data(std::vector& gpu_packed_materials) { DevicePackedTexturedMaterial* data = gpu_packed_materials.data(); size_t element_count = gpu_packed_materials.size(); // Textured part normal_map_emission_index = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, normal_map_emission_index), element_count); base_color_roughness_metallic_index = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, base_color_roughness_metallic_index), element_count); roughness_and_metallic_index = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, roughness_and_metallic_index), element_count); anisotropic_specular_index = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, anisotropic_specular_index), element_count); coat_sheen_index = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, coat_sheen_index), element_count); specular_transmission_index = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, specular_transmission_index), element_count); // Non textured parameters flags = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, flags), element_count); emission = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, emission), element_count); base_color_roughness = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, base_color_roughness), element_count); oren_nayar_sigma = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, oren_nayar_sigma), element_count); metallic_F90_and_metallic = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, metallic_F90_and_metallic), element_count); metallic_F82_packed_and_diffuse_transmission = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, metallic_F82_packed_and_diffuse_transmission), element_count); metallic_F90_falloff_exponent = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, metallic_F90_falloff_exponent), element_count); anisotropy_and_rotation_and_second_roughness = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, anisotropy_and_rotation_and_second_roughness), element_count); specular_color_and_tint_factor = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, specular_color_and_tint_factor), element_count); specular_and_darkening_and_coat_roughness = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, specular_and_darkening_and_coat_roughness), element_count); coat_medium_thickness = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, coat_medium_thickness), element_count); coat_and_medium_absorption = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, coat_and_medium_absorption), element_count); coat_roughening_darkening_anisotropy_and_rotation = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, coat_roughening_darkening_anisotropy_and_rotation), element_count); coat_ior = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, coat_ior), element_count); sheen_and_color = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, sheen_and_color), element_count); ior = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, ior), element_count); absorption_color_packed = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, absorption_color_packed), element_count); absorption_at_distance = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, absorption_at_distance), element_count); sheen_roughness_transmission_dispersion_thin_film = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, sheen_roughness_transmission_dispersion_thin_film), element_count); dispersion_abbe_number = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, dispersion_abbe_number), element_count); thin_film_ior = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, thin_film_ior), element_count); thin_film_thickness = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, thin_film_thickness), element_count); thin_film_kappa_3 = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, thin_film_kappa_3), element_count); thin_film_base_ior_override = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, thin_film_base_ior_override), element_count); alpha_thin_film_hue_dielectric_priority = expand_from_gpu_packed_materials(0, data, offsetof(DevicePackedTexturedMaterial, alpha_thin_film_hue_dielectric_priority), element_count); } DevicePackedTexturedMaterialSoA get_device_SoA_struct() { DevicePackedTexturedMaterialSoA out; out.normal_map_emission_index = normal_map_emission_index.data(); out.base_color_roughness_metallic_index = base_color_roughness_metallic_index.data(); out.roughness_and_metallic_index = roughness_and_metallic_index.data(); out.anisotropic_specular_index = anisotropic_specular_index.data(); out.coat_sheen_index = coat_sheen_index.data(); out.specular_transmission_index = specular_transmission_index.data(); out.flags = flags.data(); out.emission = emission.data(); out.base_color_roughness = base_color_roughness.data(); out.oren_nayar_sigma = oren_nayar_sigma.data(); out.metallic_F90_and_metallic = metallic_F90_and_metallic.data(); out.metallic_F82_packed_and_diffuse_transmission = metallic_F82_packed_and_diffuse_transmission.data(); out.metallic_F90_falloff_exponent = metallic_F90_falloff_exponent.data(); out.anisotropy_and_rotation_and_second_roughness = anisotropy_and_rotation_and_second_roughness.data(); out.specular_color_and_tint_factor = specular_color_and_tint_factor.data(); out.specular_and_darkening_and_coat_roughness = specular_and_darkening_and_coat_roughness.data(); out.coat_medium_thickness = coat_medium_thickness.data(); out.coat_and_medium_absorption = coat_and_medium_absorption.data(); out.coat_roughening_darkening_anisotropy_and_rotation = coat_roughening_darkening_anisotropy_and_rotation.data(); out.coat_ior = coat_ior.data(); out.sheen_and_color = sheen_and_color.data(); out.ior = ior.data(); out.absorption_color_packed = absorption_color_packed.data(); out.absorption_at_distance = absorption_at_distance.data(); out.sheen_roughness_transmission_dispersion_thin_film = sheen_roughness_transmission_dispersion_thin_film.data(); out.dispersion_abbe_number = dispersion_abbe_number.data(); out.thin_film_ior = thin_film_ior.data(); out.thin_film_thickness = thin_film_thickness.data(); out.thin_film_kappa_3 = thin_film_kappa_3.data(); out.thin_film_base_ior_override = thin_film_base_ior_override.data(); out.alpha_thin_film_hue_dielectric_priority = alpha_thin_film_hue_dielectric_priority.data(); return out; } size_t m_element_count = 0; }; #endif ================================================ FILE: src/Renderer/CPUDataStructures/NEEPlusPlusCPUData.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef RENDERER_NEE_PLUS_PLUS_CPU_DATA_H #define RENDERER_NEE_PLUS_PLUS_CPU_DATA_H // For int3 and AtomicType #include "HostDeviceCommon/Math.h" #include struct NEEPlusPlusCPUData { int frame_timer_before_visibility_map_update = 1; std::vector> total_unoccluded_rays; std::vector> total_num_rays; std::vector> num_rays_staging; std::vector> unoccluded_rays_staging; std::vector> checksum_buffer; AtomicType total_shadow_ray_queries; AtomicType shadow_rays_actually_traced; AtomicType total_cell_alive_count; }; #endif ================================================ FILE: src/Renderer/CPUGPUCommonDataStructures/DevicePackedMaterialSoACPUGPUCommonData.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef RENDERER_DEVICE_PACKED_MATERIAL_SOA_CPU_GPU_COMMON_DATA_H #define RENDERER_DEVICE_PACKED_MATERIAL_SOA_CPU_GPU_COMMON_DATA_H struct DevicePackedMaterialSoACPUGPUCommonData { /** * Takes a pointer to some 'DevicePackedTexturedMaterial' in the 'gpu_packed_materials' array (which could be std::vector().data() for example) * and returns a vector of type T that contains 'element_count' elements at offset 'offset' of the 'DevicePackedTexturedMaterial' structure * * For example: * expand_from_gpu_packed_materials(3, gpu_packed_materials, offsetof(DevicePackedTexturedMaterial, normal_map_emission_index), 2) * * return an std::vector that contains the 'normal_map_emission_index' of gpu_packed_materials[3] and gpu_packed_materials[4] */ template std::vector expand_from_gpu_packed_materials(unsigned int start_index, const DevicePackedTexturedMaterial* gpu_packed_materials, size_t offset_in_struct, size_t element_count) { std::vector out(element_count); for (int i = 0; i < element_count; i++) out[i] = *reinterpret_cast(reinterpret_cast(&gpu_packed_materials[start_index + i]) + offset_in_struct); return out; } }; #endif ================================================ FILE: src/Renderer/CPUGPUCommonDataStructures/GMoNCPUGPUCommonData.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef RENDERER_GMON_CPU_GPU_COMMON_DATA_H #define RENDERER_GMON_CPU_GPU_COMMON_DATA_H #include "HostDeviceCommon/KernelOptions/GMoNOptions.h" struct GMoNCPUGPUCommonData { // Whether or not GMoN is actively being used bool using_gmon = HIPRTRenderSettings::DEBUG_DEV_GMON_BLEND_WEIGHTS; // How much to blend between the non-GMoN output and the GMoN output float gmon_blend_factor = 0.0f; bool gmon_auto_blend_factor = true; int2 current_resolution = make_int2(1280, 720); unsigned int current_number_of_sets = GMoNMSetsCount; }; #endif ================================================ FILE: src/Renderer/CPUGPUCommonDataStructures/GenericSoA.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef RENDERER_GENERIC_SOA_H #define RENDERER_GENERIC_SOA_H #include #include #include #include #include "HostDeviceCommon/AtomicType.h" template class Container> using GenericAtomicType = typename std::conditional_t, std::vector>::value, AtomicType, T>; // Helper to detect std::atomic<...> // // std::false_type and std::true_type are structures that // have ::value equal to 'false' or ::value equal to 'true' respectively // // By inheriting from std::false_type or std::true_type, we can check at compile time // what's our ::value and use a constexpr if() on that template struct IsStdAtomic : std::false_type {}; template struct IsStdAtomic> : std::true_type {}; /** * Can be used to create a structure of arrays for multiple buffers of different types. * * For example, to declare an SoA of 3 buffers: float, float and int, this can be used as: * * GenericSoA if the SoA is meant to be used on the CPU (std::vector) * GenericSoA if the SoA is meant to be used on the GPU (OrochiBuffer) * * The 'Container' type must support the following operations: * - resize(int new_element_count) -> resizes the container to hold new_element_count elements * - size() -> returns the number of elements in the container */ template< template class Container, typename... Ts> struct GenericSoA { template using BufferTypeFromVariable = typename std::decay_t::value_type; template using BufferTypeFromIndex = typename std::tuple_element...>>::type::value_type; using IsCPUBuffer = std::is_same>, std::vector>>; void resize(std::size_t new_element_count) { // Applies resize(new_element_count) on each buffer in the tuple std::apply([this, new_element_count](auto&... buffer) { (resize_buffer_internal(buffer, new_element_count), ...); }, buffers); } std::size_t get_byte_size() const { std::size_t total = 0; // For each container, add sizeof(value_type) * size() std::apply([&](auto const&... buffer) { ((total += buffer.size() * sizeof(BufferTypeFromVariable)), ...); }, buffers); return total; } unsigned int size() const { return std::get<0>(buffers).size(); } template void memset_buffer(BufferTypeFromIndex memset_value) { if constexpr (IsCPUBuffer::value) { if constexpr (IsStdAtomic>::value) { // For atomic types, we have to store into them with a loop because they do not have an =operator() // so we can't use std::fill for (auto& value : get_buffer()) value.store(memset_value); } else std::fill(get_buffer().begin(), get_buffer().end(), memset_value); } else { std::vector> data(size(), memset_value); get_buffer().upload_data(data); } } template auto& get_buffer() { return std::get(buffers); } template auto* get_buffer_data_ptr() { return std::get(buffers).data(); } template auto* get_buffer_data_atomic_ptr() { if constexpr (IsCPUBuffer::value) return std::get(buffers).data(); else // For the GPU, calling the 'get_atomic_device_pointer' of OrochiBuffer return std::get(buffers).get_atomic_device_pointer(); } template void upload_to_buffer(const std::vector>& data) { if constexpr (IsCPUBuffer::value) { // If our main container type for this SoA is std::vector (i.e. this is for the CPU), then we're uploading // to the buffer simply by copying get_buffer() = data; } else { // If our main container type for this SoA is OrochiBuffer (i.e. this is for the GPU), then we're uploading // to the buffer by uploading to the GPU get_buffer().upload_data(data); } } void free() { // Applies clear() on each buffer in the tuple std::apply([](auto&... buffer) { // decltype here gives us the exact type of 'buffer' which can be std::vector& for example, // **with** the reference type // // But we want to clear the buffer by overriding it with a newly instantiated buffer so we don't want // the reference, hence the use of std::decay_t ((buffer = std::decay_t{}), ...); }, buffers); } private: template void resize_buffer_internal(BufferType& buffer, std::size_t new_element_count) { if constexpr (IsStdAtomic::value) // If the buffer is a buffer of std::atomic on the CPU, we cannot use resize // (because std::atomic are missing some operators used by // std::vector.resize() so we have to recreate the buffer instead buffer = std::decay_t(new_element_count); else buffer.resize(new_element_count); } std::tuple...> buffers; }; namespace GenericSoAHelpers { template class BufferContainer, typename T, typename U> void memset_buffer(BufferContainer& buffer, U memset_value) { if constexpr (std::is_same_v, std::vector>) { // std::vector type if constexpr (IsStdAtomic::value) { // For atomic types, we have to store into them with a loop because they do not have an =operator() // so we can't use std::fill for (auto& value : buffer) value.store(memset_value); } else std::fill(buffer.begin(), buffer.end(), memset_value); } else { std::vector data(buffer.size(), memset_value); buffer.upload_data(data); } } template class BufferContainer, typename T> void resize(BufferContainer& buffer, std::size_t new_size) { if constexpr (IsStdAtomic::value) // If the buffer is a buffer of std::atomic on the CPU, we cannot use resize // (because std::atomic are missing some operators used by // std::vector.resize() so we have to recreate the buffer instead buffer = std::decay_t(new_size); else buffer.resize(new_size); } } #endif ================================================ FILE: src/Renderer/CPUGPUCommonDataStructures/PrecomputedEmissiveTrianglesDataSoAHost.h ================================================ /* * Copyright 2025 Tom Clabault. GNU GPL3 license. * GNU GPL3 license copy: https://www.gnu.org/licenses/gpl-3.0.txt */ #ifndef RENDERER_PERCOMPUTED_EMISSIVE_TRIANGLE_DATA_SOA_HOST_H #define RENDERER_PERCOMPUTED_EMISSIVE_TRIANGLE_DATA_SOA_HOST_H #include "HostDeviceCommon/PrecomputedEmissiveTrianglesDataSoADevice.h" #include "Renderer/CPUGPUCommonDataStructures/GenericSoA.h" template