vk: rt: compute legacy blending in sRGB-γ colorspace

This makes all blending look very close to the original, and fixes a
whole class of blending-looking-wrong issues.

Fixes #668
This commit is contained in:
Ivan Avdeev 2023-12-29 12:04:38 -05:00
parent 1028564eec
commit 3bc293d8aa
8 changed files with 132 additions and 51 deletions

View File

@ -1116,3 +1116,41 @@ There's nothing we can to do `a` only that would make it fake the "original" mix
As usual -- original sRGB-specialized game art is painfully incompatible with modern linear PBR.
The best way to address it (hopefully w/o breaking too much linear rendering math) remains to be discovered.
# 2023-12-29 E354
## Sprite animation lerping woes
Problem: converting alpha from sRGB to linear fixes various blending glitches, but makes animation blink.
Possible approaches:
1. Original math: pass and compute colors and alphas for simple blending in the original (sRGB-γ) colorspace. PBR-incorrect, but should give the original look.
Pro:
- original look
- should solve a whole class of issues.
- Relatively separate from physically-correct math, doesn't interfere that much.
Except for background + emissive part.
- Individual PRB-ized parts of blending could be extracted out from legacy mode gradually.
Cons:
- special legacy blending code.
- Passing these things around is obnoxious: needs lots of special code for model passing.
- Large amount of work.
Possible implementation plan:
- `vk_ray_model.c`: sRGB-to-linear colorspace conversion should be made based on `material_mode`:
do not convert for legacy blending modes
- what to do with `mat->base_color`, which is assumed linear? Leaving it as-is for now.
- sRGB-γ-ize linear texture color (still a bit different from legacy. alt: specifically for sprites and beams textures mark them as UNORM)
- keep/lerp vertex colors in sRGB space
2. Special code for sprite lerping: add second texture channel, add lerp parameter, etc.
Pro: should be relatively easy to do.
Cons: Fragile special code for special case.
3. Track alpha channel with animation lerping in mind: only linearize it for no-animation case.
Pro: no additional parameters to pass to shaders.
Cons: math might not converge on a good solution.
4. Generate intermediate textures.
Pro: no special code for shaders/model passing.
Cons: ridiculous texture explosion
5. Hand-patch things that look weird. E.g. for known sprite/beam textures specify how their alphas should be mapped.

View File

@ -1,6 +1,10 @@
# 2023-12-29 E354
- [x] Figure out why additive transparency differs visibly from raster
- [x] Implement special legacy-blending in sRGB-γ colorspace
# 2023-12-28 E353
- [x] track color spaces when passing colors into shaders
- [ ] validation failure at startup, #723 -- seems like memory corruption
- [-] validation failure at startup, #723 -- seems like memory corruption
Longer-term agenda for current season:
- [ ] Better PBR math, e.g.:
@ -9,13 +13,12 @@ Longer-term agenda for current season:
- [ ] Just make sure that all the BRDF math is correct
- [ ] Transparency/translucency:
- [ ] Proper material mode for translucency, with reflections, refraction (index), fresnel, etc.
- [ ] Figure out why additive transparency differs visibly from raster
- [ ] Extract and specialize effects, e.g.
- [ ] Rays -> volumetrics
- [ ] Glow -> bloom
- [ ] Smoke -> volumetrics
- [ ] Sprites/portals -> emissive volumetrics
- [ ] Holo models -> emissive additive
- [x] Holo models -> emissive additive
- [ ] Some additive -> translucent
- [ ] what else
- [ ] Render-graph-ish approach to resources.

View File

