0

I'm writing a lighting system for 2D games using a rather common method of 2D radiosity. The idea is to generate a JFA voronoi of the game scene (black, alpha = 1.0 for occluders and color, alpha = 1.0 for emitters) and generate an SDF from the JFA. Next you raymarch every pixel on screen for N rays with M max steps on the SDF with random angle offsets for each pixel. You then sample the emitter/occluder surface at the end point of each ray, step back into empty space and sample again for light emitted in the nearest empty space. This gives you a nice result as seen below:

2D Radiosity

That isn't the problem, it works great. The problem is efficiency. The idea behind fixing this is to render the GI at 1/N sample size (width/N, height/N) and then upscale the GI using interpolation. As I've done below:

2D Radiosity

This is the problem. The upscaling I've accomplished using weighted color-interpolation, but it produces these nasty results near occluders:

2D Radiosity

Here's the full shader:

The uniforms passed are the GI downsampled texture (in_GIField), Scene (emitters/occluders only) Texture (gm_basetexture), Signed Distance Field (in_SDField), Resolution (in_Screen) and the Downsample ratio (in_Sample).

/*
    UPSCALING SHADER:
        Find the nearest 4 boundign samples to the current pixel (xyDelta & xyShift)
        Calculate all of the sample's weights based on whether they're marchable or source pixels.
        Final perform a composite weighted interpolation for the current pixel to the nearest 4 samples.
*/
varying vec2 in_Coord;
uniform float in_Sample;
uniform vec2 in_Screen;
uniform sampler2D in_GIField;
uniform sampler2D in_SDField;

#define TPI 9.4247779607693797153879301498385
#define PI  3.1415926535897932384626433832795
#define TAU 6.2831853071795864769252867665590
#define EPSILON         0.001 // floating point precision check
#define dot(f)       dot(f,f) // shorthand dot of a single float

float ATAN2(float yy, float xx) { return mod(atan(yy, xx), TAU); }
float DIRECT(vec2 v1, vec2 v2) { vec2 v3 = v2 - v1; return ATAN2(-v3.y, v3.x); }
float DIFFERENCE(float src, float dst) { return mod(dst - src + TPI, TAU) - PI; }

float V2_F16(vec2 v) { return v.x + (v.y / 255.0); }
float VMAX(vec3 v) { return max(v.r, max(v.g, v.b)); }
vec2 SAMPLEXY(vec2 xycoord) { return (floor(xycoord / in_Sample) * in_Sample) + (in_Sample*0.5); }
vec3 TONEMAP(vec3 color, float dist) { return color * (1.0 / (1.0 + dot(dist / min(in_Screen.x, in_Screen.y)))); }

float TESTMARCH(vec2 pix, vec2 end) {
    float aspect = in_Screen.x / in_Screen.y,
        dst = distance(pix, end);
    vec2 dir = normalize((end*in_Screen) - (pix*in_Screen)) / in_Screen;
    
    for(float i = 0.0; i < in_Sample; i += 1.0) {
        vec2 test = vec2(pix.x * aspect, pix.y) + (dir * (i/in_Screen));
        test.x /= aspect;
        
        vec4 sourceCol = texture2D(gm_BaseTexture, test);
        float source = max(sourceCol.r, max(sourceCol.g, sourceCol.b));
        if (source < EPSILON && sourceCol.a > 0.0) return 0.0;
    }
    
    return 1.0;
}

vec3 WCOMPOSITE(vec3 colors[4], float weights[4], vec2 uv) {
    // (uv * A * B) + (B * (1.0 - A)) //0, 2, 1, 3
    float weightA = (uv.y * weights[0] * weights[2]) + (weights[2] * (1.0 - weights[0])),
        weightB = (uv.y * weights[1] * weights[3]) + (weights[3] * (1.0 - weights[1]));
    vec3 colorA = mix(colors[0], colors[2], weightA),
        colorB = mix(colors[1], colors[3], weightB);
    return mix(colorA, colorB, uv.x);
}

