Eevee-next: Reflection Probe Packing

All probes (including the world background probe) are stored in a single texture. Each probe
can be of any resolution as long as it is a power of 2 and not larger than 2048. So valid options
are (2048x2048, 1024x1024, 512x512, etc).

Each probe can be stored in their own resolution and can be set by the user.
> NOTE: Eventually we would like to add automatic resolution selection.

The probes are packed in an 2d texture array with the dimension of 2048*2048. The number of
layers depends on the actual needed layers. If more layers are needed the texture will be recreated.
This can happen when a new reflection probe is added, or an existing reflection probe is made visible
to the scene or its resolution is changed.

### Octahedral mapping

Probes are rendered into a cubemap. To reduce memory needs and improve sampling performance the cubemap
is stored in octahedral mapping space. This is done in `eevee_reflection_probe_remap_comp.glsl`.

The regular octahedral mapping has been extended to fix leakages at the edges of the texture
and to be able to be used on an atlas texture and by sampling the texture once.

To reduce sampling cost and improve the quality we add an border around the
octahedral map and extend the octahedral coordinates. This also allows us to
generate lower resolution mipmaps of the atlas texture using 2x2 box filtering
from a higher resolution.

### Subdivisions and areas

Probes data are stored besides the texture. The data is used to find out where the probe is stored
in the texture. It is also used to find free space to store new probes.

This approach ensures that we can be flexible at storing probes with different
resolutions on the same layer. Lets see an example how that works

Code-wise this is implemented by `ProbeLocationFinder`. ProbeLocationFinder can view a texture in a
given subdivision level and mark areas that are covered by probes. When finding a free spot it returns
the first empty area.

**Notes**

* Currently the cubemap is rendered with a fixed resolution and mipmaps are generated in order to
  increase the quality of the atlas. Eventually we should use dynamic resolution and no mipmaps.
  This will be done as part of the light probe baking change.

Pull Request: https://projects.blender.org/blender/blender/pulls/109688
This commit is contained in:
Jeroen Bakker
2023-07-07 15:37:26 +02:00
parent f0ee4c3ffe
commit 17a58f7db0
26 changed files with 842 additions and 45 deletions

View File

@@ -14,6 +14,7 @@
#include "DNA_modifier_types.h"
#include "DNA_movieclip_types.h"
#include "DNA_scene_types.h"
#include "DNA_world_types.h"
#include "DNA_genfile.h"
@@ -286,6 +287,19 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain)
}
}
/* Set default bake resolution. */
if (!DNA_struct_elem_find(fd->filesdna, "LightProbe", "int", "resolution")) {
LISTBASE_FOREACH (LightProbe *, lightprobe, &bmain->lightprobes) {
lightprobe->resolution = LIGHT_PROBE_RESOLUTION_1024;
}
}
if (!DNA_struct_elem_find(fd->filesdna, "World", "int", "probe_resolution")) {
LISTBASE_FOREACH (World *, world, &bmain->worlds) {
world->probe_resolution = LIGHT_PROBE_RESOLUTION_1024;
}
}
/* Clear removed "Z Buffer" flag. */
{
const int R_IMF_FLAG_ZBUF_LEGACY = 1 << 0;

View File

@@ -27,7 +27,12 @@
#define CULLING_TILE_GROUP_SIZE 256
/* Reflection Probes. */
#define REFLECTION_PROBES_MAX 256
#define REFLECTION_PROBE_GROUP_SIZE 16
/* 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 REFLECTION_PROBE_MIPMAP_LEVELS 5
#define REFLECTION_PROBE_BORDER_SIZE float(1 << (REFLECTION_PROBE_MIPMAP_LEVELS - 1))
/**
* IMPORTANT: Some data packing are tweaked for these values.
@@ -133,6 +138,9 @@
#define HIZ_BUF_SLOT 3
#define IRRADIANCE_GRID_BUF_SLOT 4
#define AO_BUF_SLOT 5
/* SLOT 6 is used by render shaders (Film, DoF and Motion Blur). Need to check if it should be
* assigned a different slot. */
#define REFLECTION_PROBE_BUF_SLOT 7
/* Only during pre-pass. */
#define VELOCITY_CAMERA_PREV_BUF 3
#define VELOCITY_CAMERA_CURR_BUF 4

View File

@@ -138,6 +138,7 @@ void Instance::begin_sync()
shadows.begin_sync();
pipelines.begin_sync();
cryptomatte.begin_sync();
reflection_probes.begin_sync();
light_probes.begin_sync();
gpencil_engine_enabled = false;
@@ -215,7 +216,7 @@ void Instance::object_sync(Object *ob)
sync.sync_gpencil(ob, ob_handle, res_handle);
break;
case OB_LIGHTPROBE:
light_probes.sync_probe(ob, ob_handle);
sync.sync_light_probe(ob, ob_handle);
break;
default:
break;
@@ -247,6 +248,7 @@ void Instance::end_sync()
cryptomatte.end_sync();
pipelines.end_sync();
light_probes.end_sync();
reflection_probes.end_sync();
}
void Instance::render_sync()

View File