@ -178,9 +178,8 @@ void computeBounce(ivec2 pix, vec3 direction, out vec3 diffuse, out vec3 specula
vec3 background = payload.base_color_a.rgb * ldiffuse;
background += lspecular * mix(vec3(1.), payload.base_color_a.rgb, hit_material.metalness);
vec3 emissive = vec3(0.);
traceSimpleBlending(pos, bounce_direction, payload.hit_t.w, emissive, background);
lighting = emissive + background;
const vec4 blend = traceLegacyBlending(pos, bounce_direction, payload.hit_t.w);
lighting = SRGBtoLINEAR(blend.rgb) + background * blend.a;
} else {
lighting = texture(skybox, bounce_direction).rgb * ubo.ubo.skybox_exposure;
//payload.emissive.rgb = texture(skybox, bounce_direction).rgb * ubo.ubo.skybox_exposure;

View File

@ -42,7 +42,9 @@ layout(set = 0, binding = 18) uniform sampler3D blue_noise_texture;
#include "bluenoise.glsl"
#endif
//layout(set = 0, binding = 19) uniform sampler2D textures[MAX_TEXTURES];
layout(set = 0, binding = 19, rgba16f) uniform readonly image2D legacy_blend;
//layout(set = 0, binding = 20) uniform sampler2D textures[MAX_TEXTURES];
const int INDIRECT_SCALE = 2;
@ -306,18 +308,19 @@ void main() {
colour = diffuse + specular;
}
const vec4 legacy_blend = imageLoad(legacy_blend, pix);
colour += imageLoad(emissive, pix).rgb;
// Revealage. TODO: which colorspace?
colour *= legacy_blend.a;
colour = LINEARtoSRGB(colour);
// See issue https://github.com/w23/xash3d-fwgs/issues/668, map test_blendmode_additive_alpha.
// This macro enabled adding emissive to the final color in the *incorrect* sRGB-γ space. But it makes
// Adding emissive_blend to the final color in the *incorrect* sRGB-γ space. It makes
// it look much more like the original. Adding emissive in the *correct* linear space differs
// from the original a lot, and looks perceptively worse.
#define ADD_EMISSIVE_IN_GAMMA_SPACE
#ifndef ADD_EMISSIVE_IN_GAMMA_SPACE
// Physically correct, but looks dull
colour += imageLoad(emissive, pix).rgb;
colour = LINEARtoSRGB(colour);
#else // INCORRECT
colour = LINEARtoSRGB(colour) + LINEARtoSRGB(imageLoad(emissive, pix).rgb);
#endif
colour += legacy_blend.rgb;
imageStore(out_dest, pix, vec4(colour, 0./*unused*/));
}

View File

@ -23,6 +23,7 @@ layout(set = 0, binding = 12, rgba16f) uniform writeonly image2D out_normals_gs;
layout(set = 0, binding = 13, rgba8) uniform writeonly image2D out_material_rmxx;
layout(set = 0, binding = 14, rgba16f) uniform writeonly image2D out_emissive;
layout(set = 0, binding = 15, rgba32f) uniform writeonly image2D out_geometry_prev_position;
layout(set = 0, binding = 16, rgba16f) uniform writeonly image2D out_legacy_blend;
layout(set = 0, binding = 30, std430) readonly buffer ModelHeaders { ModelHeader a[]; } model_headers;
layout(set = 0, binding = 31, std430) readonly buffer Kusochki { Kusok a[]; } kusochki;
@ -118,7 +119,8 @@ void main() {
payload.emissive.rgb = texture(skybox, ray.direction).rgb * ubo.ubo.skybox_exposure;
}
traceSimpleBlending(ray.origin, ray.direction, L, payload.emissive.rgb, payload.base_color_a.rgb);
const vec4 blend = traceLegacyBlending(ray.origin, ray.direction, L);
imageStore(out_legacy_blend, pix, blend);
imageStore(out_position_t, pix, payload.hit_t);
imageStore(out_base_color_a, pix, LINEARtoSRGB(payload.base_color_a));

View File

