Idea for a new shader

Hi all, just wanted to throw out an idea for a shader since it’s something I’ve never seen before and not sure how feasible it is.

The idea is a game screenshot filter…I remember my young days as a youth cracking open old issues of nintendo power or EGM and seeing in-game shots of games and recall how vibrant and contrast-y they were, even more the screenshots tended to mask the pixels and gave it almost an illustrated, painterly look. I don’t have any old mags to scan but found a few good examples of arcade flyers

http://flyers.arcade-museum.com/?page=flyer&db=videodb&id=1095&image=4 http://flyers.arcade-museum.com/?page=flyer&db=videodb&id=2358&image=1 http://flyers.arcade-museum.com/?page=flyer&db=videodb&id=444&image=2 http://flyers.arcade-museum.com/?page=flyer&db=videodb&id=1180&image=1

I’m not sure how this would best be replicated, a conversion of rgb pixels to cmyk halftone dots like an actual page print and magnified with some blur on top is how I would imagine it, maybe even a faint paper fiber texture on top for good measure. It would be cool if someone had the knowledge and will to make one because sadly I’m just a designer, not a programmer :frowning:

Any thoughts?

That’s an interesting idea. I found a good tutorial for a WebGL CMYK halftone shader but didn’t have much luck converting the entire thing. I’ll try going through step-by-step and see if I have any better luck.

Also, I lol’ed at the TMNT costumes in that last image.

EDIT: Ok, going through step-by-step, I was able to get something pretty good (looks best up close / in-motion) :smiley: Here’s the code (XML/GLSL at the moment; I’ll try to get a Cg version going soon):

<?xml version="1.0" encoding="UTF-8"?>
<shader language="GLSL" style="GLES2">
   <vertex><![CDATA[
      #version 120
      uniform mat4 MVPMatrix;
      attribute vec2 VertexCoord;
      attribute vec2 TexCoord;
      varying vec2 tex_coord;

      void main()
      {
         gl_Position = MVPMatrix * vec4(VertexCoord, 0.0, 1.0);
         tex_coord = TexCoord;
      }
   ]]></vertex>
   <fragment><![CDATA[
// GLSL halftone shader demo for WebGL
// Stefan Gustavson 2012-02-16 ([email protected])
//
// 2D simplex noise by Ian McEwan, distributed under
// the MIT license. All other code in this shader is
// my original work, and is in the public domain.
// Credit is appreciated where appropriate, though.
// Ported to RetroArch by hunterk

uniform sampler2D Texture;
varying vec2 tex_coord; // Texcoords

#define frequency  600.0

float aastep(float threshold, float value) {
  float afwidth = 0.7 * length(vec2(dFdx(value), dFdy(value)));
  return smoothstep(threshold-afwidth, threshold+afwidth, value);
}

// Description : Array- and textureless GLSL 2D simplex noise.
// Author : Ian McEwan, Ashima Arts. Version: 20110822
// Copyright (C) 2011 Ashima Arts. All rights reserved.
// Distributed under the MIT License. See LICENSE file.
// https://github.com/ashima/webgl-noise
 
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289((( x * 34.0) + 1.0) * x); }
 
float snoise(vec2 v) {
  const vec4 C = vec4(0.211324865405187,  // (3.0-sqrt(3.0))/6.0
                      0.366025403784439,  // 0.5*(sqrt(3.0)-1.0)
                     -0.577350269189626,  // -1.0 + 2.0 * C.x
                      0.024390243902439); // 1.0 / 41.0
  // First corner
  vec2 i = floor(v + dot(v, C.yy) );
  vec2 x0 = v - i + dot(i, C.xx);
  // Other corners
  vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
  vec4 x12 = x0.xyxy + C.xxzz;
  x12.xy -= i1;
  // Permutations
  i = mod289(i); // Avoid truncation effects in permutation
  vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
                           + i.x + vec3(0.0, i1.x, 1.0 ));
  vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
                          dot(x12.zw,x12.zw)), 0.0);
  m = m*m; m = m*m;
  // Gradients
  vec3 x = 2.0 * fract(p * C.www) - 1.0;
  vec3 h = abs(x) - 0.5;
  vec3 a0 = x - floor(x + 0.5);
  // Normalise gradients implicitly by scaling m
  m *= 1.792843 - 0.853735 * ( a0*a0 + h*h );
  // Compute final noise value at P
  vec3 g;
  g.x = a0.x * x0.x + h.x * x0.y;
  g.yz = a0.yz * x12.xz + h.yz * x12.yw;
  return 130.0 * dot(m, g);
}
 
