RobertBeckebans / RBDOOM-3-BFG

Doom 3 BFG Edition source port with updated DX12 / Vulkan renderer and modern game engine features
https://www.moddb.com/mods/rbdoom-3-bfg
GNU General Public License v3.0
1.46k stars 253 forks source link

Add optional 8 bit retro post processing effect #571

Closed RobertBeckebans closed 5 months ago

RobertBeckebans commented 3 years ago

References:

https://github.com/RobertBeckebans/KinoEight/blob/master/Packages/jp.keijiro.kino.post-processing.eight/Resources/EightColor.shader

https://www.shadertoy.com/view/tsKGDm

Arl90 commented 3 years ago

Neat! The auto dithering seems also very good.

Imagine if you can also apply a color palette to the image.

RobertBeckebans commented 1 year ago

Backup of experimental local shadertoy shader to dither any image using a palette from lospec.com

//#iChannel0 "file://circlemask_1010p.png"
#iChannel0 "file://WIN_20211016_15_57_04_Pro.jpg"

//#iChannel0 "file://rbdoom-3-bfg-20210219-182155-001.png"

const float PIXEL_FACTOR = 320.; // Lower num - bigger pixels (this will be the screen width)
const float COLOR_FACTOR = 8.;   // Higher num - higher colors quality

const mat4 ditherTable = mat4(
    -4.0, 0.0, -3.0, 1.0,
    2.0, -2.0, 3.0, -1.0,
    -3.0, 1.0, -4.0, 0.0,
    3.0, -1.0, 2.0, -2.0
);

// https://lospec.com/palette-list/luftrausers7
//#iChannel1 "file://luftrausers7-32x.png"
//#iChannel1 "file://vieilles-cartes-32x.png"
//#iChannel1 "file://smooth-polished-silver-32x.png"

//#iChannel1 "file://justparchment8-8x.png"
//#iChannel1 "file://witching-hour-32x.png"
//#iChannel1 "file://goosebumps-gold-32x.png"
#iChannel1 "file://commodore-8-32x.png"

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{                  
    // Reduce pixels            
    vec2 size = PIXEL_FACTOR * iResolution.xy/iResolution.x;
    vec2 coor = floor( fragCoord/iResolution.xy * size) ;
    vec2 uv = coor / size;   

    // Get source color
    vec3 col = texture( iChannel0, uv ).xyz;     

    // Dither
    float dither = ditherTable[int( coor.x ) % 4][int( coor.y ) % 4] * 0.005; // last number is dithering strength
    col += vec3( dither );

    // Reduce colors    
    //col = floor(col * COLOR_FACTOR) / COLOR_FACTOR;

    // Alias for each color
    const float step = 1.0 / 8.0;
    vec3 c1 = texture( iChannel1, vec2( 0.0, 0.0 ) ).rgb;
    vec3 c2 = texture( iChannel1, vec2( step * 2.0, 0.0) ).rgb;
    vec3 c3 = texture( iChannel1, vec2( step * 3.0, 0.0) ).rgb;
    vec3 c4 = texture( iChannel1, vec2( step * 4.0, 0.0) ).rgb;
    vec3 c5 = texture( iChannel1, vec2( step * 5.0, 0.0) ).rgb;
    vec3 c6 = texture( iChannel1, vec2( step * 6.0, 0.0) ).rgb;
    vec3 c7 = texture( iChannel1, vec2( step * 7.0, 0.0) ).rgb;
    vec3 c8 = texture( iChannel1, vec2( step * 8.0, 0.0) ).rgb;

    // Euclidean distance
    float d1 = distance(c1, col.rgb);
    float d2 = distance(c2, col.rgb);
    float d3 = distance(c3, col.rgb);
    float d4 = distance(c4, col.rgb);
    float d5 = distance(c5, col.rgb);
    float d6 = distance(c6, col.rgb);
    float d7 = distance(c7, col.rgb);
    float d8 = distance(c8, col.rgb);

    // Best fit search
    vec4 rgb_d = vec4(c1, d1);
    rgb_d = rgb_d.a < d2 ? rgb_d : vec4(c2, d2);
    rgb_d = rgb_d.a < d3 ? rgb_d : vec4(c3, d3);
    rgb_d = rgb_d.a < d4 ? rgb_d : vec4(c4, d4);
    rgb_d = rgb_d.a < d5 ? rgb_d : vec4(c5, d5);
    rgb_d = rgb_d.a < d6 ? rgb_d : vec4(c6, d6);
    rgb_d = rgb_d.a < d7 ? rgb_d : vec4(c7, d7);
    rgb_d = rgb_d.a < d8 ? rgb_d : vec4(c8, d8);

    col = rgb_d.rgb;

    // Output to screen
    fragColor = vec4(col, 1.0);
}

Output: Doom3-8colors

RobertBeckebans commented 1 year ago

Backup of some interesting palettes:

witching-hour-32x commodore-8-32x goosebumps-gold-32x justparchment8-8x luftrausers7-32x smooth-polished-silver-32x vieilles-cartes-32x

RobertBeckebans commented 10 months ago

This can be done in a more efficient way: see https://www.shadertoy.com/view/MtjGRd


#define DITHER
#define AUTO_MODE
#define DOWN_SCALE 2.0

#define MAX_STEPS 196
#define MIN_DIST 0.002
#define NORMAL_SMOOTHNESS 0.1
#define PI 3.14159265359

#define PALETTE_SIZE 16
#define SUB_PALETTE_SIZE 8

#define RGB(r,g,b) (vec3(r,g,b) / 255.0)

vec3 palette[PALETTE_SIZE];
vec3 subPalette[SUB_PALETTE_SIZE];

//Initalizes the color palette.
void InitPalette()
{
    //16-Color C64 color palette.
    palette = vec3[](
        RGB(  0,  0,  0),
        RGB(255,255,255),
        RGB(152, 75, 67),
        RGB(121,193,200),   
        RGB(155, 81,165),
        RGB(104,174, 92),
        RGB( 62, 49,162),
        RGB(201,214,132),   
        RGB(155,103, 57),
        RGB(106, 84,  0),
        RGB(195,123,117),
        RGB( 85, 85, 85),   
        RGB(138,138,138),
        RGB(163,229,153),
        RGB(138,123,206),
        RGB(173,173,173)
    );

    //8-Color metalic-like sub palette.
    subPalette = vec3[](
        palette[ 6],
        palette[11],
        palette[ 4],
        palette[14],
        palette[ 5],
        palette[ 3],
        palette[13],
        palette[ 1]
    );

}

//Blends the nearest two palette colors with dithering.
vec3 GetDitheredPalette(float x,vec2 pixel)
{
    float idx = clamp(x,0.0,1.0)*float(SUB_PALETTE_SIZE-1);

    vec3 c1 = vec3(0);
    vec3 c2 = vec3(0);

    c1 = subPalette[int(idx)];
    c2 = subPalette[int(idx) + 1];

    #ifdef DITHER
        float dith = texture(iChannel0, pixel / iChannelResolution[0].xy).r;
        float mixAmt = float(fract(idx) > dith);
    #else
        float mixAmt = fract(idx);
    #endif

    return mix(c1,c2,mixAmt);
}

//Returns a 2D rotation matrix for the given angle.
mat2 Rotate(float angle)
{
    return mat2(cos(angle), sin(angle), -sin(angle), cos(angle));   
}

//Distance field functions & operations by iq. (https://iquilezles.org/articles/distfunctions)
float opU( float d1, float d2 )
{
    return min(d1,d2);
}

float opS( float d1, float d2 )
{
    return max(-d1,d2);
}

float opI( float d1, float d2 )
{
    return max(d1,d2);
}

vec3 opRep( vec3 p, vec3 c )
{
    vec3 q = mod(p,c)-0.5*c;
    return q;
}

float sdSphere( vec3 p, float s )
{
  return length(p)-s;
}

float sdBox( vec3 p, vec3 b )
{
  vec3 d = abs(p) - b;
  return min(max(d.x,max(d.y,d.z)),0.0) +
         length(max(d,0.0));
}

float sdCylinder( vec3 p, vec3 c )
{
  return length(p.xz-c.xy)-c.z;
}

//Scene definition/distance function.
float Scene(vec3 pos)
{
    float map = -sdSphere(pos, 24.0);

    vec3 rep = opRep(pos - 2.0, vec3(4.0));

    map = opU(map, opI(sdBox(pos, vec3(5.5)), sdSphere(rep, 1.0)));

    vec3 gSize = vec3(0, 0, 0.25);

    float grid = opU(opU(sdCylinder(rep.xyz, gSize), sdCylinder(rep.xzy, gSize)), sdCylinder(rep.zxy, gSize));

    grid = opI(sdBox(pos,vec3(4.5)),grid);

    map = opU(map, grid);

    return map;
}

//Returns the normal of the surface at the given position.
vec3 Normal(vec3 pos)
{
    vec3 offset = vec3(NORMAL_SMOOTHNESS, 0, 0);

    vec3 normal = vec3
    (
        Scene(pos - offset.xyz) - Scene(pos + offset.xyz),
        Scene(pos - offset.zxy) - Scene(pos + offset.zxy),
        Scene(pos - offset.yzx) - Scene(pos + offset.yzx)
    );

    return normalize(normal);
}

//Marches a ray defined by the origin and direction and returns the hit position.
vec3 RayMarch(vec3 origin,vec3 direction)
{
    float hitDist = 0.0;

    for(int i = 0;i < MAX_STEPS;i++)
    {
        float sceneDist = Scene(origin + direction * hitDist);

        hitDist += sceneDist;

        if(sceneDist < MIN_DIST)
        {
            break;
        }
    }

    return origin + direction * hitDist;
}

//Scene shading.
vec3 Shade(vec3 position, vec3 normal, vec3 rayOrigin,vec3 rayDirection,vec2 pixel)
{
    vec3 color = vec3(0);

    float ang = iTime * 2.0;

    vec3 lightPos = vec3(cos(ang), cos(ang*2.0), sin(ang)) * 2.0;  

    //Normal shading
    float shade = 0.4 * max(0.0, dot(normal, normalize(-lightPos)));

    //Specular highlight
    shade += 0.6 * max(0.0, dot(-reflect(normalize(position - lightPos), normal), rayDirection));

    //Linear falloff
    shade *= (16.0-distance(position, lightPos))/16.0,

    //Apply palette
    color = GetDitheredPalette(shade, pixel);

    //color = mix(color, vec3(0.1), step(22.0, length(position)));

    return color;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    InitPalette();

    vec2 aspect = iResolution.xy / iResolution.y;

    fragCoord = floor(fragCoord / DOWN_SCALE) * DOWN_SCALE;

    vec2 uv = fragCoord.xy / iResolution.y;

    vec2 mouse = iMouse.xy / iResolution.xy - 0.5;

    vec2 camAngle = vec2(0);

    #ifdef AUTO_MODE
        camAngle.x = PI * (-1.0 / 8.0) * sin(iTime * 0.5);
        camAngle.y = -iTime;
    #else
        camAngle.x = PI * mouse.y + PI / 2.0;
        camAngle.x += PI / 3.0;

        camAngle.y = 2.0 * PI * -mouse.x;
        camAngle.y += PI;
    #endif

    vec3 rayOrigin = vec3(0 , 0, -16.0);
    vec3 rayDirection = normalize(vec3(uv - aspect / 2.0, 1.0));

    mat2 rotateX = Rotate(camAngle.x);
    mat2 rotateY = Rotate(camAngle.y);

    //Transform ray origin and direction
    rayOrigin.yz *= rotateX;
    rayOrigin.xz *= rotateY;
    rayDirection.yz *= rotateX;
    rayDirection.xz *= rotateY;

    vec3 scenePosition = RayMarch(rayOrigin, rayDirection);

    vec3 outColor = Shade(scenePosition,Normal(scenePosition), rayOrigin, rayDirection, fragCoord / DOWN_SCALE);

    //Palette preview
    if(uv.x < 0.05) 
    {
        outColor = GetDitheredPalette(uv.y, fragCoord / DOWN_SCALE);
    }

    fragColor = vec4(outColor, 1.0);
}```
RobertBeckebans commented 10 months ago

Very interesting R2 sequence dither algo suited for 1-bit aesthetics for game jams: https://www.shadertoy.com/view/WdjGWy

// Dithering using the R-mask
// http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/
// Uncomment the #define to get color
#define PER_CHANNEL

// Triangle Wave
float T(float z) {
    return z >= 0.5 ? 2.-2.*z : 2.*z;
}

// R dither mask
float intensity(ivec2 pixel) {
    const float a1 = 0.75487766624669276;
    const float a2 = 0.569840290998;
    return fract(a1 * float(pixel.x) + a2 * float(pixel.y));
}

float dither(float gray, int ng) {
    // Calculated noised gray value
    float noised = (2./float(ng)) * T(intensity(ivec2(gl_FragCoord.xy))) + gray - (1./float(ng));
    // Clamp to the number of gray levels we want
    return clamp(floor(float(ng) * noised) / (float(ng)-1.), 0.f, 1.f);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    const int ng = 2; // Number of gray levels to use

    vec2 uv = fragCoord/iResolution.xy;

    vec3 tsample = pow(texture(iChannel0, uv).rgb, vec3(2.2));

    #ifdef PER_CHANNEL
        vec3 col = vec3(dither(tsample.r, ng),
                        dither(tsample.g, ng),
                        dither(tsample.b, ng));
    #else
        vec3 col = vec3(dither(dot(tsample, vec3(0.3, 0.59, 0.11)), ng));
    #endif

    // Output to screen, gamma corrected
    fragColor = vec4(vec3(pow(col, vec3(1.0/2.2))),1.0);
}
RobertBeckebans commented 10 months ago

2 awesome C64 shaders: C64 screen with NTSC filter - https://www.shadertoy.com/view/3tVBWR Palettization + OrderedDithering (very close to my previous shader) - https://www.shadertoy.com/view/Xdt3Dr

RobertBeckebans commented 10 months ago

Another good C64 shader:

#define RGB(r, g, b) vec3(float(r)/255., float(g)/255., float(b)/255.)

// C64 palette: http://unusedino.de/ec64/technical/misc/vic656x/colors/
#define NUM_COLORS 16
vec3 palette[NUM_COLORS];

// pre GLES3 GPUs don't support array constructors, so need to initialize array explicitly 
void InitPalette()
{
    palette[0]  = RGB(0, 0, 0);
    palette[1]  = RGB(255, 255, 255);
    palette[2]  = RGB(116, 67, 53);
    palette[3]  = RGB(124, 172, 186);
    palette[4]  = RGB(123, 72, 144);
    palette[5]  = RGB(100, 151, 79);
    palette[6]  = RGB(64, 50, 133);
    palette[7]  = RGB(191, 205, 122);
    palette[8]  = RGB(123, 91, 47);
    palette[9]  = RGB(79, 69, 0);
    palette[10] = RGB(163, 114, 101);
    palette[11] = RGB(80, 80, 80);
    palette[12] = RGB(120, 120, 120);
    palette[13] = RGB(164, 215, 142);
    palette[14] = RGB(120, 106, 189);
    palette[15] = RGB(159, 159, 150);
}

// find nearest palette color using Euclidean distance
vec4 EuclidDist(vec3 c, vec3[NUM_COLORS] pal)
{
    int idx = 0;
    float nd = distance(c, pal[0]);

    for(int i = 1; i < NUM_COLORS; i++)
    {
        float d = distance(c, pal[i]);

        if(d < nd)
        {
            nd = d;
            idx = i;
        }
    }

    /*
    // older GPUs/drivers require constant array indexing, so can't use idx directly
    if(idx == 0)  return vec4(pal[0], 1.);
    if(idx == 1)  return vec4(pal[1], 1.);
    if(idx == 2)  return vec4(pal[2], 1.);
    if(idx == 3)  return vec4(pal[3], 1.);
    if(idx == 4)  return vec4(pal[4], 1.);
    if(idx == 5)  return vec4(pal[5], 1.);
    if(idx == 6)  return vec4(pal[6], 1.);
    if(idx == 7)  return vec4(pal[7], 1.);
    if(idx == 8)  return vec4(pal[8], 1.);
    if(idx == 9)  return vec4(pal[9], 1.);
    if(idx == 10) return vec4(pal[10], 1.);
    if(idx == 11) return vec4(pal[11], 1.);
    if(idx == 12) return vec4(pal[12], 1.);
    if(idx == 13) return vec4(pal[13], 1.);
    if(idx == 14) return vec4(pal[14], 1.);
    return vec4(pal[15], 1.);
    */

    // sleek but not guaranteed to work on older GPUs!
    return vec4(pal[idx], 1.);
}

void mainImage(out vec4 o, in vec2 p)
{
    vec2 uv = vec2(floor(p.x * .25) * 4., floor(p.y * .5) * 2.) / iResolution.xy;
    InitPalette();   

    o = EuclidDist(texture(iChannel0, uv).rgb, palette);
}
RobertBeckebans commented 10 months ago

The color palette of the CPC 6128, my first computer: https://www.cpcwiki.eu/index.php/CPC_Palette