/*------------------. | :: Description :: | '-------------------/ Ascii (Version 0.9) Author: CeeJay.dk License: MIT About: Converts the image to ASCII characters using a greyscale algoritm, cherrypicked characters and a custom bitmap font stored in a set of floats. It has 17 gray levels but uses dithering to greatly increase that number. Ideas for future improvement: * Cleanup code * Maybe find a better/faster pattern - possibly blur the pixels first with a 2 pass aproach * Try using a font atlas for more fonts or perhaps more performance * Try making an ordered dither rather than the random one. I think the random looks a bit too noisy. * Calculate luma from linear colorspace History: (*) Feature (+) Improvement (x) Bugfix (-) Information (!) Compatibility Version 0.7 by CeeJay.dk * Added the 3x5 font Version 0.8 by CeeJay.dk + Cleaned up settings UI for Reshade 3.x Version 0.9 by CeeJay.dk x Fixed an issue with the settings where the 3x5 could not be selected. - Cleaned up and commented the code. More cleanup is still needed. * Added the ability to toggle dithering on/off x Removed temporal dither code due to incompatibility with humans - it was giving me headaches and I didn't want to cause anyones seizure x Fixed an uneven distribution of the greyscale shades */ /*---------------. | :: Includes :: | '---------------*/ #include "ReShade.fxh" /*------------------. | :: UI Settings :: | '------------------*/ #include "ReShadeUI.fxh" /* uniform float Version < ui_label = "Version"; ui_min = 0.8; ui_max = 0.8; ui_step = 1.0; ui_category = "Author : CeeJay.dk\n\n" "To increase the size of the characters on screen simply lower your resolution in-game\n\n" "Try using this together with Nostagia. It also looks best if you first increase the contrast with Curves.\n\n"; > = float(0.8); */ uniform int Ascii_spacing < __UNIFORM_SLIDER_INT1 ui_min = 0; ui_max = 5; ui_label = "Character Spacing"; ui_tooltip = "Determines the spacing between characters. I feel 1 to 3 looks best."; ui_category = "Font style"; > = 1; /* uniform int Ascii_font < ui_type = "drag"; ui_min = 1; ui_max = 2; ui_label = "Font Size"; ui_tooltip = "1 = 5x5 font, 2 = 3x5 font"; ui_category = "Font style"; > = 1; */ uniform int Ascii_font < ui_type = "combo"; ui_label = "Font Size"; ui_tooltip = "Choose font size"; ui_category = "Font style"; ui_items = "Smaller 3x5 font\0" "Normal 5x5 font\0" ; > = 1; uniform int Ascii_font_color_mode < __UNIFORM_SLIDER_INT1 ui_min = 0; ui_max = 2; ui_label = "Font Color Mode"; ui_tooltip = "0 = Foreground color on background color, 1 = Colorized grayscale, 2 = Full color"; ui_category = "Color options"; > = 1; uniform float3 Ascii_font_color < __UNIFORM_COLOR_FLOAT3 ui_label = "Font Color"; ui_tooltip = "Choose a font color"; ui_category = "Color options"; > = float3(1.0, 1.0, 1.0); uniform float3 Ascii_background_color < __UNIFORM_COLOR_FLOAT3 ui_label = "Background Color"; ui_tooltip = "Choose a background color"; ui_category = "Color options"; > = float3(0.0, 0.0, 0.0); uniform bool Ascii_swap_colors < ui_label = "Swap Colors"; ui_tooltip = "Swaps the font and background color when you are too lazy to edit the settings above (I know I am)"; ui_category = "Color options"; > = 0; uniform bool Ascii_invert_brightness < ui_label = "Invert Brightness"; ui_category = "Color options"; > = 0; uniform bool Ascii_dithering < ui_label = "Dithering"; ui_category = "Dithering"; > = 1; uniform float Ascii_dithering_intensity < __UNIFORM_SLIDER_FLOAT1 ui_min = 0.0; ui_max = 4.0; ui_label = "Dither shift intensity"; ui_tooltip = "For debugging purposes"; ui_category = "Debugging"; > = 2.0; uniform bool Ascii_dithering_debug_gradient < ui_label = "Dither debug gradient"; ui_category = "Debugging"; > = 0; /*-------------------------. | :: Sampler and timers :: | '-------------------------*/ #define asciiSampler ReShade::BackBuffer uniform float timer < source = "timer"; >; uniform float framecount < source = "framecount"; >; /*-------------. | :: Effect :: | '-------------*/ float3 AsciiPass( float2 tex ) { /*-------------------------. | :: Sample and average :: | '-------------------------*/ //if (Ascii_font != 1) float2 Ascii_font_size = float2(3.0,5.0); //3x5 float num_of_chars = 14. ; if (Ascii_font == 1) { Ascii_font_size = float2(5.0,5.0); //5x5 num_of_chars = 17.; } float quant = 1.0/(num_of_chars-1.0); //value used for quantization float2 Ascii_block = Ascii_font_size + float(Ascii_spacing); float2 cursor_position = trunc((BUFFER_SCREEN_SIZE / Ascii_block) * tex) * (Ascii_block / BUFFER_SCREEN_SIZE); //TODO Cleanup - and maybe find a better/faster pattern - possibly blur the pixels first with a 2 pass aproach //-- Pattern 2 - Sample ALL the pixels! -- float3 color = tex2D(asciiSampler, cursor_position + float2( 1.5, 1.5) * BUFFER_PIXEL_SIZE).rgb; color += tex2D(asciiSampler, cursor_position + float2( 1.5, 3.5) * BUFFER_PIXEL_SIZE).rgb; color += tex2D(asciiSampler, cursor_position + float2( 1.5, 5.5) * BUFFER_PIXEL_SIZE).rgb; //color += tex2D(asciiSampler, cursor_position + float2( 0.5, 6.5) * BUFFER_PIXEL_SIZE).rgb; color += tex2D(asciiSampler, cursor_position + float2( 3.5, 1.5) * BUFFER_PIXEL_SIZE).rgb; color += tex2D(asciiSampler, cursor_position + float2( 3.5, 3.5) * BUFFER_PIXEL_SIZE).rgb; color += tex2D(asciiSampler, cursor_position + float2( 3.5, 5.5) * BUFFER_PIXEL_SIZE).rgb; //color += tex2D(asciiSampler, cursor_position + float2( 2.5, 6.5) * BUFFER_PIXEL_SIZE).rgb; color += tex2D(asciiSampler, cursor_position + float2( 5.5, 1.5) * BUFFER_PIXEL_SIZE).rgb; color += tex2D(asciiSampler, cursor_position + float2( 5.5, 3.5) * BUFFER_PIXEL_SIZE).rgb; color += tex2D(asciiSampler, cursor_position + float2( 5.5, 5.5) * BUFFER_PIXEL_SIZE).rgb; //color += tex2D(asciiSampler, cursor_position + float2( 4.5, 6.5) * BUFFER_PIXEL_SIZE).rgb; //color += tex2D(asciiSampler, cursor_position + float2( 6.5, 0.5) * BUFFER_PIXEL_SIZE).rgb; //color += tex2D(asciiSampler, cursor_position + float2( 6.5, 2.5) * BUFFER_PIXEL_SIZE).rgb; //color += tex2D(asciiSampler, cursor_position + float2( 6.5, 4.5) * BUFFER_PIXEL_SIZE).rgb; //color += tex2D(asciiSampler, cursor_position + float2( 6.5, 6.5) * BUFFER_PIXEL_SIZE).rgb; color /= 9.0; /* //-- Pattern 3 - Just one -- float3 color = tex2D(asciiSampler, cursor_position + float2(4.0,4.0) * BUFFER_PIXEL_SIZE) .rgb; //this may be fast but it's not very temporally stable */ /*------------------------. | :: Make it grayscale :: | '------------------------*/ float luma = dot(color,float3(0.2126, 0.7152, 0.0722)); float gray = luma; if (Ascii_invert_brightness) gray = 1.0 - gray; /*----------------. | :: Debugging :: | '----------------*/ if (Ascii_dithering_debug_gradient) { //gray = sqrt(dot(float2((cursor_position.x - 0.5)*1.778,cursor_position.y - 0.5),float2((cursor_position.x - 0.5)*1.778,cursor_position.y - 0.5))) * 1.1; //gray = (cursor_position.x + cursor_position.y) * 0.5; //diagonal test gradient //gray = smoothstep(0.0,1.0,gray); //increase contrast gray = cursor_position.x; //horizontal test gradient //gray = cursor_position.y; //vertical test gradient } /*-------------------. | :: Get position :: | '-------------------*/ float2 p = frac((BUFFER_SCREEN_SIZE / Ascii_block) * tex); //p is the position of the current pixel inside the character p = trunc(p * Ascii_block); //p = trunc(p * Ascii_block - float2(1.5,1.5)) ; float x = (Ascii_font_size.x * p.y + p.x); //x is the number of the position in the bitfield /*----------------. | :: Dithering :: | '----------------*/ //TODO : Try make an ordered dither rather than the random dither. Random looks a bit too noisy for my taste. if (Ascii_dithering != 0) { //Pseudo Random Number Generator // -- PRNG 1 - Reference -- float seed = dot(cursor_position, float2(12.9898,78.233)); //I could add more salt here if I wanted to float sine = sin(seed); //cos also works well. Sincos too if you want 2D noise. float noise = frac(sine * 43758.5453 + cursor_position.y); float dither_shift = (quant * Ascii_dithering_intensity); // Using noise to determine shift. float dither_shift_half = (dither_shift * 0.5); // The noise should vary between +- 0.5 dither_shift = dither_shift * noise - dither_shift_half; // MAD //shift the color by dither_shift gray += dither_shift; //apply dithering } /*---------------------------. | :: Convert to character :: | '---------------------------*/ float n = 0; if (Ascii_font == 1) { // -- 5x5 bitmap font by CeeJay.dk -- // .:^"~cvo*wSO8Q0# //17 characters including space which is handled as a special case //The serial aproach to this would take 16 cycles, so I instead used an upside down binary tree to parallelize this to only 5 cycles float n12 = (gray < (2. * quant)) ? 4194304. : 131200. ; // . or : float n34 = (gray < (4. * quant)) ? 324. : 330. ; // ^ or " float n56 = (gray < (6. * quant)) ? 283712. : 12650880.; // ~ or c float n78 = (gray < (8. * quant)) ? 4532768. : 13191552.; // v or o float n910 = (gray < (10. * quant)) ? 10648704. : 11195936.; // * or w float n1112 = (gray < (12. * quant)) ? 15218734. : 15255086.; // S or O float n1314 = (gray < (14. * quant)) ? 15252014. : 32294446.; // 8 or Q float n1516 = (gray < (16. * quant)) ? 15324974. : 11512810.; // 0 or # float n1234 = (gray < (3. * quant)) ? n12 : n34; float n5678 = (gray < (7. * quant)) ? n56 : n78; float n9101112 = (gray < (11. * quant)) ? n910 : n1112; float n13141516 = (gray < (15. * quant)) ? n1314 : n1516; float n12345678 = (gray < (5. * quant)) ? n1234 : n5678; float n910111213141516 = (gray < (13. * quant)) ? n9101112 : n13141516; n = (gray < (9. * quant)) ? n12345678 : n910111213141516; } else // Ascii_font == 0 , the 3x5 font { // -- 3x5 bitmap font by CeeJay.dk -- // .:;s*oSOXH0 //14 characters including space which is handled as a special case /* Font reference : //The plusses are "likes". I was rating how much I liked that character over other alternatives. 3 ^ 42. 3 - 448. 3 i (short) 9232. 3 ; 5136. ++ 4 " 45. 4 i 9346. 4 s 5200. ++ 5 + 1488. 5 * 2728. ++ 6 c 25200. 6 o 11088. ++ 7 v 11112. 7 S 14478. ++ 8 O 11114. ++ 9 F 5071. 9 5 (rounded) 14543. 9 X 23213. ++ 10 A 23530. 10 D 15211. + 11 H 23533. + 11 5 (square) 31183. 11 2 (square) 29671. ++ 5 (rounded) 14543. */ float n12 = (gray < (2. * quant)) ? 4096. : 1040. ; // . or : float n34 = (gray < (4. * quant)) ? 5136. : 5200. ; // ; or s float n56 = (gray < (6. * quant)) ? 2728. : 11088.; // * or o float n78 = (gray < (8. * quant)) ? 14478. : 11114.; // S or O float n910 = (gray < (10. * quant)) ? 23213. : 15211.; // X or D float n1112 = (gray < (12. * quant)) ? 23533. : 31599.; // H or 0 float n13 = 31727.; // 8 float n1234 = (gray < (3. * quant)) ? n12 : n34; float n5678 = (gray < (7. * quant)) ? n56 : n78; float n9101112 = (gray < (11. * quant)) ? n910 : n1112; float n12345678 = (gray < (5. * quant)) ? n1234 : n5678; float n910111213 = (gray < (13. * quant)) ? n9101112 : n13; n = (gray < (9. * quant)) ? n12345678 : n910111213; } /*--------------------------------. | :: Decode character bitfield :: | '--------------------------------*/ float character = 0.0; //Test values //n = -(exp2(24.)-1.0); //-(2^24-1) All bits set - a white 5x5 box float lit = (gray <= (1. * quant)) //If black then set all pixels to black (the space character) ? 0.0 //That way I don't have to use a character bitfield for space : 1.0 ; //I simply let it to decode to the second darkest "." and turn its pixels off float signbit = (n < 0.0) //is n negative? (I would like to test for negative 0 here too but can't) ? lit : 0.0 ; signbit = (x > 23.5) //is this the first pixel in the character? ? signbit //if so set to the signbit (which may be on or off depending on if the number was negative) : 0.0 ; //else make it black //Tenary Multiply exp2 character = ( frac( abs( n*exp2(-x-1.0))) >= 0.5) ? lit : signbit; //If the bit for the right position is set, then light up the pixel //UNLESS gray was dark enough, then keep the pixel dark to make a space //If the bit for the right position was not set, then turn off the pixel //UNLESS it's the first pixel in the character AND n is negative - if so light up the pixel. //This way I can use all 24 bits in the mantissa as well as the signbit for characters. if (clamp(p.x, 0.0, Ascii_font_size.x - 1.0) != p.x || clamp(p.y, 0.0, Ascii_font_size.y - 1.0) != p.y) //Is this the space around the character? character = 0.0; //If so make the pixel black. /*---------------. | :: Colorize :: | '---------------*/ if (Ascii_swap_colors) { if (Ascii_font_color_mode == 2) { color = (character) ? character * color : Ascii_font_color; } else if (Ascii_font_color_mode == 1) { color = (character) ? Ascii_background_color * gray : Ascii_font_color; } else // Ascii_font_color_mode == 0 { color = (character) ? Ascii_background_color : Ascii_font_color; } } else { if (Ascii_font_color_mode == 2) { color = (character) ? character * color : Ascii_background_color; } else if (Ascii_font_color_mode == 1) { color = (character) ? Ascii_font_color * gray : Ascii_background_color; } else // Ascii_font_color_mode == 0 { color = (character) ? Ascii_font_color : Ascii_background_color; } } /*-------------. | :: Return :: | '-------------*/ //color = gray; return saturate(color); } float3 PS_Ascii(float4 position : SV_Position, float2 texcoord : TEXCOORD) : SV_Target { float3 color = AsciiPass(texcoord); return color.rgb; } technique ASCII { pass ASCII { VertexShader=PostProcessVS; PixelShader=PS_Ascii; } } /* .---------------------. | :: Character set :: | '---------------------' Here are some various chacters and gradients I created in my quest to get the best look .'~:;!>+=icjtJY56SXDQKHNWM .':!+ijY6XbKHNM .:%oO$8@#M .:+j6bHM .:coCO8@ .:oO8@ .:oO8 :+# .:^"~cso*wSO8Q0# .:^"~csoCwSO8Q0# .:^"~c?o*wSO8Q0# n value // # of pixels // character ------------//----//------------------- 4194304. // 1 // . (bottom aligned) * 131200. // 2 // : (middle aligned) * 4198400. // 2 // : (bottom aligned) 132. // 2 // ' 2228352. // 3 // ; 4325504. // 3 // i (short) 14336. // 3 // - (small) 324. // 3 // ^ 4329476. // 4 // i (tall) 330. // 4 // " 31744. // 5 // - (larger) 283712. // 5 // ~ 10627072. // 5 // x 145536. // 5 // * or + (small and centered) 6325440. // 6 // c (narrow - left aligned) 12650880. // 6 // c (narrow - center aligned) 9738240. // 6 // n (left aligned) 6557772. // 7 // s (tall) 8679696. // 7 // f 4532768. // 7 // v (1st) 4539936. // 7 // v (2nd) 4207118. // 7 // ? -17895696. // 7 // % 6557958. // 7 // 3 6595776. // 8 // o (left aligned) 13191552. // 8 // o (right aligned) 14714304. // 8 // c (wide) 12806528. // 9 // e (right aligned) 332772. // 9 // * (top aligned) 10648704. // 9 // * (bottom aligned) 4357252. // 9 // + -18157904. // 9 // X 11195936. // 10 // w 483548. // 10 // s (thick) 15218734. // 11 // S 31491134. // 11 // C 15238702. // 11 // C (rounded) 22730410. // 11 // M (more like a large m) 10648714. // 11 // * (larger) 4897444. // 11 // * (2nd larger) 14726438. // 11 // @ (also looks like a large e) 23385164. // 11 // & 15255086. // 12 // O 16267326. // 13 // S (slightly larger) 15252014. // 13 // 8 15259182. // 13 // 0 (O with dot in the middle) 15517230. // 13 // Q (1st) -18405232. // 13 // M -11196080. // 13 // W 32294446. // 14 // Q (2nd) 15521326. // 14 // Q (3rd) 32298542. // 15 // Q (4th) 15324974. // 15 // 0 or Ø 16398526. // 15 // $ 11512810. // 16 // # -33061950. // 17 // 5 or S (stylized) -33193150. // 19 // $ (stylized) -33150782. // 19 // 0 (stylized) */