@@ -5,26 +5,51 @@
#include "eevee_reflection_probes.hh"
#include "eevee_instance.hh"
/* Generate dummy light probe texture.
*
* Baking of Light probes aren't implemented yet. For testing purposes this can be enabled to
* generate a dummy texture.
*/
#define GENERATE_DUMMY_LIGHT_PROBE_TEXTURE false
namespace blender::eevee {
void ReflectionProbeModule::init()
{
if (!initialized_) {
initialized_ = true;
if (probes_.is_empty()) {
ReflectionProbeData init_probe_data = {};
init_probe_data.layer = -1;
for (int i : IndexRange(REFLECTION_PROBES_MAX)) {
data_buf_[i] = init_probe_data;
}
/* Initialize the world probe. */
ReflectionProbeData world_probe_data{};
world_probe_data.layer = 0;
world_probe_data.layer_subdivision = 0;
world_probe_data.area_index = 0;
world_probe_data.pos = float3(0.0f);
data_buf_[0] = world_probe_data;
ReflectionProbe world_probe;
world_probe.type = ReflectionProbe::Type::World;
world_probe.do_update_data = true;
world_probe.do_render = true;
world_probe.index = 0;
probes_.add(world_object_key_, world_probe);
const int max_mipmap_levels = log(max_resolution_) + 1;
probes_tx_.ensure_2d_array(GPU_RGBA16F,
int2(max_resolution_),
max_probes_,
init_num_probes_,
GPU_TEXTURE_USAGE_SHADER_WRITE,
nullptr,
max_mipmap_levels);
REFLECTION_PROBE_MIPMAP_LEVELS);
GPU_texture_mipmap_mode(probes_tx_, true, true);
/* Cube-map is half of the resolution of the octahedral map. */
cubemap_tx_.ensure_cube(
GPU_RGBA16F, max_resolution_ / 2, GPU_TEXTURE_USAGE_ATTACHMENT, nullptr, 1);
GPU_texture_mipmap_mode(cubemap_tx_, false, true);
GPU_RGBA16F, max_resolution_ / 2, GPU_TEXTURE_USAGE_ATTACHMENT, nullptr, 9999);
GPU_texture_mipmap_mode(cubemap_tx_, true, true);
}
{
@@ -33,13 +58,428 @@ void ReflectionProbeModule::init()
pass.shader_set(instance_.shaders.static_shader_get(REFLECTION_PROBE_REMAP));
pass.bind_texture("cubemap_tx", cubemap_tx_);
pass.bind_image("octahedral_img", probes_tx_);
pass.dispatch(int2(ceil_division(max_resolution_, REFLECTION_PROBE_GROUP_SIZE)));
pass.bind_ssbo(REFLECTION_PROBE_BUF_SLOT, data_buf_);
pass.dispatch(&dispatch_probe_pack_);
}
}
void ReflectionProbeModule::begin_sync()
{
for (ReflectionProbe &reflection_probe : probes_.values()) {
if (reflection_probe.type == ReflectionProbe::Type::Probe) {
reflection_probe.is_probe_used = false;
}
}
}
int ReflectionProbeModule::needed_layers_get() const
{
const int max_probe_data_index = reflection_probe_data_index_max();
int max_layer = 0;
for (const ReflectionProbeData &data :
Span<ReflectionProbeData>(data_buf_.data(), max_probe_data_index + 1))
{
max_layer = max_ii(max_layer, data.layer);
}
return max_layer + 1;
}
void ReflectionProbeModule::sync(const ReflectionProbe &probe)
{
switch (probe.type) {
case ReflectionProbe::Type::World: {
break;
}
case ReflectionProbe::Type::Probe: {
if (probe.do_render) {
upload_dummy_texture(probe);
}
break;
}
case ReflectionProbe::Type::Unused: {
break;
}
}
}
static int layer_subdivision_for(const int max_resolution,
const eLightProbeResolution probe_resolution)
{
int i_probe_resolution = int(probe_resolution);
return max_ii(int(log2(max_resolution)) - i_probe_resolution, 0);
}
void ReflectionProbeModule::sync_world(::World *world, WorldHandle & /*ob_handle*/)
{
const ReflectionProbe &probe = probes_.lookup(world_object_key_);
ReflectionProbeData &probe_data = data_buf_[probe.index];
probe_data.layer_subdivision = layer_subdivision_for(
max_resolution_, static_cast<eLightProbeResolution>(world->probe_resolution));
}
void ReflectionProbeModule::sync_object(Object *ob, ObjectHandle &ob_handle)
{
#if GENERATE_DUMMY_LIGHT_PROBE_TEXTURE
const ::LightProbe *light_probe = (::LightProbe *)ob->data;
if (light_probe->type != LIGHTPROBE_TYPE_CUBE) {
return;
}
const bool is_dirty = ob_handle.recalc != 0;
int subdivision = layer_subdivision_for(
max_resolution_, static_cast<eLightProbeResolution>(light_probe->resolution));
ReflectionProbe &probe = find_or_insert(ob_handle, subdivision);
probe.do_update_data |= is_dirty;
probe.is_probe_used = true;
ReflectionProbeData &probe_data = data_buf_[probe.index];
probe_data.pos = float3(float4x4(ob->object_to_world) * float4(0.0, 0.0, 0.0, 1.0));
probe_data.layer_subdivision = subdivision;
#else
UNUSED_VARS(ob, ob_handle);
#endif
}
ReflectionProbe &ReflectionProbeModule::find_or_insert(ObjectHandle &ob_handle,
int subdivision_level)
{
ReflectionProbe &reflection_probe = probes_.lookup_or_add_cb(
ob_handle.object_key.hash_value, [this, subdivision_level]() {
ReflectionProbe probe;
ReflectionProbeData probe_data = find_empty_reflection_probe_data(subdivision_level);
probe.do_update_data = true;
probe.do_render = true;
probe.type = ReflectionProbe::Type::Probe;
probe.index = reflection_probe_data_index_max() + 1;
data_buf_[probe.index] = probe_data;
return probe;
});
return reflection_probe;
}
int ReflectionProbeModule::reflection_probe_data_index_max() const
{
int result = -1;
for (const ReflectionProbe &probe : probes_.values()) {
if (probe.type != ReflectionProbe::Type::Unused) {
result = max_ii(result, probe.index);
}
}
return result;
}
/**
* Utility class to find a location in the probes_tx_ that can be used to store a new probe in
* a specified subdivision level.
*/
class ProbeLocationFinder {
BitVector<> taken_spots_;
int probes_per_dimension_;
int probes_per_layer_;
int subdivision_level_;
public:
ProbeLocationFinder(int num_layers, int subdivision_level)
{
subdivision_level_ = subdivision_level;
probes_per_dimension_ = 1 << subdivision_level_;
probes_per_layer_ = probes_per_dimension_ * probes_per_dimension_;
int num_spots = num_layers * probes_per_layer_;
taken_spots_.resize(num_spots, false);
}
/**
* Mark space to be occupied by the given probe_data.
*
* The input probe data can be stored in a different subdivision level and should be converted to
* the subdivision level what we are looking for.
*/
void mark_space_used(const ReflectionProbeData &probe_data)
{
/* Number of spots that the probe data will occupied in a single dimension. */
int clamped_subdivision_shift = max_ii(probe_data.layer_subdivision - subdivision_level_, 0);
int spots_per_dimension = 1 << max_ii(subdivision_level_ - probe_data.layer_subdivision, 0);
int probes_per_dimension_in_probe_data = 1 << probe_data.layer_subdivision;
int2 pos_in_probe_data = int2(probe_data.area_index % probes_per_dimension_in_probe_data,
probe_data.area_index / probes_per_dimension_in_probe_data);
int2 pos_in_location_finder = int2(pos_in_probe_data.x >> clamped_subdivision_shift,
pos_in_probe_data.y >> clamped_subdivision_shift);
int layer_offset = probe_data.layer * probes_per_layer_;
for (int y : IndexRange(spots_per_dimension)) {
for (int x : IndexRange(spots_per_dimension)) {
int2 pos = pos_in_location_finder + int2(x, y);
int area_index = pos.x + pos.y * probes_per_dimension_;
taken_spots_[area_index + layer_offset].set();
}
}
}
/**
* Get the first free spot.
*
* .x contains the layer the first free spot was detected.
* .y contains the area_index to use.
*
* Asserts when no free spot is found. ProbeLocationFinder should always be initialized with an
* additional layer to make sure that there is always a free spot.
*/
ReflectionProbeData first_free_spot() const
{
ReflectionProbeData result = {};
result.layer_subdivision = subdivision_level_;
for (int index : taken_spots_.index_range()) {
if (!taken_spots_[index]) {
int layer = index / probes_per_layer_;
int area_index = index % probes_per_layer_;
result.layer = layer;
result.area_index = area_index;
return result;
}
}
BLI_assert_unreachable();
return result;
}
};
ReflectionProbeData ReflectionProbeModule::find_empty_reflection_probe_data(
int subdivision_level) const
{
ProbeLocationFinder location_finder(needed_layers_get() + 1, subdivision_level);
for (const ReflectionProbeData &data :
Span<ReflectionProbeData>(data_buf_.data(), reflection_probe_data_index_max() + 1))
{
location_finder.mark_space_used(data);
}
return location_finder.first_free_spot();
}
void ReflectionProbeModule::end_sync()
{
remove_unused_probes();
int number_layers_needed = needed_layers_get();
int current_layers = probes_tx_.depth();
bool resize_layers = current_layers < number_layers_needed;
if (resize_layers) {
/* TODO: Create new texture and copy previous texture so we don't need to rerender all the
* probes.*/
probes_tx_.ensure_2d_array(GPU_RGBA16F,
int2(max_resolution_),
number_layers_needed,
GPU_TEXTURE_USAGE_SHADER_WRITE,
nullptr,
REFLECTION_PROBE_MIPMAP_LEVELS);
GPU_texture_mipmap_mode(probes_tx_, true, true);
}
recalc_lod_factors();
data_buf_.push_update();
/* Regenerate mipmaps when a probe texture is updated. It can be postponed when the world probe
* is also updated. In this case it would happen as part of the WorldProbePipeline. */
bool regenerate_mipmaps = false;
bool regenerate_mipmaps_postponed = false;
for (ReflectionProbe &probe : probes_.values()) {
if (resize_layers) {
probe.do_update_data = true;
probe.do_render = true;
}
if (!probe.needs_update()) {
continue;
}
sync(probe);
switch (probe.type) {
case ReflectionProbe::Type::World:
regenerate_mipmaps_postponed = true;
break;
case ReflectionProbe::Type::Probe:
regenerate_mipmaps = probe.do_render;
break;
case ReflectionProbe::Type::Unused:
BLI_assert_unreachable();
break;
}
probe.do_update_data = false;
probe.do_render = false;
}
if (regenerate_mipmaps) {
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
if (!regenerate_mipmaps_postponed) {
GPU_texture_update_mipmap_chain(probes_tx_);
}
}
}
void ReflectionProbeModule::remove_unused_probes()
{
bool found = false;
do {
found = false;
uint64_t key_to_remove = 0;
for (const Map<uint64_t, ReflectionProbe>::Item &item : probes_.items()) {
const ReflectionProbe &probe = item.value;
if (probe.type == ReflectionProbe::Type::Probe && !probe.is_probe_used) {
key_to_remove = item.key;
found = true;
break;
}
}
if (found) {
probes_.remove(key_to_remove);
}
} while (found);
}
void ReflectionProbeModule::remove_reflection_probe_data(int reflection_probe_data_index)
{
int max_index = reflection_probe_data_index_max();
BLI_assert_msg(reflection_probe_data_index <= max_index,
"Trying to remove reflection probes when it isn't part of the reflection probe "
"data. This can also happens when the state is set to "
"ReflectionProbe::Type::UNUSED, before removing the data.");
for (int index = reflection_probe_data_index; index < max_index; index++) {
data_buf_[index] = data_buf_[index + 1];
}
for (ReflectionProbe &probe : probes_.values()) {
if (probe.index == reflection_probe_data_index) {
probe.index = -1;
}
if (probe.index > reflection_probe_data_index) {
probe.index--;
}
}
data_buf_[max_index].layer = -1;
BLI_assert(reflection_probe_data_index_max() == max_index - 1);
}
void ReflectionProbeModule::recalc_lod_factors()
{
for (ReflectionProbeData &probe_data : data_buf_) {
if (probe_data.layer == -1) {
return;
}
const float bias = 0.0;
const float lod_factor =
bias +
0.5 * log(float(square_i(probes_tx_.width() >> probe_data.layer_subdivision))) / log(2.0);
probe_data.lod_factor = lod_factor;
}
}
/* -------------------------------------------------------------------- */
/** \name Debugging
*
* \{ */
void ReflectionProbeModule::debug_print() const
{
std::ostream &os = std::cout;
for (const ReflectionProbe &probe : probes_.values()) {
os << probe;
if (probe.index != -1) {
os << data_buf_[probe.index];
}
}
}
std::ostream &operator<<(std::ostream &os, const ReflectionProbeData &probe_data)
{
os << " - layer: " << probe_data.layer;
os << " subdivision: " << probe_data.layer_subdivision;
os << " area: " << probe_data.area_index;
os << "\n";
return os;
}
std::ostream &operator<<(std::ostream &os, const ReflectionProbe &probe)
{
switch (probe.type) {
case ReflectionProbe::Type::Unused: {
os << "UNUSED\n";
break;
}
case ReflectionProbe::Type::World: {
os << "WORLD";
os << " is_dirty: " << probe.do_update_data;
os << " index: " << probe.index;
os << "\n";
break;
}
case ReflectionProbe::Type::Probe: {
os << "PROBE";
os << " is_dirty: " << probe.do_update_data;
os << " is_used: " << probe.is_probe_used;
os << " index: " << probe.index;
os << "\n";
break;
}
}
return os;
}
void ReflectionProbeModule::upload_dummy_texture(const ReflectionProbe &probe)
{
const ReflectionProbeData &probe_data = data_buf_[probe.index];
const int resolution = max_resolution_ >> probe_data.layer_subdivision;
float4 *data = static_cast<float4 *>(
MEM_mallocN(sizeof(float4) * resolution * resolution, __func__));
/* Generate dummy checker pattern. */
int index = 0;
const int BLOCK_SIZE = max_ii(1024 >> probe_data.layer_subdivision, 1);
for (int y : IndexRange(resolution)) {
for (int x : IndexRange(resolution)) {
int tx = (x / BLOCK_SIZE) & 1;
int ty = (y / BLOCK_SIZE) & 1;
bool solid = (tx + ty) & 1;
if (solid) {
data[index] = float4((probe.index & 1) == 0 ? 0.0f : 1.0f,
(probe.index & 2) == 0 ? 0.0f : 1.0f,
(probe.index & 4) == 0 ? 0.0f : 1.0f,
1.0f);
}
else {
data[index] = float4(0.0f);
}
index++;
}
}
/* Upload the checker pattern. */
int probes_per_dimension = 1 << probe_data.layer_subdivision;
int2 probe_area_pos(probe_data.area_index % probes_per_dimension,
probe_data.area_index / probes_per_dimension);
int2 pos = probe_area_pos * int2(max_resolution_ / probes_per_dimension);
GPU_texture_update_sub(
probes_tx_, GPU_DATA_FLOAT, data, UNPACK2(pos), probe_data.layer, resolution, resolution, 1);
MEM_freeN(data);
}
/** \} */
void ReflectionProbeModule::remap_to_octahedral_projection()
{
const ReflectionProbe &world_probe = probes_.lookup(world_object_key_);
const ReflectionProbeData &probe_data = data_buf_[world_probe.index];
dispatch_probe_pack_ = int3(int2(ceil_division(max_resolution_ >> probe_data.layer_subdivision,
REFLECTION_PROBE_GROUP_SIZE)),
1);
instance_.manager->submit(remap_ps_);
/* TODO: Performance - Should only update the area that has changed. */
GPU_texture_update_mipmap_chain(probes_tx_);
}