void main() {
    // Distance to nearest point in a grid of
    // (frequency x frequency) points over the unit square
    vec2 st2 = mat2(0.707, -0.707, 0.707, 0.707) * tex_coord;
    vec2 nearest = 2.0*fract(frequency * st2) - 1.0;
    float dist = length(nearest);
    vec3 texcolor = texture2D(Texture, tex_coord).rgb;
    float radius = sqrt(1.0-texcolor.g);//0.5;
    float n = 0.1*snoise(tex_coord * 200.0); // Fractal noise
    n += 0.05*snoise(tex_coord * 400.0);
    n += 0.025*snoise(tex_coord * 800.0);
// start fast version
    vec3 white = vec3(1.0, 1.0, 1.0);
    vec3 black = vec3(0.0, 0.0, 0.0);
// end fast version

// start slow version
//    vec3 white = vec3(n * 0.5 + 0.98);
//    vec3 black = vec3(n + 0.1);
// end slow version

    // Perform a rough RGB-to-CMYK conversion
    vec4 cmyk;
    cmyk.xyz = 1.0 - texcolor;
    cmyk.w = min(cmyk.x, min(cmyk.y, cmyk.z)); // Create K
    cmyk.xyz -= cmyk.w; // Subtract K equivalent from CMY
 
    // Distance to nearest point in a grid of
    // (frequency x frequency) points over the unit square
    vec2 Kst = frequency * mat2(0.707, -0.707, 0.707, 0.707) * tex_coord;
    vec2 Kuv = 2.0 * fract(Kst) - 1.0;
    float k = aastep(0.0, sqrt(cmyk.w) - length(Kuv) + n);
    vec2 Cst = frequency * mat2(0.966, -0.259, 0.259, 0.966) * tex_coord;
    vec2 Cuv = 2.0 * fract(Cst) - 1.0;
    float c = aastep(0.0, sqrt(cmyk.x) - length(Cuv) + n);
    vec2 Mst = frequency * mat2(0.966, 0.259, -0.259, 0.966) * tex_coord;
    vec2 Muv = 2.0 * fract(Mst) - 1.0;
    float m = aastep(0.0, sqrt(cmyk.y) - length(Muv) + n);
    vec2 Yst = frequency * tex_coord; // 0 deg
    vec2 Yuv = 2.0 * fract(Yst) - 1.0;
    float y = aastep(0.0, sqrt(cmyk.z) - length(Yuv) + n);
 
    vec3 rgbscreen = 1.0 - 0.9*vec3(c,m,y) + n;
    rgbscreen = mix(rgbscreen, black, 0.85*k + 0.3*n);

    vec3 fragcolor = mix(black, white, aastep(radius, dist));
    gl_FragColor = vec4(rgbscreen * rgbscreen, 1.0);
}
]]></fragment>
</shader>

To get the effect you’re describing, it might be good to add some blur in there or something, though, and maybe increase the color saturation.

Note: You can change the ‘frequency’ variable’s value to increase/decrease the dot density. 400 makes everything all Lichtenstein-y

Wow you work fast Hunter. I admit I just got home and haven’t had time to poke around in the settings but it is definitely interesting, it actually looks rather magazine-like if you use a filter like 2xsai or Super Eagle. I think the one thing holding it back is the static nature of the faux grain, but it is a tricky balancing act because if you added motion grain to it it might look less like a piece of paper…

What I always thought was a great filter was Stella’s CRT filter that made Atari games look like you were playing on an old-school 70s TV. The moving fuzz mimicking old RF cables actually gave it a rather ethereal quality that gave life to non-moving pixels. That might look good in this instance.

I definitely think this filter could be really awesome with some TLC, it’s different from all the various CRT or 4XBR shader variations I see alot.

Yeah, it’s a neat effect :slight_smile:

I think you can make the halftone dots move around pretty easily by hooking the framecounter into the position/angle calculations. At that point, it kinda stops being a halftone shader and becomes more of a noise shader, but that may not be a bad thing. If you dig that sort of thing, I recommend checking out Mudlord’s OldTV shader(s) from the common-shaders github repo.

Here’s a slapdash photoshop mockup of an extension to the halftone shader that would be pretty easy to whip up using a background/border shader with a book-on-desk lookup texture:

