Eevee: LTC area lights

Using Linear Transform Cosines to compute area lighting. This is far more accurate than other techniques but also slower.

We use rotating quad to mimic sphere area light. For a better approximation, we use a rotating octogon.
This commit is contained in:
Clément Foucault
2017-04-03 11:04:42 +02:00
parent a78e97b206
commit 46cd87f5da
11 changed files with 407 additions and 17 deletions

View File

@@ -105,6 +105,7 @@ data_to_c_simple(engines/eevee/shaders/lit_surface_vert.glsl SRC)
data_to_c_simple(engines/eevee/shaders/tonemap_frag.glsl SRC)
data_to_c_simple(engines/eevee/shaders/bsdf_direct_lib.glsl SRC)
data_to_c_simple(engines/eevee/shaders/bsdf_common_lib.glsl SRC)
data_to_c_simple(engines/eevee/shaders/ltc_lib.glsl SRC)
data_to_c_simple(modes/shaders/common_globals_lib.glsl SRC)
data_to_c_simple(modes/shaders/edit_overlay_frag.glsl SRC)

View File

@@ -29,6 +29,7 @@
#include "eevee.h"
#include "eevee_private.h"
#include "eevee_lut.h"
#define EEVEE_ENGINE "BLENDER_EEVEE"
@@ -37,9 +38,12 @@ static struct {
struct GPUShader *default_lit;
struct GPUShader *depth_sh;
struct GPUShader *tonemap;
struct GPUTexture *ltc_mat;
struct GPUTexture *ltc_mag;
float camera_pos[3];
} e_data = {NULL}; /* Engine data */
extern char datatoc_ltc_lib_glsl[];
extern char datatoc_bsdf_common_lib_glsl[];
extern char datatoc_bsdf_direct_lib_glsl[];
extern char datatoc_lit_surface_frag_glsl[];
@@ -70,6 +74,7 @@ static void EEVEE_engine_init(void *vedata)
DynStr *ds_vert = BLI_dynstr_new();
BLI_dynstr_append(ds_vert, datatoc_bsdf_common_lib_glsl);
BLI_dynstr_append(ds_vert, datatoc_ltc_lib_glsl);
BLI_dynstr_append(ds_vert, datatoc_bsdf_direct_lib_glsl);
lib_str = BLI_dynstr_get_cstring(ds_vert);
BLI_dynstr_free(ds_vert);
@@ -83,6 +88,14 @@ static void EEVEE_engine_init(void *vedata)
e_data.tonemap = DRW_shader_create_fullscreen(datatoc_tonemap_frag_glsl, NULL);
}
if (!e_data.ltc_mat) {
e_data.ltc_mat = DRW_texture_create_2D(64, 64, DRW_TEX_RGBA_16, DRW_TEX_FILTER, ltc_mat_ggx);
}
if (!e_data.ltc_mag) {
e_data.ltc_mag = DRW_texture_create_2D(64, 64, DRW_TEX_R_16, DRW_TEX_FILTER, ltc_mag_ggx);
}
if (stl->lights_info == NULL)
EEVEE_lights_init(stl);
@@ -134,6 +147,8 @@ static void EEVEE_cache_init(void *vedata)
DRW_shgroup_uniform_block(stl->g_data->default_lit_grp, "light_block", stl->lights_ubo, 0);
DRW_shgroup_uniform_int(stl->g_data->default_lit_grp, "light_count", &stl->lights_info->light_count, 1);
DRW_shgroup_uniform_vec3(stl->g_data->default_lit_grp, "cameraPos", e_data.camera_pos, 1);
DRW_shgroup_uniform_texture(stl->g_data->default_lit_grp, "ltcMat", e_data.ltc_mat, 0);
DRW_shgroup_uniform_texture(stl->g_data->default_lit_grp, "ltcMag", e_data.ltc_mag, 1);
}
{
@@ -220,6 +235,10 @@ static void EEVEE_engine_free(void)
DRW_shader_free(e_data.default_lit);
if (e_data.tonemap)
DRW_shader_free(e_data.tonemap);
if (e_data.ltc_mat)
DRW_texture_free(e_data.ltc_mat);
if (e_data.ltc_mag)
DRW_texture_free(e_data.ltc_mag);
}
static void EEVEE_collection_settings_create(RenderEngine *UNUSED(engine), IDProperty *props)

