Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 46 additions & 30 deletions drivers/gles3/shaders/scene.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -335,24 +335,27 @@ uniform lowp uint spot_light_index;

#if !defined(MODE_RENDER_DEPTH) && !defined(MODE_UNSHADED) && defined(USE_VERTEX_LIGHTING)

// Eyeballed approximation of `exp2(15.0 * (1.0 - roughness) + 1.0) * 0.25`.
// Uses slightly more FMA instructions (2x rate) to avoid special instructions (0.25x rate).
// Range is reduced to [0.64,4977] from [068,2,221,528] which makes mediump feasible for the rest of the shader.
// Converts GGX roughness to Blinn-Phong shininess.
// Roughly approximates `2.0 / roughness * roughness - 2.0` with a much lower high end.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does 2.0 / roughness * roughness - 2.0 come from? I don't think its a very good approximation for converting roughness to specular power.

Here is a desmos graph showing the different approaches (and our approximations) https://www.desmos.com/calculator/nzby8ouch4

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe here is where I first encountered it, and of the methods I've tried I consider it the most perceptually accurate for matching Blinn-Phong to GGX.
I'm also not sure why exp2(15.0 * (1.0 - roughness) + 1.0) * 0.25 was chosen originally, as shown in my comparison screenshots it's not a very close fit to what GGX produces. There are certainly other mappings we could use and I'm open to trying them, but the current method really leaves a lot to be desired.

@clayjohn clayjohn Dec 19, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was from the Frostbite PBR slides, but looking quickly at them now I don't see it. AFAIK, this, or something like this has been the standard for a long time.

I guess if our goal is a visual approximation then it doesn't matter how close the curve is as long as the end result looks good.

I like the range of the new function, it does a good job of keeping the intermediate values in a sufficient range where you won't risk losing precision when using mediump. Its a shame the new approximation is slightly more expensive than the old, but if it looks better, then oh well.

// Range is ~[0.05,656].
mediump float roughness_to_shininess(mediump float roughness) {
mediump float r = 1.2 - roughness;
mediump float r2 = r * r;
return r * r2 * r2 * 2000.0;
mediump float s = 1.5 - roughness * 0.667;
s *= s;
s *= s;
s *= s;
s *= s; // s^16
return s;
}

