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:
parent
1028564eec
commit
3bc293d8aa
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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*/));
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue