WIP: eevee-next-world-irradiance #108304

Closed
Jeroen Bakker wants to merge 79 commits from Jeroen-Bakker:eevee-next-world-irradiance into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
18 changed files with 319 additions and 58 deletions
Showing only changes of commit 408c5d7281 - Show all commits

View File

@ -491,6 +491,7 @@ set(GLSL_SRC
engines/eevee_next/shaders/eevee_shadow_tilemap_init_comp.glsl
engines/eevee_next/shaders/eevee_shadow_tilemap_lib.glsl
engines/eevee_next/shaders/eevee_spherical_harmonics_lib.glsl
engines/eevee_next/shaders/eevee_surf_capture_frag.glsl
engines/eevee_next/shaders/eevee_surf_deferred_frag.glsl
engines/eevee_next/shaders/eevee_surf_depth_frag.glsl
engines/eevee_next/shaders/eevee_surf_forward_frag.glsl

View File

@ -122,6 +122,10 @@
#define LIGHT_BUF_SLOT 1
#define LIGHT_ZBIN_BUF_SLOT 2
#define LIGHT_TILE_BUF_SLOT 3
/* Only during surface capture. */
#define SURFEL_BUF_SLOT 4
/* Only during surface capture. */
#define CAPTURE_BUF_SLOT 5
/* Only during surface shading. */
#define RBUFS_AOV_BUF_SLOT 5
/* Only during shadow rendering. */

View File

@ -249,6 +249,8 @@ void Instance::render_sample()
main_view.render();
irradiance_cache.create_surfels();
motion_blur.step();
}

View File

@ -1,38 +1,13 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_rand.hh"
#include "eevee_instance.hh"
#include "eevee_irradiance_cache.hh"
namespace blender::eevee {
void IrradianceCache::generate_random_surfels()
{
const int surfels_len = 256;
debug_surfels.resize(surfels_len);
RandomNumberGenerator rng;
rng.seed(0);
for (DebugSurfel &surfel : debug_surfels) {
float3 random = rng.get_unit_float3();
surfel.position = random * 3.0f;
surfel.normal = random;
surfel.color = float4(rng.get_float(), rng.get_float(), rng.get_float(), 1.0f);
}
debug_surfels.push_update();
}
void IrradianceCache::init()
{
if (debug_surfels_sh_ == nullptr) {
debug_surfels_sh_ = inst_.shaders.static_shader_get(DEBUG_SURFELS);
}
/* TODO: Remove this. */
generate_random_surfels();
}
void IrradianceCache::sync()
@ -42,24 +17,114 @@ void IrradianceCache::sync()
void IrradianceCache::debug_pass_sync()
{
if (inst_.debug_mode == eDebugMode::DEBUG_IRRADIANCE_CACHE_SURFELS) {
debug_surfels_ps_.init();
debug_surfels_ps_.state_set(DRW_STATE_WRITE_COLOR | DRW_STATE_WRITE_DEPTH |
DRW_STATE_DEPTH_LESS_EQUAL);
debug_surfels_ps_.shader_set(debug_surfels_sh_);
debug_surfels_ps_.bind_ssbo("surfels_buf", debug_surfels);
debug_surfels_ps_.push_constant("surfel_radius", 0.25f);
debug_surfels_ps_.draw_procedural(GPU_PRIM_TRI_STRIP, debug_surfels.size(), 4);
if (!ELEM(inst_.debug_mode,
eDebugMode::DEBUG_IRRADIANCE_CACHE_SURFELS_NORMAL,
eDebugMode::DEBUG_IRRADIANCE_CACHE_SURFELS_IRRADIANCE)) {
return;
}
debug_surfels_ps_.init();
debug_surfels_ps_.state_set(DRW_STATE_WRITE_COLOR | DRW_STATE_WRITE_DEPTH |
DRW_STATE_DEPTH_LESS_EQUAL);
debug_surfels_ps_.shader_set(inst_.shaders.static_shader_get(DEBUG_SURFELS));
debug_surfels_ps_.bind_ssbo("surfels_buf", surfels_buf_);
debug_surfels_ps_.push_constant("surfel_radius", 0.5f / 4.0f);
debug_surfels_ps_.push_constant("debug_mode", static_cast<int>(inst_.debug_mode));
debug_surfels_ps_.draw_procedural(GPU_PRIM_TRI_STRIP, surfels_buf_.size(), 4);
}
void IrradianceCache::debug_draw(View &view, GPUFrameBuffer *view_fb)
{
if (inst_.debug_mode == eDebugMode::DEBUG_IRRADIANCE_CACHE_SURFELS) {
inst_.info = "Debug Mode: Irradiance Cache Surfels";
GPU_framebuffer_bind(view_fb);
inst_.manager->submit(debug_surfels_ps_, view);
switch (inst_.debug_mode) {
case eDebugMode::DEBUG_IRRADIANCE_CACHE_SURFELS_NORMAL:
inst_.info = "Debug Mode: Surfels Normal";
break;
case eDebugMode::DEBUG_IRRADIANCE_CACHE_SURFELS_IRRADIANCE:
inst_.info = "Debug Mode: Surfels Irradiance";
break;
default:
/* Nothing to display. */
return;
}
GPU_framebuffer_bind(view_fb);
inst_.manager->submit(debug_surfels_ps_, view);
}
void IrradianceCache::create_surfels()
{
/**
* We rasterize the scene along the 3 axes. Each generated fragment will write a surface element
* so raster grid density need to match the desired surfel density. We do a first pass to know
* how much surfel to allocate then render again to create the surfels.
*/
using namespace blender::math;
/* Attachment-less frame-buffer. */
empty_raster_fb_.ensure(int2(40 * 4));
/** We could use multi-view rendering here to avoid multiple submissions but it is unlikely to
* make any difference. The bottleneck is still the light propagation loop. */
auto render_axis = [&](Axis axis) {
/* TODO(fclem): get scene bounds GPU or CPU side. Or use the irradiance grid extents. */
float4x4 winmat = math::projection::orthographic(-20.0f, 20.0f, -20.0f, 20.0f, -20.0f, 20.0f);
CartesianBasis basis = from_orthonormal_axes(AxisSigned(axis).next_after(), axis);
view_.sync(from_rotation<float4x4>(basis), winmat);
view_.visibility_test(false);
inst_.pipelines.capture.render(view_);
};
DRW_stats_group_start("IrradianceBake.SurfelsCount");
/* Raster the scene to query the number of surfel needed. */
capture_info_buf_.do_surfel_count = true;
capture_info_buf_.do_surfel_output = false;
capture_info_buf_.surfel_len = 0u;
capture_info_buf_.push_update();
render_axis(Axis::X);
render_axis(Axis::Y);
render_axis(Axis::Z);
DRW_stats_group_end();
/* Allocate surfel pool. */
capture_info_buf_.read();
if (capture_info_buf_.surfel_len == 0) {
/* Not surfel to allocated. */
return;
}
/* 1000000 for testing. */
if (capture_info_buf_.surfel_len * sizeof(Surfel) > 1000000) {
/* TODO(fclem): Display error message. */
/* Not enough GPU memory to fit all needed surfels. */
return;
}
surfels_buf_.resize(capture_info_buf_.surfel_len);
DRW_stats_group_start("IrradianceBake.SurfelsCreate");
/* Raster the scene to generate the surfels. */
capture_info_buf_.do_surfel_count = true;
capture_info_buf_.do_surfel_output = true;
capture_info_buf_.surfel_len = 0u;
capture_info_buf_.push_update();
render_axis(Axis::X);
render_axis(Axis::Y);
render_axis(Axis::Z);
DRW_stats_group_end();
/* TODO(fclem): Resize at the end of the light propagation. */
}
void IrradianceCache::propagate_light()
{
/* Evaluate direct lighting (and also clear the surfels radiance). */
/* For every ray direction over the sphere. */
/* Create the surfels lists. */
/* Sort the surfels lists. */
/* Propagate light. */
}
} // namespace blender::eevee

View File

@ -1,5 +1,9 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup eevee
*/
#pragma once
#include "eevee_shader_shared.hh"
@ -7,14 +11,23 @@
namespace blender::eevee {
class Instance;
class CapturePipeline;
class IrradianceCache {
friend CapturePipeline;
private:
Instance &inst_;
DebugSurfelBuf debug_surfels;
/** Surface elements that represent the scene. */
SurfelBuf surfels_buf_;
/** Capture state. */
CaptureInfoBuf capture_info_buf_;
PassSimple debug_surfels_ps_ = {"IrradianceCache.Debug"};
GPUShader *debug_surfels_sh_ = nullptr;
Framebuffer empty_raster_fb_ = {"empty_raster_fb_"};
View view_ = {"ortho_raster_view"};
/* TODO: Remove this. */
void generate_random_surfels();
@ -26,6 +39,9 @@ class IrradianceCache {
void init();
void sync();
void create_surfels();
void propagate_light();
void debug_pass_sync();
void debug_draw(View &view, GPUFrameBuffer *view_fb);
};

View File

@ -242,6 +242,11 @@ Material &MaterialModule::material_sync(Object *ob,
/* Order is important for transparent. */
mat.prepass = material_pass_get(ob, blender_mat, prepass_pipe, geometry_type);
mat.shading = material_pass_get(ob, blender_mat, surface_pipe, geometry_type);
if (true) {
/* TODO(fclem): This can be expensive since it can trigger a shader compilation. So better
* avoid this if we can. */
mat.capture = material_pass_get(ob, blender_mat, MAT_PIPE_CAPTURE, geometry_type);
}
if (blender_mat->blend_shadow == MA_BS_NONE) {
mat.shadow = MaterialPass();
}

View File

@ -34,6 +34,7 @@ enum eMaterialPipeline {
MAT_PIPE_FORWARD_PREPASS_VELOCITY,
MAT_PIPE_VOLUME,
MAT_PIPE_SHADOW,
MAT_PIPE_CAPTURE,
};
enum eMaterialGeometry {
@ -48,16 +49,16 @@ static inline void material_type_from_shader_uuid(uint64_t shader_uuid,
eMaterialPipeline &pipeline_type,
eMaterialGeometry &geometry_type)
{
const uint64_t geometry_mask = ((1u << 3u) - 1u);
const uint64_t pipeline_mask = ((1u << 3u) - 1u);
const uint64_t geometry_mask = ((1u << 4u) - 1u);
const uint64_t pipeline_mask = ((1u << 4u) - 1u);
geometry_type = static_cast<eMaterialGeometry>(shader_uuid & geometry_mask);
pipeline_type = static_cast<eMaterialPipeline>((shader_uuid >> 3u) & pipeline_mask);
pipeline_type = static_cast<eMaterialPipeline>((shader_uuid >> 4u) & pipeline_mask);
}
static inline uint64_t shader_uuid_from_material_type(eMaterialPipeline pipeline_type,
eMaterialGeometry geometry_type)
{
return geometry_type | (pipeline_type << 3);
return geometry_type | (pipeline_type << 4);
}
ENUM_OPERATORS(eClosureBits, CLOSURE_AMBIENT_OCCLUSION)
@ -209,7 +210,7 @@ struct MaterialPass {
struct Material {
bool is_alpha_blend_transparent;
MaterialPass shadow, shading, prepass;
MaterialPass shadow, shading, prepass, capture;
};
struct MaterialArray {

View File

@ -503,4 +503,37 @@ void DeferredPipeline::render(View &view,
/** \} */
/* -------------------------------------------------------------------- */
/** \name Capture Pipeline
*
* \{ */
void CapturePipeline::sync()
{
surface_ps_.init();
/* Surfel output is done using a SSBO, so no need for a fragment shader output color or depth. */
/* WORKAROUND: Avoid rasterizer discard, but the shaders actually use no fragment output. */
surface_ps_.state_set(DRW_STATE_WRITE_STENCIL);
surface_ps_.framebuffer_set(&inst_.irradiance_cache.empty_raster_fb_);
surface_ps_.bind_ssbo(SURFEL_BUF_SLOT, &inst_.irradiance_cache.surfels_buf_);
surface_ps_.bind_ssbo(CAPTURE_BUF_SLOT, &inst_.irradiance_cache.capture_info_buf_);
surface_ps_.bind_texture(RBUFS_UTILITY_TEX_SLOT, inst_.pipelines.utility_tx);
/* TODO(fclem): Remove. There should be no view dependent behavior during capture. */
surface_ps_.bind_ubo(CAMERA_BUF_SLOT, inst_.camera.ubo_get());
}
PassMain::Sub *CapturePipeline::surface_material_add(GPUMaterial *gpumat)
{
return &surface_ps_.sub(GPU_material_get_name(gpumat));
}
void CapturePipeline::render(View &view)
{
inst_.manager->submit(surface_ps_, view);
}
/** \} */
} // namespace blender::eevee

View File

@ -184,6 +184,28 @@ class DeferredPipeline {
/** \} */
/* -------------------------------------------------------------------- */
/** \name Capture Pipeline
*
* \{ */
class CapturePipeline {
private:
Instance &inst_;
PassMain surface_ps_ = {"Capture.Surface"};
public:
CapturePipeline(Instance &inst) : inst_(inst){};
PassMain::Sub *surface_material_add(GPUMaterial *gpumat);
void sync();
void render(View &view);
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Utility texture
*
@ -271,17 +293,20 @@ class PipelineModule {
DeferredPipeline deferred;
ForwardPipeline forward;
ShadowPipeline shadow;
CapturePipeline capture;
UtilityTexture utility_tx;
public:
PipelineModule(Instance &inst) : world(inst), deferred(inst), forward(inst), shadow(inst){};
PipelineModule(Instance &inst)
: world(inst), deferred(inst), forward(inst), shadow(inst), capture(inst){};
void begin_sync()
{
deferred.begin_sync();
forward.sync();
shadow.sync();
capture.sync();
}
void end_sync()
@ -324,6 +349,8 @@ class PipelineModule {
return nullptr;
case MAT_PIPE_SHADOW:
return shadow.surface_material_add(gpumat);
case MAT_PIPE_CAPTURE:
return capture.surface_material_add(gpumat);
}
return nullptr;
}

View File

@ -414,7 +414,6 @@ void ShaderModule::material_create_info_ammend(GPUMaterial *gpumat, GPUCodegenOu
info.additional_info("eevee_geom_curves");
break;
case MAT_GEOM_MESH:
default:
info.additional_info("eevee_geom_mesh");
break;
}
@ -439,6 +438,9 @@ void ShaderModule::material_create_info_ammend(GPUMaterial *gpumat, GPUCodegenOu
case MAT_PIPE_SHADOW:
info.additional_info("eevee_surf_shadow");
break;
case MAT_PIPE_CAPTURE:
info.additional_info("eevee_surf_capture");
break;
case MAT_PIPE_DEFERRED:
info.additional_info("eevee_surf_deferred");
break;
@ -446,7 +448,7 @@ void ShaderModule::material_create_info_ammend(GPUMaterial *gpumat, GPUCodegenOu
info.additional_info("eevee_surf_forward");
break;
default:
BLI_assert(0);
BLI_assert_unreachable();
break;
}
break;

View File

@ -51,7 +51,8 @@ enum eDebugMode : uint32_t {
/**
* Display IrradianceCache surfels.
*/
DEBUG_IRRADIANCE_CACHE_SURFELS = 3u,
DEBUG_IRRADIANCE_CACHE_SURFELS_NORMAL = 3u,
DEBUG_IRRADIANCE_CACHE_SURFELS_IRRADIANCE = 4u,
/**
* Show tiles depending on their status.
*/
@ -826,17 +827,48 @@ static inline ShadowTileDataPacked shadow_tile_pack(ShadowTileData tile)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Debug
/** \name Irradiance Cache
* \{ */
struct DebugSurfel {
struct Surfel {
/** World position of the surfel. */
packed_float3 position;
int _pad0;
/** World orientation of the surface. */
packed_float3 normal;
int _pad1;
float4 color;
/** Surface albedo to apply to incoming radiance. */
packed_float3 albedo;
int _pad2;
/** Accumulated reflected radiance at this point. */
packed_float3 radiance;
int _pad3;
};
BLI_STATIC_ASSERT_ALIGN(DebugSurfel, 16)
BLI_STATIC_ASSERT_ALIGN(Surfel, 16)
struct CaptureInfoData {
/** True if the surface shader needs to write the surfel data. */
bool1 do_surfel_output;
/** True if the surface shader needs to increment the surfel_len. */
bool1 do_surfel_count;
/** Number of surfels inside the surfel buffer or the needed len. */
uint surfel_len;
int _pad0;
};
BLI_STATIC_ASSERT_ALIGN(CaptureInfoData, 16)
enum SurfelListEntryType : uint32_t {
ENTRY_SURFEL = 0u,
ENTRY_IRRADIANCE_SAMPLE = 1u,
};
struct SurfelListEntry {
uint next_entry_index;
SurfelListEntryType type;
uint payload;
uint _pad0;
};
BLI_STATIC_ASSERT_ALIGN(SurfelListEntry, 16)
/** \} */
@ -947,7 +979,6 @@ using DepthOfFieldDataBuf = draw::UniformBuffer<DepthOfFieldData>;
using DepthOfFieldScatterListBuf = draw::StorageArrayBuffer<ScatterRect, 16, true>;
using DrawIndirectBuf = draw::StorageBuffer<DrawCommand, true>;
using FilmDataBuf = draw::UniformBuffer<FilmData>;
using DebugSurfelBuf = draw::StorageArrayBuffer<DebugSurfel, 64>;
using HiZDataBuf = draw::UniformBuffer<HiZData>;
using LightCullingDataBuf = draw::StorageBuffer<LightCullingData>;
using LightCullingKeyBuf = draw::StorageArrayBuffer<uint, LIGHT_CHUNK, true>;
@ -965,6 +996,8 @@ using ShadowPageCacheBuf = draw::StorageArrayBuffer<uint2, SHADOW_MAX_PAGE, true
using ShadowTileMapDataBuf = draw::StorageVectorBuffer<ShadowTileMapData, SHADOW_MAX_TILEMAP>;
using ShadowTileMapClipBuf = draw::StorageArrayBuffer<ShadowTileMapClip, SHADOW_MAX_TILEMAP, true>;
using ShadowTileDataBuf = draw::StorageArrayBuffer<ShadowTileDataPacked, SHADOW_MAX_TILE, true>;
using SurfelBuf = draw::StorageArrayBuffer<Surfel, 64, true>;
using CaptureInfoBuf = draw::StorageBuffer<CaptureInfoData>;
using VelocityGeometryBuf = draw::StorageArrayBuffer<float4, 16, true>;
using VelocityIndexBuf = draw::StorageArrayBuffer<VelocityIndex, 16>;
using VelocityObjectBuf = draw::StorageArrayBuffer<float4x4, 16>;

View File

@ -131,6 +131,7 @@ void SyncModule::sync_mesh(Object *ob,
geometry_call(material.shading.sub_pass, geom, res_handle);
geometry_call(material.prepass.sub_pass, geom, res_handle);
geometry_call(material.shadow.sub_pass, geom, res_handle);
geometry_call(material.capture.sub_pass, geom, res_handle);
is_shadow_caster = is_shadow_caster || material.shadow.sub_pass != nullptr;
is_alpha_blend = is_alpha_blend || material.is_alpha_blend_transparent;

View File

@ -134,7 +134,6 @@ void ShadingView::render()
inst_.lights.debug_draw(render_view_new_, combined_fb_);
inst_.hiz_buffer.debug_draw(render_view_new_, combined_fb_);
inst_.shadows.debug_draw(render_view_new_, combined_fb_);
inst_.irradiance_cache.debug_draw(render_view_new_, combined_fb_);
GPUTexture *combined_final_tx = render_postfx(rbufs.combined_tx);

View File

@ -1,8 +1,17 @@
void main()
{
DebugSurfel surfel = surfels_buf[surfel_index];
out_color = surfel.color;
Surfel surfel = surfels_buf[surfel_index];
switch (eDebugMode(debug_mode)) {
default:
case DEBUG_IRRADIANCE_CACHE_SURFELS_NORMAL:
out_color = vec4(surfel.normal, 1.0);
break;
case DEBUG_IRRADIANCE_CACHE_SURFELS_IRRADIANCE:
out_color = vec4(surfel.radiance, 1.0);
break;
}
/* Display surfels as circles. */
if (distance(P, surfel.position) > surfel_radius) {

View File

@ -4,7 +4,7 @@
void main()
{
surfel_index = gl_InstanceID;
DebugSurfel surfel = surfels_buf[surfel_index];
Surfel surfel = surfels_buf[surfel_index];
vec3 lP;
@ -35,4 +35,5 @@ void main()
P = (model_matrix * vec4(lP, 1)).xyz;
gl_Position = point_world_to_ndc(P);
gl_Position.z -= 2.5e-5;
}

View File

@ -0,0 +1,53 @@
/**
* Surface Capture: Output surface parameters to diverse storage.
*
* This is a separate shader to allow custom closure behavior and avoid putting more complexity
* into other surface shaders.
*/
#pragma BLENDER_REQUIRE(gpu_shader_math_vector_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_gbuffer_lib.glsl)
#pragma BLENDER_REQUIRE(common_view_lib.glsl)
#pragma BLENDER_REQUIRE(common_math_lib.glsl)
#pragma BLENDER_REQUIRE(common_hair_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_surf_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_nodetree_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_sampling_lib.glsl)
vec4 closure_to_rgba(Closure cl)
{
return vec4(0.0);
}
void main()
{
init_globals();
/* TODO(fclem): Remove random sampling for capture and accumulate color. */
g_closure_rand = 0.5;
nodetree_surface();
g_diffuse_data.color *= g_diffuse_data.weight;
g_reflection_data.color *= g_reflection_data.weight;
g_refraction_data.color *= g_refraction_data.weight;
vec3 albedo = g_diffuse_data.color + g_reflection_data.color;
/* ----- Surfel output ----- */
if (capture_info_buf.do_surfel_count) {
/* Generate a surfel only once. This check allow cases where no axis is dominant. */
bool is_surface_view_aligned = dominant_axis(g_data.Ng) == dominant_axis(cameraForward);
if (is_surface_view_aligned) {
uint surfel_id = atomicAdd(capture_info_buf.surfel_len, 1u);
if (capture_info_buf.do_surfel_output) {
surfel_buf[surfel_id].position = g_data.P;
surfel_buf[surfel_id].normal = gl_FrontFacing ? g_data.Ng : -g_data.Ng;
surfel_buf[surfel_id].albedo = albedo;
surfel_buf[surfel_id].radiance = g_emission;
}
}
}
}

View File

@ -11,6 +11,7 @@ GPU_SHADER_CREATE_INFO(eevee_debug_surfels)
.vertex_out(eeve_debug_surfel_iface)
.fragment_source("eevee_debug_surfels_frag.glsl")
.fragment_out(0, Type::VEC4, "out_color")
.storage_buf(0, Qualifier::READ, "DebugSurfel", "surfels_buf[]")
.storage_buf(0, Qualifier::READ, "Surfel", "surfels_buf[]")
.push_constant(Type::FLOAT, "surfel_radius")
.push_constant(Type::INT, "debug_mode")
.do_static_compilation(true);

View File

@ -147,6 +147,13 @@ GPU_SHADER_CREATE_INFO(eevee_surf_forward)
// "eevee_render_pass_out",
);
GPU_SHADER_CREATE_INFO(eevee_surf_capture)
.vertex_out(eevee_surf_iface)
.storage_buf(SURFEL_BUF_SLOT, Qualifier::WRITE, "Surfel", "surfel_buf[]")
.storage_buf(CAPTURE_BUF_SLOT, Qualifier::READ_WRITE, "CaptureInfoData", "capture_info_buf")
.fragment_source("eevee_surf_capture_frag.glsl")
.additional_info("eevee_camera", "eevee_utility_texture");
GPU_SHADER_CREATE_INFO(eevee_surf_depth)
.vertex_out(eevee_surf_iface)
.fragment_source("eevee_surf_depth_frag.glsl")
@ -233,6 +240,7 @@ GPU_SHADER_CREATE_INFO(eevee_material_stub).define("EEVEE_MATERIAL_STUBS");
EEVEE_MAT_GEOM_VARIATIONS(name##_depth, "eevee_surf_depth", __VA_ARGS__) \
EEVEE_MAT_GEOM_VARIATIONS(name##_deferred, "eevee_surf_deferred", __VA_ARGS__) \
EEVEE_MAT_GEOM_VARIATIONS(name##_forward, "eevee_surf_forward", __VA_ARGS__) \
EEVEE_MAT_GEOM_VARIATIONS(name##_capture, "eevee_surf_capture", __VA_ARGS__) \
EEVEE_MAT_GEOM_VARIATIONS(name##_shadow, "eevee_surf_shadow", __VA_ARGS__)
EEVEE_MAT_PIPE_VARIATIONS(eevee_surface, "eevee_material_stub")