void light_compute(vec3 N, vec3 L, vec3 V, vec3 light_color, bool is_directional, float roughness,
void light_compute_vertex(vec3 N, vec3 L, vec3 V, float A, vec3 light_color, bool is_directional, float roughness, float specular_amount,
inout vec3 diffuse_light, inout vec3 specular_light) {
float NdotL = min(dot(N, L), 1.0);
float cNdotL = max(NdotL, 0.0); // clamped NdotL
float NdotL = min(A + dot(N, L), 1.0);
float cNdotL = max(NdotL, 0.0);

#if defined(DIFFUSE_LAMBERT_WRAP)
// Energy conserving lambert wrap shader.
// https://web.archive.org/web/20210228210901/http://blog.stevemcauley.com/2011/12/03/energy-conserving-wrapped-diffuse/
float diffuse_brdf_NL = max(0.0, (cNdotL + roughness) / ((1.0 + roughness) * (1.0 + roughness))) * (1.0 / M_PI);
float diffuse_brdf_NL = max(0.0, (NdotL + roughness) / ((1.0 + roughness) * (1.0 + roughness))) * (1.0 / M_PI);
#else
// lambert
float diffuse_brdf_NL = cNdotL * (1.0 / M_PI);
Expand All @@ -364,10 +367,10 @@ void light_compute(vec3 N, vec3 L, vec3 V, vec3 light_color, bool is_directional
float specular_brdf_NL = 0.0;
// Normalized blinn always unless disabled.
vec3 H = normalize(V + L);
float cNdotH = clamp(dot(N, H), 0.0, 1.0);
float cNdotH = clamp(A + dot(N, H), 0.0, 1.0);
float shininess = roughness_to_shininess(roughness);
float blinn = pow(cNdotH, shininess);
blinn *= (shininess + 2.0) * (1.0 / (8.0 * M_PI)) * cNdotL;
blinn *= (shininess + 8.0) * (1.0 / (8.0 * M_PI)) * cNdotL * specular_amount;
specular_brdf_NL = blinn;
specular_light += specular_brdf_NL * light_color;
#endif
Expand All @@ -383,21 +386,27 @@ float get_omni_spot_attenuation(float distance, float inv_range, float decay) {
}

#if !defined(DISABLE_LIGHT_OMNI) || (defined(ADDITIVE_OMNI) && defined(USE_ADDITIVE_LIGHTING))
void light_process_omni(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal, float roughness,
void light_process_omni_vertex(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal, float roughness,
inout vec3 diffuse_light, inout vec3 specular_light) {
vec3 light_rel_vec = omni_lights[idx].position - vertex;
float light_length = length(light_rel_vec);
float omni_attenuation = get_omni_spot_attenuation(light_length, omni_lights[idx].inv_radius, omni_lights[idx].attenuation);
vec3 color = omni_lights[idx].color * omni_attenuation; // No light shaders here, so combine.

light_compute(normal, normalize(light_rel_vec), eye_vec, color, false, roughness,
diffuse_light,
specular_light);
// Compute area light solid angle.
float size_A = 0.0;
if (omni_lights[idx].size > 0.0) {
float t = omni_lights[idx].size / max(0.001, light_length);
size_A = max(0.0, 1.0 - 1.0 / sqrt(1.0 + t * t));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed intentionally. The purpose of vertex lighting is twofold:

  1. To be very fast when you need fast but simple lighting (i.e for particles)
  2. To allow users to make old school looking games.

Neither of those cases require having area lights. so we dropped the code. The nature of specular reflections in vertex shaded materials also makes the effect barely visible except at extreme values, so its really not worth the cost IMO.

I wouldn't bring it back unless we have enough users saying that they absolutely need it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I figured the cost wasn't much of a concern since it's only calculated when the user wants it to be.
That being said, if the only concern is performance, I can easily optimize this so it only uses one quarter-rate instruction instead of three (or even none, as size_A = max(0.0, 1.0 - 1.0 / sqrt(1.0 + t * t)) simply clamps the value below 1.0 with a very subtle rolloff, so can easily be dropped in favor of size_A = clamp(t, 0.0, 1.0), which would bring the area light code down to a multiply, a clamp, and three adds (two if we also drop the geometry term I added)).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would wait and see if users ask for it. There is no point in throwing away performance for something that may never get used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking as a user, this is something that I want. Since area lights affect diffuse shading too, and our area lights aren't energy conserving (larger area lights simply emit more light than smaller ones), ignoring area lights causes a brightness discrepancy between per-vertex and per-pixel shaded materials. If I'm using per-vertex shading for particles, I want them to be lit consistently with the rest of their environment, regardless of my lighting setup.

Perhaps it's outside the scope of this discussion, but I don't think we should always wait for users to notice deficiencies before addressing them, especially when the performance cost for doing so can be reduced to as little as three instructions (two when not using specularity).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly agree with @StaydMcMuffin. Personally, I would expect and want this behavior too.

Approximating omni lights with a non-zero radius as point lights merely to save a few operations (per vertex!) is a bad idea. These approximations create noticeable visual inconsistencies for a negligible performance gain.
Particularly in both scenarios mentioned above: particle systems and retro-style games (as well as other use cases).
In general, per-vertex specular lightning looks better with larger light sources.

However, if desired, I could do some benchmarking to verify that this computation wouldn't be a problem performance-wise.


light_compute_vertex(normal, normalize(light_rel_vec), eye_vec, size_A, color, false, roughness, omni_lights[idx].specular_amount,
diffuse_light, specular_light);
}
#endif // !defined(DISABLE_LIGHT_OMNI) || (defined(ADDITIVE_OMNI) && defined(USE_ADDITIVE_LIGHTING))

#if !defined(DISABLE_LIGHT_SPOT) || (defined(ADDITIVE_SPOT) && defined(USE_ADDITIVE_LIGHTING))
void light_process_spot(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal, float roughness,
void light_process_spot_vertex(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal, float roughness,
inout vec3 diffuse_light,
inout vec3 specular_light) {
vec3 light_rel_vec = spot_lights[idx].position - vertex;
Expand All @@ -412,7 +421,14 @@ void light_process_spot(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal, float

vec3 color = spot_lights[idx].color * spot_attenuation;

light_compute(normal, normalize(light_rel_vec), eye_vec, color, false, roughness,
// Compute area light solid angle.
float size_A = 0.0;
if (spot_lights[idx].size > 0.0) {
float t = spot_lights[idx].size / max(0.001, light_length);
size_A = max(0.0, 1.0 - 1.0 / sqrt(1.0 + t * t));
}

light_compute_vertex(normal, normalize(light_rel_vec), eye_vec, size_A, color, false, roughness, spot_lights[idx].specular_amount,
diffuse_light, specular_light);
}
#endif // !defined(DISABLE_LIGHT_SPOT) || (defined(ADDITIVE_SPOT) && defined(USE_ADDITIVE_LIGHTING))
Expand Down Expand Up @@ -668,7 +684,7 @@ void main() {
// Apply normal bias at draw time to avoid issues with scaling non-fused geometry.
vec3 light_rel_vec = positional_shadows[positional_shadow_index].light_position - vertex_interp;
float light_length = length(light_rel_vec);
float aNdotL = abs(dot(normalize(normal_interp), normalize(light_rel_vec)));
float aNdotL = abs(dot(normal_interp, normalize(light_rel_vec)));
vec3 normal_offset = (1.0 - aNdotL) * positional_shadows[positional_shadow_index].shadow_normal_bias * light_length * normal_interp;

#ifdef ADDITIVE_SPOT
Expand All @@ -681,7 +697,7 @@ void main() {
shadow_coord = vec4(vertex_interp + normal_offset, 1.0);
#endif
#else // ADDITIVE_DIRECTIONAL
vec3 base_normal_bias = normalize(normal_interp) * (1.0 - max(0.0, dot(directional_shadows[directional_shadow_index].direction, -normalize(normal_interp))));
vec3 base_normal_bias = normal_interp * (1.0 - max(0.0, dot(directional_shadows[directional_shadow_index].direction, -normal_interp)));
vec3 normal_offset = base_normal_bias * directional_shadows[directional_shadow_index].shadow_normal_bias.x;
shadow_coord = directional_shadows[directional_shadow_index].shadow_matrix1 * vec4(vertex_interp + normal_offset, 1.0);

Expand Down Expand Up @@ -735,22 +751,22 @@ void main() {
continue;
}
#endif
light_compute(normal_interp, normalize(directional_lights[i].direction), normalize(view), directional_lights[i].color * directional_lights[i].energy, true, roughness,
diffuse_light_interp.rgb,
specular_light_interp.rgb);
light_compute_vertex(normal_interp, normalize(directional_lights[i].direction), normalize(view), directional_lights[i].size,
directional_lights[i].color * directional_lights[i].energy, true, roughness, 1.0,
diffuse_light_interp.rgb, specular_light_interp.rgb);
}
#endif // !DISABLE_LIGHT_DIRECTIONAL

#ifndef DISABLE_LIGHT_OMNI
for (uint i = 0u; i < omni_light_count; i++) {
light_process_omni(omni_light_indices[i], vertex_interp, view, normal_interp, roughness,
light_process_omni_vertex(omni_light_indices[i], vertex_interp, view, normal_interp, roughness,
diffuse_light_interp.rgb, specular_light_interp.rgb);
}
#endif // !DISABLE_LIGHT_OMNI

#ifndef DISABLE_LIGHT_SPOT
for (uint i = 0u; i < spot_light_count; i++) {
light_process_spot(spot_light_indices[i], vertex_interp, view, normal_interp, roughness,
light_process_spot_vertex(spot_light_indices[i], vertex_interp, view, normal_interp, roughness,
diffuse_light_interp.rgb, specular_light_interp.rgb);
}
#endif // !DISABLE_LIGHT_SPOT
Expand All @@ -762,18 +778,18 @@ void main() {
additive_specular_light_interp = vec3(0.0);
#if !defined(ADDITIVE_OMNI) && !defined(ADDITIVE_SPOT)

light_compute(normal_interp, normalize(directional_lights[directional_shadow_index].direction), normalize(view), directional_lights[directional_shadow_index].color * directional_lights[directional_shadow_index].energy, true, roughness,
additive_diffuse_light_interp.rgb,
additive_specular_light_interp.rgb);
light_compute_vertex(normal_interp, normalize(directional_lights[directional_shadow_index].direction), normalize(view), directional_lights[directional_shadow_index].size,
directional_lights[directional_shadow_index].color * directional_lights[directional_shadow_index].energy, true, roughness, 1.0,
additive_diffuse_light_interp.rgb, additive_specular_light_interp.rgb);
#endif // !defined(ADDITIVE_OMNI) && !defined(ADDITIVE_SPOT)

#ifdef ADDITIVE_OMNI
light_process_omni(omni_light_index, vertex_interp, view, normal_interp, roughness,
light_process_omni_vertex(omni_light_index, vertex_interp, view, normal_interp, roughness,
additive_diffuse_light_interp.rgb, additive_specular_light_interp.rgb);
#endif // ADDITIVE_OMNI

#ifdef ADDITIVE_SPOT
light_process_spot(spot_light_index, vertex_interp, view, normal_interp, roughness,
light_process_spot_vertex(spot_light_index, vertex_interp, view, normal_interp, roughness,
additive_diffuse_light_interp.rgb, additive_specular_light_interp.rgb);
#endif // ADDITIVE_SPOT

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ void vertex_shader(vec3 vertex_input,
continue; // Statically baked light and object uses lightmap, skip
}

light_process_omni_vertex(light_index, vertex, view, normal, roughness,
light_process_omni_vertex(light_index, vertex, view, normal_interp, roughness,
diffuse_light_interp.rgb, specular_light_interp.rgb);
}
}
Expand Down Expand Up @@ -584,7 +584,7 @@ void vertex_shader(vec3 vertex_input,
continue; // Statically baked light and object uses lightmap, skip
}

light_process_spot_vertex(light_index, vertex, view, normal, roughness,
light_process_spot_vertex(light_index, vertex, view, normal_interp, roughness,
diffuse_light_interp.rgb, specular_light_interp.rgb);
}
}
Expand All @@ -604,16 +604,19 @@ void vertex_shader(vec3 vertex_input,
if (directional_lights.data[i].bake_mode == LIGHT_BAKE_STATIC && bool(instances.data[draw_call.instance_index].flags & INSTANCE_FLAGS_USE_LIGHTMAP)) {
continue; // Statically baked light and object uses lightmap, skip.
}

float size_A = sc_use_directional_soft_shadows() ? directional_lights.data[i].size : 0.0;

if (i == 0) {
light_compute_vertex(normal, directional_lights.data[0].direction, view,
light_compute_vertex(normal_interp, directional_lights.data[0].direction, view, size_A,
directional_lights.data[0].color * directional_lights.data[0].energy,
true, roughness,
true, roughness, 1.0,
directional_diffuse,
directional_specular);
} else {
light_compute_vertex(normal, directional_lights.data[i].direction, view,
light_compute_vertex(normal_interp, directional_lights.data[i].direction, view, size_A,
directional_lights.data[i].color * directional_lights.data[i].energy,
true, roughness,
true, roughness, 1.0,
diffuse_light_interp.rgb,
specular_light_interp.rgb);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,16 +481,19 @@ void main() {
if (directional_lights.data[i].bake_mode == LIGHT_BAKE_STATIC && bool(instances.data[draw_call.instance_index].flags & INSTANCE_FLAGS_USE_LIGHTMAP)) {
continue; // Statically baked light and object uses lightmap, skip.
}

float size_A = sc_use_light_soft_shadows() ? directional_lights.data[i].size : 0.0;

if (i == 0) {
light_compute_vertex(normal_interp, directional_lights.data[0].direction, view,
light_compute_vertex(normal_interp, directional_lights.data[0].direction, view, size_A,
directional_lights.data[0].color * directional_lights.data[0].energy,
true, roughness,
true, roughness, 1.0,
directional_diffuse,
directional_specular);
} else {
light_compute_vertex(normal_interp, directional_lights.data[i].direction, view,
light_compute_vertex(normal_interp, directional_lights.data[i].direction, view, size_A,
directional_lights.data[i].color * directional_lights.data[i].energy,
true, roughness,
true, roughness, 1.0,
diffuse_light_interp.rgb,
specular_light_interp.rgb);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
// Simplified versions of light functions intended for the vertex shader.

// Eyeballed approximation of `exp2(15.0 * (1.0 - roughness) + 1.0) * 0.25`.
// Uses slightly more FMA instructions (2x rate) to avoid special instructions (0.25x rate).
// Range is reduced to [0.64,4977] from [068,2,221,528] which makes mediump feasible for the rest of the shader.
// Converts GGX roughness to Blinn-Phong shininess.
// Roughly approximates `2.0 / roughness * roughness - 2.0` with a much lower high end.
// Range is ~[0.05,656].
mediump float roughness_to_shininess(mediump float roughness) {
mediump float r = 1.2 - roughness;
mediump float r2 = r * r;
return r * r2 * r2 * 2000.0;
mediump float s = 1.5 - roughness * 0.667;
s *= s;
s *= s;
s *= s;
s *= s; // pow(s, 16)
return s;
}

void light_compute_vertex(vec3 N, vec3 L, vec3 V, vec3 light_color, bool is_directional, float roughness,
void light_compute_vertex(vec3 N, vec3 L, vec3 V, float A, vec3 light_color, bool is_directional, float roughness, float specular_amount,
inout vec3 diffuse_light, inout vec3 specular_light) {
float NdotL = min(dot(N, L), 1.0);
float cNdotL = max(NdotL, 0.0); // clamped NdotL
float NdotL = min(A + dot(N, L), 1.0);
float cNdotL = max(NdotL, 0.0);

#if defined(DIFFUSE_LAMBERT_WRAP)
// Energy conserving lambert wrap shader.
// https://web.archive.org/web/20210228210901/http://blog.stevemcauley.com/2011/12/03/energy-conserving-wrapped-diffuse/
float diffuse_brdf_NL = max(0.0, (cNdotL + roughness) / ((1.0 + roughness) * (1.0 + roughness))) * (1.0 / M_PI);
float diffuse_brdf_NL = max(0.0, (NdotL + roughness) / ((1.0 + roughness) * (1.0 + roughness))) * (1.0 / M_PI);
#else
// lambert
float diffuse_brdf_NL = cNdotL * (1.0 / M_PI);
Expand All @@ -29,10 +32,10 @@ void light_compute_vertex(vec3 N, vec3 L, vec3 V, vec3 light_color, bool is_dire
float specular_brdf_NL = 0.0;
// Normalized blinn always unless disabled.
vec3 H = normalize(V + L);
float cNdotH = clamp(dot(N, H), 0.0, 1.0);
float cNdotH = clamp(A + dot(N, H), 0.0, 1.0);
float shininess = roughness_to_shininess(roughness);
float blinn = pow(cNdotH, shininess);
blinn *= (shininess + 2.0) * (1.0 / (8.0 * M_PI)) * cNdotL;
blinn *= (shininess + 8.0) * (1.0 / (8.0 * M_PI)) * cNdotL * specular_amount;
specular_brdf_NL = blinn;
specular_light += specular_brdf_NL * light_color;
#endif
Expand All @@ -54,7 +57,14 @@ void light_process_omni_vertex(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal,
float omni_attenuation = get_omni_attenuation(light_length, omni_lights.data[idx].inv_radius, omni_lights.data[idx].attenuation);
vec3 color = omni_lights.data[idx].color * omni_attenuation;

light_compute_vertex(normal, normalize(light_rel_vec), eye_vec, color, false, roughness,
// Compute area light solid angle.
float size_A = 0.0;
if (sc_use_light_soft_shadows() && omni_lights.data[idx].size > 0.0) {
float t = omni_lights.data[idx].size / max(0.001, light_length);
size_A = max(0.0, 1.0 - 1.0 / sqrt(1.0 + t * t));
}

light_compute_vertex(normal, normalize(light_rel_vec), eye_vec, size_A, color, false, roughness, omni_lights.data[idx].specular_amount,
diffuse_light,
specular_light);
}
Expand All @@ -77,6 +87,13 @@ void light_process_spot_vertex(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal,
vec3 color = spot_lights.data[idx].color * spot_attenuation;
float specular_amount = spot_lights.data[idx].specular_amount;

light_compute_vertex(normal, normalize(light_rel_vec), eye_vec, color, false, roughness,
// Compute area light solid angle.
float size_A = 0.0;
if (sc_use_light_soft_shadows() && spot_lights.data[idx].size > 0.0) {
float t = spot_lights.data[idx].size / max(0.001, light_length);
size_A = max(0.0, 1.0 - 1.0 / sqrt(1.0 + t * t));
}

light_compute_vertex(normal, normalize(light_rel_vec), eye_vec, size_A, color, false, roughness, spot_lights.data[idx].specular_amount,
diffuse_light, specular_light);
}