EEVEE-Next: Implement Sphere Light-Probe convolution

This adds back sphere probe pre-convolution.
The difference is that we use spherical
Gaussian instead of the GGX NDF.
This allows us to reuse the previous mip as
a source for the convolution and thus reduce
the sample count and give a noiseless result.

However since we don't use filtered importance
sampling anymore, we have to compensate with
some more samples. This could be addressed in
a follow up PR if needed.

This also changes the octahedral mapping
procedure to avoid padding texels and
interpolation artifacts.
Also cleanup to make sure all functions
related to mapping are in the same file.

The change to Spherical Gaussian has some impact
on the look. The resulting visual is a less "foggy"
but most of the energy is where it should be.
Only the caracteristic "GGX tail" is missing.

These sphere light-probes convolved mips are only
used when raytracing is off or un-available (forward
surfaces).

Ref #118256

Pull Request: https://projects.blender.org/blender/blender/pulls/118354
This commit is contained in:
Clément Foucault 2024-02-19 16:36:23 +01:00 committed by Clément Foucault
parent 661014ff8f
commit b7998f1046
16 changed files with 381 additions and 164 deletions

View File

@ -567,6 +567,7 @@ set(GLSL_SRC
engines/eevee_next/shaders/eevee_reflection_probe_convolve_comp.glsl
engines/eevee_next/shaders/eevee_reflection_probe_eval_lib.glsl
engines/eevee_next/shaders/eevee_reflection_probe_lib.glsl
engines/eevee_next/shaders/eevee_reflection_probe_mapping_lib.glsl
engines/eevee_next/shaders/eevee_reflection_probe_remap_comp.glsl
engines/eevee_next/shaders/eevee_reflection_probe_select_comp.glsl
engines/eevee_next/shaders/eevee_reflection_probe_update_irradiance_comp.glsl

View File

@ -32,11 +32,17 @@
/* Reflection Probes. */
#define SPHERE_PROBE_GROUP_SIZE 16
#define SPHERE_PROBE_SELECT_GROUP_SIZE 64
/* Number of additional pixels on the border of an octahedral map to reserve for fixing seams.
* Border size requires depends on the max number of mipmap levels. */
#define SPHERE_PROBE_MIPMAP_LEVELS 5
#define SPHERE_PROBE_SH_GROUP_SIZE 512
#define SPHERE_PROBE_SH_SAMPLES_PER_GROUP 64
/* Must be power of two for correct partitioning. */
#define SPHERE_PROBE_ATLAS_MAX_SUBDIV 10
#define SPHERE_PROBE_ATLAS_RES (1 << SPHERE_PROBE_ATLAS_MAX_SUBDIV)
/* Start and end value for mixing sphere probe and volume probes. */
#define SPHERE_PROBE_MIX_START_ROUGHNESS 0.7
#define SPHERE_PROBE_MIX_END_ROUGHNESS 0.9
/* Roughness of the last mip map for sphere probes. */
#define SPHERE_PROBE_MIP_MAX_ROUGHNESS 0.7
/**
* Limited by the UBO size limit `(16384 bytes / sizeof(SphereProbeData))`.
*/

View File

@ -125,7 +125,7 @@ void LightProbeModule::sync_sphere(const Object *ob, ObjectHandle &handle)
cube.atlas_coord = find_empty_atlas_region(subdivision_lvl);
SphereProbeData &cube_data = *static_cast<SphereProbeData *>(&cube);
/* Update gpu data sampling coordinates. */
cube_data.atlas_coord = cube.atlas_coord.as_sampling_coord(probe_module.max_resolution_);
cube_data.atlas_coord = cube.atlas_coord.as_sampling_coord();
/* Coordinates have changed. Area might contain random data. Do not use for rendering. */
cube.use_for_render = false;
}
@ -204,8 +204,7 @@ void LightProbeModule::sync_world(const ::World *world, bool has_update)
world_sphere_.atlas_coord.free();
world_sphere_.atlas_coord = find_empty_atlas_region(subdivision_lvl);
SphereProbeData &world_data = *static_cast<SphereProbeData *>(&world_sphere_);
world_data.atlas_coord = world_sphere_.atlas_coord.as_sampling_coord(
sph_module.max_resolution_);
world_data.atlas_coord = world_sphere_.atlas_coord.as_sampling_coord();
has_update = true;
}

View File