View File

@@ -133,17 +133,17 @@ void EEVEE_lights_update(EEVEE_StorageList *stl)
}
}
else {
evli->sizex = MAX2(0.0001f, la->area_size);
evli->sizex = MAX2(0.001f, la->area_size);
}
/* Make illumination power constant */
if (la->type == LA_AREA) {
power = 1.0f / (evli->sizex * evli->sizey * 4.0f * M_PI) /* 1/(w*h*Pi) */
* M_PI * 10.0f; /* XXX : Empirical, Fit cycles power */
* 80.0f; /* XXX : Empirical, Fit cycles power */
}
else if (la->type == LA_SPOT || la->type == LA_LOCAL) {
power = 1.0f / (4.0f * evli->sizex * evli->sizex * M_PI * M_PI) /* 1/(4*r²*Pi²) */
* M_PI * 100.0; /* XXX : Empirical, Fit cycles power */
* M_PI * M_PI * M_PI * 10.0; /* XXX : Empirical, Fit cycles power */
/* for point lights (a.k.a radius == 0.0) */
// power = M_PI * M_PI * 0.78; /* XXX : Empirical, Fit cycles power */

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,8 @@
#define M_PI 3.14159265358979323846 /* pi */
#define M_1_PI 0.318309886183790671538 /* 1/pi */
#define M_1_2PI 0.159154943091895335768 /* 1/(2*pi) */
#define M_1_PI2 0.101321183642337771443 /* 1/(pi^2) */
/* ------- Structures -------- */
@@ -73,6 +75,26 @@ vec3 line_plane_intersect(vec3 lineorigin, vec3 linedirection, vec3 planeorigin,
return lineorigin + linedirection * dist;
}
float line_aligned_plane_intersect_dist(vec3 lineorigin, vec3 linedirection, vec3 planeorigin)
{
/* aligned plane normal */
vec3 L = planeorigin - lineorigin;
float diskdist = length(L);
vec3 planenormal = -normalize(L);
return -diskdist / dot(planenormal, linedirection);
}
vec3 line_aligned_plane_intersect(vec3 lineorigin, vec3 linedirection, vec3 planeorigin)
{
float dist = line_aligned_plane_intersect_dist(lineorigin, linedirection, planeorigin);
if (dist < 0) {
/* if intersection is behind we fake the intersection to be
* really far and (hopefully) not inside the radius of interest */
dist = 1e16;
}
return lineorigin + linedirection * dist;
}
float rectangle_solid_angle(AreaData ad)
{
vec3 n0 = normalize(cross(ad.corner[0], ad.corner[1]));
@@ -90,13 +112,12 @@ float rectangle_solid_angle(AreaData ad)
vec3 get_specular_dominant_dir(vec3 N, vec3 R, float roughness)
{
return normalize(mix(N, R, (1.0 - roughness * roughness)));
return normalize(mix(N, R, 1.0 - roughness * roughness));
}
/* From UE4 paper */
vec3 mrp_sphere(LightData ld, ShadingData sd, vec3 dir, inout float roughness, out float energy_conservation)
{
/* Sphere Light */
roughness = max(3e-3, roughness); /* Artifacts appear with roughness below this threshold */
/* energy preservation */
@@ -114,8 +135,13 @@ vec3 mrp_sphere(LightData ld, ShadingData sd, vec3 dir, inout float roughness, o
return normalize(closest_point_on_sphere);
}
vec3 mrp_area(LightData ld, ShadingData sd, vec3 dir)
vec3 mrp_area(LightData ld, ShadingData sd, vec3 dir, inout float roughness, out float energy_conservation)
{
roughness = max(3e-3, roughness); /* Artifacts appear with roughness below this threshold */
/* FIXME : This needs to be fixed */
energy_conservation = pow(roughness / saturate(roughness + 0.5 * sd.area_data.solid_angle), 2.0);
vec3 refproj = line_plane_intersect(sd.W, dir, ld.l_position, ld.l_forward);
/* Project the point onto the light plane */
@@ -129,7 +155,10 @@ vec3 mrp_area(LightData ld, ShadingData sd, vec3 dir)
/* go back in world space */
vec3 closest_point_on_rectangle = sd.l_vector + mrp.x * ld.l_right + mrp.y * ld.l_up;
return normalize(closest_point_on_rectangle);
float len = length(closest_point_on_rectangle);
energy_conservation /= len * len;
return closest_point_on_rectangle / len;
}
/* GGX */

View File

@@ -42,7 +42,7 @@ float direct_diffuse_sphere(LightData ld, ShadingData sd)
}
bsdf = max(bsdf, 0.0);
bsdf *= M_1_PI * M_1_PI;
bsdf *= M_1_PI2;
return bsdf;
}
@@ -51,6 +51,10 @@ float direct_diffuse_sphere(LightData ld, ShadingData sd)
* http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf */
float direct_diffuse_rectangle(LightData ld, ShadingData sd)
{
#ifdef USE_LTC
float bsdf = ltc_evaluate(sd.N, sd.V, mat3(1.0), sd.area_data.corner);
bsdf *= M_1_2PI;
#else
float bsdf = sd.area_data.solid_angle * 0.2 * (
max(0.0, dot(normalize(sd.area_data.corner[0]), sd.N)) +
max(0.0, dot(normalize(sd.area_data.corner[1]), sd.N)) +
@@ -58,7 +62,8 @@ float direct_diffuse_rectangle(LightData ld, ShadingData sd)
max(0.0, dot(normalize(sd.area_data.corner[3]), sd.N)) +
max(0.0, dot(sd.L, sd.N))
);
bsdf *= M_1_PI;
#endif
return bsdf;
}
@@ -87,24 +92,76 @@ float direct_ggx_point(ShadingData sd, float roughness)
float direct_ggx_sphere(LightData ld, ShadingData sd, float roughness)
{
#ifdef USE_LTC
vec3 P = line_aligned_plane_intersect(vec3(0.0), sd.spec_dominant_dir, sd.l_vector);
vec3 Px = normalize(P - sd.l_vector) * ld.l_radius;
vec3 Py = cross(Px, sd.L);
float NV = max(dot(sd.N, sd.V), 1e-8);
vec2 uv = ltc_coords(NV, sqrt(roughness));
mat3 ltcmat = ltc_matrix(uv);
// #define HIGHEST_QUALITY
#ifdef HIGHEST_QUALITY
vec3 Pxy1 = normalize( Px + Py) * ld.l_radius;
vec3 Pxy2 = normalize(-Px + Py) * ld.l_radius;
/* counter clockwise */
vec3 points[8];
points[0] = sd.l_vector + Px;
points[1] = sd.l_vector - Pxy2;
points[2] = sd.l_vector - Py;
points[3] = sd.l_vector - Pxy1;
points[4] = sd.l_vector - Px;
points[5] = sd.l_vector + Pxy2;
points[6] = sd.l_vector + Py;
points[7] = sd.l_vector + Pxy1;
float bsdf = ltc_evaluate_circle(sd.N, sd.V, ltcmat, points);
#else
vec3 points[4];
points[0] = sd.l_vector + Px;
points[1] = sd.l_vector - Py;
points[2] = sd.l_vector - Px;
points[3] = sd.l_vector + Py;
float bsdf = ltc_evaluate(sd.N, sd.V, ltcmat, points);
/* sqrt(pi/2) difference between square and disk area */
bsdf *= 1.25331413731;
#endif
bsdf *= texture(ltcMag, uv).r; /* Bsdf intensity */
bsdf *= M_1_2PI * M_1_PI;
#else
float energy_conservation;
vec3 L = mrp_sphere(ld, sd, sd.spec_dominant_dir, roughness, energy_conservation);
float bsdf = bsdf_ggx(sd.N, L, sd.V, roughness);
bsdf *= energy_conservation / (sd.l_distance * sd.l_distance);
bsdf *= max(ld.l_radius * ld.l_radius, 1e-16); /* radius is already inside energy_conservation */
#endif
return bsdf;
}
float direct_ggx_rectangle(LightData ld, ShadingData sd, float roughness)
{
vec3 L = mrp_area(ld, sd, sd.spec_dominant_dir);
#ifdef USE_LTC
float NV = max(dot(sd.N, sd.V), 1e-8);
vec2 uv = ltc_coords(NV, sqrt(roughness));
mat3 ltcmat = ltc_matrix(uv);
float bsdf = bsdf_ggx(sd.N, L, sd.V, roughness) * sd.area_data.solid_angle;
bsdf *= max(0.0, dot(-sd.spec_dominant_dir, ld.l_forward)); /* fade mrp artifacts */
float bsdf = ltc_evaluate(sd.N, sd.V, ltcmat, sd.area_data.corner);
bsdf *= texture(ltcMag, uv).r; /* Bsdf intensity */
bsdf *= M_1_2PI;
#else
float energy_conservation;
vec3 L = mrp_area(ld, sd, sd.spec_dominant_dir, roughness, energy_conservation);
float bsdf = bsdf_ggx(sd.N, L, sd.V, roughness);
bsdf *= energy_conservation;
/* fade mrp artifacts */
bsdf *= max(0.0, dot(-sd.spec_dominant_dir, ld.l_forward));
bsdf *= max(0.0, -dot(L, ld.l_forward));
#endif
return bsdf;
}

View File

@@ -88,7 +88,9 @@ float light_common(inout LightData ld, inout ShadingData sd)
sd.area_data.corner[1] = sd.l_vector + ld.l_right * -ld.l_sizex + ld.l_up * -ld.l_sizey;
sd.area_data.corner[2] = sd.l_vector + ld.l_right * ld.l_sizex + ld.l_up * -ld.l_sizey;
sd.area_data.corner[3] = sd.l_vector + ld.l_right * ld.l_sizex + ld.l_up * ld.l_sizey;
#ifndef USE_LTC
sd.area_data.solid_angle = rectangle_solid_angle(sd.area_data);
#endif
}
return vis;
@@ -107,7 +109,7 @@ void main()
/* hardcoded test vars */
vec3 albedo = vec3(0.0);
vec3 specular = mix(vec3(1.0), vec3(1.0), pow(max(0.0, 1.0 - dot(sd.N, sd.V)), 5.0));
float roughness = 0.5;
float roughness = 0.1;
sd.spec_dominant_dir = get_specular_dominant_dir(sd.N, sd.R, roughness);

