EEVEE-Next: Ray-Tracing: Add Planar Tracing

This traces planar lightprobe captures just like
the screen-space tracing does.

This is implemented as a separate shader that
loads the ray before the screen trace and
check if it can be traced against any available
planar probe. If it does it marks the ray as
traced (negative pdf) so that the screen tracing
pass does not override the result or try to
trace it.

Pull Request: https://projects.blender.org/blender/blender/pulls/113453
This commit is contained in:
Clément Foucault 2023-10-10 12:55:18 +02:00 committed by Clément Foucault
parent d5139065f1
commit 80a6a8efe9
27 changed files with 583 additions and 129 deletions

@ -155,6 +155,7 @@ class DATA_PT_lightprobe_eevee_next(DataButtonsPanel, Panel):
col = layout.column()
row = col.row()
col.prop(probe, "clip_start", text="Clipping Offset")
col.prop(probe, "influence_distance", text="Distance")
pass
else:
# Currently unsupported

@ -473,6 +473,7 @@ set(GLSL_SRC
engines/eevee_next/shaders/eevee_deferred_light_frag.glsl
engines/eevee_next/shaders/eevee_deferred_capture_frag.glsl
engines/eevee_next/shaders/eevee_deferred_combine_frag.glsl
engines/eevee_next/shaders/eevee_deferred_planar_frag.glsl
engines/eevee_next/shaders/eevee_depth_of_field_accumulator_lib.glsl
engines/eevee_next/shaders/eevee_depth_of_field_bokeh_lut_comp.glsl
engines/eevee_next/shaders/eevee_depth_of_field_downsample_comp.glsl
@ -532,6 +533,7 @@ set(GLSL_SRC
engines/eevee_next/shaders/eevee_ray_tile_classify_comp.glsl
engines/eevee_next/shaders/eevee_ray_tile_compact_comp.glsl
engines/eevee_next/shaders/eevee_ray_trace_fallback_comp.glsl
engines/eevee_next/shaders/eevee_ray_trace_planar_comp.glsl
engines/eevee_next/shaders/eevee_ray_trace_screen_comp.glsl
engines/eevee_next/shaders/eevee_ray_trace_screen_lib.glsl
engines/eevee_next/shaders/eevee_ray_types_lib.glsl

@ -40,6 +40,8 @@
#define REFLECTION_PROBE_SH_GROUP_SIZE 512
#define REFLECTION_PROBE_SH_SAMPLES_PER_GROUP 64
#define PLANAR_PROBES_MAX 16
/**
* IMPORTANT: Some data packing are tweaked for these values.
* Be sure to update them accordingly.
@ -166,6 +168,9 @@
#define REFLECTION_PROBE_TEX_SLOT 7
#define VOLUME_SCATTERING_TEX_SLOT 8
#define VOLUME_TRANSMITTANCE_TEX_SLOT 9
/* Currently only used by ray-tracing, but might become used by forward too. */
#define PLANAR_PROBE_DEPTH_TEX_SLOT 10
#define PLANAR_PROBE_RADIANCE_TEX_SLOT 11
/* Images. */
#define RBUFS_COLOR_SLOT 0
@ -188,6 +193,7 @@
/* Only during surface shading (forward and deferred eval). */
#define IRRADIANCE_GRID_BUF_SLOT 2
#define REFLECTION_PROBE_BUF_SLOT 3
#define PLANAR_PROBE_BUF_SLOT 4
/* Only during pre-pass. */
#define VELOCITY_CAMERA_PREV_BUF 2
#define VELOCITY_CAMERA_CURR_BUF 3

@ -17,8 +17,6 @@ namespace blender::eevee {
void HiZBuffer::sync()
{
RenderBuffers &render_buffers = inst_.render_buffers;
int2 render_extent = inst_.film.render_extent_get();
/* Padding to avoid complexity during down-sampling and screen tracing. */
int2 hiz_extent = math::ceil_to_multiple(render_extent, int2(1u << (HIZ_MIP_COUNT - 1)));
@ -33,22 +31,45 @@ void HiZBuffer::sync()
data_.uv_scale = float2(render_extent) / float2(hiz_extent);
{
hiz_update_ps_.init();
hiz_update_ps_.shader_set(inst_.shaders.static_shader_get(HIZ_UPDATE));
hiz_update_ps_.bind_ssbo("finished_tile_counter", atomic_tile_counter_);
hiz_update_ps_.bind_texture("depth_tx", &render_buffers.depth_tx, with_filter);
hiz_update_ps_.bind_image("out_mip_0", hiz_tx_.mip_view(0));
hiz_update_ps_.bind_image("out_mip_1", hiz_tx_.mip_view(1));
hiz_update_ps_.bind_image("out_mip_2", hiz_tx_.mip_view(2));
hiz_update_ps_.bind_image("out_mip_3", hiz_tx_.mip_view(3));
hiz_update_ps_.bind_image("out_mip_4", hiz_tx_.mip_view(4));
hiz_update_ps_.bind_image("out_mip_5", hiz_tx_.mip_view(5));
hiz_update_ps_.bind_image("out_mip_6", hiz_tx_.mip_view(6));
PassSimple &pass = hiz_update_ps_;
pass.init();
pass.shader_set(inst_.shaders.static_shader_get(HIZ_UPDATE));
pass.bind_ssbo("finished_tile_counter", atomic_tile_counter_);
/* TODO(fclem): Should be a parameter to avoid confusion. */
pass.bind_texture("depth_tx", &src_tx_, with_filter);
pass.bind_image("out_mip_0", hiz_tx_.mip_view(0));
pass.bind_image("out_mip_1", hiz_tx_.mip_view(1));
pass.bind_image("out_mip_2", hiz_tx_.mip_view(2));
pass.bind_image("out_mip_3", hiz_tx_.mip_view(3));
pass.bind_image("out_mip_4", hiz_tx_.mip_view(4));
pass.bind_image("out_mip_5", hiz_tx_.mip_view(5));
pass.bind_image("out_mip_6", hiz_tx_.mip_view(6));
/* TODO(@fclem): There might be occasions where we might not want to
* copy mip 0 for performance reasons if there is no need for it. */
hiz_update_ps_.push_constant("update_mip_0", true);
hiz_update_ps_.dispatch(int3(dispatch_size, 1));
hiz_update_ps_.barrier(GPU_BARRIER_TEXTURE_FETCH);
pass.push_constant("update_mip_0", true);
pass.dispatch(int3(dispatch_size, 1));
pass.barrier(GPU_BARRIER_TEXTURE_FETCH);
}
{
PassSimple &pass = hiz_update_layer_ps_;
pass.init();
pass.shader_set(inst_.shaders.static_shader_get(HIZ_UPDATE_LAYER));
pass.bind_ssbo("finished_tile_counter", atomic_tile_counter_);
/* TODO(fclem): Should be a parameter to avoid confusion. */
pass.bind_texture("depth_layered_tx", &src_tx_, with_filter);
pass.bind_image("out_mip_0", hiz_tx_.mip_view(0));
pass.bind_image("out_mip_1", hiz_tx_.mip_view(1));
pass.bind_image("out_mip_2", hiz_tx_.mip_view(2));
pass.bind_image("out_mip_3", hiz_tx_.mip_view(3));
pass.bind_image("out_mip_4", hiz_tx_.mip_view(4));
pass.bind_image("out_mip_5", hiz_tx_.mip_view(5));
pass.bind_image("out_mip_6", hiz_tx_.mip_view(6));
/* TODO(@fclem): There might be occasions where we might not want to
* copy mip 0 for performance reasons if there is no need for it. */
pass.push_constant("update_mip_0", true);
pass.push_constant("layer_id", &layer_id_);
pass.dispatch(int3(dispatch_size, 1));
pass.barrier(GPU_BARRIER_TEXTURE_FETCH);
}
if (inst_.debug_mode == eDebugMode::DEBUG_HIZ_VALIDATION) {
@ -66,18 +87,13 @@ void HiZBuffer::update()
return;
}
/* Bind another framebuffer in order to avoid triggering the feedback loop check.
* This is safe because we only use compute shaders in this section of the code.
* Ideally the check should be smarter. */
GPUFrameBuffer *fb = GPU_framebuffer_active_get();
if (G.debug & G_DEBUG_GPU) {
GPU_framebuffer_restore();
src_tx_ = *src_tx_ptr_;
if (layer_id_ == -1) {
inst_.manager->submit(hiz_update_ps_);
}
inst_.manager->submit(hiz_update_ps_);
if (G.debug & G_DEBUG_GPU) {
GPU_framebuffer_bind(fb);
else {
inst_.manager->submit(hiz_update_layer_ps_);
}
}

@ -37,10 +37,16 @@ class HiZBuffer {
draw::StorageBuffer<uint4, true> atomic_tile_counter_ = {"atomic_tile_counter"};
/** Single pass recursive down-sample. */
PassSimple hiz_update_ps_ = {"HizUpdate"};
/** Single pass recursive down-sample for layered depth buffer. Only downsample 1 layer. */
PassSimple hiz_update_layer_ps_ = {"HizUpdate.Layer"};
int layer_id_ = -1;
/** Debug pass. */
PassSimple debug_draw_ps_ = {"HizUpdate.Debug"};
/** Dirty flag to check if the update is necessary. */
bool is_dirty_ = true;
/** Reference to the depth texture to downsample. */
GPUTexture *src_tx_;
GPUTexture **src_tx_ptr_;
HiZData &data_;
@ -52,6 +58,15 @@ class HiZBuffer {
void sync();
/**
* Set source texture for the hiz downsampling.
*/
void set_source(GPUTexture **texture, int layer = -1)
{
src_tx_ptr_ = texture;
layer_id_ = layer;
}
/**
* Tag the buffer for update if needed.
*/

@ -895,6 +895,7 @@ void DeferredProbeLayer::render(View &view,
GPU_framebuffer_bind(prepass_fb);
inst_.manager->submit(prepass_ps_, view);
inst_.hiz_buffer.set_source(&inst_.render_buffers.depth_tx);
inst_.hiz_buffer.set_dirty();
inst_.lights.set_view(view, extent);
inst_.shadows.set_view(view);
@ -981,6 +982,11 @@ void PlanarProbePipeline::begin_sync()
inst_.bind_uniform_data(&gbuffer_ps_);
inst_.sampling.bind_resources(&gbuffer_ps_);
inst_.hiz_buffer.bind_resources(&gbuffer_ps_);
/* Cryptomatte. */
gbuffer_ps_.bind_image(RBUFS_CRYPTOMATTE_SLOT, &inst_.render_buffers.cryptomatte_tx);
/* RenderPasses & AOVs. */
gbuffer_ps_.bind_image(RBUFS_COLOR_SLOT, &inst_.render_buffers.rp_color_tx);
gbuffer_ps_.bind_image(RBUFS_VALUE_SLOT, &inst_.render_buffers.rp_value_tx);
inst_.cryptomatte.bind_resources(&gbuffer_ps_);
DRWState state = DRW_STATE_WRITE_COLOR | DRW_STATE_BLEND_CUSTOM | DRW_STATE_DEPTH_EQUAL;
@ -994,13 +1000,16 @@ void PlanarProbePipeline::begin_sync()
{
PassSimple &pass = eval_light_ps_;
pass.init();
pass.shader_set(inst_.shaders.static_shader_get(DEFERRED_CAPTURE_EVAL));
pass.state_set(DRW_STATE_WRITE_COLOR | DRW_STATE_BLEND_ADD_FULL);
pass.shader_set(inst_.shaders.static_shader_get(DEFERRED_PLANAR_EVAL));
pass.bind_texture(RBUFS_UTILITY_TEX_SLOT, inst_.pipelines.utility_tx);
inst_.bind_uniform_data(&pass);
inst_.gbuffer.bind_resources(&pass);
inst_.lights.bind_resources(&pass);
inst_.shadows.bind_resources(&pass);
inst_.sampling.bind_resources(&pass);
inst_.hiz_buffer.bind_resources(&pass);
inst_.reflection_probes.bind_resources(&pass);
inst_.irradiance_cache.bind_resources(&pass);
pass.barrier(GPU_BARRIER_TEXTURE_FETCH | GPU_BARRIER_SHADER_IMAGE_ACCESS);
pass.draw_procedural(GPU_PRIM_TRIS, 1, 3);
@ -1033,10 +1042,13 @@ PassMain::Sub *PlanarProbePipeline::material_add(::Material *blender_mat, GPUMat
return &pass->sub(GPU_material_get_name(gpumat));
}
void PlanarProbePipeline::render(View &view, Framebuffer &combined_fb, int2 extent)
void PlanarProbePipeline::render(View &view, Framebuffer &combined_fb, int layer_id, int2 extent)
{
GPU_debug_group_begin("Planar.Capture");
inst_.hiz_buffer.set_source(&inst_.planar_probes.depth_tx_, layer_id);
inst_.hiz_buffer.set_dirty();
GPU_framebuffer_bind(combined_fb);
GPU_framebuffer_clear_depth(combined_fb, 1.0f);
inst_.manager->submit(prepass_ps_, view);
@ -1047,6 +1059,7 @@ void PlanarProbePipeline::render(View &view, Framebuffer &combined_fb, int2 exte
inst_.gbuffer.acquire(extent, closure_bits_);
inst_.hiz_buffer.update();
GPU_framebuffer_bind(combined_fb);
GPU_framebuffer_clear_color(combined_fb, float4(0.0f, 0.0f, 0.0f, 1.0f));
inst_.manager->submit(gbuffer_ps_, view);

@ -352,7 +352,7 @@ class PlanarProbePipeline : DeferredLayerBase {
PassMain::Sub *prepass_add(::Material *material, GPUMaterial *gpumat);
PassMain::Sub *material_add(::Material *material, GPUMaterial *gpumat);
void render(View &view, Framebuffer &combined_fb, int2 extent);
void render(View &view, Framebuffer &combined_fb, int layer_id, int2 extent);
};
/** \} */

@ -7,6 +7,49 @@
namespace blender::eevee {
using namespace blender::math;
/* -------------------------------------------------------------------- */
/** \name Planar Probe
* \{ */
void PlanarProbe::sync(const float4x4 &world_to_object,
float clipping_offset,
float influence_distance)
{
this->plane_to_world = float4x4(world_to_object);
this->plane_to_world.z_axis() = normalize(this->plane_to_world.z_axis()) * influence_distance;
this->world_to_plane = invert(this->plane_to_world);
this->clipping_offset = clipping_offset;
}
void PlanarProbe::set_view(const draw::View &view, int layer_id)
{
this->viewmat = view.viewmat() * reflection_matrix_get();
this->winmat = view.winmat();
this->world_to_object_transposed = float3x4(transpose(world_to_plane));
this->normal = normalize(plane_to_world.z_axis());
bool view_is_below_plane = dot(view.location() - plane_to_world.location(),
plane_to_world.z_axis()) < 0.0;
if (view_is_below_plane) {
this->normal = -this->normal;
}
this->layer_id = layer_id;
}
float4x4 PlanarProbe::reflection_matrix_get()
{
return plane_to_world * from_scale<float4x4>(float3(1, 1, -1)) * world_to_plane;
}
float4 PlanarProbe::reflection_clip_plane_get()
{
return float4(-normal, dot(normal, plane_to_world.location()) - clipping_offset);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Planar Probe Module
* \{ */
@ -20,8 +63,8 @@ void PlanarProbeModule::init()
void PlanarProbeModule::begin_sync()
{
for (PlanarProbe &planar_probe : probes_.values()) {
planar_probe.is_probe_used = false;
for (PlanarProbe &probe : probes_.values()) {
probe.is_probe_used = false;
}
}
@ -32,18 +75,15 @@ void PlanarProbeModule::sync_object(Object *ob, ObjectHandle &ob_handle)
return;
}
/* TODO Cull out of view planars. */
PlanarProbe &probe = find_or_insert(ob_handle);
probe.plane_to_world = float4x4(ob->object_to_world);
probe.world_to_plane = float4x4(ob->world_to_object);
probe.clipping_offset = light_probe->clipsta;
probe.sync(float4x4(ob->object_to_world), light_probe->clipsta, light_probe->distinf);
probe.is_probe_used = true;
}
void PlanarProbeModule::end_sync()
{
remove_unused_probes();
probes_.remove_if(
[](const PlanarProbes::MutableItem &item) { return !item.value.is_probe_used; });
/* When first planar probes are enabled it can happen that the first sample is off. */
if (!update_probes_ && !probes_.is_empty()) {
@ -51,60 +91,55 @@ void PlanarProbeModule::end_sync()
}
}
float4x4 PlanarProbeModule::reflection_matrix_get(const float4x4 &plane_to_world,
const float4x4 &world_to_plane)
{
return math::normalize(plane_to_world) * math::from_scale<float4x4>(float3(1, 1, -1)) *
math::normalize(world_to_plane);
}
float4 PlanarProbeModule::reflection_clip_plane_get(const float4x4 &plane_to_world,
float clip_offset)
{
/* Compute clip plane equation / normal. */
float4 plane_equation = float4(-math::normalize(plane_to_world.z_axis()));
plane_equation.w = -math::dot(plane_equation.xyz(), plane_to_world.location());
plane_equation.w -= clip_offset;
return plane_equation;
}
void PlanarProbeModule::set_view(const draw::View &main_view, int2 main_view_extent)
{
const int64_t num_probes = probes_.size();
if (resources_.size() != num_probes) {
resources_.reinitialize(num_probes);
}
eGPUTextureUsage usage = GPU_TEXTURE_USAGE_ATTACHMENT | GPU_TEXTURE_USAGE_SHADER_READ;
if (num_probes == 0) {
color_tx_.ensure_2d_array(GPU_R11F_G11F_B10F, int2(1), 1, usage);
depth_tx_.ensure_2d_array(GPU_DEPTH_COMPONENT32F, int2(1), 1, usage);
return;
}
/* TODO resolution percentage. */
int2 extent = main_view_extent;
color_tx_.ensure_2d_array(GPU_R11F_G11F_B10F, extent, num_probes, usage);
depth_tx_.ensure_2d_array(GPU_DEPTH_COMPONENT32F, extent, num_probes, usage);
int layer_count = num_probes;
if (num_probes == 0) {
/* Create valid dummy texture. */
extent = int2(1);
layer_count = 1;
}
eGPUTextureUsage usage = GPU_TEXTURE_USAGE_ATTACHMENT | GPU_TEXTURE_USAGE_SHADER_READ;
radiance_tx_.ensure_2d_array(GPU_R11F_G11F_B10F, extent, layer_count, usage);
depth_tx_.ensure_2d_array(GPU_DEPTH_COMPONENT32F, extent, layer_count, usage);
int resource_index = 0;
for (PlanarProbe &probe : probes_.values()) {
if (resource_index == PLANAR_PROBES_MAX) {
break;
}
PlanarProbeResources &res = resources_[resource_index];
float4x4 winmat = main_view.winmat();
float4x4 viewmat = main_view.viewmat();
viewmat = viewmat * reflection_matrix_get(probe.plane_to_world, probe.world_to_plane);
res.view.sync(viewmat, winmat);
res.view.visibility_test(false);
/* TODO Cull out of view planars. */
world_clip_buf_.plane = reflection_clip_plane_get(probe.plane_to_world, probe.clipping_offset);
probe.set_view(main_view, resource_index);
probe_planar_buf_[resource_index] = probe;
res.view.sync(probe.viewmat, probe.winmat);
world_clip_buf_.plane = probe.reflection_clip_plane_get();
world_clip_buf_.push_update();
res.combined_fb.ensure(GPU_ATTACHMENT_TEXTURE_LAYER(depth_tx_, resource_index),
GPU_ATTACHMENT_TEXTURE_LAYER(color_tx_, resource_index));
GPU_ATTACHMENT_TEXTURE_LAYER(radiance_tx_, resource_index));
instance_.pipelines.planar.render(res.view, res.combined_fb, main_view_extent);
instance_.pipelines.planar.render(res.view, res.combined_fb, resource_index, extent);
resource_index++;
}
if (resource_index < PLANAR_PROBES_MAX) {
/* Tag the end of the array. */
probe_planar_buf_[resource_index].layer_id = -1;
}
probe_planar_buf_.push_update();
}
PlanarProbe &PlanarProbeModule::find_or_insert(ObjectHandle &ob_handle)
@ -113,12 +148,6 @@ PlanarProbe &PlanarProbeModule::find_or_insert(ObjectHandle &ob_handle)
return planar_probe;
}
void PlanarProbeModule::remove_unused_probes()
{
probes_.remove_if(
[](const PlanarProbes::MutableItem &item) { return !item.value.is_probe_used; });
}
/** \} */
} // namespace blender::eevee

@ -19,13 +19,14 @@ struct Material;
namespace blender::eevee {
class Instance;
class HiZBuffer;
struct ObjectHandle;
/* -------------------------------------------------------------------- */
/** \name Planar Probe
* \{ */
struct PlanarProbe {
struct PlanarProbe : ProbePlanarData {
/* Copy of object matrices. */
float4x4 plane_to_world;
float4x4 world_to_plane;
@ -35,6 +36,30 @@ struct PlanarProbe {
int resource_index;
/* Pruning flag. */
bool is_probe_used = false;
public:
void sync(const float4x4 &world_to_object, float clipping_offset, float influence_distance);
/**
* Update the ProbePlanarData part of the struct.
* `view` is the view we want to render this probe with.
*/
void set_view(const draw::View &view, int layer_id);
/**
* Create the reflection clip plane equation that clips along the XY plane of the given
* transform. The `clip_offset` will push the clip plane a bit further to avoid missing pixels in
* reflections. The transform does not need to be normalized but is expected to be orthogonal.
* \note Only works after `set_view` was called.
*/
float4 reflection_clip_plane_get();
private:
/**
* Create the reflection matrix that reflect along the XY plane of the given transform.
* The transform does not need to be normalized but is expected to be orthogonal.
*/
float4x4 reflection_matrix_get();
};
struct PlanarProbeResources : NonCopyable {
@ -50,18 +75,18 @@ struct PlanarProbeResources : NonCopyable {
class PlanarProbeModule {
using PlanarProbes = Map<uint64_t, PlanarProbe>;
using Resources = Array<PlanarProbeResources>;
private:
Instance &instance_;
PlanarProbes probes_;
Resources resources_;
std::array<PlanarProbeResources, PLANAR_PROBES_MAX> resources_;
Texture color_tx_ = {"planar.color_tx"};
Texture radiance_tx_ = {"planar.radiance_tx"};
Texture depth_tx_ = {"planar.depth_tx"};
ClipPlaneBuf world_clip_buf_ = {"world_clip_buf"};
ProbePlanarDataBuf probe_planar_buf_ = {"probe_planar_buf"};
bool update_probes_ = false;
@ -75,26 +100,25 @@ class PlanarProbeModule {
void set_view(const draw::View &main_view, int2 main_view_extent);
template<typename T> void bind_resources(draw::detail::PassBase<T> * /*pass*/) {}
template<typename T> void bind_resources(draw::detail::PassBase<T> *pass)
{
/* Disable filter to avoid interpolation with missing background. */
GPUSamplerState no_filter = GPUSamplerState::default_sampler();
pass->bind_ubo(PLANAR_PROBE_BUF_SLOT, &probe_planar_buf_);
pass->bind_texture(PLANAR_PROBE_RADIANCE_TEX_SLOT, &radiance_tx_, no_filter);
pass->bind_texture(PLANAR_PROBE_DEPTH_TEX_SLOT, &depth_tx_);
}
bool enabled() const
{
return update_probes_;
}
private:
PlanarProbe &find_or_insert(ObjectHandle &ob_handle);
void remove_unused_probes();
/**
* Create the reflection matrix that reflect along the XY plane of the given transform.
* The transform does not need to be normalized but is expected to be orthogonal.
*/
float4x4 reflection_matrix_get(const float4x4 &plane_to_world, const float4x4 &world_to_plane);
/**
* Create the reflection clip plane equation that clips along the XY plane of the given
* transform. The `clip_offset` will push the clip plane a bit further to avoid missing pixels in
* reflections. The transform does not need to be normalized but is expected to be orthogonal.
*/
float4 reflection_clip_plane_get(const float4x4 &plane_to_world, float clip_offset);
friend class Instance;
friend class HiZBuffer;
friend class PlanarProbePipeline;
};

@ -96,6 +96,23 @@ void RayTraceModule::sync()
for (auto type : IndexRange(3)) {
PassSimple &pass = PASS_VARIATION(trace_, type, _ps_);
pass.init();
if (inst_.planar_probes.enabled() && (&pass == &trace_reflect_ps_)) {
/* Inject planar tracing in the same pass as reflection tracing. */
PassSimple::Sub &sub = pass.sub("Trace.Planar");
sub.shader_set(inst_.shaders.static_shader_get(RAY_TRACE_PLANAR));
sub.bind_ssbo("tiles_coord_buf", &ray_tiles_buf_);
sub.bind_image("ray_data_img", &ray_data_tx_);
sub.bind_image("ray_time_img", &ray_time_tx_);
sub.bind_image("ray_radiance_img", &ray_radiance_tx_);
sub.bind_texture("depth_tx", &depth_tx);
inst_.bind_uniform_data(&sub);
inst_.planar_probes.bind_resources(&sub);
inst_.irradiance_cache.bind_resources(&sub);
inst_.reflection_probes.bind_resources(&sub);
/* TODO(@fclem): Use another dispatch with only tiles that touches planar captures. */
sub.dispatch(ray_dispatch_buf_);
sub.barrier(GPU_BARRIER_SHADER_IMAGE_ACCESS);
}
pass.shader_set(inst_.shaders.static_shader_get(SHADER_VARIATION(RAY_TRACE_SCREEN_, type)));
pass.bind_ssbo("tiles_coord_buf", &ray_tiles_buf_);
pass.bind_image("ray_data_img", &ray_data_tx_);

@ -98,10 +98,14 @@ const char *ShaderModule::static_shader_create_info_name_get(eShaderType shader_
return "eevee_deferred_light";
case DEFERRED_CAPTURE_EVAL:
return "eevee_deferred_capture_eval";
case DEFERRED_PLANAR_EVAL:
return "eevee_deferred_planar_eval";
case HIZ_DEBUG:
return "eevee_hiz_debug";
case HIZ_UPDATE:
return "eevee_hiz_update";
case HIZ_UPDATE_LAYER:
return "eevee_hiz_update_layer";
case MOTION_BLUR_GATHER:
return "eevee_motion_blur_gather";
case MOTION_BLUR_TILE_DILATE:
@ -182,6 +186,8 @@ const char *ShaderModule::static_shader_create_info_name_get(eShaderType shader_
return "eevee_ray_generate_refract";
case RAY_TRACE_FALLBACK:
return "eevee_ray_trace_fallback";
case RAY_TRACE_PLANAR:
return "eevee_ray_trace_planar";
case RAY_TRACE_SCREEN_DIFFUSE:
return "eevee_ray_trace_screen_diffuse";
case RAY_TRACE_SCREEN_REFLECT:

@ -35,6 +35,7 @@ enum eShaderType {
DEFERRED_COMBINE,
DEFERRED_LIGHT,
DEFERRED_CAPTURE_EVAL,
DEFERRED_PLANAR_EVAL,
DEBUG_SURFELS,
DEBUG_IRRADIANCE_GRID,
@ -60,6 +61,7 @@ enum eShaderType {
DOF_TILES_FLATTEN,
HIZ_UPDATE,
HIZ_UPDATE_LAYER,
HIZ_DEBUG,
LIGHT_CULLING_DEBUG,
@ -91,6 +93,7 @@ enum eShaderType {
RAY_TILE_CLASSIFY,
RAY_TILE_COMPACT,
RAY_TRACE_FALLBACK,
RAY_TRACE_PLANAR,
RAY_TRACE_SCREEN_DIFFUSE,
RAY_TRACE_SCREEN_REFLECT,
RAY_TRACE_SCREEN_REFRACT,

@ -1331,6 +1331,19 @@ struct ReflectionProbeData {
};
BLI_STATIC_ASSERT_ALIGN(ReflectionProbeData, 16)
struct ProbePlanarData {
/** Matrices used to render the planar capture. */
float4x4 viewmat;
float4x4 winmat;
/** Transform world to local position with influence distance as Z scale. */
float3x4 world_to_object_transposed;
/** World space plane normal. */
packed_float3 normal;
/** Layer in the planar capture textures used by this probe. */
int layer_id;
};
BLI_STATIC_ASSERT_ALIGN(ProbePlanarData, 16)
struct ClipPlaneData {
/** World space clip plane equation. Used to render planar light-probes. */
float4 plane;
@ -1454,6 +1467,7 @@ using RayTraceTileBuf = draw::StorageArrayBuffer<uint, 1024, true>;
using SubsurfaceTileBuf = RayTraceTileBuf;
using ReflectionProbeDataBuf =
draw::UniformArrayBuffer<ReflectionProbeData, REFLECTION_PROBES_MAX>;
using ProbePlanarDataBuf = draw::UniformArrayBuffer<ProbePlanarData, PLANAR_PROBES_MAX>;
using SamplingDataBuf = draw::StorageBuffer<SamplingData>;
using ShadowStatisticsBuf = draw::StorageBuffer<ShadowStatistics>;
using ShadowPagesInfoDataBuf = draw::StorageBuffer<ShadowPagesInfoData>;

@ -102,8 +102,6 @@ void ShadingView::render()
update_view();
inst_.hiz_buffer.set_dirty();
DRW_stats_group_start(name_);
DRW_view_set_active(render_view_);
@ -120,6 +118,7 @@ void ShadingView::render()
GPU_framebuffer_bind(combined_fb_);
GPU_framebuffer_clear_color_depth(combined_fb_, clear_color, 1.0f);
inst_.hiz_buffer.set_source(&inst_.render_buffers.depth_tx);
inst_.hiz_buffer.set_dirty();
inst_.pipelines.background.render(render_view_new_);

@ -0,0 +1,48 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/**
* Compute light objects lighting contribution using captured Gbuffer data.
*/
#pragma BLENDER_REQUIRE(eevee_gbuffer_lib.glsl)
#pragma BLENDER_REQUIRE(common_view_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_light_eval_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_lightprobe_eval_lib.glsl)
void main()
{
ivec2 texel = ivec2(gl_FragCoord.xy);
float depth = texelFetch(hiz_tx, texel, 0).r;
GBufferData gbuf = gbuffer_read(gbuf_header_tx, gbuf_closure_tx, gbuf_color_tx, texel);
vec3 P = get_world_space_from_depth(uvcoordsvar.xy, depth);
vec3 Ng = gbuf.diffuse.N;
vec3 V = cameraVec(P);
float vPz = dot(cameraForward, P) - dot(cameraForward, cameraPos);
ClosureLightStack stack;
stack.cl[0].N = gbuf.diffuse.N;
stack.cl[0].ltc_mat = LTC_LAMBERT_MAT;
stack.cl[0].type = LIGHT_DIFFUSE;
stack.cl[1].N = gbuf.reflection.N;
stack.cl[1].ltc_mat = LTC_GGX_MAT(dot(gbuf.reflection.N, V), gbuf.reflection.roughness);
stack.cl[1].type = LIGHT_SPECULAR;
/* Direct light. */
light_eval(stack, P, Ng, V, vPz, gbuf.thickness);
/* Indirect light. */
LightProbeSample samp = lightprobe_load(P, Ng, V);
vec3 radiance = vec3(0.0);
radiance += (stack.cl[0].light_shadowed + lightprobe_eval(samp, gbuf.diffuse, V, vec2(0.0))) *
gbuf.diffuse.color;
radiance += (stack.cl[1].light_shadowed + lightprobe_eval(samp, gbuf.reflection, V, vec2(0.0))) *
gbuf.reflection.color;
out_radiance = vec4(radiance, 0.0);
}

@ -45,7 +45,11 @@ void main()
/* Copy level 0. */
ivec2 src_px = ivec2(kernel_origin + local_px) * 2;
vec2 samp_co = (vec2(src_px) + 0.5) / vec2(textureSize(depth_tx, 0));
#ifdef HIZ_LAYER
vec4 samp = textureGather(depth_layered_tx, vec3(samp_co, float(layer_id)));
#else
vec4 samp = textureGather(depth_tx, samp_co);
#endif
if (update_mip_0) {
imageStore(out_mip_0, src_px + ivec2(0, 1), samp.xxxx);

@ -44,3 +44,42 @@ ivec3 lightprobe_irradiance_grid_cell_corner(int cell_corner_id)
{
return (ivec3(cell_corner_id) >> ivec3(0, 1, 2)) & 1;
}
float lightprobe_planar_score(ProbePlanarData planar, vec3 P, vec3 V, vec3 L)
{
vec3 lP = vec4(P, 1.0) * planar.world_to_object_transposed;
if (any(greaterThan(abs(lP), vec3(1.0)))) {
/* TODO: Transition in Z. Dither? */
return 0.0;
}
/* Return how much the ray is lined up with the captured ray. */
vec3 R = -reflect(V, planar.normal);
/* TODO: Use saturate (dependency hell). */
return clamp(dot(L, R), 0.0, 1.0);
}
#ifdef PLANAR_PROBES
/**
* Return the best planar probe index for a given light direction vector and postion.
*/
int lightprobe_planar_select(vec3 P, vec3 V, vec3 L)
{
/* Initialize to the score of a camera ray. */
/* TODO: Use saturate (dependency hell). */
float best_score = clamp(dot(L, -V), 0.0, 1.0);
int best_index = -1;
for (int index = 0; index < PLANAR_PROBES_MAX; index++) {
if (probe_planar_buf[index].layer_id == -1) {
/* ProbePlanarData doesn't contain any gap, exit at first item that is invalid. */
break;
}
float score = lightprobe_planar_score(probe_planar_buf[index], P, V, L);
if (score > best_score) {
best_score = score;
best_index = index;
}
}
return best_index;
}
#endif

@ -22,24 +22,6 @@
#pragma BLENDER_REQUIRE(eevee_bxdf_lib.glsl)
#pragma BLENDER_REQUIRE(common_view_lib.glsl)
void gbuffer_load_closure_data(sampler2DArray gbuf_closure_tx,
ivec2 texel,
out ClosureDiffuse closure)
{
}
void gbuffer_load_closure_data(sampler2DArray gbuf_closure_tx,
ivec2 texel,
out ClosureRefraction closure)
{
}
void gbuffer_load_closure_data(sampler2DArray gbuf_closure_tx,
ivec2 texel,
out ClosureReflection closure)
{
}
float bxdf_eval(ClosureDiffuse closure, vec3 L, vec3 V)
{
return bsdf_lambert(closure.N, L);
@ -179,7 +161,7 @@ void main()
vec4 ray_radiance = imageLoad(ray_radiance_img, sample_texel);
vec3 ray_direction = ray_data.xyz;
float ray_pdf_inv = ray_data.w;
float ray_pdf_inv = abs(ray_data.w);
/* Skip invalid pixels. */
if (ray_pdf_inv == 0.0) {
continue;

@ -0,0 +1,103 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/**
* Use screen space tracing against depth buffer of recorded planar capture to find intersection
* with the scene and its radiance.
* This pass runs before the screen trace and evaluates valid rays for planar probes. These rays
* are then tagged to avoid re-evaluation by screen trace.
*/
#pragma BLENDER_REQUIRE(eevee_lightprobe_eval_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_bxdf_sampling_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_sampling_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_ray_types_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_ray_trace_screen_lib.glsl)
#pragma BLENDER_REQUIRE(common_math_lib.glsl)
#pragma BLENDER_REQUIRE(common_math_geom_lib.glsl)
void main()
{
const uint tile_size = RAYTRACE_GROUP_SIZE;
uvec2 tile_coord = unpackUvec2x16(tiles_coord_buf[gl_WorkGroupID.x]);
ivec2 texel = ivec2(gl_LocalInvocationID.xy + tile_coord * tile_size);
vec4 ray_data = imageLoad(ray_data_img, texel);
float ray_pdf_inv = ray_data.w;
if (ray_pdf_inv == 0.0) {
/* Invalid ray or pixels without ray. Do not trace. */
imageStore(ray_time_img, texel, vec4(0.0));
imageStore(ray_radiance_img, texel, vec4(0.0));
return;
}
ivec2 texel_fullres = texel * uniform_buf.raytrace.resolution_scale +
uniform_buf.raytrace.resolution_bias;
float depth = texelFetch(depth_tx, texel_fullres, 0).r;
vec2 uv = (vec2(texel_fullres) + 0.5) * uniform_buf.raytrace.full_resolution_inv;
vec3 P = get_world_space_from_depth(uv, depth);
vec3 V = cameraVec(P);
int planar_id = lightprobe_planar_select(P, V, ray_data.xyz);
if (planar_id == -1) {
return;
}
ProbePlanarData planar = probe_planar_buf[planar_id];
/* Tag the ray data so that screen trace will not try to evaluate it and override the result. */
imageStore(ray_data_img, texel, vec4(ray_data.xyz, -ray_data.w));
Ray ray;
ray.origin = P;
ray.direction = ray_data.xyz;
vec3 radiance = vec3(0.0);
float noise_offset = sampling_rng_1D_get(SAMPLING_RAYTRACE_W);
float rand_trace = interlieved_gradient_noise(vec2(texel), 5.0, noise_offset);
/* TODO(fclem): Take IOR into account in the roughness LOD bias. */
/* TODO(fclem): pdf to roughness mapping is a crude approximation. Find something better. */
float roughness = saturate(sample_pdf_uniform_hemisphere() / ray_pdf_inv);
/* Transform the ray into planar view-space. */
Ray ray_view;
ray_view.origin = transform_point(planar.viewmat, ray.origin);
ray_view.direction = transform_direction(planar.viewmat, ray.direction);
/* Extend the ray to cover the whole view. */
ray_view.max_time = 1000.0;
ScreenTraceHitData hit = raytrace_planar(
uniform_buf.raytrace, planar_depth_tx, planar, rand_trace, ray_view);
if (hit.valid) {
/* Evaluate radiance at hit-point. */
radiance = textureLod(planar_radiance_tx, vec3(hit.ss_hit_P.xy, planar_id), 0.0).rgb;
/* Transmit twice if thickness is set and ray is longer than thickness. */
// if (thickness > 0.0 && length(ray_data.xyz) > thickness) {
// ray_radiance.rgb *= color;
// }
}
else {
/* Using ray direction as geometric normal to bias the sampling position.
* This is faster than loading the gbuffer again and averages between reflected and normal
* direction over many rays. */
vec3 Ng = ray.direction;
/* Fallback to nearest light-probe. */
LightProbeSample samp = lightprobe_load(P, Ng, V);
radiance = lightprobe_eval_direction(samp, ray.direction, safe_rcp(ray_pdf_inv));
/* Set point really far for correct reprojection of background. */
hit.time = 10000.0;
}
float luma = max(1e-8, max_v3(radiance));
radiance *= 1.0 - max(0.0, luma - uniform_buf.raytrace.brightness_clamp) / luma;
imageStore(ray_time_img, texel, vec4(hit.time));
imageStore(ray_radiance_img, texel, vec4(radiance, 0.0));
}

@ -20,15 +20,14 @@ void main()
uvec2 tile_coord = unpackUvec2x16(tiles_coord_buf[gl_WorkGroupID.x]);
ivec2 texel = ivec2(gl_LocalInvocationID.xy + tile_coord * tile_size);
ivec2 texel_fullres = texel * uniform_buf.raytrace.resolution_scale +
uniform_buf.raytrace.resolution_bias;
float depth = texelFetch(depth_tx, texel_fullres, 0).r;
vec2 uv = (vec2(texel_fullres) + 0.5) * uniform_buf.raytrace.full_resolution_inv;
vec4 ray_data = imageLoad(ray_data_img, texel);
float ray_pdf_inv = ray_data.w;
if (ray_pdf_inv < 0.0) {
/* Ray destined to planar trace. */
return;
}
if (ray_pdf_inv == 0.0) {
/* Invalid ray or pixels without ray. Do not trace. */
imageStore(ray_time_img, texel, vec4(0.0));
@ -36,6 +35,12 @@ void main()
return;
}
ivec2 texel_fullres = texel * uniform_buf.raytrace.resolution_scale +
uniform_buf.raytrace.resolution_bias;
float depth = texelFetch(depth_tx, texel_fullres, 0).r;
vec2 uv = (vec2(texel_fullres) + 0.5) * uniform_buf.raytrace.full_resolution_inv;
vec3 P = get_world_space_from_depth(uv, depth);
vec3 V = cameraVec(P);
Ray ray;

@ -68,7 +68,7 @@ METAL_ATTR ScreenTraceHitData raytrace_screen(RayTraceData rt_data,
raytrace_clip_ray_to_near_plane(ray);
}
/* NOTE: The 2.0 factor here is because we are applying it in. */
/* NOTE: The 2.0 factor here is because we are applying it in NDC space. */
ScreenSpaceRay ssray = raytrace_screenspace_ray_create(
ray, 2.0 * rt_data.full_resolution_inv, rt_data.thickness);
@ -141,9 +141,9 @@ METAL_ATTR ScreenTraceHitData raytrace_screen(RayTraceData rt_data,
ScreenTraceHitData result;
result.valid = hit;
/* Convert to world space ray time. */
result.ss_hit_P = ssray.origin.xyz + ssray.direction.xyz * time;
result.v_hit_P = project_point(drw_view.wininv, result.ss_hit_P * 2.0 - 1.0);
/* Convert to world space ray time. */
result.time = length(result.v_hit_P - ray.origin) / length(ray.direction);
#ifdef METAL_AMD_RAYTRACE_WORKAROUND
@ -156,3 +156,65 @@ METAL_ATTR ScreenTraceHitData raytrace_screen(RayTraceData rt_data,
}
#undef METAL_ATTR
#ifdef PLANAR_PROBES
ScreenTraceHitData raytrace_planar(RayTraceData rt_data,
depth2DArray planar_depth_tx,
ProbePlanarData planar,
float stride_rand,
Ray ray)
{
/* Clip to near plane for perspective view where there is a singularity at the camera origin. */
if (ProjectionMatrix[3][3] == 0.0) {
raytrace_clip_ray_to_near_plane(ray);
}
vec2 inv_texture_size = 1.0 / textureSize(planar_depth_tx, 0).xy;
/* NOTE: The 2.0 factor here is because we are applying it in NDC space. */
/* TODO(@fclem): This uses the main view's projection matrix, not the planar's one.
* This works fine for reflection, but this prevent the use of any other projection capture. */
ScreenSpaceRay ssray = raytrace_screenspace_ray_create(ray, 2.0 * inv_texture_size);
float prev_delta = 0.0, prev_time = 0.0;
float depth_sample = texture(planar_depth_tx, vec3(ssray.origin.xy, planar.layer_id)).r;
float delta = depth_sample - ssray.origin.z;
float t = 0.0, time = 0.0;
bool hit = false;
const int max_steps = 32;
for (int iter = 1; !hit && (time < ssray.max_time) && (iter < max_steps); iter++) {
float stride = 1.0 + float(iter) * rt_data.quality;
prev_time = time;
prev_delta = delta;
time = min(t + stride * stride_rand, ssray.max_time);
t += stride;
vec4 ss_ray = ssray.origin + ssray.direction * time;
depth_sample = texture(planar_depth_tx, vec3(ss_ray.xy, planar.layer_id)).r;
delta = depth_sample - ss_ray.z;
/* Check if the ray is below the surface. */
hit = (delta < 0.0);
}
/* Reject hit if background. */
hit = hit && (depth_sample != 1.0);
/* Refine hit using intersection between the sampled height-field and the ray.
* This simplifies nicely to this single line. */
time = mix(prev_time, time, saturate(prev_delta / (prev_delta - delta)));
ScreenTraceHitData result;
result.valid = hit;
result.ss_hit_P = ssray.origin.xyz + ssray.direction.xyz * time;
/* TODO(@fclem): This uses the main view's projection matrix, not the planar's one.
* This works fine for reflection, but this prevent the use of any other projection capture. */
result.v_hit_P = project_point(drw_view.wininv, result.ss_hit_P * 2.0 - 1.0);
/* Convert to world space ray time. */
result.time = length(result.v_hit_P - ray.origin) / length(ray.direction);
return result;
}
#endif

@ -76,5 +76,26 @@ GPU_SHADER_CREATE_INFO(eevee_deferred_capture_eval)
.fragment_source("eevee_deferred_capture_frag.glsl")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(eevee_deferred_planar_eval)
/* Early fragment test is needed to avoid processing fragments without correct GBuffer data. */
.early_fragment_test(true)
/* Inputs. */
.fragment_out(0, Type::VEC4, "out_radiance")
.define("REFLECTION_PROBE")
.define("SSS_TRANSMITTANCE")
.define("LIGHT_CLOSURE_EVAL_COUNT", "2")
.additional_info("eevee_shared",
"eevee_gbuffer_data",
"eevee_utility_texture",
"eevee_sampling_data",
"eevee_light_data",
"eevee_lightprobe_data",
"eevee_shadow_data",
"eevee_hiz_data",
"draw_view",
"draw_fullscreen")
.fragment_source("eevee_deferred_planar_frag.glsl")
.do_static_compilation(true);
#undef image_out
#undef image_in

@ -24,6 +24,13 @@ GPU_SHADER_CREATE_INFO(eevee_hiz_update)
.push_constant(Type::BOOL, "update_mip_0")
.compute_source("eevee_hiz_update_comp.glsl");
GPU_SHADER_CREATE_INFO(eevee_hiz_update_layer)
.do_static_compilation(true)
.define("HIZ_LAYER")
.sampler(1, ImageType::DEPTH_2D_ARRAY, "depth_layered_tx")
.push_constant(Type::INT, "layer_id")
.additional_info("eevee_hiz_update");
GPU_SHADER_CREATE_INFO(eevee_hiz_debug)
.do_static_compilation(true)
.fragment_out(0, Type::VEC4, "out_debug_color_add", DualBlend::SRC_0)

@ -209,4 +209,10 @@ GPU_SHADER_CREATE_INFO(eevee_volume_probe_data)
GPU_SHADER_CREATE_INFO(eevee_lightprobe_data)
.additional_info("eevee_reflection_probe_data", "eevee_volume_probe_data");
GPU_SHADER_CREATE_INFO(eevee_lightprobe_planar_data)
.define("REFLECTION_PROBE")
.uniform_buf(PLANAR_PROBE_BUF_SLOT, "ProbePlanarData", "probe_planar_buf[PLANAR_PROBES_MAX]")
.sampler(PLANAR_PROBE_RADIANCE_TEX_SLOT, ImageType::FLOAT_2D_ARRAY, "planar_radiance_tx")
.sampler(PLANAR_PROBE_DEPTH_TEX_SLOT, ImageType::DEPTH_2D_ARRAY, "planar_depth_tx");
/** \} */

@ -73,6 +73,23 @@ GPU_SHADER_CREATE_INFO(eevee_ray_trace_fallback)
.storage_buf(5, Qualifier::READ, "uint", "tiles_coord_buf[]")
.compute_source("eevee_ray_trace_fallback_comp.glsl");
GPU_SHADER_CREATE_INFO(eevee_ray_trace_planar)
.do_static_compilation(true)
.local_group_size(RAYTRACE_GROUP_SIZE, RAYTRACE_GROUP_SIZE)
.define("PLANAR_PROBES")
.additional_info("eevee_shared",
"eevee_global_ubo",
"eevee_sampling_data",
"draw_view",
"eevee_lightprobe_data",
"eevee_lightprobe_planar_data")
.image(0, GPU_RGBA16F, Qualifier::READ_WRITE, ImageType::FLOAT_2D, "ray_data_img")
.image(1, RAYTRACE_RAYTIME_FORMAT, Qualifier::WRITE, ImageType::FLOAT_2D, "ray_time_img")
.image(2, RAYTRACE_RADIANCE_FORMAT, Qualifier::WRITE, ImageType::FLOAT_2D, "ray_radiance_img")
.sampler(2, ImageType::DEPTH_2D, "depth_tx")
.storage_buf(5, Qualifier::READ, "uint", "tiles_coord_buf[]")
.compute_source("eevee_ray_trace_planar_comp.glsl");
GPU_SHADER_CREATE_INFO(eevee_ray_trace_screen)
.local_group_size(RAYTRACE_GROUP_SIZE, RAYTRACE_GROUP_SIZE)
.additional_info("eevee_shared",

@ -84,6 +84,10 @@ void View::frustum_boundbox_calc(int view_id)
for (float4 &corner : corners) {
mul_m4_v3(data_[view_id].viewinv.ptr(), corner);
corner.w = 1.0;
/* Special case for planar reflection. */
if (is_inverted_) {
corner.z = -corner.z;
}
}
}
@ -101,6 +105,11 @@ void View::frustum_culling_planes_calc(int view_id)
/* Normalize. */
for (float4 &plane : culling_[view_id].frustum_planes.planes) {
plane.w /= normalize_v3(plane);
/* Special case for planar reflection. */
if (is_inverted_) {
plane.z = -plane.z;
}
}
}

@ -120,6 +120,12 @@ class View {
return -(data_[view_id].winmat[3][2] + 1.0f) / data_[view_id].winmat[2][2];
}
const float3 &location(int view_id = 0) const
{
BLI_assert(view_id < view_len_);
return data_[view_id].viewinv.location();
}
const float4x4 &viewmat(int view_id = 0) const
{
BLI_assert(view_id < view_len_);