@ -15,6 +15,7 @@
#include "BLI_bit_vector.hh"
#include "BLI_map.hh"
#include "eevee_defines.hh"
#include "eevee_sync.hh"
namespace blender::eevee {
@ -41,9 +42,9 @@ struct SphereProbeAtlasCoord {
}
/* Return the area extent in pixel. */
int area_extent(int atlas_extent) const
int area_extent(int mip_lvl = 0) const
{
return atlas_extent >> subdivision_lvl;
return SPHERE_PROBE_ATLAS_RES >> (subdivision_lvl + mip_lvl);
}
/* Coordinate of the area in [0..area_count_per_dimension[ range. */
@ -53,51 +54,26 @@ struct SphereProbeAtlasCoord {
return int2(area_index % area_count_per_dimension, area_index / area_count_per_dimension);
}
/* Coordinate of the bottom left corner of the area in [0..atlas_extent[ range. */
int2 area_offset(int atlas_extent) const
/* Coordinate of the bottom left corner of the area in [0..SPHERE_PROBE_ATLAS_RES[ range. */
int2 area_offset(int mip_lvl = 0) const
{
return area_location() * area_extent(atlas_extent);
return area_location() * area_extent(mip_lvl);
}
SphereProbeUvArea as_sampling_coord(int atlas_extent) const
SphereProbeUvArea as_sampling_coord() const
{
/**
* We want to cover the last mip exactly at the pixel center to reduce padding texels and
* interpolation artifacts.
* This is a diagram of a 2px^2 map with `c` being the texels corners and `x` the pixels
* centers.
*
* c-------c-------c
* | | |
* | x | x | <
* | | | |
* c-------c-------c | sampling area
* | | | |
* | x | x | <
* | | |
* c-------c-------c
* ^-------^
* sampling area
*/
/* Max level only need half a pixel of padding around the sampling area. */
const int mip_max_lvl_padding = 1;
const int mip_min_lvl_padding = mip_max_lvl_padding << SPHERE_PROBE_MIPMAP_LEVELS;
/* Extent and offset in mip 0 texels. */
const int sampling_area_extent = area_extent(atlas_extent) - mip_min_lvl_padding;
const int2 sampling_area_offset = area_offset(atlas_extent) + mip_min_lvl_padding / 2;
/* Convert to atlas UVs. */
SphereProbeUvArea coord;
coord.scale = sampling_area_extent / float(atlas_extent);
coord.offset = float2(sampling_area_offset) / float(atlas_extent);
coord.scale = float(area_extent()) / SPHERE_PROBE_ATLAS_RES;
coord.offset = float2(area_offset()) / SPHERE_PROBE_ATLAS_RES;
coord.layer = atlas_layer;
return coord;
}
SphereProbePixelArea as_write_coord(int atlas_extent, int mip_lvl) const
SphereProbePixelArea as_write_coord(int mip_lvl) const
{
SphereProbePixelArea coord;
coord.extent = atlas_extent >> (subdivision_lvl + mip_lvl);
coord.offset = area_location() * coord.extent;
coord.extent = area_extent(mip_lvl);
coord.offset = area_offset();
coord.layer = atlas_layer;
return coord;
}

View File

@ -53,12 +53,15 @@ void SphereProbeModule::begin_sync()
PassSimple &pass = convolve_ps_;
pass.init();
pass.shader_set(instance_.shaders.static_shader_get(SPHERE_PROBE_CONVOLVE));
pass.bind_image("in_atlas_mip_img", &convolve_input_);
pass.bind_texture("cubemap_tx", &cubemap_tx_);
pass.bind_texture("in_atlas_mip_tx", &convolve_input_);
pass.bind_image("out_atlas_mip_img", &convolve_output_);
pass.push_constant("probe_coord_packed", reinterpret_cast<int4 *>(&probe_sampling_coord_));
pass.push_constant("write_coord_packed", reinterpret_cast<int4 *>(&probe_write_coord_));
pass.barrier(GPU_BARRIER_SHADER_IMAGE_ACCESS);
pass.push_constant("read_coord_packed", reinterpret_cast<int4 *>(&probe_read_coord_));
pass.push_constant("read_lod", &convolve_lod_);
pass.barrier(GPU_BARRIER_TEXTURE_FETCH);
pass.dispatch(&dispatch_probe_convolve_);
pass.barrier(GPU_BARRIER_SHADER_IMAGE_ACCESS);
}
{
PassSimple &pass = update_irradiance_ps_;
@ -89,7 +92,7 @@ bool SphereProbeModule::ensure_atlas()
eGPUTextureUsage usage = GPU_TEXTURE_USAGE_SHADER_WRITE | GPU_TEXTURE_USAGE_SHADER_READ;
if (probes_tx_.ensure_2d_array(GPU_RGBA16F,
int2(max_resolution_),
int2(SPHERE_PROBE_ATLAS_RES),
instance_.light_probes.sphere_layer_count(),
usage,
nullptr,
@ -98,10 +101,12 @@ bool SphereProbeModule::ensure_atlas()
probes_tx_.ensure_mip_views();
/* TODO(fclem): Clearing means that we need to render all probes again.
* If existing data exists, copy it using `CopyImageSubData`. */
probes_tx_.clear(float4(0.0f));
for (auto i : IndexRange(SPHERE_PROBE_MIPMAP_LEVELS)) {
/* Avoid undefined pixel data. Clear all mips. */
float4 data(0.0f);
GPU_texture_clear(probes_tx_.mip_view(i), GPU_DATA_FLOAT, &data);
}
GPU_texture_mipmap_mode(probes_tx_, true, true);
/* Avoid undefined pixel data. Update all mips. */
GPU_texture_update_mipmap_chain(probes_tx_);
return true;
}
return false;
@ -140,11 +145,9 @@ void SphereProbeModule::ensure_cubemap_render_target(int resolution)
SphereProbeModule::UpdateInfo SphereProbeModule::update_info_from_probe(const SphereProbe &probe)
{
const int max_shift = int(log2(max_resolution_));
SphereProbeModule::UpdateInfo info = {};
info.atlas_coord = probe.atlas_coord;
info.resolution = 1 << (max_shift - probe.atlas_coord.subdivision_lvl - 1);
info.cube_target_extent = probe.atlas_coord.area_extent() / 2;
info.clipping_distances = probe.clipping_distances;
info.probe_pos = probe.location;
info.do_render = probe.do_render;
@ -162,7 +165,7 @@ std::optional<SphereProbeModule::UpdateInfo> SphereProbeModule::world_update_inf
info.do_world_irradiance_update = do_world_irradiance_update;
world_probe.do_render = false;
do_world_irradiance_update = false;
ensure_cubemap_render_target(info.resolution);
ensure_cubemap_render_target(info.cube_target_extent);
return info;
}
@ -180,7 +183,7 @@ std::optional<SphereProbeModule::UpdateInfo> SphereProbeModule::probe_update_inf
SphereProbeModule::UpdateInfo info = update_info_from_probe(probe);
probe.do_render = false;
probe.use_for_render = true;
ensure_cubemap_render_target(info.resolution);
ensure_cubemap_render_target(info.cube_target_extent);
return info;
}
@ -189,19 +192,21 @@ std::optional<SphereProbeModule::UpdateInfo> SphereProbeModule::probe_update_inf
void SphereProbeModule::remap_to_octahedral_projection(const SphereProbeAtlasCoord &atlas_coord)
{
int resolution = max_resolution_ >> atlas_coord.subdivision_lvl;
/* Update shader parameters that change per dispatch. */
probe_sampling_coord_ = atlas_coord.as_sampling_coord(max_resolution_);
probe_write_coord_ = atlas_coord.as_write_coord(max_resolution_, 0);
probe_sampling_coord_ = atlas_coord.as_sampling_coord();
probe_write_coord_ = atlas_coord.as_write_coord(0);
int resolution = probe_write_coord_.extent;
dispatch_probe_pack_ = int3(int2(ceil_division(resolution, SPHERE_PROBE_GROUP_SIZE)), 1);
instance_.manager->submit(remap_ps_);
/* Populate the mip levels */
for (auto i : IndexRange(SPHERE_PROBE_MIPMAP_LEVELS - 1)) {
convolve_lod_ = i;
convolve_input_ = probes_tx_.mip_view(i);
convolve_output_ = probes_tx_.mip_view(i + 1);
probe_write_coord_ = atlas_coord.as_write_coord(max_resolution_, i + 1);
int out_mip_res = resolution >> (i + 1);
probe_read_coord_ = atlas_coord.as_write_coord(i);
probe_write_coord_ = atlas_coord.as_write_coord(i + 1);
int out_mip_res = probe_write_coord_.extent;
dispatch_probe_convolve_ = int3(int2(ceil_division(out_mip_res, SPHERE_PROBE_GROUP_SIZE)), 1);
instance_.manager->submit(convolve_ps_);
}

View File

@ -30,19 +30,12 @@ class CaptureView;
class SphereProbeModule {
friend LightProbeModule;
/* Capture View requires access to the cube-maps texture for frame-buffer configuration. */
/* Capture View requires access to the probe texture for frame-buffer configuration. */
friend class CaptureView;
/* Instance requires access to #update_probes_this_sample_ */
friend class Instance;
private:
/**
* The maximum resolution of a cube-map side.
*
* Must be a power of two; intention to be used as a cube-map atlas.
*/
static constexpr int max_resolution_ = 2048;
Instance &instance_;
SphereProbeDataBuf data_buf_;
@ -61,6 +54,7 @@ class SphereProbeModule {
GPUTexture *convolve_input_ = nullptr;
/** Output mip level for the convolution. */
GPUTexture *convolve_output_ = nullptr;
int convolve_lod_ = 0;
int3 dispatch_probe_pack_ = int3(1);
int3 dispatch_probe_convolve_ = int3(1);
@ -77,6 +71,8 @@ class SphereProbeModule {
/** Updated Probe coordinates in the atlas. */
SphereProbeUvArea probe_sampling_coord_;
SphereProbePixelArea probe_write_coord_;
/** Source Probe coordinates in the atlas. */
SphereProbePixelArea probe_read_coord_;
/** World coordinates in the atlas. */
SphereProbeUvArea world_sampling_coord_;
/** Number of the probe to process in the select phase. */
@ -134,7 +130,7 @@ class SphereProbeModule {
* Result is safely clamped to max resolution. */
int subdivision_level_get(const eLightProbeResolution probe_resolution)
{
return max_ii(int(log2(max_resolution_)) - int(probe_resolution), 0);
return max_ii(SPHERE_PROBE_ATLAS_MAX_SUBDIV - int(probe_resolution), 0);
}
/**
@ -151,7 +147,7 @@ class SphereProbeModule {
struct UpdateInfo {
float3 probe_pos;
/** Resolution of the cube-map to be rendered. */
int resolution;
int cube_target_extent;
float2 clipping_distances;

View File

@ -284,7 +284,7 @@ void CaptureView::render_probes()
inst_.uniform_data.push_update();
}
int2 extent = int2(update_info->resolution);
int2 extent = int2(update_info->cube_target_extent);
inst_.render_buffers.acquire(extent);
inst_.render_buffers.vector_tx.clear(float4(0.0f));

View File

@ -3,6 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma BLENDER_REQUIRE(gpu_shader_math_base_lib.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_math_fast_lib.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_codegen_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_lightprobe_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_ray_generate_lib.glsl)
@ -84,18 +85,6 @@ vec3 lightprobe_eval_direction(LightProbeSample samp, vec3 P, vec3 L, float pdf)
return radiance_sh;
}
float lightprobe_roughness_to_cube_sh_mix_fac(float roughness)
{
/* Temporary. Do something better. */
return square(saturate(roughness * 4.0 - 2.0));
}
float lightprobe_roughness_to_lod(float roughness)
{
/* Temporary. Do something better. */
return sqrt(roughness) * SPHERE_PROBE_MIPMAP_LEVELS;
}
vec3 lightprobe_eval(LightProbeSample samp, ClosureDiffuse cl, vec3 P, vec3 V)
{
vec3 radiance_sh = spherical_harmonics_evaluate_lambert(cl.N, samp.volume_irradiance);
@ -110,9 +99,14 @@ vec3 lightprobe_eval(LightProbeSample samp, ClosureTranslucent cl, vec3 P, vec3
vec3 lightprobe_reflection_dominant_dir(vec3 N, vec3 V, float roughness)
{
/* From Frostbite PBR Course
* http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf
* Listing 22.
* Note that the reference labels squared roughness (GGX input) as roughness. */
float m = square(roughness);
vec3 R = -reflect(V, N);
float smoothness = 1.0 - roughness;
float fac = smoothness * (sqrt(smoothness) + roughness);
float smoothness = 1.0 - m;
float fac = smoothness * (sqrt(smoothness) + m);
return normalize(mix(N, R, fac));
}
@ -120,20 +114,23 @@ vec3 lightprobe_eval(LightProbeSample samp, ClosureReflection reflection, vec3 P
{
vec3 L = lightprobe_reflection_dominant_dir(reflection.N, V, reflection.roughness);
float lod = lightprobe_roughness_to_lod(reflection.roughness);
float lod = sphere_probe_roughness_to_lod(reflection.roughness);
vec3 radiance_cube = lightprobe_spherical_sample_normalized_with_parallax(
samp.spherical_id, P, L, lod, samp.volume_irradiance);
float fac = lightprobe_roughness_to_cube_sh_mix_fac(reflection.roughness);
float fac = sphere_probe_roughness_to_mix_fac(reflection.roughness);
vec3 radiance_sh = spherical_harmonics_evaluate_lambert(L, samp.volume_irradiance);
return mix(radiance_cube, radiance_sh, fac);
}
vec3 lightprobe_refraction_dominant_dir(vec3 N, vec3 V, float ior, float roughness)
{
/* Reusing same thing as lightprobe_reflection_dominant_dir for now.
* TODO(fclem): Find something better that take IOR and roughness into account. */
float m = square(roughness);
vec3 R = refract(-V, N, 1.0 / ior);
float smoothness = 1.0 - roughness;
float fac = smoothness * (sqrt(smoothness) + roughness);
float smoothness = 1.0 - m;
float fac = smoothness * (sqrt(smoothness) + m);
return normalize(mix(-N, R, fac));
}
@ -141,11 +138,11 @@ vec3 lightprobe_eval(LightProbeSample samp, ClosureRefraction cl, vec3 P, vec3 V
{
vec3 L = lightprobe_refraction_dominant_dir(cl.N, V, cl.ior, cl.roughness);
float lod = lightprobe_roughness_to_lod(cl.roughness);
float lod = sphere_probe_roughness_to_lod(cl.roughness);
vec3 radiance_cube = lightprobe_spherical_sample_normalized_with_parallax(
samp.spherical_id, P, L, lod, samp.volume_irradiance);
float fac = lightprobe_roughness_to_cube_sh_mix_fac(cl.roughness);
float fac = sphere_probe_roughness_to_mix_fac(cl.roughness);
vec3 radiance_sh = spherical_harmonics_evaluate_lambert(L, samp.volume_irradiance);
return mix(radiance_cube, radiance_sh, fac);
}

View File

@ -36,3 +36,16 @@ vec3 octahedral_uv_to_direction(vec2 co)
return v;
}
/* Mirror the UV if they are not on the diagonal or unit UV squares.
* Doesn't extend outside of [-1..2] range. But this is fine since we use it only for borders. */
vec2 octahedral_mirror_repeat_uv(vec2 uv)
{
vec2 m = abs(uv - 0.5) + 0.5;
vec2 f = floor(m);
float x = f.x - f.y;
if (x != 0.0) {
uv.xy = 1.0 - uv.xy;
}
return fract(uv);
}

View File

@ -4,36 +4,144 @@
/* Shader to convert cube-map to octahedral projection. */
#pragma BLENDER_REQUIRE(eevee_octahedron_lib.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_utildefines_lib.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_math_base_lib.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_math_matrix_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_reflection_probe_mapping_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_sampling_lib.glsl)
SphereProbePixelArea reinterpret_as_write_coord(ivec4 packed_coord)
/* Bypass convolution cascade and projection logic. */
// #define ALWAYS_SAMPLE_CUBEMAP
/* Debugging texel alignment. */
// #define USE_PIXEL_CHECKERBOARD
float roughness_from_relative_mip(float prev_mip_roughness, float curr_mip_roughness)
{
SphereProbePixelArea unpacked;
unpacked.offset = packed_coord.xy;
unpacked.extent = packed_coord.z;
unpacked.layer = packed_coord.w;
return unpacked;
#ifdef ALWAYS_SAMPLE_CUBEMAP
/* For reference and debugging. */
return curr_mip_roughness;
#else
/* The exponent should be 2 but result is a bit less blurry than expected in practice. */
const float exponent = 3.0;
/* From linear roughness to GGX roughness input. */
float m_prev = pow(prev_mip_roughness, exponent);
float m_curr = pow(curr_mip_roughness, exponent);
/* Given that spherical gaussians are very close to regular gaussian in 1D,
* we reuse the same rule for successive convolution (i.e: G(x,a) X G(x,b) = G(x,a+b)).
* While this isn't technically correct, this still works quite well in practice. */
float m_target = m_curr - m_prev;
/* From GGX roughness input to linear roughness. */
return pow(m_target, 1.0 / exponent);
#endif
}
float cone_cosine_from_roughness(float linear_roughness)
{
/* From linear roughness to GGX roughness input. */
float m = square(linear_roughness);
/* Chosen so that roughness of 1.0 maps to half pi cone aperture. */
float cutoff_value = mix(0.01, 0.14, m);
/* Inversion of the spherical gaussian. This gives the cutoff for the half angle from N.H. */
float half_angle_cos = 1.0 + (log(cutoff_value) * square(m)) / 2.0;
float half_angle_sin = safe_sqrt(1.0 - square(half_angle_cos));
/* Use cosine rule to avoid acos. Return cos(2 * half_angle). */
return square(half_angle_cos) - square(half_angle_sin);
}
int sample_count_get()
{
/* After experimenting this is likely to be the best value if we keep the max resolution to 2048.
* This isn't ideal, but the better solution would be to use multiple steps per mip which would
* reduce the number of sample per step (use sum of gaussian per step). */
return 196;
}
float sample_weight(vec3 out_direction, vec3 in_direction, float linear_roughness)
{
out_direction = normalize(out_direction);
in_direction = normalize(in_direction);
float cos_theta = saturate(dot(out_direction, in_direction));
/* From linear roughness to GGX roughness input. */
float m = square(linear_roughness);
/* Map GGX roughness to spherical gaussian sharpness.
* From "SG Series Part 4: Specular Lighting From an SG Light Source" by MJP
* https://therealmjp.github.io/posts/sg-series-part-4-specular-lighting-from-an-sg-light-source/
*/
vec3 N = out_direction;
vec3 H = normalize(out_direction + in_direction);
float NH = saturate(dot(N, H));
/* GGX. */
// return exp(-square(acos(NH) / m));
/* Spherical Gaussian. */
return exp(2.0 * (NH - 1.0) / square(m));
}
mat3x3 tangent_basis(vec3 N)
{
/* TODO(fclem): This create a discontinuity at Z=0. */
return from_up_axis(N);
}
void main()
{
SphereProbePixelArea write_coord = reinterpret_as_write_coord(write_coord_packed);
SphereProbeUvArea sample_coord = reinterpret_as_atlas_coord(probe_coord_packed);
SphereProbePixelArea out_texel_area = reinterpret_as_write_coord(write_coord_packed);
SphereProbePixelArea in_texel_area = reinterpret_as_write_coord(read_coord_packed);
/* Texel in probe. */
ivec2 local_texel = ivec2(gl_GlobalInvocationID.xy);
ivec2 out_local_texel = ivec2(gl_GlobalInvocationID.xy);
/* Exit when pixel being written doesn't fit in the area reserved for the probe. */
if (any(greaterThanEqual(local_texel, ivec2(write_coord.extent)))) {
if (any(greaterThanEqual(out_local_texel, ivec2(out_texel_area.extent)))) {
return;
}
ivec2 texel_out = write_coord.offset + local_texel;
vec4 color = vec4(0.0);
color += imageLoad(in_atlas_mip_img, ivec3(texel_out * 2 + ivec2(0, 0), write_coord.layer));
color += imageLoad(in_atlas_mip_img, ivec3(texel_out * 2 + ivec2(1, 0), write_coord.layer));
color += imageLoad(in_atlas_mip_img, ivec3(texel_out * 2 + ivec2(0, 1), write_coord.layer));
color += imageLoad(in_atlas_mip_img, ivec3(texel_out * 2 + ivec2(1, 1), write_coord.layer));
color *= 0.25;
/* From mip to linear roughness (same as UI). */
float prev_mip_roughness = sphere_probe_lod_to_roughness(float(read_lod));
float curr_mip_roughness = sphere_probe_lod_to_roughness(float(read_lod + 1));
/* In order to reduce the sample count, we sample the content of previous mip level.
* But this one has already been convolved. So we have to derive the equivalent roughness
* that produces the same result. */
float mip_roughness = roughness_from_relative_mip(prev_mip_roughness, curr_mip_roughness);
/* Clamp to avoid numerical imprecision. */
float mip_roughness_clamped = max(mip_roughness, BSDF_ROUGHNESS_THRESHOLD);
float cone_cos = cone_cosine_from_roughness(mip_roughness_clamped);
imageStore(out_atlas_mip_img, ivec3(texel_out, write_coord.layer), color);
vec3 out_direction = sphere_probe_texel_to_direction(
out_local_texel, out_texel_area, sample_coord);
out_direction = normalize(out_direction);
mat3x3 basis = tangent_basis(out_direction);
ivec2 out_texel = out_texel_area.offset + out_local_texel;
float weight_accum = 0.0;
vec4 radiance_accum = vec4(0.0);
int sample_count = sample_count_get();
for (int i = 0; i < sample_count; i++) {
vec2 rand = hammersley_2d(i, sample_count);
vec3 in_direction = basis * sample_uniform_cone(rand, cone_cos);
#ifndef ALWAYS_SAMPLE_CUBEMAP
vec2 in_uv = sphere_probe_direction_to_uv(in_direction, float(read_lod), sample_coord);
vec4 radiance = texture(in_atlas_mip_tx, vec3(in_uv, sample_coord.layer));
#else /* For reference and debugging. */
vec4 radiance = texture(cubemap_tx, in_direction);
#endif
float weight = sample_weight(out_direction, in_direction, mip_roughness_clamped);
radiance_accum += radiance * weight;
weight_accum += weight;
}
vec4 out_radiance = radiance_accum * safe_rcp(weight_accum);
#ifdef USE_PIXEL_CHECKERBOARD
ivec2 a = out_texel % 2;
out_radiance = vec4(a.x == a.y);
#endif
imageStore(out_atlas_mip_img, ivec3(out_texel, out_texel_area.layer), out_radiance);
}

View File

@ -5,12 +5,21 @@
#pragma BLENDER_REQUIRE(gpu_shader_math_vector_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_octahedron_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_spherical_harmonics_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_reflection_probe_mapping_lib.glsl)
#ifdef SPHERE_PROBE
vec4 reflection_probes_sample(vec3 L, float lod, SphereProbeUvArea atlas_coord)
vec4 reflection_probes_sample(vec3 L, float lod, SphereProbeUvArea uv_area)
{
vec2 octahedral_uv = octahedral_uv_from_direction(L) * atlas_coord.scale + atlas_coord.offset;
return textureLod(reflection_probes_tx, vec3(octahedral_uv, atlas_coord.layer), lod);
float lod_min = floor(lod);
float lod_max = ceil(lod);
float mix_fac = lod - lod_min;
vec2 altas_uv_min, altas_uv_max;
sphere_probe_direction_to_uv(L, lod_min, lod_max, uv_area, altas_uv_min, altas_uv_max);
vec4 color_min = textureLod(reflection_probes_tx, vec3(altas_uv_min, uv_area.layer), lod_min);
vec4 color_max = textureLod(reflection_probes_tx, vec3(altas_uv_max, uv_area.layer), lod_max);
return mix(color_min, color_max, mix_fac);
}
#endif

View File

@ -0,0 +1,128 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma BLENDER_REQUIRE(gpu_shader_utildefines_lib.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_math_base_lib.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_math_fast_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_octahedron_lib.glsl)
SphereProbePixelArea reinterpret_as_write_coord(ivec4 packed_coord)
{
SphereProbePixelArea unpacked;
unpacked.offset = packed_coord.xy;
unpacked.extent = packed_coord.z;
unpacked.layer = packed_coord.w;
return unpacked;
}
SphereProbeUvArea reinterpret_as_atlas_coord(ivec4 packed_coord)
{
SphereProbeUvArea unpacked;
unpacked.offset = intBitsToFloat(packed_coord.xy);
unpacked.scale = intBitsToFloat(packed_coord.z);
unpacked.layer = intBitsToFloat(packed_coord.w);
return unpacked;
}
/* local_texel is the texel coordinate inside the probe area [0..texel_area.extent) range.
* Returned vector is not normalized. */
vec3 sphere_probe_texel_to_direction(ivec2 local_texel,
SphereProbePixelArea texel_area,
SphereProbeUvArea uv_area,
out vec2 sampling_uv)
{
/* Texel in probe atlas. */
ivec2 texel = local_texel + texel_area.offset;
/* UV in sampling area. No half pixel bias to texel as the octahedral map edges area lined up
* with texel center. Note that we don't use the last row & column of pixel, hence the -2 instead
* of -1. See sphere_probe_miplvl_scale_bias. */
sampling_uv = vec2(texel) / vec2(texel_area.extent - 2);
/* Direction in world space. */
return octahedral_uv_to_direction(sampling_uv);
}
/* local_texel is the texel coordinate inside the probe area [0..texel_area.extent) range.
* Returned vector is not normalized. */
vec3 sphere_probe_texel_to_direction(ivec2 local_texel,
SphereProbePixelArea texel_area,
SphereProbeUvArea uv_area)
{
vec2 sampling_uv_unused;
return sphere_probe_texel_to_direction(local_texel, texel_area, uv_area, sampling_uv_unused);
}
/* Apply correct bias and scale for the given level of detail. */
vec2 sphere_probe_miplvl_scale_bias(float mip_lvl, SphereProbeUvArea uv_area, vec2 uv)
{
/* Add 0.5 to avoid rounding error. */
int mip_0_res = int(float(SPHERE_PROBE_ATLAS_RES) * uv_area.scale + 0.5);
float mip_lvl_res = float(mip_0_res >> int(mip_lvl));
float mip_lvl_res_inv = 1.0 / mip_lvl_res;
/* We place texel centers at the edges of the octahedron, to avoid artifacts caused by
* interpolating across the edges.
* The first pixel scaling aligns all the border edges (half pixel border).
* The second pixel scaling aligns the center edges (odd number of pixel). */
float scale = (mip_lvl_res - 2.0) * mip_lvl_res_inv;
float offset = 0.5 * mip_lvl_res_inv;
return uv * scale + offset;
}
void sphere_probe_direction_to_uv(vec3 L,
float lod_min,
float lod_max,
SphereProbeUvArea uv_area,
out vec2 altas_uv_min,
out vec2 altas_uv_max)
{
vec2 octahedral_uv = octahedral_uv_from_direction(L);
/* We use a custom per mip level scaling and bias. This avoid some projection artifact and
* padding border waste. But we need to do the mipmap interpolation ourself. */
vec2 local_uv_min = sphere_probe_miplvl_scale_bias(lod_min, uv_area, octahedral_uv);
vec2 local_uv_max = sphere_probe_miplvl_scale_bias(lod_max, uv_area, octahedral_uv);
/* Remap into atlas location. */
altas_uv_min = local_uv_min * uv_area.scale + uv_area.offset;
altas_uv_max = local_uv_max * uv_area.scale + uv_area.offset;
}
/* Single mip variant. */
vec2 sphere_probe_direction_to_uv(vec3 L, float lod, SphereProbeUvArea uv_area)
{
vec2 altas_uv_min, altas_uv_max_unused;
sphere_probe_direction_to_uv(L, lod, 0.0, uv_area, altas_uv_min, altas_uv_max_unused);
return altas_uv_min;
}
float sphere_probe_roughness_to_mix_fac(float roughness)
{
const float scale = 1.0 / (SPHERE_PROBE_MIX_END_ROUGHNESS - SPHERE_PROBE_MIX_START_ROUGHNESS);
const float bias = scale * SPHERE_PROBE_MIX_START_ROUGHNESS;
return square(saturate(roughness * scale - bias));
}
/* Input roughness is linear roughness (UI roughness). */
float sphere_probe_roughness_to_lod(float roughness)
{
/* From "Moving Frostbite to Physically Based Rendering 3.0" eq 53. */
float ratio = saturate(roughness / SPHERE_PROBE_MIP_MAX_ROUGHNESS);
float ratio_sqrt = sqrt_fast(ratio);
/* Mix with linear to avoid mip 1 being too sharp. */
float mip_ratio = mix(ratio, ratio_sqrt, 0.4);
return mip_ratio * float(SPHERE_PROBE_MIPMAP_LEVELS - 1);
}
/* Return linear roughness (UI roughness). */
float sphere_probe_lod_to_roughness(float lod)
{
/* Inverse of sphere_probe_roughness_to_lod. */
float mip_ratio = lod / float(SPHERE_PROBE_MIPMAP_LEVELS - 1);
float a = mip_ratio;
const float b = 0.6; /* Factor of ratio. */
const float c = 0.4; /* Factor of ratio_sqrt. */
float b2 = square(b);
float c2 = square(c);
float c4 = square(c2);
/* In wolfram alpha we trust. */
float ratio = (-sqrt(4.0 * a * b * c2 + c4) + 2.0 * a * b + c2) / (2.0 * b2);
return ratio * SPHERE_PROBE_MIP_MAX_ROUGHNESS;
}

View File

@ -4,40 +4,9 @@
/* Shader to convert cube-map to octahedral projection. */
#pragma BLENDER_REQUIRE(eevee_octahedron_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_reflection_probe_mapping_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_colorspace_lib.glsl)
SphereProbeUvArea reinterpret_as_atlas_coord(ivec4 packed_coord)
{
SphereProbeUvArea unpacked;
unpacked.offset = intBitsToFloat(packed_coord.xy);
unpacked.scale = intBitsToFloat(packed_coord.z);
unpacked.layer = intBitsToFloat(packed_coord.w);
return unpacked;
}
SphereProbePixelArea reinterpret_as_write_coord(ivec4 packed_coord)
{
SphereProbePixelArea unpacked;
unpacked.offset = packed_coord.xy;
unpacked.extent = packed_coord.z;
unpacked.layer = packed_coord.w;
return unpacked;
}
/* Mirror the UV if they are not on the diagonal or unit UV squares.
* Doesn't extend outside of [-1..2] range. But this is fine since we use it only for borders. */
vec2 mirror_repeat_uv(vec2 uv)
{
vec2 m = abs(uv - 0.5) + 0.5;
vec2 f = floor(m);
float x = f.x - f.y;
if (x != 0.0) {
uv.xy = 1.0 - uv.xy;
}
return fract(uv);
}
void main()
{
SphereProbeUvArea world_coord = reinterpret_as_atlas_coord(world_coord_packed);
@ -52,15 +21,9 @@ void main()
return;
}
/* Texel in probe atlas. */
ivec2 texel = local_texel + write_coord.offset;
/* UV in probe atlas. */
vec2 atlas_uv = (vec2(texel) + 0.5) / vec2(imageSize(atlas_img).xy);
/* UV in sampling area. */
vec2 sampling_uv = (atlas_uv - sample_coord.offset) / sample_coord.scale;
vec2 wrapped_uv = mirror_repeat_uv(sampling_uv);
/* Direction in world space. */
vec3 direction = octahedral_uv_to_direction(wrapped_uv);
vec2 wrapped_uv;
vec3 direction = sphere_probe_texel_to_direction(
local_texel, write_coord, sample_coord, wrapped_uv);
vec4 radiance_and_transmittance = texture(cubemap_tx, direction);
vec3 radiance = radiance_and_transmittance.xyz;
@ -76,5 +39,6 @@ void main()
radiance = colorspace_brightness_clamp_max(radiance, probe_brightness_clamp);
imageStore(atlas_img, ivec3(texel, write_coord.layer), vec4(radiance, 1.0));
ivec3 texel = ivec3(local_texel + write_coord.offset, write_coord.layer);
imageStore(atlas_img, texel, vec4(radiance, 1.0));
}

View File

@ -5,19 +5,10 @@
/* Shader to extract spherical harmonics cooefs from octahedral mapped reflection probe. */
#pragma BLENDER_REQUIRE(eevee_reflection_probe_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_reflection_probe_mapping_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_spherical_harmonics_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_octahedron_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_sampling_lib.glsl)
SphereProbeUvArea reinterpret_as_atlas_coord(ivec4 packed_coord)
{
SphereProbeUvArea unpacked;
unpacked.offset = intBitsToFloat(packed_coord.xy);
unpacked.scale = intBitsToFloat(packed_coord.z);
unpacked.layer = intBitsToFloat(packed_coord.w);
return unpacked;
}
void atlas_store(vec4 sh_coefficient, ivec2 atlas_coord, int layer)
{
for (int x = 0; x < IRRADIANCE_GRID_BRICK_SIZE; x++) {
@ -43,7 +34,7 @@ void main()
cooef.L1.Mp1 = vec4(0.0);
SphereProbeUvArea atlas_coord = reinterpret_as_atlas_coord(world_coord_packed);
float layer_mipmap = 5;
float layer_mipmap = 2;
/* Perform multiple sample. */
uint store_index = gl_LocalInvocationID.x;
float total_samples = float(gl_WorkGroupSize.x * SPHERE_PROBE_SH_SAMPLES_PER_GROUP);

View File

@ -103,6 +103,13 @@ vec2 hammersley_2d(int i, int sample_count)
return hammersley_2d(uint(i), uint(sample_count));
}
/* Not random but still useful. sample_count should be an even. */
vec2 regular_grid_2d(int i, int sample_count)
{
int sample_per_dim = int(sqrt(float(sample_count)));
return (vec2(i % sample_per_dim, i / sample_per_dim) + 0.5) / float(sample_per_dim);
}
/** \} */
/* -------------------------------------------------------------------- */
@ -153,4 +160,17 @@ vec3 sample_hemisphere(vec2 rand)
return vec3(sin_theta * sample_circle(rand.y), cos_theta);
}
/**
* Uniform cone distribution.
* \a rand is 2 random float in the [0..1] range.
* \a cos_angle is the cosine of the half angle.
* Returns point on a Z positive hemisphere of radius 1 and centered on the origin.
*/
vec3 sample_uniform_cone(vec2 rand, float cos_angle)
{
float cos_theta = mix(cos_angle, 1.0, rand.x);
float sin_theta = safe_sqrt(1.0 - square(cos_theta));
return vec3(sin_theta * sample_circle(rand.y), cos_theta);
}
/** \} */

View File

@ -56,8 +56,12 @@ GPU_SHADER_CREATE_INFO(eevee_reflection_probe_select)
GPU_SHADER_CREATE_INFO(eevee_reflection_probe_convolve)
.local_group_size(SPHERE_PROBE_GROUP_SIZE, SPHERE_PROBE_GROUP_SIZE)
.additional_info("eevee_shared")
.push_constant(Type::IVEC4, "probe_coord_packed")
.push_constant(Type::IVEC4, "write_coord_packed")
.image(0, GPU_RGBA16F, Qualifier::READ, ImageType::FLOAT_2D_ARRAY, "in_atlas_mip_img")
.push_constant(Type::IVEC4, "read_coord_packed")
.push_constant(Type::INT, "read_lod")
.sampler(0, ImageType::FLOAT_CUBE, "cubemap_tx")
.sampler(1, ImageType::FLOAT_2D_ARRAY, "in_atlas_mip_tx")
.image(1, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D_ARRAY, "out_atlas_mip_img")
.compute_source("eevee_reflection_probe_convolve_comp.glsl")
.do_static_compilation(true);