void main() {
    vec2 xyCoord = in_Coord * in_Screen;
    vec2 xyLight = SAMPLEXY(xyCoord);
    vec2 xyDelta = sign(sign(xyCoord - xyLight) - 1.0);
    
    vec2 xyShift[4];
    xyShift[0] = vec2(0.,0.) + xyDelta;
    xyShift[1] = vec2(1.,0.) + xyDelta;
    xyShift[2] = vec2(0.,1.) + xyDelta;
    xyShift[3] = vec2(1.,1.) + xyDelta;
    
    vec2 xyField[4]; vec3 xyColor[4]; float notSource[4]; float xyWghts[4];
    for(int i = 0; i < 4; i++) {
        xyField[i] = (xyLight + (xyShift[i] * in_Sample)) * (1.0/in_Screen);
        xyColor[i] = texture2D(in_GIField, xyField[i]).rgb;
        notSource[i] = 1.0 - sign(texture2D(gm_BaseTexture, xyField[i]).a);
        xyWghts[i] = TESTMARCH(in_Coord, xyField[i]) * sign(VMAX(xyColor[i])) * notSource[i];
    }
    
    vec2 uvCoord = mod(xyCoord-xyLight, in_Sample) * (1.0/in_Sample);
    vec3 xyFinal = WCOMPOSITE(xyColor, xyWghts, uvCoord);
    
    vec4 xySource = texture2D(gm_BaseTexture, in_Coord);
    float isSource = sign(xySource.a);
    gl_FragColor = vec4((isSource * xySource.rgb) + ((1.0-isSource) * xyFinal), 1.0);
}

EDIT: This DOES produce the intended result in empty space, but ends up with nasty artifacting near emitters and occluders. I tried to solve this in the for-loop in the main function by weighting out the emitter/occluder (source pixels in the scene texture) colors, but this isn't working.

See shader code attached (Shadertoy). I noticed that the weighting function will actually produce some colors with a weight of 0 (as expected as originally written). I currently don't have a solution for how to remove colors from the interpolation process entirely.

Full Source Code

Full Color Shader Code

FatalSleep
  • 307
  • 1
  • 2
  • 15
  • 1
    what upscale filter did you use? I would probably go for [gauss filtering](https://stackoverflow.com/a/64845819/2521214) also your description reminds me on this [2D raycasting light effect](https://stackoverflow.com/a/34708022/2521214) – Spektre Dec 04 '22 at 08:18
  • @Spektre these are completely different methods for lighting. Also I used (as per post) a custom upscaling algorithm using color-interpolation. Gaussian blur will not help here because I need a solid method for upscaling, not bluring the lighting. I could clean up the final result using Gaussian, but the initial upscale needs to be cusotmized for lighting. – FatalSleep Dec 04 '22 at 08:36
  • 1
    may be you should describe how your upscale filter works ... hope its not just shifting (uneven weighting due to order of processing pixels ...) most of us are too lazy to analyze foreign code ... – Spektre Dec 04 '22 at 08:43
  • @Spektre I did. It finds the nearest 4 samples (down-scaled pixels) and then uses color-interpolation (see shader reference I wrote) in order to determine the colors of the upscaled image. – FatalSleep Dec 04 '22 at 08:47
  • The only thing I can think of is coordinate rounding or just slightly off ... maybe addin +/-0.5*pixel size to coordinate would help ... – Spektre Dec 04 '22 at 08:50
  • 1
    any upscaling is going to create artifacts, because upscaling can't restore information that isn't present. The standard solution is to use more points, which will make it less rough, but more blurry. You could try other upscaling methods to see if any other method is more acceptable to you. – aptriangle Dec 04 '22 at 09:51
  • @aptriangle indeed, that I can deal with (temporal filtering) solves the blurry-ness. The problem is JUST the edging around emitters/occluders as pointed to in red in the image. My initial though was to throw emitter/occluder pixels away in the upscale (see the for-loop in the main function), but for whatever reason it isn't working. – FatalSleep Dec 04 '22 at 10:10
  • @aptriangle yeah I'm not trying to recreate the original lighting, I'm generating an approximation, more than anything. Which actually provides a semi-decent result. Just trying to remove the edge artifacting at emitters/occluders. – FatalSleep Dec 04 '22 at 10:12

0 Answers0