View File

@@ -0,0 +1,243 @@
/* Mainly From https://eheitzresearch.wordpress.com/415-2/ */
#define USE_LTC
#define LTC_LUT_SIZE 64
uniform sampler2D ltcMat;
uniform sampler2D ltcMag;
/* from Real-Time Area Lighting: a Journey from Research to Production
* Stephen Hill and Eric Heitz */
float edge_integral(vec3 p1, vec3 p2)
{
#if 0
/* more accurate replacement of acos */
float x = dot(p1, p2);
float y = abs(x);
float a = 5.42031 + (3.12829 + 0.0902326 * y) * y;
float b = 3.45068 + (4.18814 + y) * y;
float theta_sintheta = a / b;
if (x < 0.0) {
theta_sintheta = (M_PI / sqrt(1.0 - x * x)) - theta_sintheta;
}
vec3 u = cross(p1, p2);
return theta_sintheta * dot(u, N);
#endif
float cos_theta = dot(p1, p2);
cos_theta = clamp(cos_theta, -0.9999, 0.9999);
float theta = acos(cos_theta);
vec3 u = normalize(cross(p1, p2));
return theta * cross(p1, p2).z / sin(theta);
}
int clip_quad_to_horizon(inout vec3 L[5])
{
/* detect clipping config */
int config = 0;
if (L[0].z > 0.0) config += 1;
if (L[1].z > 0.0) config += 2;
if (L[2].z > 0.0) config += 4;
if (L[3].z > 0.0) config += 8;
/* clip */
int n = 0;
if (config == 0)
{
/* clip all */
}
else if (config == 1) /* V1 clip V2 V3 V4 */
{
n = 3;
L[1] = -L[1].z * L[0] + L[0].z * L[1];
L[2] = -L[3].z * L[0] + L[0].z * L[3];
}
else if (config == 2) /* V2 clip V1 V3 V4 */
{
n = 3;
L[0] = -L[0].z * L[1] + L[1].z * L[0];
L[2] = -L[2].z * L[1] + L[1].z * L[2];
}
else if (config == 3) /* V1 V2 clip V3 V4 */
{
n = 4;
L[2] = -L[2].z * L[1] + L[1].z * L[2];
L[3] = -L[3].z * L[0] + L[0].z * L[3];
}
else if (config == 4) /* V3 clip V1 V2 V4 */
{
n = 3;
L[0] = -L[3].z * L[2] + L[2].z * L[3];
L[1] = -L[1].z * L[2] + L[2].z * L[1];
}
else if (config == 5) /* V1 V3 clip V2 V4) impossible */
{
n = 0;
}
else if (config == 6) /* V2 V3 clip V1 V4 */
{
n = 4;
L[0] = -L[0].z * L[1] + L[1].z * L[0];
L[3] = -L[3].z * L[2] + L[2].z * L[3];
}
else if (config == 7) /* V1 V2 V3 clip V4 */
{
n = 5;
L[4] = -L[3].z * L[0] + L[0].z * L[3];
L[3] = -L[3].z * L[2] + L[2].z * L[3];
}
else if (config == 8) /* V4 clip V1 V2 V3 */
{
n = 3;
L[0] = -L[0].z * L[3] + L[3].z * L[0];
L[1] = -L[2].z * L[3] + L[3].z * L[2];
L[2] = L[3];
}
else if (config == 9) /* V1 V4 clip V2 V3 */
{
n = 4;
L[1] = -L[1].z * L[0] + L[0].z * L[1];
L[2] = -L[2].z * L[3] + L[3].z * L[2];
}
else if (config == 10) /* V2 V4 clip V1 V3) impossible */
{
n = 0;
}
else if (config == 11) /* V1 V2 V4 clip V3 */
{
n = 5;
L[4] = L[3];
L[3] = -L[2].z * L[3] + L[3].z * L[2];
L[2] = -L[2].z * L[1] + L[1].z * L[2];
}
else if (config == 12) /* V3 V4 clip V1 V2 */
{
n = 4;
L[1] = -L[1].z * L[2] + L[2].z * L[1];
L[0] = -L[0].z * L[3] + L[3].z * L[0];
}
else if (config == 13) /* V1 V3 V4 clip V2 */
{
n = 5;
L[4] = L[3];
L[3] = L[2];
L[2] = -L[1].z * L[2] + L[2].z * L[1];
L[1] = -L[1].z * L[0] + L[0].z * L[1];
}
else if (config == 14) /* V2 V3 V4 clip V1 */
{
n = 5;
L[4] = -L[0].z * L[3] + L[3].z * L[0];
L[0] = -L[0].z * L[1] + L[1].z * L[0];
}
else if (config == 15) /* V1 V2 V3 V4 */
{
n = 4;
}
if (n == 3)
L[3] = L[0];
if (n == 4)
L[4] = L[0];
return n;
}
vec2 ltc_coords(float cosTheta, float roughness)
{
float theta = acos(cosTheta);
vec2 coords = vec2(roughness, theta/(0.5*3.14159));
/* scale and bias coordinates, for correct filtered lookup */
return coords * (LTC_LUT_SIZE - 1.0) / LTC_LUT_SIZE + 0.5 / LTC_LUT_SIZE;
}
mat3 ltc_matrix(vec2 coord)
{
/* load inverse matrix */
vec4 t = texture(ltcMat, coord);
mat3 Minv = mat3(
vec3( 1, 0, t.y),
vec3( 0, t.z, 0),
vec3(t.w, 0, t.x)
);
return Minv;
}
float ltc_evaluate(vec3 N, vec3 V, mat3 Minv, vec3 corners[4])
{
/* construct orthonormal basis around N */
vec3 T1, T2;
T1 = normalize(V - N*dot(V, N));
T2 = cross(N, T1);
/* rotate area light in (T1, T2, R) basis */
Minv = Minv * transpose(mat3(T1, T2, N));
/* polygon (allocate 5 vertices for clipping) */
vec3 L[5];
L[0] = Minv * corners[0];
L[1] = Minv * corners[1];
L[2] = Minv * corners[2];
L[3] = Minv * corners[3];
int n = clip_quad_to_horizon(L);
if (n == 0)
return 0.0;
/* project onto sphere */
L[0] = normalize(L[0]);
L[1] = normalize(L[1]);
L[2] = normalize(L[2]);
L[3] = normalize(L[3]);
L[4] = normalize(L[4]);
/* integrate */
float sum = 0.0;
sum += edge_integral(L[0], L[1]);
sum += edge_integral(L[1], L[2]);
sum += edge_integral(L[2], L[3]);
if (n >= 4)
sum += edge_integral(L[3], L[4]);
if (n == 5)
sum += edge_integral(L[4], L[0]);
return abs(sum);
}
/* Aproximate circle with an octogone */
#define LTC_CIRCLE_RES 8
float ltc_evaluate_circle(vec3 N, vec3 V, mat3 Minv, vec3 p[LTC_CIRCLE_RES])
{
/* construct orthonormal basis around N */
vec3 T1, T2;
T1 = normalize(V - N*dot(V, N));
T2 = cross(N, T1);
/* rotate area light in (T1, T2, R) basis */
Minv = Minv * transpose(mat3(T1, T2, N));
for (int i = 0; i < LTC_CIRCLE_RES; ++i) {
p[i] = Minv * p[i];
/* clip to horizon */
p[i].z = max(0.0, p[i].z);
/* project onto sphere */
p[i] = normalize(p[i]);
}
/* integrate */
float sum = 0.0;
for (int i = 0; i < LTC_CIRCLE_RES - 1; ++i) {
sum += edge_integral(p[i], p[i + 1]);
}
sum += edge_integral(p[LTC_CIRCLE_RES - 1], p[0]);
return max(0.0, sum);
}

View File

@@ -212,13 +212,13 @@ static void drw_texture_get_format(DRWTextureFormat format, GPUTextureFormat *da
case DRW_TEX_RG_16: *data_type = GPU_RG16F; break;
case DRW_TEX_RG_32: *data_type = GPU_RG32F; break;
case DRW_TEX_R_8: *data_type = GPU_R8; break;
case DRW_TEX_R_16: *data_type = GPU_R16F; break;
#if 0
case DRW_TEX_RGBA_32: *data_type = GPU_RGBA32F; break;
case DRW_TEX_RGB_8: *data_type = GPU_RGB8; break;
case DRW_TEX_RGB_16: *data_type = GPU_RGB16F; break;
case DRW_TEX_RGB_32: *data_type = GPU_RGB32F; break;
case DRW_TEX_RG_8: *data_type = GPU_RG8; break;
case DRW_TEX_R_16: *data_type = GPU_R16F; break;
case DRW_TEX_R_32: *data_type = GPU_R32F; break;
#endif
case DRW_TEX_DEPTH_16: *data_type = GPU_DEPTH_COMPONENT16; break;

View File

@@ -66,6 +66,7 @@ typedef enum GPUTextureFormat {
GPU_RGBA8,
GPU_RG32F,
GPU_RG16F,
GPU_R16F,
GPU_R8,
#if 0
GPU_RGBA32F,
@@ -87,7 +88,6 @@ typedef enum GPUTextureFormat {
GPU_R32F,
GPU_R32I,
GPU_R32UI,
GPU_R16F,
GPU_R16I,
GPU_R16UI,
GPU_R16,

View File

@@ -104,6 +104,7 @@ static GLenum GPU_texture_get_format(int components, GPUTextureFormat data_type,
case GPU_RG32F: return GL_RG32F;
case GPU_RG16F: return GL_RG16F;
case GPU_RGBA8: return GL_RGBA8;
case GPU_R16F: return GL_R16F;
case GPU_R8: return GL_R8;
/* Special formats texture & renderbuffer */
case GPU_DEPTH24_STENCIL8: return GL_DEPTH24_STENCIL8;