Interesting nostalgic effect. I like it!

A good source about these effects: http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT

Ok, here’s a Cg version of the shader:

/*
CMYK Halftone Dot Shader

Adapted from Stefan Gustavson's GLSL shader demo for WebGL:
http://webstaff.itn.liu.se/~stegu/OpenGLinsights/shadertutorial.html

Ported to Cg shader language by hunterk

This shader is licensed in the public domain, as per S. Gustavson's original license
*/


/* COMPATIBILITY 
   - HLSL compilers
   - Cg   compilers
*/

#define frequency 400.0 //controls the density of the dot pattern

// VERTEX SHADER //

void main_vertex
(
    float4 position    : POSITION,
    float2 texCoord : TEXCOORD0,

    uniform float4x4 modelViewProj,

    out float4 oPosition : POSITION,
    out float2 otexCoord : TEXCOORD
)
{
    oPosition = mul(modelViewProj, position);
    otexCoord = texCoord;
}

struct input
{
  float2 video_size;
  float2 texCoord_size;
  float2 output_size;
  float frame_count;
  float frame_direction;
  float frame_rotation;
  float texture_size;
  sampler2D texture : TEXUNIT0;
};

struct output 
{
  float4 col    : COLOR;
};

// FRAGMENT SHADER //

output main_fragment(in float2 texCoord : TEXCOORD0,
uniform input IN,
uniform sampler2D texture : TEXUNIT0
)
{
    // Distance to nearest point in a grid of
    // (frequency x frequency) points over the unit square
    float2x2 rotation_matrix = float2x2(0.707, 0.707, -0.707, 0.707);
    float2 st2 = mul(rotation_matrix , texCoord);
    float2 nearest = 2.0 * fract(frequency * st2) - 1.0;
    float dist = length(nearest);
    float3 texcolor = tex2D(IN.texture, texCoord).rgb; // Unrotated coords
    float3 black = float3(0.0, 0.0, 0.0);
    
    // Perform a rough RGB-to-CMYK conversion
    float4 cmyk;
    cmyk.xyz = 1.0 - texcolor;
    cmyk.w = min(cmyk.x, min(cmyk.y, cmyk.z)); // Create K
    cmyk.xyz -= cmyk.w; // Subtract K equivalent from CMY
    
    float2x2 k_matrix = float2x2(0.707, 0.707, -0.707, 0.707);
    float2 Kst = frequency * (0.48 * (IN.texture_size / IN.video_size)) * mul(k_matrix , texCoord);
    float2 Kuv = 2.0 * fract(Kst) - 1.0;
    float k = step(0.0, sqrt(cmyk.w) - length(Kuv));
    float2x2 c_matrix = float2x2(0.966, 0.259, -0.259, 0.966);
    float2 Cst = frequency * (0.48 * (IN.texture_size / IN.video_size)) * mul(c_matrix , texCoord);
    float2 Cuv = 2.0 * fract(Cst) - 1.0;
    float c = step(0.0, sqrt(cmyk.x) - length(Cuv));
    float2x2 m_matrix = float2x2(0.966, -0.259, 0.259, 0.966);
    float2 Mst = frequency * (0.48 * (IN.texture_size / IN.video_size)) * mul(m_matrix , texCoord);
    float2 Muv = 2.0 * fract(Mst) - 1.0;
    float m = step(0.0, sqrt(cmyk.y) - length(Muv));
    float2 Yst = frequency * (0.48 * (IN.texture_size / IN.video_size)) * texCoord; // 0 deg
    float2 Yuv = 2.0 * fract(Yst) - 1.0;
    float y = step(0.0, sqrt(cmyk.z) - length(Yuv));
    
    float3 rgbscreen = 1.0 - 0.9 * float3(c,m,y);
    rgbscreen = mix(rgbscreen, black, 0.85 * k);
    
    float afwidth = 2.0 * frequency * max(length(IN.video_size / (IN.output_size * IN.texture_size)), length(IN.video_size / (IN.output_size * IN.texture_size)));
    float blend = smoothstep(0.7, 1.4, afwidth);
    
    float4 color = float4(mix(rgbscreen , texcolor, blend), 1.0);

   output OUT;
   OUT.col = color;
   return OUT;
}

I purposely left out the noise functions, which used the MIT license, so the whole thing should be public domain now. EDIT: added a fix for cores that have different DPI, such as Genesis.

Awesome, thanks hunter, I’ll have fun poking around in the settings.