View File

@@ -19,16 +19,58 @@ struct Material;
namespace blender::eevee {
class Instance;
struct ObjectHandle;
struct WorldHandle;
class CaptureView;
/* -------------------------------------------------------------------- */
/** \name Reflection Probes
* \{ */
struct ReflectionProbe {
enum Type { Unused, World, Probe };
Type type = Type::Unused;
/* Probe data needs to be updated. */
bool do_update_data = false;
/* Should the area in the probes_tx_ be updated? */
bool do_render = false;
/**
* Probes that aren't used during a draw can be cleared.
*
* Only valid when type == Type::Probe.
*/
bool is_probe_used = false;
/**
* Index into ReflectionProbeDataBuf.
* -1 = not added yet
*/
int index = -1;
/**
* Check if the probe needs to be updated during this sample.
*/
bool needs_update() const
{
switch (type) {
case Type::Unused:
return false;
case Type::World:
return do_update_data || do_render;
case Type::Probe:
return (do_update_data || do_render) && is_probe_used;
}
return false;
}
};
class ReflectionProbeModule {
private:
/** The max number of probes to track. */
static constexpr int max_probes_ = 1;
/** The max number of probes to initially allocate. */
static constexpr int init_num_probes_ = 1;
/**
* The maximum resolution of a cube-map side.
@@ -37,27 +79,36 @@ class ReflectionProbeModule {
*/
static constexpr int max_resolution_ = 2048;
Instance &instance_;
static constexpr uint64_t world_object_key_ = 0;
/** Texture containing a cubemap used for updating #probes_tx_. */
Instance &instance_;
ReflectionProbeDataBuf data_buf_;
Map<uint64_t, ReflectionProbe> probes_;
/** Texture containing a cubemap used as input for updating #probes_tx_. */
Texture cubemap_tx_ = {"Probe.Cubemap"};
/** Probes texture stored in octahedral mapping. */
Texture probes_tx_ = {"Probes"};
PassSimple remap_ps_ = {"Probe.CubemapToOctahedral"};
bool initialized_ = false;
bool do_world_update_ = false;
int3 dispatch_probe_pack_ = int3(0);
public:
ReflectionProbeModule(Instance &instance) : instance_(instance) {}
void init();
void begin_sync();
void sync_world(::World *world, WorldHandle &ob_handle);
void sync_object(Object *ob, ObjectHandle &ob_handle);
void end_sync();
template<typename T> void bind_resources(draw::detail::PassBase<T> *pass)
{
pass->bind_texture(REFLECTION_PROBE_TEX_SLOT, probes_tx_);
pass->bind_ssbo(REFLECTION_PROBE_BUF_SLOT, data_buf_);
}
void do_world_update_set(bool value)
@@ -65,7 +116,36 @@ class ReflectionProbeModule {
do_world_update_ = value;
}
void debug_print() const;
private:
void sync(const ReflectionProbe &cubemap);
ReflectionProbe &find_or_insert(ObjectHandle &ob_handle, int subdivision_level);
/** Get the number of layers that is needed to store probes. */
int needed_layers_get() const;
void remove_unused_probes();
void recalc_lod_factors();
/* TODO: also add _len() which is a max + 1. */
/* Get the number of reflection probe data elements. */
int reflection_probe_data_index_max() const;
/**
* Remove reflection probe data from the module.
* Ensures that data_buf is sequential and cubemaps are relinked to its corresponding data.
*/
void remove_reflection_probe_data(int reflection_probe_data_index);
/**
* Create a reflection probe data element that points to an empty spot in the cubemap that can
* hold a texture with the given subdivision_level.
*/
ReflectionProbeData find_empty_reflection_probe_data(int subdivision_level) const;
void upload_dummy_texture(const ReflectionProbe &probe);
bool do_world_update_get() const
{
return do_world_update_;
@@ -77,4 +157,8 @@ class ReflectionProbeModule {
friend class CaptureView;
};
std::ostream &operator<<(std::ostream &os, const ReflectionProbeModule &module);
std::ostream &operator<<(std::ostream &os, const ReflectionProbeData &probe_data);
std::ostream &operator<<(std::ostream &os, const ReflectionProbe &probe);
} // namespace blender::eevee

View File

@@ -1022,6 +1022,45 @@ BLI_STATIC_ASSERT_ALIGN(SubsurfaceData, 16)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Reflection Probes
* \{ */
/** Mapping data to locate a reflection probe in texture. */
struct ReflectionProbeData {
/**
* Position of the light probe in world space.
* World probe uses origin.
*/
packed_float3 pos;
/** On which layer of the texture array is this reflection probe stored. */
int layer;
/**
* Subdivision of the layer. 0 = no subdivision and resolution would be
* ReflectionProbeModule::MAX_RESOLUTION.
*/
int layer_subdivision;
/**
* Which area of the subdivided layer is the reflection probe located.
*
* A layer has (2^layer_subdivision)^2 areas.
*/
int area_index;
/**
* LOD factor for mipmap selection.
*/
float lod_factor;
int _pad[1];
};
BLI_STATIC_ASSERT_ALIGN(ReflectionProbeData, 16)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Utility Texture
* \{ */
@@ -1078,6 +1117,8 @@ using LightCullingZdistBuf = draw::StorageArrayBuffer<float, LIGHT_CHUNK, true>;
using LightDataBuf = draw::StorageArrayBuffer<LightData, LIGHT_CHUNK>;
using MotionBlurDataBuf = draw::UniformBuffer<MotionBlurData>;
using MotionBlurTileIndirectionBuf = draw::StorageBuffer<MotionBlurTileIndirection, true>;
using ReflectionProbeDataBuf =
draw::UniformArrayBuffer<ReflectionProbeData, REFLECTION_PROBES_MAX>;
using SamplingDataBuf = draw::StorageBuffer<SamplingData>;
using ShadowStatisticsBuf = draw::StorageBuffer<ShadowStatistics>;
using ShadowPagesInfoDataBuf = draw::StorageBuffer<ShadowPagesInfoData>;

View File

@@ -356,4 +356,16 @@ void SyncModule::sync_curves(Object *ob,
/** \} */
/* -------------------------------------------------------------------- */
/** \name Light Probes
* \{ */
void SyncModule::sync_light_probe(Object *ob, ObjectHandle &ob_handle)
{
inst_.light_probes.sync_probe(ob, ob_handle);
inst_.reflection_probes.sync_object(ob, ob_handle);
}
/** \} */
} // namespace blender::eevee

View File

@@ -169,6 +169,7 @@ class SyncModule {
ObjectHandle &ob_handle,
ResourceHandle res_handle,
ModifierData *modifier_data = nullptr);
void sync_light_probe(Object *ob, ObjectHandle &ob_handle);
};
/** \} */

View File

@@ -215,6 +215,7 @@ void CaptureView::render()
view.sync(view_m4, win_m4);
inst_.pipelines.world.render(view);
}
GPU_texture_update_mipmap_chain(inst_.reflection_probes.cubemap_tx_);
inst_.reflection_probes.remap_to_octahedral_projection();
GPU_debug_group_end();
}

View File

@@ -90,6 +90,7 @@ void World::sync()
WorldHandle &wo_handle = inst_.sync.sync_world(bl_world);
if (wo_handle.recalc != 0) {
inst_.reflection_probes.sync_world(bl_world, wo_handle);
inst_.reflection_probes.do_world_update_set(true);
}
wo_handle.reset_recalc_flag();

View File

@@ -55,7 +55,7 @@ void main()
vec3 reflection_light = vec3(0.0);
float shadow = 1.0;
light_world_eval(reflection_data, P, V, reflection_light);
reflection_probes_eval(reflection_data, P, V, reflection_light);
lightprobe_eval(diffuse_data, reflection_data, P, Ng, V, diffuse_light, reflection_light);
light_eval(diffuse_data,

View File

@@ -43,7 +43,7 @@ void irradiance_capture(Surfel surfel, vec3 P, inout SphericalHarmonicL1 sh)
vec3 irradiance_sky_sample(vec3 R)
{
return light_world_sample(R, 0.0);
return reflection_probes_world_sample(R, 0.0).rgb;
}
void main()

View File

@@ -20,6 +20,7 @@ vec2 octahedral_uv_from_direction(vec3 co)
vec3 octahedral_uv_to_direction(vec2 co)
{
/* Change range to between [-1..1] */
co = co * 2.0 - 1.0;
vec2 abs_co = abs(co);
@@ -31,3 +32,47 @@ vec3 octahedral_uv_to_direction(vec2 co)
return v;
}
/**
* Return the UV coordinates on the packed octahedral texture layer when applying the given
* octahedral_uv to a specific probe.
*/
vec2 octahedral_uv_to_layer_texture_coords(vec2 octahedral_uv,
ReflectionProbeData probe_data,
vec2 texel_size)
{
/* Fix artifacts near edges. Proved one texel on each side.*/
octahedral_uv = octahedral_uv * (1.0 - 2.0 * REFLECTION_PROBE_BORDER_SIZE * texel_size) +
REFLECTION_PROBE_BORDER_SIZE * texel_size + 0.5 * texel_size;
int areas_per_dimension = 1 << probe_data.layer_subdivision;
vec2 area_scalar = vec2(1.0 / float(areas_per_dimension));
octahedral_uv *= area_scalar;
vec2 area_offset = vec2(probe_data.area_index % areas_per_dimension,
probe_data.area_index / areas_per_dimension) *
area_scalar;
return octahedral_uv + area_offset;
}
/**
* Return the octahedral uv coordinates for the given texture uv coordinate on the packed
* octahedral texture layer for the given probe.
*
* It also applies wrapping in the additional space near borders.
* NOTE: Doesn't apply the translation part of the packing.
*/
vec2 octahedral_uv_from_layer_texture_coords(vec2 uv,
ReflectionProbeData probe_data,
vec2 texel_size)
{
/* Apply border region. */
vec2 shrinked_uv = (uv - REFLECTION_PROBE_BORDER_SIZE * texel_size) /
(1.0 - 2.0 * REFLECTION_PROBE_BORDER_SIZE * texel_size);
/* Use ping/pong to extend the octahedral coordinates. */
vec2 translated_pos = clamp(-sign(shrinked_uv), vec2(0.0), vec2(1.0)) * vec2(2.0) + shrinked_uv;
ivec2 checker_pos = ivec2(translated_pos);
bool is_even = ((checker_pos.x + checker_pos.y) & 1) == 0;
return is_even ? fract(shrinked_uv) : vec2(1.0) - fract(shrinked_uv);
}

View File

@@ -1,13 +1,15 @@
#pragma BLENDER_REQUIRE(eevee_sampling_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_reflection_probe_lib.glsl)
void light_world_eval(ClosureReflection reflection, vec3 P, vec3 V, inout vec3 out_specular)
vec4 reflection_probe_eval(ClosureReflection reflection,
vec3 P,
vec3 V,
ReflectionProbeData probe_data)
{
ivec3 texture_size = textureSize(reflectionProbes, 0);
/* TODO: This should be based by actual resolution. Currently the resolution is fixed but
* eventually this should based on a user setting and part of the reflection probe data that will
* be introduced by the reflection probe patch. */
float lod_cube_max = 12.0;
float lod_cube_max = min(log(float(texture_size.x)) - float(probe_data.layer_subdivision) + 1.0,
REFLECTION_PROBE_MIPMAP_LEVELS);
/* Pow2f to distributed across lod more evenly */
float roughness = clamp(pow2f(reflection.roughness), 1e-4f, 0.9999f);
@@ -37,13 +39,8 @@ void light_world_eval(ClosureReflection reflection, vec3 P, vec3 V, inout vec3 o
const float dist = 4.0 * M_PI / 6.0;
/* http://http.developer.nvidia.com/GPUGems3/gpugems3_ch20.html : Equation 13 */
/* TODO: lod_factor should be precalculated and stored inside the reflection probe data. */
const float bias = 0.0;
const float lod_factor = bias + 0.5 * log(float(square_i(texture_size.x))) / log(2.0);
/* -2: Don't use LOD levels that are smaller than 4x4 pixels. */
float lod = clamp(lod_factor - 0.5 * log2(pdf * dist), 0.0, lod_cube_max - 2.0);
vec3 l_col = light_world_sample(L, lod);
float lod = clamp(probe_data.lod_factor - 0.5 * log2(pdf * dist), 0.0, lod_cube_max);
vec4 l_col = reflection_probes_sample(L, lod, probe_data);
/* Clamped brightness. */
/* For artistic freedom this should be read from the scene/reflection probe.
@@ -52,11 +49,46 @@ void light_world_eval(ClosureReflection reflection, vec3 P, vec3 V, inout vec3 o
* account.*/
float luma = max(1e-8, max_v3(l_col));
const float firefly_factor = 1e16;
l_col *= 1.0 - max(0.0, luma - firefly_factor) / luma;
l_col.rgb *= 1.0 - max(0.0, luma - firefly_factor) / luma;
/* TODO: for artistic freedom want to read this from the reflection probe. That will be part of
* the reflection probe patch. */
const float intensity_factor = 1.0;
out_specular += vec3(intensity_factor * l_col);
return l_col;
}
return vec4(0.0);
}
int reflection_probes_find_closest(vec3 P)
{
int closest_index = -1;
float closest_distance = FLT_MAX;
/* ReflectionProbeData doesn't contain any gab, exit at first item that is invalid. */
for (int index = 1; reflection_probe_buf[index].layer != -1 && index < REFLECTION_PROBES_MAX;
index++)
{
float distance = distance(P, reflection_probe_buf[index].pos);
if (distance < closest_distance) {
closest_distance = distance;
closest_index = index;
}
}
return closest_index;
}
void reflection_probes_eval(ClosureReflection reflection, vec3 P, vec3 V, inout vec3 out_specular)
{
int closest_reflection_probe = reflection_probes_find_closest(P);
vec4 light_color = vec4(0.0);
if (closest_reflection_probe != -1) {
light_color = reflection_probe_eval(
reflection, P, V, reflection_probe_buf[closest_reflection_probe]);
}
/* Mix world lighting. */
if (light_color.a != 1.0) {
light_color.rgb = mix(reflection_probe_eval(reflection, P, V, reflection_probe_buf[0]).rgb,
light_color.rgb,
light_color.a);
}
out_specular += light_color.rgb;
}

View File

@@ -2,8 +2,16 @@
#pragma BLENDER_REQUIRE(eevee_cubemap_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_octahedron_lib.glsl)
vec3 light_world_sample(vec3 L, float lod)
vec4 reflection_probes_sample(vec3 L, float lod, ReflectionProbeData probe_data)
{
vec2 octahedral_uv = octahedral_uv_from_direction(L);
return textureLod(reflectionProbes, vec3(octahedral_uv, 0.0), lod).rgb;
vec2 octahedral_uv_packed = octahedral_uv_from_direction(L);
vec2 texel_size = vec2(1.0 / float(1 << (11 - probe_data.layer_subdivision)));
vec2 octahedral_uv = octahedral_uv_to_layer_texture_coords(
octahedral_uv_packed, probe_data, texel_size);
return textureLod(reflectionProbes, vec3(octahedral_uv, probe_data.layer), lod);
}
vec3 reflection_probes_world_sample(vec3 L, float lod)
{
return reflection_probes_sample(L, lod, reflection_probe_buf[0]).rgb;
}

View File

@@ -5,15 +5,32 @@
void main()
{
ReflectionProbeData probe_data = reflection_probe_buf[0];
ivec3 texture_coord = ivec3(gl_GlobalInvocationID.xyz);
ivec3 texture_size = imageSize(octahedral_img);
ivec3 octahedral_coord = ivec3(gl_GlobalInvocationID.xyz);
ivec3 octahedral_size = imageSize(octahedral_img);
/* Group doesn't fit in output texture. */
ivec2 octahedral_size = ivec2(texture_size.x >> probe_data.layer_subdivision,
texture_size.y >> probe_data.layer_subdivision);
/* Exit when pixel being written doesn't fit in the area reserved for the probe. */
if (any(greaterThanEqual(octahedral_coord.xy, octahedral_size.xy))) {
return;
}
vec2 octahedral_uv = vec2(octahedral_coord.xy) / vec2(octahedral_size.xy);
vec2 texel_size = vec2(1.0) / vec2(octahedral_size);
vec2 uv = vec2(octahedral_coord.xy) / vec2(octahedral_size.xy);
vec2 octahedral_uv = octahedral_uv_from_layer_texture_coords(uv, probe_data, texel_size);
vec3 R = octahedral_uv_to_direction(octahedral_uv);
vec4 col = textureLod(cubemap_tx, R, 0.0);
imageStore(octahedral_img, octahedral_coord, col);
vec4 col = textureLod(cubemap_tx, R, float(probe_data.layer_subdivision));
// col.xy = octahedral_uv;
int probes_per_dimension = 1 << probe_data.layer_subdivision;
ivec2 area_coord = ivec2(probe_data.area_index % probes_per_dimension,
probe_data.area_index / probes_per_dimension);
ivec2 area_offset = area_coord * octahedral_size;
imageStore(octahedral_img, octahedral_coord + ivec3(area_offset, 0), col);
}

View File

@@ -65,7 +65,7 @@ void radiance_transfer_surfel(inout Surfel receiver, Surfel sender)
vec3 radiance_sky_sample(vec3 R)
{
return light_world_sample(R, 0.0);
return reflection_probes_world_sample(R, 0.0).rgb;
}
void radiance_transfer_world(inout Surfel receiver, vec3 sky_L)

View File

@@ -10,14 +10,23 @@
* \{ */
GPU_SHADER_CREATE_INFO(eevee_reflection_probe_data)
.storage_buf(REFLECTION_PROBE_BUF_SLOT,
Qualifier::READ,
"ReflectionProbeData",
"reflection_probe_buf[]")
.sampler(REFLECTION_PROBE_TEX_SLOT, ImageType::FLOAT_2D_ARRAY, "reflectionProbes");
/* Sample cubemap and remap into an octahedral texture. */
GPU_SHADER_CREATE_INFO(eevee_reflection_probe_remap)
.local_group_size(REFLECTION_PROBE_GROUP_SIZE, REFLECTION_PROBE_GROUP_SIZE)
.storage_buf(REFLECTION_PROBE_BUF_SLOT,
Qualifier::READ,
"ReflectionProbeData",
"reflection_probe_buf[]")
.sampler(0, ImageType::FLOAT_CUBE, "cubemap_tx")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D_ARRAY, "octahedral_img")
.compute_source("eevee_reflection_probe_remap_comp.glsl")
.additional_info("eevee_shared")
.do_static_compilation(true);
/** \} */

View File

@@ -31,6 +31,7 @@
.vis_blur = 0.2f, \
.intensity = 1.0f, \
.flag = LIGHTPROBE_FLAG_SHOW_INFLUENCE, \
.resolution = LIGHT_PROBE_RESOLUTION_1024, \
}
/** \} */

View File

@@ -61,7 +61,10 @@ typedef struct LightProbe {
/** Surface element density for scene surface cache. In surfel per unit distance. */
float surfel_density;
char _pad1[4];
/**
* Resolution of the light probe when baked to a texture. Contains `eLightProbeResolution`.
*/
int resolution;
/** Object to use as a parallax origin. */
struct Object *parallax_ob;
@@ -71,6 +74,16 @@ typedef struct LightProbe {
struct Collection *visibility_grp;
} LightProbe;
/* LightProbe->resolution, World->probe_resolution. */
typedef enum eLightProbeResolution {
LIGHT_PROBE_RESOLUTION_64 = 6,
LIGHT_PROBE_RESOLUTION_128 = 7,
LIGHT_PROBE_RESOLUTION_256 = 8,
LIGHT_PROBE_RESOLUTION_512 = 9,
LIGHT_PROBE_RESOLUTION_1024 = 10,
LIGHT_PROBE_RESOLUTION_2048 = 11,
} eLightProbeResolution;
/* Probe->type */
enum {
LIGHTPROBE_TYPE_CUBE = 0,

View File

@@ -27,6 +27,8 @@
.preview = NULL, \
.miststa = 5.0f, \
.mistdist = 25.0f, \
\
.probe_resolution = LIGHT_PROBE_RESOLUTION_1024, \
}
/** \} */

View File

@@ -61,7 +61,13 @@ typedef struct World {
/** Assorted settings. */
short flag;
char _pad3[6];
char _pad3[2];
/** Eevee settings. */
/**
* Resolution of the world probe when baked to a texture. Contains `eLightProbeResolution`.
*/
int probe_resolution;
/** Old animation system, deprecated for 2.5. */
struct Ipo *ipo DNA_DEPRECATED;

View File

@@ -57,6 +57,16 @@ static EnumPropertyItem lightprobe_type_items[] = {
{0, nullptr, 0, nullptr, nullptr},
};
static EnumPropertyItem lightprobe_resolution_items[] = {
{LIGHT_PROBE_RESOLUTION_64, "64", 0, "64", ""},
{LIGHT_PROBE_RESOLUTION_128, "128", 0, "128", ""},
{LIGHT_PROBE_RESOLUTION_256, "256", 0, "256", ""},
{LIGHT_PROBE_RESOLUTION_512, "512", 0, "512", ""},
{LIGHT_PROBE_RESOLUTION_1024, "1024", 0, "1024", ""},
{LIGHT_PROBE_RESOLUTION_2048, "2048", 0, "2048", ""},
{0, nullptr, 0, nullptr, nullptr},
};
static void rna_def_lightprobe(BlenderRNA *brna)
{
StructRNA *srna;
@@ -189,6 +199,12 @@ static void rna_def_lightprobe(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Visibility Blur", "Filter size of the visibility blur");
RNA_def_property_update(prop, NC_MATERIAL | ND_SHADING, "rna_LightProbe_recalc");
prop = RNA_def_property(srna, "resolution", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "resolution");
RNA_def_property_enum_items(prop, lightprobe_resolution_items);
RNA_def_property_ui_text(prop, "Resolution", "Resolution when baked to a texture");
RNA_def_property_update(prop, NC_MATERIAL | ND_SHADING, "rna_LightProbe_recalc");
prop = RNA_def_property(srna, "intensity", PROP_FLOAT, PROP_NONE);
RNA_def_property_float_sdna(prop, nullptr, "intensity");
RNA_def_property_range(prop, 0.0f, FLT_MAX);

View File

@@ -13,6 +13,7 @@
#include "rna_internal.h"
#include "DNA_lightprobe_types.h"
#include "DNA_material_types.h"
#include "DNA_texture_types.h"
#include "DNA_world_types.h"
@@ -108,6 +109,16 @@ void rna_World_lightgroup_set(PointerRNA *ptr, const char *value)
#else
static const EnumPropertyItem world_probe_resolution_items[] = {
{LIGHT_PROBE_RESOLUTION_64, "64", 0, "64", ""},
{LIGHT_PROBE_RESOLUTION_128, "128", 0, "128", ""},
{LIGHT_PROBE_RESOLUTION_256, "256", 0, "256", ""},
{LIGHT_PROBE_RESOLUTION_512, "512", 0, "512", ""},
{LIGHT_PROBE_RESOLUTION_1024, "1024", 0, "1024", ""},
{LIGHT_PROBE_RESOLUTION_2048, "2048", 0, "2048", ""},
{0, nullptr, 0, nullptr, nullptr},
};
static void rna_def_lighting(BlenderRNA *brna)
{
StructRNA *srna;
@@ -263,6 +274,13 @@ void RNA_def_world(BlenderRNA *brna)
RNA_def_property_flag(prop, PROP_EDITABLE);
RNA_def_property_ui_text(prop, "Lightgroup", "Lightgroup that the world belongs to");
/* Reflection Probe Baking. */
prop = RNA_def_property(srna, "probe_resolution", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "probe_resolution");
RNA_def_property_enum_items(prop, world_probe_resolution_items);
RNA_def_property_ui_text(prop, "Resolution", "Resolution when baked to a texture");
RNA_def_property_update(prop, 0, "rna_World_draw_update");
rna_def_lighting(brna);
rna_def_world_mist(brna);
}