@ -134,7 +134,7 @@ Geometry readHitGeometry(vec2 bary, float ray_cone_width) {
struct MiniGeometry {
vec2 uv;
uint kusok_index;
vec4 vertex_color;
vec4 vertex_color_srgb;
};
MiniGeometry readCandidateMiniGeometry(rayQueryEXT rq) {
@ -156,16 +156,23 @@ MiniGeometry readCandidateMiniGeometry(rayQueryEXT rq) {
const vec2 bary = rayQueryGetIntersectionBarycentricsEXT(rq, false);
const vec2 uv = baryMix(uvs[0], uvs[1], uvs[2], bary);
/*
const vec4 colors[3] = {
SRGBtoLINEAR(unpackUnorm4x8(GET_VERTEX(vi1).color)),
SRGBtoLINEAR(unpackUnorm4x8(GET_VERTEX(vi2).color)),
SRGBtoLINEAR(unpackUnorm4x8(GET_VERTEX(vi3).color)),
};
*/
const vec4 colors_srgb[3] = {
unpackUnorm4x8(GET_VERTEX(vi1).color),
unpackUnorm4x8(GET_VERTEX(vi2).color),
unpackUnorm4x8(GET_VERTEX(vi3).color),
};
MiniGeometry ret;
ret.uv = uv;
ret.kusok_index = kusok_index;
ret.vertex_color = baryMix(colors[0], colors[1], colors[2], bary);
ret.vertex_color_srgb = baryMix(colors_srgb[0], colors_srgb[1], colors_srgb[2], bary);
return ret;
}
#endif // #ifdef RAY_QUERY

View File

@ -2,8 +2,11 @@
#define TRACE_SIMPLE_BLENDING_GLSL_INCLUDED
// Traces geometry with simple blending. Simple means that it's only additive or mix/coverage, and it doesn't participate in lighting, and it doesn't reflect/refract rays.
void traceSimpleBlending(vec3 pos, vec3 dir, float L, inout vec3 emissive, inout vec3 background) {
// Done in sRGB-γ space for legacy-look reasons.
// Returns vec4(emissive_srgb.rgb, revealage)
vec4 traceLegacyBlending(vec3 pos, vec3 dir, float L) {
const float glow_soft_overshoot = 16.;
vec3 emissive = vec3(0.);
// TODO probably a better way would be to sort only MIX entries.
// ADD/GLOW are order-independent relative to each other, but not to MIX
@ -45,7 +48,7 @@ void traceSimpleBlending(vec3 pos, vec3 dir, float L, inout vec3 emissive, inout
#ifdef DEBUG_BLEND_MODES
if (model.mode == MATERIAL_MODE_BLEND_GLOW) {
emissive += vec3(1., 0., 0.);
//ret += color * smoothstep(glow_soft_overshoot, 0., overshoot);
//emissive += color * smoothstep(glow_soft_overshoot, 0., overshoot);
} else if (model.mode == MATERIAL_MODE_BLEND_ADD) {
emissive += vec3(0., 1., 0.);
} else if (model.mode == MATERIAL_MODE_BLEND_MIX) {
@ -56,10 +59,12 @@ void traceSimpleBlending(vec3 pos, vec3 dir, float L, inout vec3 emissive, inout
emissive += vec3(1., 1., 1.);
}
#else
const vec4 texture_color = texture(textures[nonuniformEXT(kusok.material.tex_base_color)], geom.uv);
// Note that simple blending is legacy blending really.
// It is done in sRGB-γ space for correct legacy-look reasons.
const vec4 texture_color = LINEARtoSRGB(texture(textures[nonuniformEXT(kusok.material.tex_base_color)], geom.uv));
const vec4 mm_color = model.color * kusok.material.base_color;
float alpha = mm_color.a * texture_color.a * geom.vertex_color.a;
vec3 color = mm_color.rgb * texture_color.rgb * geom.vertex_color.rgb * alpha;
float alpha = mm_color.a * texture_color.a * geom.vertex_color_srgb.a;
vec3 color = mm_color.rgb * texture_color.rgb * geom.vertex_color_srgb.rgb * alpha;
#ifdef GLOBAL_SOFT_DEPTH
const float overshoot_factor = smoothstep(glow_soft_overshoot, 0., overshoot);
@ -98,34 +103,31 @@ void traceSimpleBlending(vec3 pos, vec3 dir, float L, inout vec3 emissive, inout
#endif // !DEBUG_BLEND_MODES
}
if (entries_count == 0)
return;
// Tyno O(N^2) sort
for (uint i = 0; i < entries_count; ++i) {
uint min_i = i;
for (uint j = i+1; j < entries_count; ++j) {
if (entries[min_i].depth > entries[j].depth) {
min_i = j;
float revealage = 1.;
if (entries_count > 0) {
// Tyno O(N^2) sort
for (uint i = 0; i < entries_count; ++i) {
uint min_i = i;
for (uint j = i+1; j < entries_count; ++j) {
if (entries[min_i].depth > entries[j].depth) {
min_i = j;
}
}
if (min_i != i) {
BlendEntry tmp = entries[min_i];
entries[min_i] = entries[i];
entries[i] = tmp;
}
}
if (min_i != i) {
BlendEntry tmp = entries[min_i];
entries[min_i] = entries[i];
entries[i] = tmp;
// Composite everything in the right order
for (uint i = 0; i < entries_count; ++i) {
emissive += entries[i].add * revealage;
revealage *= 1. - entries[i].blend;
}
}
// Composite everything in the right order
float revealage = 1.;
vec3 add = vec3(0.);
for (uint i = 0; i < entries_count; ++i) {
add += entries[i].add * revealage;
revealage *= 1. - entries[i].blend;
}
emissive = emissive * revealage + add;
background *= revealage;
return vec4(emissive, revealage);
}
#endif //ifndef TRACE_SIMPLE_BLENDING_GLSL_INCLUDED

View File

@ -323,6 +323,17 @@ rt_draw_instance_t *getDrawInstance(void) {
return g_ray_model_state.frame.instances + (g_ray_model_state.frame.instances_count++);
}
static qboolean isLegacyBlendingMode(int material_mode) {
switch (material_mode) {
case MATERIAL_MODE_BLEND_ADD:
case MATERIAL_MODE_BLEND_MIX:
case MATERIAL_MODE_BLEND_GLOW:
return true;
default:
return false;
}
}
static float sRGBtoLinearScalar(const float sRGB) {
// IEC 61966-2-1:1999
const float linearLow = sRGB / 12.92f;
@ -334,17 +345,23 @@ static void sRGBtoLinearVec4(const vec4_t in, vec4_t out) {
out[0] = sRGBtoLinearScalar(in[0]);
out[1] = sRGBtoLinearScalar(in[1]);
out[2] = sRGBtoLinearScalar(in[2]);
// Historically: sprite animation lerping is linear
// To-linear conversion should not be done on anything with blending, therefore
// it's irrelevant really.
out[3] = in[3];
}
/*
static void sRGBAtoLinearVec4(const vec4_t in, vec4_t out) {
out[0] = sRGBtoLinearScalar(in[0]);
out[1] = sRGBtoLinearScalar(in[1]);
out[2] = sRGBtoLinearScalar(in[2]);
// α also needs to be linearized.
// α also needs to be linearized for tau-cannon hit position sprite to look okay
out[3] = sRGBtoLinearScalar(in[3]);
}
*/
void RT_FrameAddModel( struct rt_model_s *model, rt_frame_add_model_t args ) {
if (!model || !model->blas)
@ -371,7 +388,13 @@ void RT_FrameAddModel( struct rt_model_s *model, rt_frame_add_model_t args ) {
draw_instance->kusochki_offset = kusochki_offset;
draw_instance->material_mode = args.material_mode;
draw_instance->material_flags = args.material_flags;
sRGBtoLinearVec4(*args.color_srgb, draw_instance->color);
// Legacy blending is done in sRGB-γ space
if (isLegacyBlendingMode(args.material_mode))
Vector4Copy(*args.color_srgb, draw_instance->color);
else
sRGBtoLinearVec4(*args.color_srgb, draw_instance->color);
Matrix3x4_Copy(draw_instance->transform_row, args.transform);
Matrix4x4_Copy(draw_instance->prev_transform_row, args.prev_transform);
}
@ -445,7 +468,6 @@ void RT_DynamicModelProcessFrame(void) {
goto tail;
}
// FIXME override color
if (!RT_KusochkiUpload(kusochki_offset, dyn->geometries, dyn->geometries_count, NULL, dyn->colors)) {
gEngine.Con_Printf(S_ERROR "Couldn't build blas for %d geoms of %s, skipping\n", dyn->geometries_count, group_names[i]);
goto tail;
@ -485,7 +507,12 @@ void RT_FrameAddOnce( rt_frame_add_once_t args ) {
break;
}
sRGBAtoLinearVec4(*args.color_srgb, dyn->colors[dyn->geometries_count]);
// Legacy blending is done in sRGB-γ space
if (isLegacyBlendingMode(material_mode))
Vector4Copy(*args.color_srgb, dyn->colors[dyn->geometries_count]);
else
sRGBtoLinearVec4(*args.color_srgb, dyn->colors[dyn->geometries_count]);
dyn->geometries[dyn->geometries_count++] = args.geometries[i];
}
}