Point ntsc-adaptive to guest.r's version; add initial cathode-retro port (#514)

* initial cathode-retro port

* move ntsc-adaptive and most associated presets over to use guest.r's modified version

* fix some ctrl^H goofs

* add license blocks to cathode-retro shaders

* updates to cathode-retro; signal stuff is still broken but less so maybe
This commit is contained in:
hunterk 2023-11-27 07:59:22 -06:00 committed by GitHub
parent 2a4f904bfd
commit 182766b2a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2952 additions and 345 deletions

View file

@ -0,0 +1,47 @@
shaders = 11
shader0 = shaders/cathode-retro/cathode-retro-util-copy.slang
alias0 = g_sourceTexture
filter_linear0 = true
shader1 = shaders/cathode-retro/cathode-retro-util-downsample-2x-horz.slang
filter_linear1 = true
scale_x1 = 0.5
shader2 = shaders/cathode-retro/cathode-retro-util-downsample-2x-vert.slang
filter_linear2 = true
scale_y2 = 0.5
shader3 = ../stock.slang
shader4 = shaders/cathode-retro/cathode-retro-util-tonemap-and-downsample-horz.slang
filter_linear4 = true
scale_x4 = 0.5
shader5 = shaders/cathode-retro/cathode-retro-util-tonemap-and-downsample-vert.slang
filter_linear5 = true
scale_y5 = 0.5
alias5 = g_sourceTex
shader6 = shaders/cathode-retro/cathode-retro-util-gaussian-blur-horz.slang
filter_linear6 = true
shader7 = shaders/cathode-retro/cathode-retro-util-gaussian-blur-vert.slang
filter_linear7 = true
alias7 = g_diffusionTexture
shader8 = shaders/cathode-retro/cathode-retro-crt-generate-masks.slang
alias8 = g_maskTexture
mipmap_input8 = true
wrap_mode8 = repeat
scale_type8 = viewport
shader9 = shaders/cathode-retro/cathode-retro-crt-generate-screen-texture.slang
alias9 = g_screenMaskTexture
mipmap_input9 = true
filter_linear9 = true
wrap_mode9 = clamp
shader10 = shaders/cathode-retro/cathode-retro-crt-rgb-to-crt_no-signal.slang
mipmap_input10 = true
filter_linear10 = true

View file

@ -1,71 +1,74 @@
shaders = "7"
shader0 = "../ntsc/shaders/ntsc-adaptive/ntsc-pass1.slang"
filter_linear0 = "false"
wrap_mode0 = "clamp_to_border"
frame_count_mod0 = "2"
mipmap_input0 = "false"
alias0 = ""
float_framebuffer0 = "true"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "4.000000"
scale_type_y0 = "source"
scale_y0 = "1.000000"
shader1 = "../ntsc/shaders/ntsc-adaptive/ntsc-pass2.slang"
filter_linear1 = "false"
wrap_mode1 = "clamp_to_border"
mipmap_input1 = "false"
alias1 = ""
float_framebuffer1 = "false"
srgb_framebuffer1 = "true"
scale_type_x1 = "source"
scale_x1 = "0.500000"
scale_type_y1 = "source"
scale_y1 = "1.000000"
shader2 = "../crt/shaders/hyllian/crt-hyllian-curvature.slang"
filter_linear2 = "false"
wrap_mode2 = "clamp_to_border"
mipmap_input2 = "false"
alias2 = "CRTPass"
float_framebuffer2 = "false"
srgb_framebuffer2 = "true"
scale_type_x2 = "viewport"
scale_x2 = "1.000000"
scale_type_y2 = "viewport"
scale_y2 = "1.000000"
shader3 = "../crt/shaders/glow/threshold.slang"
filter_linear3 = "true"
wrap_mode3 = "clamp_to_border"
mipmap_input3 = "false"
alias3 = ""
float_framebuffer3 = "false"
srgb_framebuffer3 = "true"
shader4 = "../crt/shaders/glow/blur_horiz.slang"
filter_linear4 = "true"
shaders = "9"
shader0 = ../stock.slang
alias0 = PrePass0
shader1 = shaders/guest/advanced/ntsc/ntsc-pass1.slang
alias1 = NPass1
scale_type_x1 = source
scale_type_y1 = source
scale_x1 = 4.0
scale_y1 = 1.0
float_framebuffer1 = true
filter_linear1 = false
shader2 = shaders/guest/advanced/ntsc/ntsc-pass2.slang
filter_linear2 = true
float_framebuffer2 = true
scale_type2 = source
scale_x2 = 0.5
scale_y2 = 1.0
shader3 = shaders/guest/advanced/ntsc/ntsc-pass3.slang
filter_linear3 = true
scale_type3 = source
scale_x3 = 1.0
scale_y3 = 1.0
shader4 = "../crt/shaders/hyllian/crt-hyllian-curvature.slang"
filter_linear4 = "false"
wrap_mode4 = "clamp_to_border"
mipmap_input4 = "true"
alias4 = ""
mipmap_input4 = "false"
alias4 = "CRTPass"
float_framebuffer4 = "false"
srgb_framebuffer4 = "true"
scale_type_x4 = "viewport"
scale_x4 = "0.200000"
scale_x4 = "1.000000"
scale_type_y4 = "viewport"
scale_y4 = "0.200000"
shader5 = "../crt/shaders/glow/blur_vert.slang"
scale_y4 = "1.000000"
shader5 = "../crt/shaders/glow/threshold.slang"
filter_linear5 = "true"
wrap_mode5 = "clamp_to_border"
mipmap_input5 = "false"
alias5 = ""
float_framebuffer5 = "false"
srgb_framebuffer5 = "true"
shader6 = "../crt/shaders/glow/resolve.slang"
shader6 = "../crt/shaders/glow/blur_horiz.slang"
filter_linear6 = "true"
wrap_mode6 = "clamp_to_border"
mipmap_input6 = "false"
mipmap_input6 = "true"
alias6 = ""
float_framebuffer6 = "false"
srgb_framebuffer6 = "false"
scale_type6 = viewport
srgb_framebuffer6 = "true"
scale_type_x6 = "viewport"
scale_x6 = "0.200000"
scale_type_y6 = "viewport"
scale_y6 = "0.200000"
shader7 = "../crt/shaders/glow/blur_vert.slang"
filter_linear7 = "true"
wrap_mode7 = "clamp_to_border"
mipmap_input7 = "false"
alias7 = ""
float_framebuffer7 = "false"
srgb_framebuffer7 = "true"
shader8 = "../crt/shaders/glow/resolve.slang"
filter_linear8 = "true"
wrap_mode8 = "clamp_to_border"
mipmap_input8 = "false"
alias8 = ""
float_framebuffer8 = "false"
srgb_framebuffer8 = "false"
scale_type8 = viewport
parameters = "linearize;quality;ntsc_sat;BEAM_PROFILE;HFILTER_PROFILE;BEAM_MIN_WIDTH;BEAM_MAX_WIDTH;SCANLINES_STRENGTH;COLOR_BOOST;PHOSPHOR_LAYOUT;MASK_INTENSITY;CRT_ANTI_RINGING;InputGamma;OutputGamma;VSCANLINES;CRT_CURVATURE;CRT_warpX;CRT_warpY;CRT_cornersize;CRT_cornersmooth;GLOW_WHITEPOINT;GLOW_ROLLOFF;BLOOM_STRENGTH;OUTPUT_GAMMA;CURVATURE;warpX;warpY;cornersize;cornersmooth;noise_amt;shadowMask;maskDark;maskLight"
BEAM_PROFILE = "0.000000"
HFILTER_PROFILE = "1.000000"

View file

@ -0,0 +1,76 @@
// Do a barrel distortion to a given texture coordinate to emulate a curved CRT screen.
#include "cathode-retro-util-language-helpers.inc"
vec2 ApproxAtan2(vec2 x, vec2 y)
{
// A simple approximation of atan2 will suffice here, just a few terms of the taylor series are good enough for the
// angle ranges we're dealing with.
x /= y;
vec2 x2 = x*x;
return x*(1.0 + x2*(x2*0.2 - 0.333333333));
}
vec2 DistortCRTCoordinates(
// The original texture coordinate, intended to come straight from the full-render-target quad, in [-1..1] range (not
// standard [0..1])
vec2 texCoord,
// a [horizontal, vertical] distortion pair which describes the effective curvature of the virtual screen.
vec2 distortion)
{
if (distortion.x == 0 && distortion.y == 0)
{
return texCoord;
}
const float k_distance = 2.0;
const float k_minDistortion = 0.0001;
// We don't want to let the distortion drop below this minimum value because at 0, the whole technique falls apart.
// $TODO: It's definitely possible to handle distortion.x or y == 0 as a special case, but I didn't do it for now.
distortion = max(vec2(k_minDistortion, k_minDistortion), distortion);
// We're going to cast a ray from a virtual camera position p0 (0, 0, -k_distance) and collide it with a unit sphere.
// The horizontal and vertical spread of the rays is determined by the distortion values.
vec3 ray = vec3(texCoord * distortion, -k_distance);
// Get the squared length of the ray.
float rayLenSq = dot(ray, ray);
// we have an originating point p0 and a ray direction, so we can treat that as a parametric equation:
// p = p0 + r*t
// Since we have a non-translated unit sphere (1 = x^2 + y^2 + z^2) we can sub in our parametric values and solve for
// t as a quadratic. To simplify the math, we're going to use a slightly different form of quadratic equation:
// t^2 - 2b*t + c == 0
float b = (k_distance * k_distance) / rayLenSq;
float c = (k_distance * k_distance - 1.0) / rayLenSq;
// Given our "t^2 - 2b*t + c == 0" quadratic form, the quadratic formula simplifies to -b +/- sqrt(b^2 - c).
// Get the nearer of the two coordinates (and do a max inside the sqrt so we still get continuous values outside of
// the sphere, even if they're nonsensical, they'll get masked out by our mask value anyway).
float t = b - sqrt(max(0.0, b*b - c));
// Get our uv coordinates (Basically, latitude and longitude).
vec2 uv = ApproxAtan2(ray.xy * t, k_distance + ray.zz * t);
// maxUV could be calculated on the CPU and passed in for perf.
// Do the same calculation as above, but for two additional rays: the x, 0, z ray pointing all the way to the right,
// and the 0, y, z ray pointing at the bottom (Both rays are packed into the same "diagonal" ray value, but the
// lengths are calculated separately). This gets us the uv extents for the rays that we cast, which we can use to
// scale our output UVs.
vec2 maxUV;
{
vec3 maxRayDiagonal = vec3(distortion, -k_distance);
vec2 maxRayLenSq = vec2(dot(maxRayDiagonal.xz, maxRayDiagonal.xz), dot(maxRayDiagonal.yz, maxRayDiagonal.yz));
vec2 maxB = (k_distance * k_distance) / maxRayLenSq;
vec2 maxC = (k_distance * k_distance - 1.0) / maxRayLenSq;
vec2 maxT = maxB - sqrt(max(vec2(0.0, 0.0), maxB*maxB - maxC));
maxUV = ApproxAtan2(maxRayDiagonal.xy * maxT, k_distance + maxRayDiagonal.zz * maxT);
}
// Scale our UVs by the max value.
return uv / maxUV;
}

View file

@ -0,0 +1,207 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
#include "slang_params.inc"
// The size of the render target we are rendering to.
// NOTE: It is expected that width == 2 * height.
vec2 g_texSize = vec2(params.FinalViewportSize.y * 2.0, params.FinalViewportSize.y);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader generates an approximation of a CRT slot mask (the little R, G, and B dots that you can see on some CRTs
// when you are close to them.
// It is intended to be rendered to a texture that has a 2:1 width:height ratio, and we are generating two sets of RGB
// rounded rectangles, where the right-most set is offset 50% vertically (so it wraps from bottom to top).
vec4 aperture(vec2 inTexCoord)
{
uint texelIndex = uint(inTexCoord.x * g_texSize.x - 0.5);
float x = float(texelIndex) / g_texSize.x;
x = fract(x * 2.) * 3.;
// Default to "red zone"
vec3 color = vec3(1., 0., 0.);
if (x >= 2.)
{
// blue zone
color = vec3(0.,0.,1.);
}
else if (x >= 1.)
{
// green zone
color = vec3(0.,1.,0.);
}
// Convert our coordinate to be in [0..1/3rd]. Note that it was already multiplied by 3 above so this "frac" call
// gets us into the [0..1] value for this third.
x = fract(x);
x /= 3.;
// These are the positions of where the border of the colored rectangles will go in our third, as well as how round
// the edges will be.
float border = 1.0 / 12.0;
float smoothing = border / 3.0;
border -= smoothing;
// Do similar calculations to the slot mask texture generation, except without any y component since there are no
// breaks along the vertical axis in an aperture grille. This could likely be simplified, but I already had that
// logic and this is fine for something that is intended to run once ever (at startup).
x = abs(x - 1.0 / 6.0);
x -= 1.0 / 6.0 - (smoothing + border);
x /= smoothing;
x = max(0.0, x);
float delta = 1.0 / (g_texSize.x * smoothing);
float mul = saturate(1.0 - smoothstep(1.0 - delta * 0.5, 1.0 + delta * 0.5, x));
color *= mul;
return vec4(color, 1.);
}
vec4 slotmask(vec2 inTexCoord)
{
uvec2 texelIndex = uvec2(inTexCoord * g_texSize - 0.5);
vec2 t = vec2(texelIndex) / g_texSize.x * vec2(1.0, 2.0);
// Adjust our texture coordinates based on whether we're in the left half of the output or the right half. In each
// we'll end up with a x, y in [0..1] square that will contain the R, G, B rectangles.
if (t.x > 0.5)
{
// We're in the right-most half of the texture, so offset our y coordinate by 0.5 (and wrap it to within 0..1) so
// this set draws offset vertically.
t.y = fract(t.y + 0.5);
// Scale x to be within 0..1 where 0 is the middle of our texture (and the left edge of this rightmost square) and
// 1 is on the right edge of both.
t.x = t.x * 2.0 - 1.0;
}
else
{
// We are in the left half of our texture, so scale the x coordinate so that it is 1.0 at the middle of the texture
// (so in this leftmost square it goes from [0..1]). The y coordinate is unchanged.
t.x *= 2.0;
}
// Figure out which of the three "zones" we're in - R, G, or B - i.e. which horizontal third of the square we are in.
// We'll start by multiplying by 3 so that x in [0..1) is R, [1..2) is G, and [2..3] is B.
t.x *= 3.;
// Default to "red zone"
vec3 color = vec3(1., 0., 0.);
if (t.x >= 2.)
{
// blue zone
color = vec3(0.,0.,1.);
}
else if (t.x >= 1.)
{
// green zone
color = vec3(0.,1.,0.);
}
// Convert our coordinate to be in [0..1/3rd]. Note that it was already multiplied by 3 above so this "frac" call
// gets us into the [0..1] value for this third.
t.x = fract(t.x);
t.x /= 3;
// These are the positions of where the border of the colored rectangles will go in our third, as well as how round
// the edges will be.
vec2 border = vec2(1.0/3.0 * (1.0 / 4.0), 1.0/6.0);
float rounding = border.x / 3.0;
border -= rounding;
// Center the rectangle so we can do a distance calculation from center to find the bounds of the rectangle, using
// that to make a final [0..1] multiply value to scale our chosen rectangle color down to black out the areas
// outside of the colored portion.
t -= vec2(1.0 / 6.0, 0.5);
t = abs(t);
t -= (vec2(1.0 / 6.0, 0.5)) - (rounding + border);
t /= rounding;
t = max(vec2(0.0, 0.0), t);
float distance = length(t);
float delta = 1.0 / (g_texSize.x * rounding);
float mul = saturate(1.0 - smoothstep(1.0 - delta * 0.5, 1.0 + delta * 0.5, distance));
color *= mul;
return vec4(color, 1.0);
}
vec4 shadowmask(vec2 inTexCoord)
{
// changing this from 6.,4. to make circles instead of ovals -HK
vec2 hexGridF = inTexCoord * vec2(6.0, 6.0);
bool isOddBlock = (fract(hexGridF.y * 0.5) >= 0.5);
if (isOddBlock)
{
hexGridF.x += .5;
}
ivec2 hexID = ivec2(floor(hexGridF));
vec2 cellCoord = fract(hexGridF);
const float t = 0.5773502691; // tan(30 degrees)
const float c = 0.5 * t;
if (cellCoord.y < (-t * cellCoord.x) + c)
{
hexID.y--;
hexGridF.x += isOddBlock ? -0.5 : 0.5;
hexID.x -= int(isOddBlock);
}
else if (cellCoord.y < (t * cellCoord.x) - c)
{
hexID.y--;
hexGridF.x += isOddBlock ? -0.5 : 0.5;
hexID.x += int(!isOddBlock);
}
if (fract(float(hexID.y) *0.5) >= 0.5)
{
hexGridF.x ++;
hexID.x ++;
}
vec3 color;
uint hx = uint(hexID.x + 1);
if ((hx % 3U) == 0U)
{
color = vec3(1., 0., 0.);
}
else if ((hx % 3U) == 1U)
{
color = vec3(0., 1., 0.);
}
else
{
color = vec3(0., 0., 1.);
}
cellCoord = ((hexGridF - vec2(hexID)) * vec2(1.0, 1.0 / (1.0 + c)));
cellCoord = cellCoord * 2. - 1.;
float dist = length(cellCoord);
dist = 1.0 - smoothstep(0.75, 0.8, dist);
// color *= lerp(0.1, 1.0, float(hexID.y % 2));
return vec4(color * dist, 1.);
}
void main()
{
if(int(global.cat_mask_picker) == 1) FragColor = aperture(fract(vTexCoord * (params.FinalViewportSize.xy / 6.) / global.mask_scale));
else if(int(global.cat_mask_picker) == 2) FragColor = slotmask(fract(vTexCoord * (params.FinalViewportSize.xy / 6.) / global.mask_scale));
else if(int(global.cat_mask_picker) == 3) FragColor = shadowmask(fract(vTexCoord * (params.FinalViewportSize.xy / 6.) / global.mask_scale));
// we compensate for brightness later, so better darken up the mask-less image -HK
else FragColor = vec4(0.5);
}

View file

@ -0,0 +1,208 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
#include "slang_params.inc"
#define inTexCoord vTexCoord
float animate = 1.0 + mod(params.FrameCount, 46375.0) * global.anim_noise;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader generates the Screen Texture, which is effectively overlayed on top of the actual screen image in the
// RGBToCRT shader. This includes the emulation of the shadow mask (or slot mask, aperture grill) of a CRT screen, as
// well as masking around the edges to handle blacking out values that are outside of the visible screen.
#include "cathode-retro-crt-distort-coordinates.inc"
#include "cathode-retro-util-noise.inc"
// The mask texture for the CRT. That is, if you think of an old CRT and how you could see the
// R, G, and B dots, this is that texture. Needs to be set up to wrap, as well as to have mip mapping (and, ideally,
// anisotropic filtering too, if rendering as a curved screen, to help with sharpness and aliasing).
//DECLARE_TEXTURE2D(g_maskTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D g_maskTexture;
// This is a 64-tap poisson disc filter, found here:
// https://www.geeks3d.com/20100628/3d-programming-ready-to-use-64-sample-poisson-disc/
const int k_samplePointCount = 64;
const vec2 k_samplingPattern[k_samplePointCount] = vec2[k_samplePointCount](
vec2(-0.613392, 0.617481),
vec2(0.170019, -0.040254),
vec2(-0.299417, 0.791925),
vec2(0.645680, 0.493210),
vec2(-0.651784, 0.717887),
vec2(0.421003, 0.027070),
vec2(-0.817194, -0.271096),
vec2(-0.705374, -0.668203),
vec2(0.977050, -0.108615),
vec2(0.063326, 0.142369),
vec2(0.203528, 0.214331),
vec2(-0.667531, 0.326090),
vec2(-0.098422, -0.295755),
vec2(-0.885922, 0.215369),
vec2(0.566637, 0.605213),
vec2(0.039766, -0.396100),
vec2(0.751946, 0.453352),
vec2(0.078707, -0.715323),
vec2(-0.075838, -0.529344),
vec2(0.724479, -0.580798),
vec2(0.222999, -0.215125),
vec2(-0.467574, -0.405438),
vec2(-0.248268, -0.814753),
vec2(0.354411, -0.887570),
vec2(0.175817, 0.382366),
vec2(0.487472, -0.063082),
vec2(-0.084078, 0.898312),
vec2(0.488876, -0.783441),
vec2(0.470016, 0.217933),
vec2(-0.696890, -0.549791),
vec2(-0.149693, 0.605762),
vec2(0.034211, 0.979980),
vec2(0.503098, -0.308878),
vec2(-0.016205, -0.872921),
vec2(0.385784, -0.393902),
vec2(-0.146886, -0.859249),
vec2(0.643361, 0.164098),
vec2(0.634388, -0.049471),
vec2(-0.688894, 0.007843),
vec2(0.464034, -0.188818),
vec2(-0.440840, 0.137486),
vec2(0.364483, 0.511704),
vec2(0.034028, 0.325968),
vec2(0.099094, -0.308023),
vec2(0.693960, -0.366253),
vec2(0.678884, -0.204688),
vec2(0.001801, 0.780328),
vec2(0.145177, -0.898984),
vec2(0.062655, -0.611866),
vec2(0.315226, -0.604297),
vec2(-0.780145, 0.486251),
vec2(-0.371868, 0.882138),
vec2(0.200476, 0.494430),
vec2(-0.494552, -0.711051),
vec2(0.612476, 0.705252),
vec2(-0.578845, -0.768792),
vec2(-0.772454, -0.090976),
vec2(0.504440, 0.372295),
vec2(0.155736, 0.065157),
vec2(0.391522, 0.849605),
vec2(-0.620106, -0.328104),
vec2(0.789239, -0.419965),
vec2(-0.545396, 0.538133),
vec2(-0.178564, -0.596057)
);
// $NOTE: The first four values here are the same as the first four in RGBToCRT.hlsl, and are expected to match.
// This shader is intended to render a screen of the correct shape regardless of the output render target shape,
// effectively letterboxing or pillarboxing as needed(i.e. rendering a 4:3 screen to a 16:9 render target).
// g_viewScale is the scale value necessary to get the resulting screen scale correct. In the event the output
// render target is wider than the intended screen, the screen needs to be scaled down horizontally to pillarbox,
// usually like (where screenAspectRatio is crtScreenWidth / crtScreenHeight):
// (x: (renderTargetWidth / renderTargetHeight) * (1.0 / screenAspectRatio), y: 1.0)
// if the output render target is taller than the intended screen, it will end up letterboxed using something like:
// (x: 1.0, y: (renderTargetHeight / renderTargetWidth) * screenAspectRatio)
// Note that if overscan (where the edges of the screen cover up some of the picture) is being emulated, it
// potentially needs to be taken into account in this value too. See RGBToCRT.h for details if that's the case.
vec2 g_viewScale = vec2(1.0);
// If overscan emulation is intended (where the edges of the screen cover up some of the picture), then this is the
// amount of signal texture scaling needed to account for that. Given an overscan value "overscanAmount" that's
// (overscanLeft + overscanRight, overscanTop + overscanBottom)
// this value should end up being:
// (inputImageSize.xy - overscanAmount.xy) / inputImageSize.xy
vec2 g_overscanScale = vec2(1.0);
// This is the texture coordinate offset to adjust for overscan. Because the input coordinates are [-1..1] instead
// of [0..1], this is the offset needed to recenter the value. Given an "overscanDifference" value:
// (overscanLeft - overscanRight, overscanTop - overscanBottom)
// this value should be:
// overscanDifference.xy/ inputImageSize.xy * 0.5
vec2 g_overscanOffset = vec2(0.);
// The amount along each axis to apply the virtual-curved screen distortion. Usually a value in [0..1]. "0" indicates
// no curvature (a flat screen) and "1" indicates "quite curved"
vec2 g_distortion = vec2(params.warpX, params.warpY);
// The distortion amount used for where the edges of the screen mask should be. Should be at least g_distortion, but
// if emulating an older TV which had some additional bevel curvature that cut off part of the picture, this can be
// applied by adding additional value to this.
// dunno what this does yet. revisit it when the mask tiling is working -HK
vec2 g_maskDistortion = vec2(0., 0.);
// The scale of the mask texture. Higher value means the coordinates are scaled more and, thus, the shadow mask is
// smaller.
vec2 g_maskScale = vec2(global.mask_scale);
// The dimensions of the source texture (used for aspect correction
float g_aspect = 1.0;
// How much to round the corners of the screen to emulate a TV with rounded corners that cut off a little picture.
// 0 indicates no rounding (squared-off corners), while a value of 1.0 means "the whole screen is an oval."
// Values <= 0.2 are recommended.
float g_roundedCornerSize = params.corner;
void main()
{
uvec2 pixelCoord8k = uvec2(round(inTexCoord * vec2(7680, 4320)));
// First thing we want to do is scale our input texture coordinate to be in [-1..1] instead of [0..1] and adjust for
vec2 scaledTexCoord = (inTexCoord * 2. - 1.) * g_viewScale;
// Distort these coordinates to get the -1..1 screen area:
vec2 t = DistortCRTCoordinates(scaledTexCoord, g_distortion);
// Calculate a separate set of distorted coordinates, this for the outer mask (which determines the masking off of extra-rounded screen
// edges).
vec2 maskT = DistortCRTCoordinates(t, g_maskDistortion.yx);
// Adjust for overscan
t = t * g_overscanScale + g_overscanOffset * 2.0;
// Calculate the signed distance to the edge of the "screen" taking the rounded corners into account. This will be used to generate the
// mask around the edges of the "screen" where we draw a dark color.
float edgeDist;
{
// Get our coordinate as just an "upper quadrant" coordinate, scaled to have the correct display aspect ratio.
vec2 sq = vec2(g_aspect, 1.0) / max(1.0, g_aspect);
vec2 upperQuadrantT = abs(maskT);
// Get the signed distance to a rounded rectangle to get our corners.
vec2 q = upperQuadrantT * sq - sq + g_roundedCornerSize;
edgeDist = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - g_roundedCornerSize;
}
// Use our signed distance to edge of screen along with the ddx/ddy of our mask coordinate to generate a nicely-antialiased mask where
// 0 means "fully outside of the screen" and 1 means "fully on-screen".
float maskAlpha = 1.0 - smoothstep(-length(ddx(maskT) + ddy(maskT)), 0.0, edgeDist);
if (max(abs(scaledTexCoord.x), abs(scaledTexCoord.y)) > 1.1)
{
maskAlpha = 0.0;
}
// Now supersample the mask texture with a mip-map bias to make it sharper. The supersampling will help counteract the bias and
// give us a sharp mask with minimal-to-no aliasing.
float angle = Noise2D(t * 1000., 10.0 * animate) * 6.28318531;
vec2 rotX = vec2(sin(angle), cos(angle)) * 1.414;
vec2 rotY = vec2(-rotX.y, rotX.x);
vec2 dxT = vec2(dot(rotX, ddx(t)), dot(rotY, ddx(t)));
vec2 dyT = vec2(dot(rotX, ddy(t)), dot(rotY, ddy(t)));
vec3 color = vec3(0., 0., 0.);
for (int i = 0; i < k_samplePointCount; i++)
{
color += texture(g_maskTexture, 0.5 * (t + k_samplingPattern[i].x * dxT + k_samplingPattern[i].y * dyT) * 1.0 + 0.5, -2.0).rgb;
}
color /= float(k_samplePointCount);
// Our final texture contains the rgb value from the mask, as well as the mask value in the alpha channel.
// Note that the color channel has not been premultiplied with the mask.
FragColor = vec4(color, maskAlpha);
}

View file

@ -0,0 +1,243 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
#include "slang_params.inc"
#define inTexCoord vTexCoord
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader combines the current frame, the previous frame, screen mask, and diffusion into the final render.
// It's a relatively complex shader, and certainly if there are features that aren't needed for the current render it
// could be simplified.
// $TODO: May want to do something like an ubershader version that does exactly the minimum amount of work based on how
// many features are actually in use (pulling out the distortion or, more importantly, the sampling of the previous/
// diffusion textures when they're unneeded).
#include "cathode-retro-crt-distort-coordinates.inc"
// This is the RGB current frame texture - the output of the NTSC decode shaders if decoding was needed.
// This sampler should be set up with linear texture sampling and should be set to clamp (no wrapping).
//DECLARE_TEXTURE2D(g_currentFrameTexture, g_currentFrameSampler);
layout(set = 0, binding = 2) uniform sampler2D g_currentFrameTexture;
// This is the previous frame's texture (i.e. last frame's g_currentFrameTexture).
// This sampler should be set up with linear texture sampling and should be set to clamp (no wrapping).
//DECLARE_TEXTURE2D(g_previousFrameTexture, g_previousFrameSampler);
layout(set = 0, binding = 3) uniform sampler2D PassFeedback14;
#define g_previousFrameTexture PassFeedback14
// This texture is the output of the GenerateScreenTexture shader, containing the (scaled, tiled, and antialiased) mask
// texture in the rgb channels and the edge-of-screen mask value in the alpha channel. It is expected to have been
// generated at our output resolution (i.e. it's 1:1 pixels with our output render target)
// This sampler should be set up with linear texture sampling and should be set to clamp (no wrapping).
//DECLARE_TEXTURE2D(g_screenMaskTexture, g_screenMaskSampler);
layout(set = 0, binding = 4) uniform sampler2D g_screenMaskTexture;
// This texture contains a tonemapped/blurred version of the input texture, to emulate the diffusion of the light from
// the phosphors through the glass on the front of a CRT screen.
// This sampler should be set up with linear texture sampling and should be set to clamp (no wrapping).
//DECLARE_TEXTURE2D(g_diffusionTexture, g_diffusionSampler);
layout(set = 0, binding = 5) uniform sampler2D g_diffusionTexture;
// This shader is intended to render a screen of the correct shape regardless of the output render target shape,
// effectively letterboxing or pillarboxing as needed(i.e. rendering a 4:3 screen to a 16:9 render target).
// g_viewScale is the scale value necessary to get the resulting screen scale correct. In the event the output
// render target is wider than the intended screen, the screen needs to be scaled down horizontally to pillarbox,
// usually like (where screenAspectRatio is crtScreenWidth / crtScreenHeight):
// (x: (renderTargetWidth / renderTargetHeight) * (1.0 / screenAspectRatio), y: 1.0)
// if the output render target is taller than the intended screen, it will end up letterboxed using something like:
// (x: 1.0, y: (renderTargetHeight / renderTargetWidth) * screenAspectRatio)
// Note that if overscan (where the edges of the screen cover up some of the picture) is being emulated, it
// potentially needs to be taken into account in this value too. See RGBToCRT.h for details if that's the case.
vec2 g_viewScale = vec2(1.0);
// If overscan emulation is intended (where the edges of the screen cover up some of the picture), then this is the
// amount of signal texture scaling needed to account for that. Given an overscan value "overscanAmount" that's
// (overscanLeft + overscanRight, overscanTop + overscanBottom)
// this value should end up being:
// (inputImageSize.xy - overscanAmount.xy) / inputImageSize.xy
vec2 g_overscanScale = vec2(1.0);
// This is the texture coordinate offset to adjust for overscan. Because the input coordinates are [-1..1] instead
// of [0..1], this is the offset needed to recenter the value. Given an "overscanDifference" value:
// (overscanLeft - overscanRight, overscanTop - overscanBottom)
// this value should be:
// overscanDifference.xy/ inputImageSize.xy * 0.5
vec2 g_overscanOffset = vec2(0.);
// The amount along each axis to apply the virtual-curved screen distortion. Usually a value in [0..1]. "0" indicates
// no curvature (a flat screen) and "1" indicates "quite curved"
vec2 g_distortion = vec2(params.warpX, params.warpY);
// The RGBA color of the area around the screen.
vec4 g_backgroundColor = vec4(0.0);
// How much of the previous frame's brightness to keep. 0 means "we don't use the previous frame at all" and 1 means
// "the previous frame is at full brightness". In many CRTs, the phosphor persistence is short enough that it would
// be effectively 0 at 50-60fps (As a CRT's phospors could potentially be completely faded out by then). However,
// for some cases (for instance, interlaced video or for actual NES/SNES/probably other console output) it is
// generally preferable to turn on a little bit of persistance to lessen temporal flickering on an LCD screen as it
// can tend to look bad depending on the panel (seriously, check out https://www.youtube.com/watch?v=kA8CIY0DeS8
// which is what my LCD panel was doing *after* the flickering interlace test truck I had had been gone for 10
// minutes)
float g_phosphorPersistence = global.persistence;
// How many scanlines there are in this field of the input (where a field is either the even or odd scanlines of an
// interlaced frame, or the entirety of a progressive-scan frame)
float g_scanlineCount = (params.OriginalSize.y < 400.) ? params.OriginalSize.y : params.OriginalSize.y * 0.5;
// The strength of the separation between scanlines. 0 means "no scanline separation at all" and 1 means "separate
// the scanlines as much as possible" - on high-enough resolution output render target (at 4k for sure) "1" means
// "fully black between scanlines", but to reduce aliasing that amount of separation will diminish at lower output
// resolution.
float g_scanlineStrength = global.scan_intens;
// This is the scanline-space coordinate offset to use to adjust our texture coordinate's y value based on whether
// this is a (1-based) odd frame or an even frame. It will be 0.5 (shifting the texture up half a scanline) if it's
// an odd frame and -0.5 (shifting the texture down half a scanline) if it's an even frame.
float g_curEvenOddTexelOffset = 0.5;
// Same as above, but it's the even/odd texel offset that was relevant for the previous frame (so we can blend it in
// at the proper spot). This should match g_curEvenOddTexelOffset for a progressive-scan signal and should be
// "-g_curEvenOddTexelOffset" if interlaced.
float g_prevEvenOddTexelOffset = -0.5;
// This is how much diffusion to apply, blending in the diffusion texture which is an emulation of the light from the
// screen scattering in the glass on the front of the CRT - 0 means "no diffusion" and 1 means "a whole lot of
// diffusion".
float g_diffusionStrength = global.diffusion;
// How much we want to blend in the mask. 0 means "mask is not visible" and 1 means "mask is fully visible"
float g_maskStrength = global.mask_intens;
// The darkness of the darkest part of the mask. 0 means the area between the "dots" is black, 0.9 means the spaces
// between are nearly white.
float g_maskDepth = global.mask_depth;
const float pi = 3.141592653;
void main()
{
// The screen texture is 1:1 with the output render target so sample it directly off of the input texture coordinates
vec4 screenMask = texture(g_screenMaskTexture, inTexCoord);
// Now distort the texture coordinates to get our texture into the correct space for display.
vec2 t = DistortCRTCoordinates((inTexCoord * 2. - 1.) * g_viewScale, g_distortion) * g_overscanScale
+ g_overscanOffset * 2.0;
// Use "t" (before we do the even/odd update or the scanline-sharpening) to load our diffusion texture, which is an
// approximation of the glass in front of the phosphors scattering light a little bit due to imperfections.
vec3 diffusionColor = texture(g_diffusionTexture, t * 0.5 + 0.5).rgb;
// Offset based on whether we're an even or odd frame
t.y += g_curEvenOddTexelOffset / g_scanlineCount;
// Before we adjust the y coordinate to sharpen the scanline interpolation, grab our scanline-space y coordinate.
float scanlineSpaceY = t.y * g_scanlineCount + g_scanlineCount;
// Because t.y is currently in [-1, 1], this derivative multiplied by the scanline count ends up being the number of
// total scanlines involved (including the empty ones). So this is "how much along y does one output pixel move us
// relative to g_scanlineCount*2"
float pixelLengthInScanlineSpace = length(ddy(t)) * g_scanlineCount;
// Do a little magic to sharpen up the interpolation between scanlines - a CRT (didn't really have any vertical
// smoothing, so we want to make the centers of our texels a little more solid and do less bilinear blending
// vertically (just a little to simulate the softness of the screen in general)
{
float scanlineIndex = (t.y * 0.5 + 0.5) * g_scanlineCount;
float scanlineFrac = fract(scanlineIndex);
scanlineIndex -= scanlineFrac;
scanlineFrac -= 0.5;
float signFrac = sign(scanlineFrac);
float ySharpening = 0.1; // Any value from [0, 0.5) should work here, larger means the vertical pixels are sharper
scanlineFrac = sign(scanlineFrac) * saturate(abs(scanlineFrac) - ySharpening) * 0.5 / (0.5 - ySharpening);
scanlineIndex += scanlineFrac + 0.5;
t.y = float(scanlineIndex) / g_scanlineCount * 2. - 1.;
}
// Sample the actual display texture and add in the previous frame (For phosphor persistence)
vec3 sourceColor;
{
t = t * 0.5 + 0.5; // t has been in -1..1 range this whole time, scale it to 0..1 for sampling.
sourceColor = texture(g_currentFrameTexture, t).rgb;
// Reduce the influence of the scanlines as we get small enough that aliasing is unavoidable (fully fading out at
// 0.7x nyquist - early to ensure that we don't introduce any aliasing as we get too close).
float scanlineStrength = lerp(
g_scanlineStrength,
0.,
smoothstep(1.0, 1.4, length(ddy(inTexCoord)) * g_scanlineCount * 2.));
// $TODO: We may want to find a way to precalculate this scanline value as a texture (like the screen texture).
// Unfortunately the screen texture is already using all 4 of its components so we'd need a new one, which is why
// I didn't).
float scanline;
{
// For the actual scanline value, we use the following equation:
// cos(scanlineSpaceY * pi) * 0.5 + 0.5
// That is, at scanline centers it's either 0 or 1. However, to avoid moir<69> patterns we actually want to
// supersample it. The good news is, we can supersample over a range using numeric integration since it's a
// sinusoid. The integration of that wave between y coordinates ya and yb ends up being:
// (yb - ya)/2 + 1/(2pi) * (sin(pi*ya) - sin(pi*yb)))
// but in order to turn it into an average we need to divide that result by the width of the range, which is
// (yb - ya).
// As pixelLengthInScanlineSpace gets larger (i.e. effective output resolution gets smaller) we want to ramp up
// the blurring dramatically to avoid moir<69> effects. There's no real mathematical basis for this algorithm, I
// just eyeballed a curve until I got something that looked good at 1080p and up and introduced minimal moir<69>
// (minimal meaning "it's not visible when the mask is also enabled").
float scale = pow(abs(pixelLengthInScanlineSpace), 2.6) * 7;
float ya = scanlineSpaceY - scale;
float yb = scanlineSpaceY + scale;
scanline = (0.5 * (yb - ya) + 1.0 / (2. * pi) * (sin(pi * ya) - sin(pi * yb))) / (2. * scale);
// Now multiply in the scanline-spacing darkening according to the scanline strength.
sourceColor *= lerp(1. - scanlineStrength, 1.0, scanline);
}
vec2 prevT = t;
float prevScanline = scanline;
if (g_prevEvenOddTexelOffset != g_curEvenOddTexelOffset)
{
// We have a different scanline parity in the previous frame so we need to offset our texture coordinate (to put
// the prev frame's scanline center at the correct spot) and then invert our scanline multiplier (to darken the
// alternate scanlines)
prevT.y += g_prevEvenOddTexelOffset / g_scanlineCount;
prevScanline = 1. - prevScanline;
}
// Sample the previous texture and darken the area between scanlines accordingly.
vec3 prevSourceColor = texture(g_previousFrameTexture, prevT).rgb;
prevSourceColor *= lerp(1. - scanlineStrength, 1.0, prevScanline);
// Blend our previous frame into the current one based on how much phosphor persistence we have between frames.
sourceColor = max(prevSourceColor * g_phosphorPersistence, sourceColor);
// We want to adjust the brightness to somewhat compensate for the darkening due to scanlines
sourceColor /= 1.0 - scanlineStrength * 0.5;
}
// Time to put it all together: first, by applying the screen mask (i.e. the shadow mask/aperture grill, etc)...
// $TODO: Figure out a proper scaling factor here - the 3.0 is meant to adjust for the fact that the mask cuts out
// approximately 2/3rds of the brightness, but it's not exact, we could calculate this, I just haven't.
screenMask.rgb = screenMask.rgb * ((2.0 + global.cat_mask_picker) - g_maskDepth) + g_maskDepth;
vec3 result = sourceColor * lerp(vec3(1.,1.,1.), screenMask.rgb, g_maskStrength);
// ... then bringing in some diffusion on top (This isn't physically accurate (it should really be a lerp between res
// and diffusionColor) but doing it this way preserves the brightness and still looks reasonable, especially when
// displaying bright things on a dark background)
result = max(diffusionColor * g_diffusionStrength, result);
// Finally, mask out everything outside of the edges to get our final output value.
FragColor = lerp(g_backgroundColor, vec4(result, 1.), screenMask.a);
}

View file

@ -0,0 +1,245 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
#include "slang_params.inc"
#define inTexCoord vTexCoord
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader combines the current frame, the previous frame, screen mask, and diffusion into the final render.
// It's a relatively complex shader, and certainly if there are features that aren't needed for the current render it
// could be simplified.
// $TODO: May want to do something like an ubershader version that does exactly the minimum amount of work based on how
// many features are actually in use (pulling out the distortion or, more importantly, the sampling of the previous/
// diffusion textures when they're unneeded).
#include "cathode-retro-crt-distort-coordinates.inc"
// This is the RGB current frame texture - the output of the NTSC decode shaders if decoding was needed.
// This sampler should be set up with linear texture sampling and should be set to clamp (no wrapping).
//DECLARE_TEXTURE2D(g_currentFrameTexture, g_currentFrameSampler);
layout(set = 0, binding = 2) uniform sampler2D g_sourceTexture;
// bypassing the signal generation temporarily just to get the CRT stuff working first
#define g_currentFrameTexture g_sourceTexture
// This is the previous frame's texture (i.e. last frame's g_currentFrameTexture).
// This sampler should be set up with linear texture sampling and should be set to clamp (no wrapping).
//DECLARE_TEXTURE2D(g_previousFrameTexture, g_previousFrameSampler);
layout(set = 0, binding = 3) uniform sampler2D PassFeedback0;
#define g_previousFrameTexture PassFeedback0
// This texture is the output of the GenerateScreenTexture shader, containing the (scaled, tiled, and antialiased) mask
// texture in the rgb channels and the edge-of-screen mask value in the alpha channel. It is expected to have been
// generated at our output resolution (i.e. it's 1:1 pixels with our output render target)
// This sampler should be set up with linear texture sampling and should be set to clamp (no wrapping).
//DECLARE_TEXTURE2D(g_screenMaskTexture, g_screenMaskSampler);
layout(set = 0, binding = 4) uniform sampler2D g_screenMaskTexture;
// This texture contains a tonemapped/blurred version of the input texture, to emulate the diffusion of the light from
// the phosphors through the glass on the front of a CRT screen.
// This sampler should be set up with linear texture sampling and should be set to clamp (no wrapping).
//DECLARE_TEXTURE2D(g_diffusionTexture, g_diffusionSampler);
layout(set = 0, binding = 5) uniform sampler2D g_diffusionTexture;
// This shader is intended to render a screen of the correct shape regardless of the output render target shape,
// effectively letterboxing or pillarboxing as needed(i.e. rendering a 4:3 screen to a 16:9 render target).
// g_viewScale is the scale value necessary to get the resulting screen scale correct. In the event the output
// render target is wider than the intended screen, the screen needs to be scaled down horizontally to pillarbox,
// usually like (where screenAspectRatio is crtScreenWidth / crtScreenHeight):
// (x: (renderTargetWidth / renderTargetHeight) * (1.0 / screenAspectRatio), y: 1.0)
// if the output render target is taller than the intended screen, it will end up letterboxed using something like:
// (x: 1.0, y: (renderTargetHeight / renderTargetWidth) * screenAspectRatio)
// Note that if overscan (where the edges of the screen cover up some of the picture) is being emulated, it
// potentially needs to be taken into account in this value too. See RGBToCRT.h for details if that's the case.
vec2 g_viewScale = vec2(1.0);
// If overscan emulation is intended (where the edges of the screen cover up some of the picture), then this is the
// amount of signal texture scaling needed to account for that. Given an overscan value "overscanAmount" that's
// (overscanLeft + overscanRight, overscanTop + overscanBottom)
// this value should end up being:
// (inputImageSize.xy - overscanAmount.xy) / inputImageSize.xy
vec2 g_overscanScale = vec2(1.0);
// This is the texture coordinate offset to adjust for overscan. Because the input coordinates are [-1..1] instead
// of [0..1], this is the offset needed to recenter the value. Given an "overscanDifference" value:
// (overscanLeft - overscanRight, overscanTop - overscanBottom)
// this value should be:
// overscanDifference.xy/ inputImageSize.xy * 0.5
vec2 g_overscanOffset = vec2(0.);
// The amount along each axis to apply the virtual-curved screen distortion. Usually a value in [0..1]. "0" indicates
// no curvature (a flat screen) and "1" indicates "quite curved"
vec2 g_distortion = vec2(params.warpX, params.warpY);
// The RGBA color of the area around the screen.
vec4 g_backgroundColor = vec4(0.0);
// How much of the previous frame's brightness to keep. 0 means "we don't use the previous frame at all" and 1 means
// "the previous frame is at full brightness". In many CRTs, the phosphor persistence is short enough that it would
// be effectively 0 at 50-60fps (As a CRT's phospors could potentially be completely faded out by then). However,
// for some cases (for instance, interlaced video or for actual NES/SNES/probably other console output) it is
// generally preferable to turn on a little bit of persistance to lessen temporal flickering on an LCD screen as it
// can tend to look bad depending on the panel (seriously, check out https://www.youtube.com/watch?v=kA8CIY0DeS8
// which is what my LCD panel was doing *after* the flickering interlace test truck I had had been gone for 10
// minutes)
float g_phosphorPersistence = global.persistence;
// How many scanlines there are in this field of the input (where a field is either the even or odd scanlines of an
// interlaced frame, or the entirety of a progressive-scan frame)
float g_scanlineCount = (params.OriginalSize.y < 400.) ? params.OriginalSize.y : params.OriginalSize.y * 0.5;
// The strength of the separation between scanlines. 0 means "no scanline separation at all" and 1 means "separate
// the scanlines as much as possible" - on high-enough resolution output render target (at 4k for sure) "1" means
// "fully black between scanlines", but to reduce aliasing that amount of separation will diminish at lower output
// resolution.
float g_scanlineStrength = global.scan_intens;
// This is the scanline-space coordinate offset to use to adjust our texture coordinate's y value based on whether
// this is a (1-based) odd frame or an even frame. It will be 0.5 (shifting the texture up half a scanline) if it's
// an odd frame and -0.5 (shifting the texture down half a scanline) if it's an even frame.
float g_curEvenOddTexelOffset = 0.5;
// Same as above, but it's the even/odd texel offset that was relevant for the previous frame (so we can blend it in
// at the proper spot). This should match g_curEvenOddTexelOffset for a progressive-scan signal and should be
// "-g_curEvenOddTexelOffset" if interlaced.
float g_prevEvenOddTexelOffset = -0.5;
// This is how much diffusion to apply, blending in the diffusion texture which is an emulation of the light from the
// screen scattering in the glass on the front of the CRT - 0 means "no diffusion" and 1 means "a whole lot of
// diffusion".
float g_diffusionStrength = global.diffusion;
// How much we want to blend in the mask. 0 means "mask is not visible" and 1 means "mask is fully visible"
float g_maskStrength = global.mask_intens;
// The darkness of the darkest part of the mask. 0 means the area between the "dots" is black, 0.9 means the spaces
// between are nearly white.
float g_maskDepth = global.mask_depth;
const float pi = 3.141592653;
void main()
{
// The screen texture is 1:1 with the output render target so sample it directly off of the input texture coordinates
vec4 screenMask = texture(g_screenMaskTexture, inTexCoord);
// Now distort the texture coordinates to get our texture into the correct space for display.
vec2 t = DistortCRTCoordinates((inTexCoord * 2. - 1.) * g_viewScale, g_distortion) * g_overscanScale
+ g_overscanOffset * 2.0;
// Use "t" (before we do the even/odd update or the scanline-sharpening) to load our diffusion texture, which is an
// approximation of the glass in front of the phosphors scattering light a little bit due to imperfections.
vec3 diffusionColor = texture(g_diffusionTexture, t * 0.5 + 0.5).rgb;
// Offset based on whether we're an even or odd frame
t.y += g_curEvenOddTexelOffset / g_scanlineCount;
// Before we adjust the y coordinate to sharpen the scanline interpolation, grab our scanline-space y coordinate.
float scanlineSpaceY = t.y * g_scanlineCount + g_scanlineCount;
// Because t.y is currently in [-1, 1], this derivative multiplied by the scanline count ends up being the number of
// total scanlines involved (including the empty ones). So this is "how much along y does one output pixel move us
// relative to g_scanlineCount*2"
float pixelLengthInScanlineSpace = length(ddy(t)) * g_scanlineCount;
// Do a little magic to sharpen up the interpolation between scanlines - a CRT (didn't really have any vertical
// smoothing, so we want to make the centers of our texels a little more solid and do less bilinear blending
// vertically (just a little to simulate the softness of the screen in general)
{
float scanlineIndex = (t.y * 0.5 + 0.5) * g_scanlineCount;
float scanlineFrac = fract(scanlineIndex);
scanlineIndex -= scanlineFrac;
scanlineFrac -= 0.5;
float signFrac = sign(scanlineFrac);
float ySharpening = 0.1; // Any value from [0, 0.5) should work here, larger means the vertical pixels are sharper
scanlineFrac = sign(scanlineFrac) * saturate(abs(scanlineFrac) - ySharpening) * 0.5 / (0.5 - ySharpening);
scanlineIndex += scanlineFrac + 0.5;
t.y = float(scanlineIndex) / g_scanlineCount * 2. - 1.;
}
// Sample the actual display texture and add in the previous frame (For phosphor persistence)
vec3 sourceColor;
{
t = t * 0.5 + 0.5; // t has been in -1..1 range this whole time, scale it to 0..1 for sampling.
sourceColor = texture(g_currentFrameTexture, t).rgb;
// Reduce the influence of the scanlines as we get small enough that aliasing is unavoidable (fully fading out at
// 0.7x nyquist - early to ensure that we don't introduce any aliasing as we get too close).
float scanlineStrength = lerp(
g_scanlineStrength,
0.,
smoothstep(1.0, 1.4, length(ddy(inTexCoord)) * g_scanlineCount * 2.));
// $TODO: We may want to find a way to precalculate this scanline value as a texture (like the screen texture).
// Unfortunately the screen texture is already using all 4 of its components so we'd need a new one, which is why
// I didn't).
float scanline;
{
// For the actual scanline value, we use the following equation:
// cos(scanlineSpaceY * pi) * 0.5 + 0.5
// That is, at scanline centers it's either 0 or 1. However, to avoid moir<69> patterns we actually want to
// supersample it. The good news is, we can supersample over a range using numeric integration since it's a
// sinusoid. The integration of that wave between y coordinates ya and yb ends up being:
// (yb - ya)/2 + 1/(2pi) * (sin(pi*ya) - sin(pi*yb)))
// but in order to turn it into an average we need to divide that result by the width of the range, which is
// (yb - ya).
// As pixelLengthInScanlineSpace gets larger (i.e. effective output resolution gets smaller) we want to ramp up
// the blurring dramatically to avoid moir<69> effects. There's no real mathematical basis for this algorithm, I
// just eyeballed a curve until I got something that looked good at 1080p and up and introduced minimal moir<69>
// (minimal meaning "it's not visible when the mask is also enabled").
float scale = pow(abs(pixelLengthInScanlineSpace), 2.6) * 7;
float ya = scanlineSpaceY - scale;
float yb = scanlineSpaceY + scale;
scanline = (0.5 * (yb - ya) + 1.0 / (2. * pi) * (sin(pi * ya) - sin(pi * yb))) / (2. * scale);
// Now multiply in the scanline-spacing darkening according to the scanline strength.
sourceColor *= lerp(1. - scanlineStrength, 1.0, scanline);
}
vec2 prevT = t;
float prevScanline = scanline;
if (g_prevEvenOddTexelOffset != g_curEvenOddTexelOffset)
{
// We have a different scanline parity in the previous frame so we need to offset our texture coordinate (to put
// the prev frame's scanline center at the correct spot) and then invert our scanline multiplier (to darken the
// alternate scanlines)
prevT.y += g_prevEvenOddTexelOffset / g_scanlineCount;
prevScanline = 1. - prevScanline;
}
// Sample the previous texture and darken the area between scanlines accordingly.
vec3 prevSourceColor = texture(g_previousFrameTexture, prevT).rgb;
prevSourceColor *= lerp(1. - scanlineStrength, 1.0, prevScanline);
// Blend our previous frame into the current one based on how much phosphor persistence we have between frames.
sourceColor = max(prevSourceColor * g_phosphorPersistence, sourceColor);
// We want to adjust the brightness to somewhat compensate for the darkening due to scanlines
sourceColor /= 1.0 - scanlineStrength * 0.5;
}
// Time to put it all together: first, by applying the screen mask (i.e. the shadow mask/aperture grill, etc)...
// $TODO: Figure out a proper scaling factor here - the 3.0 is meant to adjust for the fact that the mask cuts out
// approximately 2/3rds of the brightness, but it's not exact, we could calculate this, I just haven't.
screenMask.rgb = screenMask.rgb * ((2.0 + global.cat_mask_picker) - g_maskDepth) + g_maskDepth;
vec3 result = sourceColor * lerp(vec3(1.,1.,1.), screenMask.rgb, g_maskStrength);
// ... then bringing in some diffusion on top (This isn't physically accurate (it should really be a lerp between res
// and diffusionColor) but doing it this way preserves the brightness and still looks reasonable, especially when
// displaying bright things on a dark background)
result = max(diffusionColor * g_diffusionStrength, result);
// Finally, mask out everything outside of the edges to get our final output value.
FragColor = lerp(g_backgroundColor, vec4(result, 1.), screenMask.a);
}

View file

@ -0,0 +1,88 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader takes a composite signal (like you'd get via a console's composite input) and separates out the luma
// from the chroma so it is effectively an SVideo signal. I tried all sorts of wacky hijinks to filter these things
// out in a solid emulation of what old CRT hardware was doing, but all of those filters were both VERY expensive and
// also had annoying artifacts (like ringing). I tried all sorts of low pass and band stop solutions to get the luma
// separated from the chroma.
//
// However, it turns out, that all you REALLY need to do (if your color cycle frequency is a nice multiple of your
// pixel resolution, which it is), is average out that many pixels' worth of signal, and that flattens out the wave
// frequency very nicely, leaving you with just the luma. This average (or box filter) is very effective at removing
// exactly the frequency that we care about and leaving the rest. And it's about the simplest (and highest-
// performance) possible way to do it.
//
// Basically, if you average every sample of the below sine wave, what you end up with is a value that represents the
// y coordinate of its center (the line in the diagram).
// _....._
// ,=" "=.
// ," ".
// / ".
// ---------------------.-------------------/
// ". ."
// "._ _,"
// "-.....-"
//
//
// That center line corresponds to the "luma" portion of the composite signal - the effective brightness of that
// portion of the scanline (or, if you prefer, the Y component in the YIQ encoding).
//
// Once you have that, you can simply subtract this luma value from the original signal and you're left with JUST the
// wave information (which is the chroma, or IQ portion of the YIQ encoding).
//
// It's also worth noting that this shader is capable of doing TWO luma/chroma demodulations at once (one to the
// r and g output channels and one to b and a), for purposes of reducing/eliminating any jitter between two frames for
// signals where the color phases alternate frame by frame (the NES and SNES do this, for instance).
#include "slang_params.inc"
#include "cathode-retro-util-box-filter.inc"
#define inTex vTexCoord
// This texture is expected to represent the visible signal part of an NTSC field (a full frame for a progressive scan
// input, or one half of an interlaced frame for an interlaced one). If this were using the input from a true NTSC
// signal, this would only contain information in the red channel - however, some emulated systems (the NES and SNES,
// for instance) have different chroma phase components on alternating frames, and we can pass a variant of the
// current frame with a different phase to average and reduce the flickering of temporal artifacts from frame to
// frame ... this doesn't make much difference if your emulator is running at a perfectly smooth 60fps, but it can
// make a big difference if there are frame drops).
// The sampler should be set to linear sampling, and clamped addressing (no wrapping).
//DECLARE_TEXTURE2D(g_sourceTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D Source;
#define g_sourceTexture Source
// How many samples (texels along a scanline) there are per colorburst cycle (the color wave in the composite
// signal). This shader currently assumes that it's integral (hence the int input) but it is totally possible to
// make the shader support a floating-point value instead - it would just need to do an extra fractional addition of
// the last texel.
uint g_samplesPerColorburstCycle = uint(global.cb_samples);
void main()
{
vec2 inputTexDim = params.OriginalSize.xy;
// Doing a box filter (an average) at exactly the colorburst cycle length is a *very* effective and efficient way to
// remove the chroma information completely, leaving with just the luma.
vec4 centerSample;
vec2 luma = BoxFilter(
g_sourceTexture,
1.0 / inputTexDim,
g_samplesPerColorburstCycle,
inTex,
centerSample).xy;
// Take our one or two computed luma channels and output them (swizzled) as r and b, then subtract those one or two
// two lumas and subtract them from the corresponding input waves to get the separated chroma component, output as
// g and a.
// This way, if our render target is just a two-component texture (we're not doing any temporal aliasing reduction),
// then r will be the luma and g will be the chroma.
FragColor = vec4(luma, centerSample.xy - luma).rbga;
}

View file

@ -0,0 +1,51 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader does a horizontal three-tap blur or sharpen to each input scanline.
#include "slang_params.inc"
#define inTexCoord vTexCoord
// The input RGB texture that will be filtered. It should be set up for linear filtering and clamp addressing.
//DECLARE_TEXTURE2D(g_sourceTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D Source;
#define g_sourceTexture Source
// This is the strength of the blur - 0.0 will leave the output texture unchanged from the input, 1.0 will do a full
// 3-texel average, and -1 will do a very extreme sharpen pass.
float g_blurStrength = -global.blurStrength;
// The step between each sample, in texels. Usually scaled to be relative to the original input, which may have had a
// different resolution than our input texture due to the NTSC generation/decode process (if there's no original
// input, i.e. we're not using the NTSC signal generator and have gotten a signal straight from a real NTSC signal,
// then you'd just want to pick some nice-on- average value instead)
float g_stepSize = global.stepSize;
void main()
{
vec2 inputTexelSize = vec2(ddx(inTexCoord).x, ddy(inTexCoord).y);
// Just a simple three-tap 1D filter.
// $TODO: This could absolutely be done with two samples instead of 3, by taking advantage of linear filtering the
// same way we do in the gaussian/lanczos filters.
float blurSide = g_blurStrength / 3.0;
float blurCenter = 1.0 - 2.0 * blurSide;
vec4 color = texture(g_sourceTexture, (inTexCoord - vec2(g_stepSize, 0) * inputTexelSize))
* blurSide
+ texture(g_sourceTexture, (inTexCoord))
* blurCenter
+ texture(g_sourceTexture, (inTexCoord + vec2(g_stepSize, 0) * inputTexelSize))
* blurSide;
FragColor = color;
}

View file

@ -0,0 +1,83 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This signal takes an input S-Video signal and converts it into a set of YIQ color space values.
//
// This process is called NTSC color demodulation, which is a type of QAM demodulation.
//
// An NTSC signal (either composite or S-Video) is really just encoded YIQ data, where the Y value is encoded as luma and IQ is encoded
// as a waveform in the chroma. Since this shader takes an effective S-Video signal as input, the Y component is easy - it's just the
// luma channel of the input.
//
// Just like filtering the luma from the chroma (see the comment in CompositeToSVideo.hlsl), rather than doing any sort of fancy filtering
// here, the absolute best results I've gotten were from a super-basic average (which very nicely removes a very specific frequency and
// all integer multiples of it), which is also exactly what we need here.
//
// Additionally, if we want temporal artifact reduction, we're doing two of this calculation (we have a (lumaA, chromaA, lumaB, chromaB)
// texture representing the same frame with two different color phases), and we use the temporal artifact reduction value to blend between
// them, which reduces or eliminates alternating-phase-related flickering that systems like the NES could generate.
//
// The output of this shader is a texture that contains the decoded Y, I, and Q channels in R, G, and B (plus 1.0 in alpha).
#include "slang_params.inc"
#define inTexCoord vTexCoord
// This is a 2- or 4-component texture that contains either a single luma, chroma sample pair or two luma, chroma pairs of S-Video-like
// signal. It's 2 components if we have no temporal artifact reduction (we're not blending two versions of the same frame), 4 if we do.
// This sampler should be set up for linear filtering and clamped addressing (no wrapping).
//DECLARE_TEXTURE2D(g_sourceTexture, g_sourceSampler);
layout(set = 0, binding = 2) uniform sampler2D IQ_separate;
#define g_sourceTexture IQ_separate
// This is a 1- or 2-component texture that contains the colorburst phase offsets for each scanline. It's 1 component if we have no
// temporal artifact reduction, and 2 if we do.
// Each phase value in this texture is the phase in (fractional) multiples of the colorburst wavelength.
// This sampler should be set up for linear filtering and clamped addressing (no wrapping).
//DECLARE_TEXTURE2D(g_scanlinePhases, g_scanlinePhasesSampler);
layout(set = 0, binding = 3) uniform sampler2D g_scanlinePhases;
// How many samples (horizontal texels) there are per each color wave cycle.
uint g_samplesPerColorburstCycle = uint(global.cb_samples);
// A value representing the tint offset (in colorburst wavelengths) from baseline that we want to use. Mostly used for fun to emulate the
// tint dial of a CRT TV.
float g_tint = global.tint;
// The width of the input signal
uint g_inputWidth = uint(params.SourceSize.x);
const float k_pi = 3.141592653;
void main()
{
uint sampleXIndex = uint(floor(inTexCoord.x * g_inputWidth));
// Get the reference phase for our scanline
vec2 relativePhase = texture(g_scanlinePhases, inTexCoord.yy).xy + g_tint;
// This is the chroma decode process, it's a QAM demodulation.
// You multiply the chroma signal by a reference waveform and its quadrature (Basically, sin and cos at a given time) and then filter
// out the chroma frequency (here done by a box filter (an average)). What you're left with are the approximate I and Q color space
// values for this part of the image.
vec4 chroma = texture(g_sourceTexture, inTexCoord).yyww;
// This is part of the chroma decode process, separated out into its own shader for efficiency's sake - it's the first half of a QAM
// demodulation. Each piece of the chroma signal is multiplied by a reference waveform and its quadrature (basically, sin and cos at a
// given time) and then filter out the chroma frequency (done in the next pass).
vec2 s, c;
sincos(2.0 * k_pi * (float(sampleXIndex) / g_samplesPerColorburstCycle + relativePhase), s, c);
// Return the chromas modulated with our sines and cosines (swizzled to line them up correctly)
FragColor = chroma * vec4(s, -c).xzyw;
}

View file

@ -0,0 +1,154 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This signal takes an input S-Video signal and converts it into a set of YIQ color space values.
//
// This process is called NTSC color demodulation, which is a type of QAM demodulation.
//
// An NTSC signal (either composite or S-Video) is really just encoded YIQ data, where the Y value is encoded as luma
// and IQ is encoded as a waveform in the chroma. Since this shader takes an effective S-Video signal as input, the Y
// component is easy - it's just the luma channel of the input.
//
// Just like filtering the luma from the chroma (see the comment in CompositeToSVideo.hlsl), rather than doing any sort
// of fancy filtering here, the absolute best results I've gotten were from a super-basic average (which very nicely
// removes a very specific frequency and all integer multiples of it), which is also exactly what we need here.
//
// Additionally, if we want temporal artifact reduction, we're doing two of this calculation (we have a
// (lumaA, chromaA, lumaB, chromaB) texture representing the same frame with two different color phases), and we use
// the temporal artifact reduction value to blend between them, which reduces or eliminates alternating-phase-related
// flickering that systems like the NES could generate.
//
// The output of this shader is a texture that contains the decoded Y, I, and Q channels in R, G, and B
// (plus 1.0 in alpha).
#include "slang_params.inc"
#include "cathode-retro-util-box-filter.inc"
// This is a 2- or 4-component texture that contains either a single luma, chroma sample pair or two luma, chroma pairs
// of S-Video-like signal. It's 2 components if we have no temporal artifact reduction (we're not blending two
// versions of the same frame), 4 if we do. This sampler should be set up for linear filtering and clamped addressing.
//DECLARE_TEXTURE2D(g_sourceTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D IQ_separate;
#define g_sourceTexture IQ_separate
// This is a 2- or 4-component texture that contains the chroma portions of the signal modulated with the carrier
// quadrature (basically, it's float4(chromaA * sin, chromaA * cos, chromaB * sin, chromaB * cos).
// This has been modulated in a separate pass for efficiency - otherwise we'd need to do a bunch of sines and cosines
// per pixel here.
//DECLARE_TEXTURE2D(g_modulatedChromaTexture, g_modulatedChromaSampler);
layout(set = 0, binding = 3) uniform sampler2D g_modulatedChromaTexture;
// How many samples (horizontal texels) there are per each color wave cycle.
uint g_samplesPerColorburstCycle = uint(global.cb_samples);
// This is a value representing how saturated we want the output to be. 0 basically means we'll decode as a grayscale
// image, 1 means fully saturated color (i.e. the intended input saturation), and you could even set values greater
// than 1 to oversaturate.
// This corresponds to the saturation dial of a CRT TV.
// $NOTE: This value should be pre-scaled by the g_brightness value, so that if brightness is 0, saturation is always
// 0 - otherwise, you get weird output values where there should have been nothing visible but instead you get a
// pure color instead.
float g_saturation = global.cat_sat * saturate(global.cat_bright);
// This is a value representing the brightness of the output. a value of 0 means we'll output pure black, and 1 means
// "the intended brightness based on the input signal". Values above 1 will over-brighten the output.
// This corresponds to the brightness dial of a CRT TV.
float g_brightness = global.cat_bright;
// This is the luma value of the input signal that represents black. For our synthetic signals it's typically 0.0,
// but from a real NTSC signal this can be some other voltage level, since a voltage of 0 typically indicates a
// horizontal or vertical blank instead. This is calculated from/generated with the composite or S-Video signal we
// were given.
float g_blackLevel = global.cat_black_lvl;
// This is the luma value of the input signal that represents brightest white. For our synthetic signals it's
// typically 1.0, but from a real NTSC signal (or if we've applied some signal artifacts like ghosting) it could be
// some other value. This is calculated from/generated with the composite or S-Video signal we were given.
float g_whiteLevel = global.cat_white_lvl;
// A [0..1] value indicating how much we want to blend in an alternate version of the generated signal to adjust for
// any artifacting between successive frames. 0 means we only have (or want to use) a single input luma/chroma pair.
// A value > 0 means we are going to blend the results of two parallel-computed versions of our YIQ values, with a
// value of 1.0 being a pure average of the two.
float g_temporalArtifactReduction = global.temp_artifact_blend;
// The width of the input signal (including any side padding)
uint g_inputWidth = uint(params.SourceSize.x);
// The width of the output RGB image (should be the width of the input signal minus the side padding)
uint g_outputWidth = uint(params.OutputSize.x) - uint(global.sig_pad);
const float k_pi = 3.141592653;
void main()
{
vec2 inTexCoord = vTexCoord.xy;
inTexCoord.x = (inTexCoord.x - 0.5) * float(g_outputWidth) / float(g_inputWidth) + 0.5;
vec2 inputTexelSize = vec2(ddx(inTexCoord).x, ddy(inTexCoord).y);
// Get the index of our x sample.
uint sampleXIndex = uint(floor(inTexCoord.x / inputTexelSize.x));
// This is the chroma decode process, it's a QAM demodulation.
// You multiply the chroma signal by a reference waveform and its quadrature (Basically, sin and cos at a given
// time) and then filter out the chroma frequency (here done by a box filter (an average)). What you're left with
// are the approximate I and Q color space values for this part of the image.
vec2 Y = texture(g_sourceTexture, inTexCoord).xz;
// We're going to sample chroma at double our actual colorburst cycle to eliminate some deeply rough artifacting on
// the edges. In this case we're going to average by DOUBLE the color burst cycle - doing just a single cycle ends
// up with a LOT of edge artifacting on things, which are too strong. 2x is much softer but not so large that you
// can really notice that it's adding a ton of extra blurring on the color channel (it needs to be an integer
// multiple of our cycle sample count for the filtering to work).
// $TODO: This could actually be reduced to 1 when we're doing an decode of a signal that was not originally
// composite, since there are no areas where luma changes will have crept into the color channel, which is typically
// the artifacting we see.
uint filterWidth = g_samplesPerColorburstCycle * 2U;
vec4 unused; // BoxFilter outputs a center sample, but we don't care about that.
vec4 IQ = BoxFilter(
g_modulatedChromaTexture,
vec2(1.0 / float(g_inputWidth), 0.0),
2U * g_samplesPerColorburstCycle,
inTexCoord,
unused);
// Adjust our components, first Y to account for the signal's black/white level (and user-chosen brightness), then IQ
// for saturation (Which should also include the signal's brightness scale)
Y = (Y - g_blackLevel) / (g_whiteLevel - g_blackLevel) * g_brightness;
IQ *= vec4(g_saturation, g_saturation, g_saturation, g_saturation);
// we have 1 or 2 components of Y, and 2 or 4 of IQ. Blend them together based on our temporal aliasing reduction to
// get our final decoded YIQ values.
Y.x = lerp(Y.x, Y.y, g_temporalArtifactReduction * 0.5);
IQ.xy = lerp(IQ.xy, IQ.zw, g_temporalArtifactReduction * 0.5);
// Do some gamma adjustments (values effectively based on eyeballing the results of NTSC signals from NES, SNES, and
// Genesis consoles)
Y.x = pow(saturate(Y.x), 2.0 / 2.2);
float iqSat = saturate(length(IQ.xy));
IQ.xy *= pow(iqSat, 2.0 / 2.2) / max(0.00001, iqSat);
// Finally, run the YIQ values through the standard (SMPTE C) YIQ to RGB conversion matrix
// (from https://en.wikipedia.org/wiki/YIQ)
vec3 yiq = vec3(Y.x, IQ.xy);
FragColor = vec4(
dot(yiq, vec3(1.0, 0.946882, 0.623557)),
dot(yiq, vec3(1.0, -0.274788, -0.635691)),
dot(yiq, vec3(1.0, -1.108545, 1.7090047)),
1.0);
}

View file

@ -0,0 +1,106 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader applies ghosting and noise to an SVideo or Composite signal.
//
// Noise is just that, some randomized signal offset per sample to simulate analog error in the signal.
//
// Ghosting basically is what happens when a copy of your signal "skips" its intended path through the cable and mixes
// in with your normal signal (like an EM leak of the signal) and is basically a pre-echo of the signal. So as a very
// rough approximation of this, we'll just blend in an offset, blurred, and scaled version of the original signal.
#include "slang_params.inc"
#include "cathode-retro-util-noise.inc"
#define inputTexCoord vTexCoord
// The texture to apply artifacts to. Should either be:
// - a 1-channel composite signal
// - a 2-channel luma/chroma S-Video signal
// - a 2-channel doubled composite texture (two composite versions of the same frame for temporal artifact reduction)
// - a 4-channel luma/chroma/luma/chroma doubled S-Video signal (two S-Video versions of the same frame for temporal
// artifact reduction)
// This sampler should be set to linear sampling, and either clamp addressing or perhaps border mode, depending.
//DECLARE_TEXTURE2D(g_sourceTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D Source;
#define g_sourceTexture Source
// This represents how much ghosting is visible - 0.0 is "no ghosting" and 1.0 is "the ghost is as strong as the
// original signal."
float g_ghostVisibility = global.ghost_vis;
// This is the distance (in colorburst cycles) before the original signal that ghosting should appear.
float g_ghostDistance = global.ghost_dist;
// How far to blur the ghost, in colorburst cycles.
float g_ghostSpreadScale = global.ghost_spread;
// The strength of noise to add to the signal - 0.0 means no noise, 1.0 means, just, a lot of noise. So much.
// Probably too much.
float g_noiseStrength = global.noise_strength;
// An integer seed used to generate the per-output-texel noise. This can be a monotonically-increasing value, or
// random every frame, just as long as it's different every frame so that the noise changes.
uint g_noiseSeed = uint(((params.noise_seed) * float(params.FrameCount)));
// The width of the input signal texture
uint g_signalTextureWidth = uint(params.SourceSize.x);
// The number of scanlines in this field of NTSC video
uint g_scanlineCount = (params.OriginalSize.y < 400.) ? uint(params.OriginalSize.y) : uint(params.OriginalSize.y * 0.5);
// How many samples (texels along a scanline) there are per colorburst cycle (the color wave in the composite signal)
uint g_samplesPerColorburstCycle = uint(global.cb_samples);
void main()
{
vec4 signal = texture(g_sourceTexture, inputTexCoord);
if (g_ghostVisibility != 0)
{
// Calculate the center sample position of our ghost based on our input params, as well as how far to spread our
// sampling for the blur
vec2 ghostCenterCoord = inputTexCoord
+ vec2(g_ghostDistance * g_samplesPerColorburstCycle, 0.) / vec2(g_signalTextureWidth, g_scanlineCount);
vec2 ghostSampleSpread = vec2(g_ghostSpreadScale * g_samplesPerColorburstCycle, 0)
/ vec2(g_signalTextureWidth, g_scanlineCount);
// The following is a 9-tap gaussian, written as 5 samples using bilinear interpolation to sample two taps at once:
// 0.00761441700, 0.0360749699, 0.109586075, 0.2134445460,
// 0.26655998800, 0.2134445460, 0.109586075, 0.0360749699, 0.00761441700
vec4 ghost;
ghost = texture(g_sourceTexture, ghostCenterCoord - ghostSampleSpread * 1.174285279339)
* 0.0436893869;
ghost += texture(g_sourceTexture, ghostCenterCoord - ghostSampleSpread * 1.339243613069)
* 0.323030611;
ghost += texture(g_sourceTexture, ghostCenterCoord)
* 0.266559988;
ghost += texture(g_sourceTexture, ghostCenterCoord + ghostSampleSpread * 1.339243613069)
* 0.323030611;
ghost += texture(g_sourceTexture, ghostCenterCoord + ghostSampleSpread * 1.174285279339)
* 0.0436893869;
signal += ghost * g_ghostVisibility;
}
// Also add some noise for each texel. Use the noise seed and our x/y position to c
vec2 pixelIndex = inputTexCoord
* vec2(g_signalTextureWidth / (g_samplesPerColorburstCycle * 2.0 / 3.0), g_scanlineCount);
float xFrac = fract(pixelIndex.x);
pixelIndex = floor(pixelIndex);
// Do a little perlin-style interpolation to make the noise seem more analogue. This is one of those things that
// isn't strictly accurate (like I would prefer all the things be), but it *looks* right to me.
float noiseL = Noise2D(pixelIndex, float(g_noiseSeed));
float noiseR = Noise2D(pixelIndex + vec2(1.0, 0.0), float(g_noiseSeed));
float noise = lerp(noiseL, noiseR, xFrac) * 2.0 - 1.0;
// Finally we'll scale and bias the noise and return it added to our signal.
FragColor = (signal + noise * g_noiseStrength) / (1.0 + g_ghostVisibility);
}

View file

@ -0,0 +1,79 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader generates the phase of the colorburst for each scanline.
//
// Basically, outside of the visible portion of a color NTSC scanline, there is a colorburst which the TV uses to tell
// which color is the reference phase (which is yellow in color). We're not generating a full NTSC signal, but we
// still need to generate this data, and to do that we have a set of emulated timings for the virtual system that is
// generating our output.
#include "slang_params.inc"
#include "cathode-retro-util-tracking-instability.inc"
#define inTexCoord vTexCoord
// This is the colorburst phase (in fractional multiples of the colorburst wavelength) for the first scanline in our
// generated signal.
float g_initialFrameStartPhase = global.cb_first_start;
// This is the colorburst phase for the first scanline of the previous frame - this is used if we're generating two
// sets of phase information for use with temporal artifact reduction.
float g_prevFrameStartPhase = global.cb_last_start;
// The amount that the phase increments every scanline, in (fractional) multiples of the colorburst wavelength.
float g_phaseIncrementPerScanline = global.cb_phase_inc;
// How many samples (texels along a scanline) there are per colorburst cycle (the color wave in the composite signal)
uint g_samplesPerColorburstCycle = uint(global.cb_samples);
// The scale of any picture instability (horizontal scanline-by-scanline tracking issues). This is used to ensure the
// phase values generated line up with the offset of the texture sampling that happens in RGBToSVideoOrComposite.
// Must match the similarly-named value in cathode-retro-generator-rgb-to-svideo-or-composite.
float g_instabilityScale = global.horz_track_scale;
// A seed for the noise used to generate the scanline-by-scanline picture instability. Must match the simiarly-named
// value in cathode-retro-generator-rgb-to-svideo-or-composite.
uint g_noiseSeed = uint(mod(params.FrameCount, 13637));
// The width of the signal texture that is being generated, in texels.
uint g_signalTextureWidth = uint(params.OriginalSize.x);
// The number of scanlines for this field of video.
uint g_scanlineCount = (params.OriginalSize.y < 400.) ? uint(params.OriginalSize.y) : uint(params.OriginalSize.y * 0.5);
void main()
{
uint scanlineIndex = uint(round(inTexCoord.y * g_scanlineCount - 0.5));
// Calculate the phase for the start of the given scanline.
vec2 phases = vec2(g_initialFrameStartPhase, g_prevFrameStartPhase)
+ g_phaseIncrementPerScanline * float(scanlineIndex);
// Offset by half a cycle so that it lines up with the *center* of the filter instead of the left edge of it.
phases += 0.5 / float(g_samplesPerColorburstCycle);
// Finally, offset by the scaled instability so that if there are color artifacts, they'll have the correct phase.
// $TODO: This is actually slightly wrong, and I'm assuming it's due to the way the texture sampling works when
// generating the signal texture. This value is slightly too high when we're at half a colorburst phase offset (by
// something like 0.05, found by manually tweaking the tint slider to get a close match), and I'm not entirely sure
// why. Intuition says that because we're not oversampling the RGB texture to generate the signal texture, any
// intentional linear-blending texture offsets (like for 1bpp CGA modes) aren't actually offset *exactly* halfway at
// that point, and so the calculated phase on NTSC demodulation is slightly off from true.
float instability = CalculateTrackingInstabilityOffset(
scanlineIndex,
g_noiseSeed,
g_instabilityScale,
g_signalTextureWidth);
phases += instability * float(g_signalTextureWidth) / float(g_samplesPerColorburstCycle);
FragColor = vec4(fract(phases), 0, 0);
}

View file

@ -0,0 +1,127 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader takes an RGB image and turns it into either an S-Video or Composite signal (Based on whether g_compositeBlend is 0 or 1).
// We might also be generating a PAIR of these, using two different sets of phase inputs (if g_scanlinePhases is a two-component input),
// for purposes of temporal aliasing reduction.
#include "slang_params.inc"
#include "cathode-retro-util-tracking-instability.inc"
#define signalTexCoord vTexCoord
// This is the RGB input texture. It is expected to be g_inputWidth x g_scanlineCount in size.
// This sampler should be set up with linear filtering, and either clamp or border addressing.
//DECLARE_TEXTURE2D(g_sourceTexture, g_sourceSampler);
layout(set = 0, binding = 2) uniform sampler2D g_sourceTexture;
// This is the scanline phases texture, generated by GeneratePhaseTexture. It is g_scanlineCount x 1 in size, and each texel component in
// it represents the phase offset of the NTSC colorburst for the corresponding scanline, in multiples of the colorburst wavelength.
// This sampler should be set up with linear filtering, and either clamp or border addressing.
//DECLARE_TEXTURE2D(g_scanlinePhases, g_scanlinePhasesSampler);
layout(set = 0, binding = 3) uniform sampler2D g_scanlinePhases;
// The number of texels that the output texture will contain for each color cycle wave (i.e. the wavelength in output samples of
// the color carrier wave).
uint g_outputTexelsPerColorburstCycle = uint(global.cb_samples);
// The width of the input texture.
uint g_inputWidth = uint(params.OriginalSize.x);
// The width of the output render target.
uint g_outputWidth = uint(params.OutputSize.x);
// The number of scanlines in the current field of video (the height of the input texture).
uint g_scanlineCount = (params.OriginalSize.y < 400.) ? uint(params.OriginalSize.y) : uint(params.OriginalSize.y * 0.5);
// This is whether we're blending the generated luma/chroma into a single output channel or not. It is expected to be 0 or 1 (no
// intermediate values), where "0" means "keep luma and chroma separate, like an S-Video signal" and "1" means "add the two together,
// like a composite signal".
float g_compositeBlend = global.composite;
// The scale of any picture instability (horizontal scanline-by-scanline tracking issues). This is used to offset our texture sampling
// when generating the output so the picture tracking is imperfect. Must match the similarly-named value in GeneratePhaseTexture.
float g_instabilityScale = global.horz_track_scale;
// A seed for the noise used to generate the scanline-by-scanline picture instability. Must match the simiarly-named value in
// GeneratePhaseTexture.
uint g_noiseSeed = uint(mod(params.FrameCount, 13637));
// the number of output texels to pad on either side of the signal texture (so that filtering won't have visible artifacts on the left
// and right sides).
uint g_sidePaddingTexelCount = uint(global.sig_pad);
const float pi = 3.141592653;
void main()
{
uvec2 signalTexelIndex = uvec2(floor(signalTexCoord * vec2(g_outputWidth, g_scanlineCount)));
// The texcoord we're using to sample the input texture neesd to be adjusted slightly. The 0.25 offset ensures that our generated texture
// is centered on the RGB texture.
vec2 texCoord = (vec2(signalTexelIndex) * vec2(float(g_inputWidth) / float(g_outputWidth), 1.) + vec2(0.25, 0.5))
/ vec2(g_inputWidth, g_scanlineCount);
// Expand our sampling a little bit to adjust for the padding we want on the sides.
uint effectiveOutputWidth = g_outputWidth - g_sidePaddingTexelCount;
texCoord.x = (texCoord.x - 0.5) * float(g_outputWidth) / float(effectiveOutputWidth) + 0.5;
// Add in the instability to our x coordinate to wiggle our generated texture around.
float instability = CalculateTrackingInstabilityOffset(
signalTexelIndex.y,
g_noiseSeed,
g_instabilityScale,
g_outputWidth);
texCoord.x += instability;
vec3 rgb = texture(g_sourceTexture, texCoord).rgb;
// Convert RGB to YIQ using the NTSC standard (SMPTE C) conversion (from https://en.wikipedia.org/wiki/YIQ)
vec3 yiq;
yiq.r = dot(rgb, vec3(0.3000, 0.5900, 0.1100));
yiq.g = dot(rgb, vec3(0.5990, -0.2773, -0.3217));
yiq.b = dot(rgb, vec3(0.2130, -0.5251, 0.3121));
// Do some gamma adjustments to counter for the gamma that will be used at decode time.
yiq.x = pow(saturate(yiq.x), 2.2 / 2.0);
float iqSat = saturate(length(yiq.yz));
yiq.yz *= pow(iqSat, 2.2 / 2.0) / max(0.00001, iqSat);
// Separate the YIQ channels out because YIQ.y ended up being confusing (is it Y? no it's I! but also it's y!)
float Y = yiq.x;
float I = yiq.y;
float Q = yiq.z;
// Calculate the phase for our current x position on the current scanline.
vec2 scanlinePhase = texture(
g_scanlinePhases,
(vec2(0.0, signalTexelIndex.y + 0.5) / g_scanlineCount)).xy;
vec2 phase = scanlinePhase + signalTexelIndex.x / float(g_outputTexelsPerColorburstCycle);
// Now we need to encode our IQ component in the carrier wave at the correct phase. This is QAM modulation.
vec2 s, c;
sincos(2.0 * pi * phase, s, c);
vec2 luma = vec2(Y, Y);
vec2 chroma = s * I - c * Q;
if (g_compositeBlend > 0.5)
{
// We are outputting a composite signal so combine luma and chroma and output it into our expected 1- or 2-channel texture.
FragColor = (luma + chroma).xyxy;
}
else
{
// Outputting svideo, so don't combine luma and chroma, and write them out into our expected 2- or 4-channel texture.
FragColor = vec4(luma, chroma).xzyw;
}
}

View file

@ -0,0 +1,50 @@
// Perform a centered box filter - which means we might need to sample a half-texel off of either end of the filter, if
// the filter width is even (and it probably is).
vec4 BoxFilter(
sampler2D decal,
vec2 invTextureSize,
uint filterWidth,
vec2 texCoord,
out vec4 centerSample)
{
// Get the center sample (which we'll write out to the caller)
centerSample = texture(decal, texCoord);
// Average starts with the center sample
vec4 avg = centerSample;
// Excluding the center texel, iterate over every two samples to either side of the center sampling in the middle to
// get a nice bilinear average of the two (getting two texel samples for free). Multiply by 2 so that we get 1.0 of
// each of them.
uint iterEnd = uint((filterWidth - 1U) / 2U);
for (uint i = 2U; i < iterEnd; i += 2U)
{
avg += 2 * texture(decal, (texCoord + vec2(i - 0.5, 0) * invTextureSize));
avg += 2 * texture(decal, (texCoord - vec2(i - 0.5, 0) * invTextureSize));
}
// Now we either have no texels, a half texel, a full texel, or 1.5 texels remaining (or in other words, our
// width-minus-center modulo 4 is one of 0 through 3. If it's 0, we don't have to do anything, we've averaged all
// of the texels.
uint remainder = (filterWidth - 1U) % 4U;
if (remainder == 3U)
{
// If it's 3, we have 1.5 texels left per end. Sample 1/3rd of the way into the inner-most texel (Because of how
// lerp works, that means we get 2/3rd of the inner texel and 1/3rd of the outermost), then multiply the result by
// 1.5 to give us 1.0 of the innermost and 0.5 of the outermost texel (we want one half of the outer texel and
// full weight of the inner one).
avg += 1.5 * texture(decal, (texCoord + vec2(iterEnd + 1.0 / 3.0, 0) * invTextureSize));
avg += 1.5 * texture(decal, (texCoord - vec2(iterEnd + 1.0 / 3.0, 0) * invTextureSize));
}
else if (remainder > 0U)
{
// If the remainder is either 1 or 2, we either have a half texel or a full texel remaining on either end. Sample
// the center of that texel and then scale it down by 0.5 if we only need half of its weight.
float scale = (remainder == 2U) ? 1.0 : 0.0;
avg += scale * texture(decal, (texCoord + vec2(iterEnd + 1.0, 0) * invTextureSize));
avg += scale * texture(decal, (texCoord - vec2(iterEnd + 1.0, 0) * invTextureSize));
}
return avg / float(filterWidth);
}

View file

@ -0,0 +1,24 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Do a 1:1 copy of a texture
#include "slang_params.inc"
#include "cathode-retro-util-lanczos.inc"
#define inTexCoord vTexCoord
//DECLARE_TEXTURE2D(g_sourceTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D Source;
#define g_sourceTexture Source
void main()
{
FragColor = texture(g_sourceTexture, inTexCoord);
}

View file

@ -0,0 +1,32 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Downsample an input image by 2x along a given axis by using a lanczos filter.
#include "slang_params.inc"
#include "cathode-retro-util-lanczos.inc"
#define inTexCoord vTexCoord
//DECLARE_TEXTURE2D(g_sourceTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D Source;
#define g_sourceTexture Source
// The direction that we're downsampling along. Should either be (1, 0) to downsample to a half-width texture or
// (0, 1) to downsample to a half-height texture.
vec2 g_filterDir = vec2(1., 0.);
void main()
{
FragColor = Lanczos2xDownsample(
g_sourceTexture,
inTexCoord,
g_filterDir);
}

View file

@ -0,0 +1,32 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Downsample an input image by 2x along a given axis by using a lanczos filter.
#include "slang_params.inc"
#include "cathode-retro-util-lanczos.inc"
#define inTexCoord vTexCoord
//DECLARE_TEXTURE2D(g_sourceTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D Source;
#define g_sourceTexture Source
// The direction that we're downsampling along. Should either be (1, 0) to downsample to a half-width texture or
// (0, 1) to downsample to a half-height texture.
vec2 g_filterDir = vec2(0., 1.);
void main()
{
FragColor = Lanczos2xDownsample(
g_sourceTexture,
inTexCoord,
g_filterDir);
}

View file

@ -0,0 +1,86 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader does a 1D gaussian blur using a 13-tap filter.
#include "slang_params.inc"
#include "cathode-retro-util-language-helpers.inc"
#define inTexCoord vTexCoord
// 13-tap gaussian kernel coefficients for bilinear shading, optimized to only require 7 texture samples by taking
// advantage of linear texture filtering. These coefficients were generated using https://drilian.com/gaussian-kernel/
/*
The actual coefficients for this blur are:
1.107819053e-2
2.478669128e-2
4.790462288e-2
7.997337680e-2
1.153247662e-1
1.436510723e-1
1.545625599e-1
1.436510723e-1
1.153247662e-1
7.997337680e-2
4.790462288e-2
2.478669128e-2
1.107819053e-2
*/
//DECLARE_TEXTURE2D(g_sourceTex, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D g_sourceTex;
const int k_sampleCount = 7;
const float k_coeffs[k_sampleCount] = float[k_sampleCount](
3.586488181e-2,
1.278779997e-1,
2.589758386e-1,
1.545625599e-1,
2.589758386e-1,
1.278779997e-1,
3.586488181e-2
);
const float k_offsets[k_sampleCount] = float[k_sampleCount](
-5.308886854,
-3.374611919,
-1.445310910,
0.000000000,
1.445310910,
3.374611919,
5.308886854
);
// Blur a texture along the blur direction (for a horizontal blur, use (1, 0) and for vertical use (0, 1)), centered at
// "centerTexCoord" (which is in standard [0..1] texture space).
vec4 Blur(vec2 centerTexCoord, vec2 blurDirection)
{
ivec2 dim = ivec2(params.SourceSize.xy);
vec4 v = vec4(0., 0., 0., 0.);
for (int i = 0; i < k_sampleCount; i++)
{
vec2 c = centerTexCoord + blurDirection / vec2(dim) * k_offsets[i];
v += texture(g_sourceTex, c) * k_coeffs[i];
}
return v;
}
// The direction to blur along. Should be (1, 0) to do a horizontal blur and (0, 1) to do a vertical blur.
vec2 g_blurDir = vec2(1., 0.);
void main()
{
FragColor = Blur(inTexCoord, g_blurDir);
}

View file

@ -0,0 +1,88 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader does a 1D gaussian blur using a 13-tap filter.
#include "slang_params.inc"
#define inTexCoord vTexCoord
// 13-tap gaussian kernel coefficients for bilinear shading, optimized to only require 7 texture samples by taking
// advantage of linear texture filtering. These coefficients were generated using https://drilian.com/gaussian-kernel/
/*
The actual coefficients for this blur are:
1.107819053e-2
2.478669128e-2
4.790462288e-2
7.997337680e-2
1.153247662e-1
1.436510723e-1
1.545625599e-1
1.436510723e-1
1.153247662e-1
7.997337680e-2
4.790462288e-2
2.478669128e-2
1.107819053e-2
*/
//DECLARE_TEXTURE2D(g_sourceTex, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D Source;
// access the previous pass, since we're separate horz and vert
#define g_sourceTex Source
const int k_sampleCount = 7;
const float k_coeffs[k_sampleCount] = float[k_sampleCount](
3.586488181e-2,
1.278779997e-1,
2.589758386e-1,
1.545625599e-1,
2.589758386e-1,
1.278779997e-1,
3.586488181e-2
);
const float k_offsets[k_sampleCount] = float[k_sampleCount](
-5.308886854,
-3.374611919,
-1.445310910,
0.000000000,
1.445310910,
3.374611919,
5.308886854
);
// Blur a texture along the blur direction (for a horizontal blur, use (1, 0) and for vertical use (0, 1)), centered at
// "centerTexCoord" (which is in standard [0..1] texture space).
vec4 Blur(vec2 centerTexCoord, vec2 blurDirection)
{
ivec2 dim = ivec2(params.SourceSize.xy);
vec4 v = vec4(0., 0., 0., 0.);
for (int i = 0; i < k_sampleCount; i++)
{
vec2 c = centerTexCoord + blurDirection / vec2(dim) * k_offsets[i];
v += texture(g_sourceTex, c) * k_coeffs[i];
}
return v;
}
// The direction to blur along. Should be (1, 0) to do a horizontal blur and (0, 1) to do a vertical blur.
vec2 g_blurDir = vec2(0., 1.);
void main()
{
FragColor = Blur(inTexCoord, g_blurDir);
}

View file

@ -0,0 +1,40 @@
// This is a lanczos2 kernel for a nice 2x downsample. I wish I'd documented how I generated it but I didn't so instead
// this is what you get. Sorry about that.
//static const float k_lanczos2[8] = {-0.009, -0.042, 0.117, 0.434, 0.434, 0.117, -0.042, -0.009};
// However, it's been optimized to take advantage of the linear filtering using the same technique I used for the
// gaussian filters at https://drilian.com/gaussian-kernel/
// That means it's only 4 texture samples for an 8-tap lanczos, which is, if I did my math correctly, twice as good!
const int k_sampleCount = 4;
const float k_coeffs[k_sampleCount] = float[k_sampleCount](
-0.051,
0.551,
0.551,
-0.051
);
const float k_offsets[k_sampleCount] = float[k_sampleCount](
-2.67647052,
-0.712341249,
0.712341189,
2.67647052
);
vec4 Lanczos2xDownsample(
sampler2D decal,
vec2 centerTexCoord,
vec2 filterDir)
{
ivec2 dim = ivec2(params.SourceSize.xy);
vec4 v = vec4(0.0, 0.0, 0.0, 0.0);
for (int i = 0; i < k_sampleCount; i++)
{
vec2 c = centerTexCoord + filterDir / float2(dim) * k_offsets[i];
v += texture(decal, c) * k_coeffs[i];
}
return v;
}

View file

@ -0,0 +1,114 @@
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This file contains all the stuff used to make these shaders compile both as GLSL and HLSL.
//
// Note that one of GLSL or HLSL must be defined.
#ifndef NTSC_UTIL_LANG
#define NTSC_UTIL_LANG
#ifndef GLSL
#ifndef HLSL
// Decfine this to make my syntax highlighting less angry
#define HLSL
#endif
#endif
#ifdef GLSL
#define uint2 uvec2
#define uint3 uvec3
#define uint4 uvec4
#define int2 ivec2
#define int3 ivec3
#define int4 ivec4
#define float2 vec2
#define float3 vec3
#define float4 vec4
#define ddx dFdx
#define ddy dFdy
#define frac fract
#define lerp mix
precision mediump float;
float saturate(float a)
{
return clamp(a, 0.0, 1.0);
}
float2 saturate(float2 a)
{
return clamp(a, 0.0, 1.0);
}
float3 saturate(float3 a)
{
return clamp(a, 0.0, 1.0);
}
float4 saturate(float4 a)
{
return clamp(a, 0.0, 1.0);
}
void sincos(float angle, out float s, out float c)
{
s = sin(angle);
c = cos(angle);
}
void sincos(float2 angle, out float2 s, out float2 c)
{
s = sin(angle);
c = cos(angle);
}
void sincos(float3 angle, out float3 s, out float3 c)
{
s = sin(angle);
c = cos(angle);
}
void sincos(float4 angle, out float4 s, out float4 c)
{
s = sin(angle);
c = cos(angle);
}
#define CONST const
#define BEGIN_CONST_ARRAY(type, name, size) const type name[size] = type[size](
#define END_CONST_ARRAY );
// GLSL doesn't have separate samplers and textures, so we're using texName for everything
#define DECLARE_TEXTURE2D(texName, samplerName) uniform sampler2D texName
#define DECLARE_TEXTURE2D_AND_SAMPLER_PARAM(texName, samplerName) sampler2D texName
#define PASS_TEXTURE2D_AND_SAMPLER_PARAM(texName, samplerName) texName
#define SAMPLE_TEXTURE(texName, samplerName, coord) texture2D(texName, vec2((coord).x, 1.0 - (coord).y))
#define SAMPLE_TEXTURE_BIAS(texName, samplerName, coord, bias) \
texture2D(texName, vec2((coord).x, 1.0 - (coord).y), bias)
#define GET_TEXTURE_SIZE(tex, outVar) outVar = textureSize(tex, 0)
#define PS_MAIN in float2 vsOutTexCoord; out float4 psOutPos; void main() { psOutPos = Main(vsOutTexCoord); }
#define CBUFFER uniform
#endif
#ifdef HLSL
#define CONST static const
#define BEGIN_CONST_ARRAY(type, name, size) static const type name[size] = {
#define END_CONST_ARRAY };
#define DECLARE_TEXTURE2D(texName, samplerName) Texture2D<float4> texName; sampler samplerName
#define DECLARE_TEXTURE2D_AND_SAMPLER_PARAM(texName, samplerName) Texture2D<float4> texName, sampler samplerName
#define PASS_TEXTURE2D_AND_SAMPLER_PARAM(texName, samplerName) texName, samplerName
#define SAMPLE_TEXTURE(texName, samplerName, coord) texName.Sample(samplerName, coord)
#define SAMPLE_TEXTURE_BIAS(texName, samplerName, coord, bias) texName.SampleBias(samplerName, coord, bias)
#define GET_TEXTURE_SIZE(tex, outVar) tex.GetDimensions(outVar.x, outVar.y)
#define PS_MAIN float4 main(float2 inTexCoord: TEX): SV_TARGET { return Main(inTexCoord); }
#define CBUFFER cbuffer
#endif
#endif

View file

@ -0,0 +1,17 @@
#ifndef noise_func
#define noise_func
float Noise2D(vec2 coord, float iseed)
{
float fseed = fract(iseed / 10000.0);
float angle = fract(distance(coord.xy, 1000.0 * (vec2(fseed + 0.3, 0.1) + 1.0)));
return fract(tan(angle) * distance(coord.xy, 1000.0 * (vec2(fseed + 0.1, fseed + 0.2) - 2.0)));
}
float Noise1D(float coord, float iseed)
{
return Noise2D(vec2(coord, 0.0), iseed);
}
#endif

View file

@ -0,0 +1,48 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader does a tonemap and 1D downsample of a texture, which is intended to be for the diffusion emulation in
// the CRT side of the whole Cathode Retro process. It's worth noting that in practice we're not doing a true 2x
// downsample, it's going to be something in that ballpark, but not exact. However, the output of this shader is going
// to be blurred so it doesn't actually matter in practice.
#include "slang_params.inc"
#include "cathode-retro-util-lanczos.inc"
#define inTexCoord vTexCoord
//DECLARE_TEXTURE2D(g_sourceTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D Source;
#define g_sourceTexture Source
// The direction we want to apply the downsample.
vec2 g_downsampleDir = vec2(1., 0.);
float g_minLuminosity = 1.0 - global.minlum;
float g_colorPower = 2.0 - global.colorpower;
void main()
{
vec4 samp = Lanczos2xDownsample(
g_sourceTexture,
inTexCoord,
g_downsampleDir);
// Calculate the luminosity of the input.
float inLuma = dot(samp.rgb, vec3(0.30, 0.59, 0.11));
// Calculate the desired output luminosity.
float outLuma = (inLuma - g_minLuminosity) / (1.0 - g_minLuminosity);
outLuma = pow(saturate(outLuma), g_colorPower);
// Apply the luminosity scaling.
samp.rgb *= outLuma / inLuma;
FragColor = samp;
}

View file

@ -0,0 +1,48 @@
#version 450
/*
A port of DeadlyRedCube's Cathode-Retro shader to slang format
based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
ported by hunterk
license: MIT
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader does a tonemap and 1D downsample of a texture, which is intended to be for the diffusion emulation in
// the CRT side of the whole Cathode Retro process. It's worth noting that in practice we're not doing a true 2x
// downsample, it's going to be something in that ballpark, but not exact. However, the output of this shader is going
// to be blurred so it doesn't actually matter in practice.
#include "slang_params.inc"
#include "cathode-retro-util-lanczos.inc"
#define inTexCoord vTexCoord
//DECLARE_TEXTURE2D(g_sourceTexture, g_sampler);
layout(set = 0, binding = 2) uniform sampler2D Source;
#define g_sourceTexture Source
// The direction we want to apply the downsample.
vec2 g_downsampleDir = vec2(0., 1.);
float g_minLuminosity = 1.0 - global.minlum;
float g_colorPower = 2.0 - global.colorpower;
void main()
{
vec4 samp = Lanczos2xDownsample(
g_sourceTexture,
inTexCoord,
g_downsampleDir);
// Calculate the luminosity of the input.
float inLuma = dot(samp.rgb, vec3(0.30, 0.59, 0.11));
// Calculate the desired output luminosity.
float outLuma = (inLuma - g_minLuminosity) / (1.0 - g_minLuminosity);
outLuma = pow(saturate(outLuma), g_colorPower);
// Apply the luminosity scaling.
samp.rgb *= outLuma / inLuma;
FragColor = samp;
}

View file

@ -0,0 +1,7 @@
#include "cathode-retro-util-noise.inc"
// Calculate the instability noise per scanline
float CalculateTrackingInstabilityOffset(uint scanlineIndex, uint noiseSeed, float scale, uint signalTextureWidth)
{
return (Noise1D(float(scanlineIndex), float(noiseSeed)) - 0.5) * scale / signalTextureWidth;
}

View file

@ -0,0 +1,103 @@
shaders = 18
#mipmap_input1 = true
#mipmap_input2 = true
#mipmap_input3 = true
#mipmap_input4 = true
#mipmap_input5 = true
#mipmap_input6 = true
#mipmap_input7 = true
#mipmap_input8 = true
#mipmap_input9 = true
#mipmap_input10 = true
#mipmap_input11 = true
#mipmap_input12 = true
#mipmap_input13 = true
#mipmap_input14 = true
#mipmap_input15 = true
#mipmap_input16 = true
#mipmap_input17 = true
shader0 = cathode-retro-util-copy.slang
alias0 = g_sourceTexture
filter_linear0 = true
shader1 = cathode-retro-util-downsample-2x-horz.slang
filter_linear1 = true
scale_x1 = 0.5
shader2 = cathode-retro-util-downsample-2x-vert.slang
filter_linear2 = true
scale_y2 = 0.5
shader3 = cathode-retro-util-tonemap-and-downsample-horz.slang
filter_linear3 = true
scale_x3 = 0.5
shader4 = cathode-retro-util-tonemap-and-downsample-vert.slang
filter_linear4 = true
scale_y4 = 0.5
alias4 = g_sourceTex
shader5 = cathode-retro-util-gaussian-blur-horz.slang
filter_linear5 = true
shader6 = cathode-retro-util-gaussian-blur-vert.slang
filter_linear6 = true
alias6 = g_diffusionTexture
shader7 = cathode-retro-generator-gen-phase.slang
float_framebuffer7 = true
alias7 = g_scanlinePhases
scale_type7 = absolute
scale_y7 = 1.0
scale_x7 = 224.0
filter_linear7 = true
shader8 = ../../../stock.slang
scale_type_y8 = absolute
scale_y8 = 224.0
shader9 = cathode-retro-generator-rgb-to-svideo-or-composite.slang
float_framebuffer9 = true
alias9 = rgb2yiq
shader10 = cathode-retro-generator-apply-artifacts.slang
filter_linear10 = true
float_framebuffer10 = true
alias10 = YIQ_tex
shader11 = cathode-retro-decoder-composite-to-svideo.slang
filter_linear11 = true
float_framebuffer11 = true
alias11 = IQ_separate
shader12 = cathode-retro-decoder-svideo-to-modulated-chroma.slang
float_framebuffer12 = true
alias12 = g_modulatedChromaTexture
shader13 = cathode-retro-decoder-svideo-to-rgb.slang
float_framebuffer13 = true
alias13 = yiq2rgb
shader14 = cathode-retro-decoder-filter-rgb.slang
float_framebuffer14 = true
alias14 = g_currentFrameTexture
shader15 = cathode-retro-crt-generate-masks.slang
alias15 = g_maskTexture
#mipmap_input15 = false
wrap_mode15 = repeat
scale_type15 = viewport
float_framebuffer15 = true
shader16 = cathode-retro-crt-generate-screen-texture.slang
alias16 = g_screenMaskTexture
#mipmap_input16 = false
filter_linear16 = true
wrap_mode16 = clamp_to_edge
float_framebuffer16 = true
shader17 = cathode-retro-crt-rgb-to-crt.slang
#mipmap_input17 = false
filter_linear17 = true

View file

@ -0,0 +1,132 @@
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
vec4 FinalViewportSize;
float warpX, warpY, corner, noise_seed;
} params;
#pragma parameter div0 "--------Screen Settings--------" 0.0 0.0 0.0 0.0
#pragma parameter scan_intens "Scanline Intensity" 0.4 0.0 1.0 0.01
#pragma parameter cat_mask_picker "Mask (0=none, 1=aperture, 2=slot, 3=shadow)" 1.0 0.0 3.0 1.0
#pragma parameter mask_intens "Mask Strength" 0.4 0.0 1.0 0.01
#pragma parameter mask_scale "Mask Scale (2 or 3 for 4K)" 1.0 1.0 200.0 1.0
#pragma parameter mask_depth "Mask Background Darkness" 0.3 0.0 1.0 0.01
#pragma parameter warpX "Barrel Distortion X" 0.2 0.0 1.0 0.01
#pragma parameter warpY "Barrel Distortion Y" 0.1 0.0 1.0 0.01
#pragma parameter anim_noise "Animate Anti-Moire Noise" 0.0 0.0 1.0 1.0
#pragma parameter corner "Rounded Corner Size" 0.03 0.0 1.0 0.01
#pragma parameter persistence "Phosphor Persistence" 0.25 0.0 1.0 0.01
#pragma parameter diffusion "Diffusion Strength" 0.5 0.0 1.0 0.01
#pragma parameter div1 "---------TV Knob Settings---------" 0.0 0.0 0.0 0.0
#pragma parameter cat_sat "Saturation" 1.0 0.0 2.0 0.01
#pragma parameter cat_bright "Brightness" 1.0 0.0 2.0 0.01
#pragma parameter cat_white_lvl "White Level" 1.0 0.0 2.0 0.01
#pragma parameter cat_black_lvl "Black Level" 0.0 0.0 2.0 0.01
#pragma parameter tint "Tint Knob Adjustment" 0.0 -1.0 1.0 0.01
#pragma parameter blurStrength "Sharpness" -0.15 -1.0 1.0 0.01
#pragma parameter div2 "---------Signal Parameters---------" 0.0 0.0 0.0 0.0
#pragma parameter composite "Blend Chrome/Luma (aka Composite)" 1.0 0.0 1.0 1.0
#pragma parameter sig_pad "Signal Padding at Edges" 0.0 0.0 10.0 1.0
#pragma parameter minlum "Minimum Luminance" 1.0 0.0 1.0 0.01
#pragma parameter colorpower "Color Power" 1.0 0.0 2.0 0.01
#pragma parameter noise_seed "Noise Seed" 247.0 179.0 313.0 1.0
#pragma parameter cb_samples "Samples Per Color Burst Cyle" 2.0 1.0 100.0 1.0
#pragma parameter cb_first_start "Color Burst Phase First Scanline" 0.0 0.0 100.0 1.0
#pragma parameter cb_last_start "CB Phase Prev Frame First Scanline" 1.0 0.0 100.0 1.0
#pragma parameter cb_phase_inc "Color Burst Phase Increment" 1.66666 0.0 3.0 0.01
#pragma parameter stepSize "Texels Between Each Sample" 1.0 1.0 100.0 1.0
#pragma parameter div3 "-------Artifact Settings-------" 0.0 0.0 0.0 0.0
#pragma parameter horz_track_scale "Horizontal Tracking Instability Scale" 1.0 0.0 3.0 0.05
#pragma parameter ghost_vis "Ghost Visibility" 0.15 0.0 1.0 0.01
#pragma parameter ghost_dist "Ghost Delay Cycles" 1.0 0.0 100.0 1.0
#pragma parameter ghost_spread "Ghost Spread Cycles" 1.0 0.0 100.0 1.0
#pragma parameter noise_strength "Artifact Noise Strength" 0.15 0.0 1.0 0.01
#pragma parameter temp_artifact_blend "Temporal Artifact Blending (Toggle)" 0.0 0.0 1.0 1.0
#define uint2 uvec2
#define uint3 uvec3
#define uint4 uvec4
#define int2 ivec2
#define int3 ivec3
#define int4 ivec4
#define float2 vec2
#define float3 vec3
#define float4 vec4
#define ddx dFdx
#define ddy dFdy
#define frac fract
#define lerp mix
float saturate(float a)
{
return clamp(a, 0.0, 1.0);
}
vec2 saturate(vec2 a)
{
return clamp(a, 0.0, 1.0);
}
vec3 saturate(vec3 a)
{
return clamp(a, 0.0, 1.0);
}
vec4 saturate(vec4 a)
{
return clamp(a, 0.0, 1.0);
}
void sincos(float angle, out float s, out float c)
{
s = sin(angle);
c = cos(angle);
}
void sincos(vec2 angle, out vec2 s, out vec2 c)
{
s = sin(angle);
c = cos(angle);
}
void sincos(vec3 angle, out vec3 s, out vec3 c)
{
s = sin(angle);
c = cos(angle);
}
void sincos(vec4 angle, out vec4 s, out vec4 c)
{
s = sin(angle);
c = cos(angle);
}
layout(std140, set = 0, binding = 0) uniform UBO
{
mat4 MVP;
vec4 OriginalSize;
float cb_first_start, cb_last_start, cb_phase_inc, cb_samples, horz_track_scale,
minlum, colorpower, composite, sig_pad, ghost_vis, ghost_dist, ghost_spread,
noise_strength, tint, cat_sat, cat_bright, cat_white_lvl, cat_black_lvl,
temp_artifact_blend, blurStrength, stepSize, persistence, scan_intens,
diffusion, mask_intens, mask_depth, cat_mask_picker, mask_scale, anim_noise,
div0, div1, div2, div3;
} global;
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;//vec4(Position.x * 2.0 - 1.0, Position.y * 2.0 - 1.0, 0.0, 1.0);//
vTexCoord = vec2(TexCoord.x, TexCoord.y) * 1.0001;//vec2(Position.x, 1.0 - Position.y);//
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;

View file

@ -1,25 +1,37 @@
shaders = 4
shaders = 6
shader0 = ../stock.slang
alias0 = vt220_refpass
shader1 = ../ntsc/shaders/ntsc-adaptive/ntsc-pass1.slang
scale_type1 = source
scale_x1 = 4.0
filter_linear1 = false
scale_y1 = 1.0
float_framebuffer1 = true
shader1 = ../stock.slang
alias1 = PrePass0
shader2 = ../ntsc/shaders/ntsc-adaptive/ntsc-pass2.slang
scale_type2 = source
scale_x2 = 0.5
shader2 = shaders/guest/advanced/ntsc/ntsc-pass1.slang
alias2 = NPass1
scale_type_x2 = source
scale_type_y2 = source
scale_x2 = 4.0
scale_y2 = 1.0
float_framebuffer2 = true
filter_linear2 = false
shader3 = shaders/vt220/vt220.slang
mipmap_input3 = true
wrap_mode3 = mirrored_repeat
scale_type3 = viewport
shader3 = shaders/guest/advanced/ntsc/ntsc-pass2.slang
filter_linear3 = true
float_framebuffer3 = true
scale_type3 = source
scale_x3 = 0.5
scale_y3 = 1.0
shader4 = shaders/guest/advanced/ntsc/ntsc-pass3.slang
filter_linear4 = true
scale_type4 = source
scale_x4 = 1.0
scale_y4 = 1.0
shader5 = shaders/vt220/vt220.slang
mipmap_input5 = true
wrap_mode5 = mirrored_repeat
scale_type5 = viewport
parameters = "quality"
quality = 1.0

View file

@ -1,14 +1,26 @@
shaders = 2
shaders = 4
shader0 = shaders/ntsc-adaptive/ntsc-pass1.slang
scale_type0 = source
scale_x0 = 4.0
filter_linear0 = false
scale_y0 = 1.0
float_framebuffer0 = true
shader0 = ../stock.slang
alias0 = PrePass0
shader1 = shaders/ntsc-adaptive/ntsc-pass2.slang
scale_type1 = source
scale_x1 = 0.5
shader1 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass1.slang
alias1 = NPass1
scale_type_x1 = source
scale_type_y1 = source
scale_x1 = 4.0
scale_y1 = 1.0
filter_linear1 = true
float_framebuffer1 = true
filter_linear1 = false
shader2 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass2.slang
filter_linear2 = true
float_framebuffer2 = true
scale_type2 = source
scale_x2 = 0.5
scale_y2 = 1.0
shader3 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass3.slang
filter_linear3 = true
scale_type3 = source
scale_x3 = 1.0
scale_y3 = 1.0

View file

@ -1,35 +1,47 @@
shaders = 7
shaders = 9
shader0 = ../ntsc/shaders/ntsc-adaptive/ntsc-pass1.slang
scale_type0 = source
scale_x0 = 4.0
filter_linear0 = false
scale_y0 = 1.0
float_framebuffer0 = true
shader0 = ../stock.slang
alias0 = PrePass0
shader1 = ../ntsc/shaders/ntsc-adaptive/ntsc-pass2.slang
scale_type1 = source
scale_x1 = 0.5
shader1 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass1.slang
alias1 = NPass1
scale_type_x1 = source
scale_type_y1 = source
scale_x1 = 4.0
scale_y1 = 1.0
filter_linear1 = true
float_framebuffer1 = true
filter_linear1 = false
shader2 = ../crt/shaders/geom-deluxe/phosphor_apply.slang
alias2 = internal1
shader2 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass2.slang
filter_linear2 = true
float_framebuffer2 = true
scale_type2 = source
scale_x2 = 0.5
scale_y2 = 1.0
shader3 = ../crt/shaders/geom-deluxe/phosphor_update.slang
alias3 = phosphor
shader3 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass3.slang
filter_linear3 = true
scale_type3 = source
scale_x3 = 1.0
scale_y3 = 1.0
shader4 = ../crt/shaders/geom-deluxe/gaussx.slang
filter_linear4 = true
alias4 = internal2
shader4 = ../crt/shaders/geom-deluxe/phosphor_apply.slang
alias4 = internal1
shader5 = ../crt/shaders/geom-deluxe/gaussy.slang
filter_linear5 = true
alias5 = blur_texture
shader5 = ../crt/shaders/geom-deluxe/phosphor_update.slang
alias5 = phosphor
shader6 = ../crt/shaders/geom-deluxe/crt-geom-deluxe.slang
shader6 = ../crt/shaders/geom-deluxe/gaussx.slang
filter_linear6 = true
mipmap_input6 = true
alias6 = internal2
shader7 = ../crt/shaders/geom-deluxe/gaussy.slang
filter_linear7 = true
alias7 = blur_texture
shader8 = ../crt/shaders/geom-deluxe/crt-geom-deluxe.slang
filter_linear8 = true
mipmap_input8 = true
# ntsc-adaptive
# These settings are meant to get correct looking transparencies and

View file

@ -13,25 +13,36 @@
# 1.) bloom_approx_scale_x = scale_x2
# 2.) mask_resize_viewport_scale = float2(scale_x6, scale_y5)
# Finally, shader passes need to know the value of geom_max_aspect_ratio used to
# calculate scale_y5 (among other values):
# calculate scale_y7 (among other values):
# 1.) geom_max_aspect_ratio = (geom_max_aspect_ratio used to calculate scale_y5)
shaders = "14"
shaders = "16"
# NTSC Shader Passes
shader0 = "../ntsc/shaders/ntsc-adaptive/ntsc-pass1.slang"
shader1 = "../ntsc/shaders/ntsc-adaptive/ntsc-pass2.slang"
shader0 = ../stock.slang
alias0 = PrePass0
scale_type0 = source
scale_x0 = 4.0
filter_linear0 = false
scale_y0 = 1.0
float_framebuffer0 = true
scale_type1 = source
scale_x1 = 0.5
shader1 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass1.slang
alias1 = NPass1
scale_type_x1 = source
scale_type_y1 = source
scale_x1 = 4.0
scale_y1 = 1.0
filter_linear1 = true
float_framebuffer1 = true
filter_linear1 = false
shader2 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass2.slang
filter_linear2 = true
float_framebuffer2 = true
scale_type2 = source
scale_x2 = 0.5
scale_y2 = 1.0
shader3 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass3.slang
filter_linear3 = true
scale_type3 = source
scale_x3 = 1.0
scale_y3 = 1.0
# Set an identifier, filename, and sampling traits for the phosphor mask texture.
# Load an aperture grille, slot mask, and an EDP shadow mask, and load a small
@ -64,32 +75,32 @@ mask_shadow_texture_small_mipmap = "false" # Mipmapping causes artifacts with m
mask_shadow_texture_large_mipmap = "true" # Essential for hardware-resized masks
# Pass2: Linearize the input based on CRT gamma and bob interlaced fields.
# Pass 4: Linearize the input based on CRT gamma and bob interlaced fields.
# (Bobbing ensures we can immediately blur without getting artifacts.)
shader2 = "../crt/shaders/crt-royale/src/crt-royale-first-pass-linearize-crt-gamma-bob-fields.slang"
alias2 = "ORIG_LINEARIZED"
filter_linear2 = "false"
scale_type2 = "source"
scale2 = "1.0"
srgb_framebuffer2 = "true"
shader4 = "../crt/shaders/crt-royale/src/crt-royale-first-pass-linearize-crt-gamma-bob-fields.slang"
alias4 = "ORIG_LINEARIZED"
filter_linear4 = "false"
scale_type4 = "source"
scale4 = "1.0"
srgb_framebuffer4 = "true"
# Pass3: Resample interlaced (and misconverged) scanlines vertically.
# Pass 5: Resample interlaced (and misconverged) scanlines vertically.
# Separating vertical/horizontal scanline sampling is faster: It lets us
# consider more scanlines while calculating weights for fewer pixels, and
# it reduces our samples from vertical*horizontal to vertical+horizontal.
# This has to come right after ORIG_LINEARIZED, because there's no
# "original_source" scale_type we can use later.
shader3 = "../crt/shaders/crt-royale/src/crt-royale-scanlines-vertical-interlacing.slang"
alias3 = "VERTICAL_SCANLINES"
filter_linear3 = "true"
scale_type_x3 = "source"
scale_x3 = "1.0"
scale_type_y3 = "viewport"
scale_y3 = "1.0"
#float_framebuffer3 = "true"
srgb_framebuffer3 = "true"
shader5 = "../crt/shaders/crt-royale/src/crt-royale-scanlines-vertical-interlacing.slang"
alias5 = "VERTICAL_SCANLINES"
filter_linear5 = "true"
scale_type_x5 = "source"
scale_x5 = "1.0"
scale_type_y5 = "viewport"
scale_y5 = "1.0"
#float_framebuffer5 = "true"
srgb_framebuffer5 = "true"
# Pass4: Do a small resize blur of ORIG_LINEARIZED at an absolute size, and
# Pass 6: Do a small resize blur of ORIG_LINEARIZED at an absolute size, and
# account for convergence offsets. We want to blur a predictable portion of the
# screen to match the phosphor bloom, and absolute scale works best for
# reliable results with a fixed-size bloom. Picking a scale is tricky:
@ -99,44 +110,44 @@ srgb_framebuffer3 = "true"
# b.) 320x240 works well for the "real bloom" version: It's 1-1.5% faster, and
# the only noticeable visual difference is a larger halation spread (which
# may be a good thing for people who like to crank it up).
# Note the 4:3 aspect ratio assumes the input has cropped geom_overscan (so it's
# *intended* for an ~4:3 aspect ratio).
shader4 = "../crt/shaders/crt-royale/src/crt-royale-bloom-approx.slang"
alias4 = "BLOOM_APPROX"
filter_linear4 = "true"
scale_type4 = "absolute"
scale_x4 = "320"
scale_y4 = "240"
srgb_framebuffer4 = "true"
# Note the 4:5 aspect ratio assumes the input has cropped geom_overscan (so it's
# *intended* for an ~4:5 aspect ratio).
shader6 = "../crt/shaders/crt-royale/src/crt-royale-bloom-approx.slang"
alias6 = "BLOOM_APPROX"
filter_linear6 = "true"
scale_type6 = "absolute"
scale_x6 = "320"
scale_y6 = "240"
srgb_framebuffer6 = "true"
# Pass5: Vertically blur the input for halation and refractive diffusion.
# Pass 7: Vertically blur the input for halation and refractive diffusion.
# Base this on BLOOM_APPROX: This blur should be small and fast, and blurring
# a constant portion of the screen is probably physically correct if the
# viewport resolution is proportional to the simulated CRT size.
shader5 = "../blurs/shaders/royale/blur9fast-vertical.slang"
filter_linear5 = "true"
scale_type5 = "source"
scale5 = "1.0"
srgb_framebuffer5 = "true"
shader7 = "../blurs/shaders/royale/blur9fast-vertical.slang"
filter_linear7 = "true"
scale_type7 = "source"
scale7 = "1.0"
srgb_framebuffer7 = "true"
# Pass6: Horizontally blur the input for halation and refractive diffusion.
# Pass 8: Horizontally blur the input for halation and refractive diffusion.
# Note: Using a one-pass 9x9 blur is about 1% slower.
shader6 = "../blurs/shaders/royale/blur9fast-horizontal.slang"
alias6 = "HALATION_BLUR"
filter_linear6 = "true"
scale_type6 = "source"
scale6 = "1.0"
srgb_framebuffer6 = "true"
shader8 = "../blurs/shaders/royale/blur9fast-horizontal.slang"
alias8 = "HALATION_BLUR"
filter_linear8 = "true"
scale_type8 = "source"
scale8 = "1.0"
srgb_framebuffer8 = "true"
# Pass7: Lanczos-resize the phosphor mask vertically. Set the absolute
# scale_x7 == mask_texture_small_size.x (see IMPORTANT above). Larger scales
# Pass 9: Lanczos-resize the phosphor mask vertically. Set the absolute
# scale_x9 == mask_texture_small_size.x (see IMPORTANT above). Larger scales
# will blur, and smaller scales could get nasty. The vertical size must be
# based on the viewport size and calculated carefully to avoid artifacts later.
# First calculate the minimum number of mask tiles we need to draw.
# Since curvature is computed after the scanline masking pass:
# num_resized_mask_tiles = 2.0;
# If curvature were computed in the scanline masking pass (it's not):
# max_mask_texel_border = ~3.0 * (1/3.0 + 4.0*sqrt(2.0) + 0.5 + 1.0);
# max_mask_texel_border = ~3.0 * (1/3.0 + 4.0*sqrt(2.0) + 0.7 + 1.0);
# max_mask_tile_border = max_mask_texel_border/
# (min_resized_phosphor_triad_size * mask_triads_per_tile);
# num_resized_mask_tiles = max(2.0, 1.0 + max_mask_tile_border * 2.0);
@ -146,7 +157,7 @@ srgb_framebuffer6 = "true"
# to relate them to vertical resolution. The widest we expect is:
# geom_max_aspect_ratio = 4.0/3.0 # Note: Shader passes need to know this!
# The fewer triads we tile across the screen, the larger each triad will be as a
# fraction of the viewport size, and the larger scale_y5 must be to draw a full
# fraction of the viewport size, and the larger scale_y7 must be to draw a full
# num_resized_mask_tiles. Therefore, we must decide the smallest number of
# triads we'll guarantee can be displayed on screen. We'll set this according
# to 3-pixel triads at 768p resolution (the lowest anyone's likely to use):
@ -154,70 +165,70 @@ srgb_framebuffer6 = "true"
# Now calculate the viewport scale that ensures we can draw resized_mask_tiles:
# min_scale_x = resized_mask_tiles * mask_triads_per_tile /
# min_allowed_viewport_triads
# scale_y7 = geom_max_aspect_ratio * min_scale_x
# scale_y9 = geom_max_aspect_ratio * min_scale_x
# # Some code might depend on equal scales:
# scale_x8 = scale_y7
# scale_x10 = scale_y7
# Given our default geom_max_aspect_ratio and min_allowed_viewport_triads:
# scale_y7 = 4.0/3.0 * 2.0/(341.33333 / 8.0) = 0.0625
# scale_y9 = 4.0/3.0 * 2.0/(341.33335 / 8.0) = 0.0625
# IMPORTANT: The scales MUST be calculated in this way. If you wish to change
# geom_max_aspect_ratio, update that constant in user-cgp-constants.h!
shader7 = "../crt/shaders/crt-royale/src/crt-royale-mask-resize-vertical.slang"
filter_linear7 = "true"
scale_type_x7 = "absolute"
scale_x7 = "64"
scale_type_y7 = "viewport"
scale_y7 = "0.0625" # Safe for >= 341.333 horizontal triads at viewport size
#srgb_framebuffer7 = "false" # mask_texture is already assumed linear
shader9 = "../crt/shaders/crt-royale/src/crt-royale-mask-resize-vertical.slang"
filter_linear9 = "true"
scale_type_x9 = "absolute"
scale_x9 = "64"
scale_type_y9 = "viewport"
scale_y9 = "0.0625" # Safe for >= 341.333 horizontal triads at viewport size
#srgb_framebuffer9 = "false" # mask_texture is already assumed linear
# Pass8: Lanczos-resize the phosphor mask horizontally. scale_x8 = scale_y7.
# Pass 10: Lanczos-resize the phosphor mask horizontally. scale_x10 = scale_y7.
# TODO: Check again if the shaders actually require equal scales.
shader8 = "../crt/shaders/crt-royale/src/crt-royale-mask-resize-horizontal.slang"
alias8 = "MASK_RESIZE"
filter_linear8 = "false"
scale_type_x8 = "viewport"
scale_x8 = "0.0625"
scale_type_y8 = "source"
scale_y8 = "1.0"
#srgb_framebuffer8 = "false" # mask_texture is already assumed linear
shader10 = "../crt/shaders/crt-royale/src/crt-royale-mask-resize-horizontal.slang"
alias10 = "MASK_RESIZE"
filter_linear10 = "false"
scale_type_x10 = "viewport"
scale_x10 = "0.0625"
scale_type_y10 = "source"
scale_y10 = "1.0"
#srgb_framebuffer10 = "false" # mask_texture is already assumed linear
# Pass9: Resample (misconverged) scanlines horizontally, apply halation, and
# Pass 11: Resample (misconverged) scanlines horizontally, apply halation, and
# apply the phosphor mask.
shader9 = "../crt/shaders/crt-royale/src/crt-royale-scanlines-horizontal-apply-mask.slang"
alias9 = "MASKED_SCANLINES"
filter_linear9 = "true" # This could just as easily be nearest neighbor.
scale_type9 = "viewport"
scale9 = "1.0"
#float_framebuffer9 = "true"
srgb_framebuffer9 = "true"
# Pass 10: Compute a brightpass. This will require reading the final mask.
shader10 = "../crt/shaders/crt-royale/src/crt-royale-brightpass.slang"
alias10 = "BRIGHTPASS"
filter_linear10 = "true" # This could just as easily be nearest neighbor.
scale_type10 = "viewport"
scale10 = "1.0"
srgb_framebuffer10 = "true"
# Pass 11: Blur the brightpass vertically
shader11 = "../crt/shaders/crt-royale/src/crt-royale-bloom-vertical.slang"
shader11 = "../crt/shaders/crt-royale/src/crt-royale-scanlines-horizontal-apply-mask.slang"
alias11 = "MASKED_SCANLINES"
filter_linear11 = "true" # This could just as easily be nearest neighbor.
scale_type11 = "source"
scale_type11 = "viewport"
scale11 = "1.0"
#float_framebuffer11 = "true"
srgb_framebuffer11 = "true"
# Pass 12: Blur the brightpass horizontally and combine it with the dimpass:
shader12 = "../crt/shaders/crt-royale/src/crt-royale-bloom-horizontal-reconstitute.slang"
filter_linear12 = "true"
scale_type12 = "source"
# Pass 12: Compute a brightpass. This will require reading the final mask.
shader12 = "../crt/shaders/crt-royale/src/crt-royale-brightpass.slang"
alias12 = "BRIGHTPASS"
filter_linear12 = "true" # This could just as easily be nearest neighbor.
scale_type12 = "viewport"
scale12 = "1.0"
srgb_framebuffer12 = "true"
# Pass 13: Compute curvature/AA:
shader13 = "../crt/shaders/crt-royale/src/crt-royale-geometry-aa-last-pass.slang"
filter_linear13 = "true"
scale_type13 = "viewport"
mipmap_input13 = "true"
texture_wrap_mode13 = "clamp_to_edge"
# Pass 13: Blur the brightpass vertically
shader13 = "../crt/shaders/crt-royale/src/crt-royale-bloom-vertical.slang"
filter_linear13 = "true" # This could just as easily be nearest neighbor.
scale_type13 = "source"
scale13 = "1.0"
srgb_framebuffer13 = "true"
# Pass 14: Blur the brightpass horizontally and combine it with the dimpass:
shader14 = "../crt/shaders/crt-royale/src/crt-royale-bloom-horizontal-reconstitute.slang"
filter_linear14 = "true"
scale_type14 = "source"
scale14 = "1.0"
srgb_framebuffer14 = "true"
# Pass 15: Compute curvature/AA:
shader15 = "../crt/shaders/crt-royale/src/crt-royale-geometry-aa-last-pass.slang"
filter_linear15 = "true"
scale_type15 = "viewport"
mipmap_input15 = "true"
texture_wrap_mode15 = "clamp_to_edge"
parameters = "quality"
quality = 1.0

View file

@ -13,25 +13,36 @@
# 1.) bloom_approx_scale_x = scale_x2
# 2.) mask_resize_viewport_scale = float2(scale_x6, scale_y5)
# Finally, shader passes need to know the value of geom_max_aspect_ratio used to
# calculate scale_y5 (among other values):
# calculate scale_y7 (among other values):
# 1.) geom_max_aspect_ratio = (geom_max_aspect_ratio used to calculate scale_y5)
shaders = "14"
shaders = "16"
# NTSC Shader Passes
shader0 = "../ntsc/shaders/ntsc-adaptive/ntsc-pass1.slang"
shader1 = "../ntsc/shaders/ntsc-adaptive/ntsc-pass2.slang"
shader0 = ../stock.slang
alias0 = PrePass0
scale_type0 = source
scale_x0 = 4.0
filter_linear0 = false
scale_y0 = 1.0
float_framebuffer0 = true
scale_type1 = source
scale_x1 = 0.5
shader1 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass1.slang
alias1 = NPass1
scale_type_x1 = source
scale_type_y1 = source
scale_x1 = 4.0
scale_y1 = 1.0
filter_linear1 = true
float_framebuffer1 = true
filter_linear1 = false
shader2 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass2.slang
filter_linear2 = true
float_framebuffer2 = true
scale_type2 = source
scale_x2 = 0.5
scale_y2 = 1.0
shader3 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass3.slang
filter_linear3 = true
scale_type3 = source
scale_x3 = 1.0
scale_y3 = 1.0
# Set an identifier, filename, and sampling traits for the phosphor mask texture.
# Load an aperture grille, slot mask, and an EDP shadow mask, and load a small
@ -64,32 +75,32 @@ mask_shadow_texture_small_mipmap = "false" # Mipmapping causes artifacts with m
mask_shadow_texture_large_mipmap = "true" # Essential for hardware-resized masks
# Pass2: Linearize the input based on CRT gamma and bob interlaced fields.
# Pass 4: Linearize the input based on CRT gamma and bob interlaced fields.
# (Bobbing ensures we can immediately blur without getting artifacts.)
shader2 = "../crt/shaders/crt-royale/src/crt-royale-first-pass-linearize-crt-gamma-bob-fields.slang"
alias2 = "ORIG_LINEARIZED"
filter_linear2 = "false"
scale_type2 = "source"
scale2 = "1.0"
srgb_framebuffer2 = "true"
shader4 = "../crt/shaders/crt-royale/src/crt-royale-first-pass-linearize-crt-gamma-bob-fields.slang"
alias4 = "ORIG_LINEARIZED"
filter_linear4 = "false"
scale_type4 = "source"
scale4 = "1.0"
srgb_framebuffer4 = "true"
# Pass3: Resample interlaced (and misconverged) scanlines vertically.
# Pass 5: Resample interlaced (and misconverged) scanlines vertically.
# Separating vertical/horizontal scanline sampling is faster: It lets us
# consider more scanlines while calculating weights for fewer pixels, and
# it reduces our samples from vertical*horizontal to vertical+horizontal.
# This has to come right after ORIG_LINEARIZED, because there's no
# "original_source" scale_type we can use later.
shader3 = "../crt/shaders/crt-royale/src/crt-royale-scanlines-vertical-interlacing.slang"
alias3 = "VERTICAL_SCANLINES"
filter_linear3 = "true"
scale_type_x3 = "source"
scale_x3 = "1.0"
scale_type_y3 = "viewport"
scale_y3 = "1.0"
#float_framebuffer3 = "true"
srgb_framebuffer3 = "true"
shader5 = "../crt/shaders/crt-royale/src/crt-royale-scanlines-vertical-interlacing.slang"
alias5 = "VERTICAL_SCANLINES"
filter_linear5 = "true"
scale_type_x5 = "source"
scale_x5 = "1.0"
scale_type_y5 = "viewport"
scale_y5 = "1.0"
#float_framebuffer5 = "true"
srgb_framebuffer5 = "true"
# Pass4: Do a small resize blur of ORIG_LINEARIZED at an absolute size, and
# Pass 6: Do a small resize blur of ORIG_LINEARIZED at an absolute size, and
# account for convergence offsets. We want to blur a predictable portion of the
# screen to match the phosphor bloom, and absolute scale works best for
# reliable results with a fixed-size bloom. Picking a scale is tricky:
@ -99,44 +110,44 @@ srgb_framebuffer3 = "true"
# b.) 320x240 works well for the "real bloom" version: It's 1-1.5% faster, and
# the only noticeable visual difference is a larger halation spread (which
# may be a good thing for people who like to crank it up).
# Note the 4:3 aspect ratio assumes the input has cropped geom_overscan (so it's
# *intended* for an ~4:3 aspect ratio).
shader4 = "../crt/shaders/crt-royale/src/crt-royale-bloom-approx.slang"
alias4 = "BLOOM_APPROX"
filter_linear4 = "true"
scale_type4 = "absolute"
scale_x4 = "320"
scale_y4 = "240"
srgb_framebuffer4 = "true"
# Note the 4:5 aspect ratio assumes the input has cropped geom_overscan (so it's
# *intended* for an ~4:5 aspect ratio).
shader6 = "../crt/shaders/crt-royale/src/crt-royale-bloom-approx.slang"
alias6 = "BLOOM_APPROX"
filter_linear6 = "true"
scale_type6 = "absolute"
scale_x6 = "320"
scale_y6 = "240"
srgb_framebuffer6 = "true"
# Pass5: Vertically blur the input for halation and refractive diffusion.
# Pass 7: Vertically blur the input for halation and refractive diffusion.
# Base this on BLOOM_APPROX: This blur should be small and fast, and blurring
# a constant portion of the screen is probably physically correct if the
# viewport resolution is proportional to the simulated CRT size.
shader5 = "../blurs/shaders/royale/blur9fast-vertical.slang"
filter_linear5 = "true"
scale_type5 = "source"
scale5 = "1.0"
srgb_framebuffer5 = "true"
shader7 = "../blurs/shaders/royale/blur9fast-vertical.slang"
filter_linear7 = "true"
scale_type7 = "source"
scale7 = "1.0"
srgb_framebuffer7 = "true"
# Pass6: Horizontally blur the input for halation and refractive diffusion.
# Pass 8: Horizontally blur the input for halation and refractive diffusion.
# Note: Using a one-pass 9x9 blur is about 1% slower.
shader6 = "../blurs/shaders/royale/blur9fast-horizontal.slang"
alias6 = "HALATION_BLUR"
filter_linear6 = "true"
scale_type6 = "source"
scale6 = "1.0"
srgb_framebuffer6 = "true"
shader8 = "../blurs/shaders/royale/blur9fast-horizontal.slang"
alias8 = "HALATION_BLUR"
filter_linear8 = "true"
scale_type8 = "source"
scale8 = "1.0"
srgb_framebuffer8 = "true"
# Pass7: Lanczos-resize the phosphor mask vertically. Set the absolute
# scale_x7 == mask_texture_small_size.x (see IMPORTANT above). Larger scales
# Pass 9: Lanczos-resize the phosphor mask vertically. Set the absolute
# scale_x9 == mask_texture_small_size.x (see IMPORTANT above). Larger scales
# will blur, and smaller scales could get nasty. The vertical size must be
# based on the viewport size and calculated carefully to avoid artifacts later.
# First calculate the minimum number of mask tiles we need to draw.
# Since curvature is computed after the scanline masking pass:
# num_resized_mask_tiles = 2.0;
# If curvature were computed in the scanline masking pass (it's not):
# max_mask_texel_border = ~3.0 * (1/3.0 + 4.0*sqrt(2.0) + 0.5 + 1.0);
# max_mask_texel_border = ~3.0 * (1/3.0 + 4.0*sqrt(2.0) + 0.7 + 1.0);
# max_mask_tile_border = max_mask_texel_border/
# (min_resized_phosphor_triad_size * mask_triads_per_tile);
# num_resized_mask_tiles = max(2.0, 1.0 + max_mask_tile_border * 2.0);
@ -146,7 +157,7 @@ srgb_framebuffer6 = "true"
# to relate them to vertical resolution. The widest we expect is:
# geom_max_aspect_ratio = 4.0/3.0 # Note: Shader passes need to know this!
# The fewer triads we tile across the screen, the larger each triad will be as a
# fraction of the viewport size, and the larger scale_y5 must be to draw a full
# fraction of the viewport size, and the larger scale_y7 must be to draw a full
# num_resized_mask_tiles. Therefore, we must decide the smallest number of
# triads we'll guarantee can be displayed on screen. We'll set this according
# to 3-pixel triads at 768p resolution (the lowest anyone's likely to use):
@ -154,70 +165,70 @@ srgb_framebuffer6 = "true"
# Now calculate the viewport scale that ensures we can draw resized_mask_tiles:
# min_scale_x = resized_mask_tiles * mask_triads_per_tile /
# min_allowed_viewport_triads
# scale_y7 = geom_max_aspect_ratio * min_scale_x
# scale_y9 = geom_max_aspect_ratio * min_scale_x
# # Some code might depend on equal scales:
# scale_x8 = scale_y7
# scale_x10 = scale_y7
# Given our default geom_max_aspect_ratio and min_allowed_viewport_triads:
# scale_y7 = 4.0/3.0 * 2.0/(341.33333 / 8.0) = 0.0625
# scale_y9 = 4.0/3.0 * 2.0/(341.33335 / 8.0) = 0.0625
# IMPORTANT: The scales MUST be calculated in this way. If you wish to change
# geom_max_aspect_ratio, update that constant in user-cgp-constants.h!
shader7 = "../crt/shaders/crt-royale/src/crt-royale-mask-resize-vertical.slang"
filter_linear7 = "true"
scale_type_x7 = "absolute"
scale_x7 = "64"
scale_type_y7 = "viewport"
scale_y7 = "0.0625" # Safe for >= 341.333 horizontal triads at viewport size
#srgb_framebuffer7 = "false" # mask_texture is already assumed linear
shader9 = "../crt/shaders/crt-royale/src/crt-royale-mask-resize-vertical.slang"
filter_linear9 = "true"
scale_type_x9 = "absolute"
scale_x9 = "64"
scale_type_y9 = "viewport"
scale_y9 = "0.0625" # Safe for >= 341.333 horizontal triads at viewport size
#srgb_framebuffer9 = "false" # mask_texture is already assumed linear
# Pass8: Lanczos-resize the phosphor mask horizontally. scale_x8 = scale_y7.
# Pass 10: Lanczos-resize the phosphor mask horizontally. scale_x10 = scale_y7.
# TODO: Check again if the shaders actually require equal scales.
shader8 = "../crt/shaders/crt-royale/src/crt-royale-mask-resize-horizontal.slang"
alias8 = "MASK_RESIZE"
filter_linear8 = "false"
scale_type_x8 = "viewport"
scale_x8 = "0.0625"
scale_type_y8 = "source"
scale_y8 = "1.0"
#srgb_framebuffer8 = "false" # mask_texture is already assumed linear
shader10 = "../crt/shaders/crt-royale/src/crt-royale-mask-resize-horizontal.slang"
alias10 = "MASK_RESIZE"
filter_linear10 = "false"
scale_type_x10 = "viewport"
scale_x10 = "0.0625"
scale_type_y10 = "source"
scale_y10 = "1.0"
#srgb_framebuffer10 = "false" # mask_texture is already assumed linear
# Pass9: Resample (misconverged) scanlines horizontally, apply halation, and
# Pass 11: Resample (misconverged) scanlines horizontally, apply halation, and
# apply the phosphor mask.
shader9 = "../crt/shaders/crt-royale/src/crt-royale-scanlines-horizontal-apply-mask.slang"
alias9 = "MASKED_SCANLINES"
filter_linear9 = "true" # This could just as easily be nearest neighbor.
scale_type9 = "viewport"
scale9 = "1.0"
#float_framebuffer9 = "true"
srgb_framebuffer9 = "true"
# Pass 10: Compute a brightpass. This will require reading the final mask.
shader10 = "../crt/shaders/crt-royale/src/crt-royale-brightpass.slang"
alias10 = "BRIGHTPASS"
filter_linear10 = "true" # This could just as easily be nearest neighbor.
scale_type10 = "viewport"
scale10 = "1.0"
srgb_framebuffer10 = "true"
# Pass 11: Blur the brightpass vertically
shader11 = "../crt/shaders/crt-royale/src/crt-royale-bloom-vertical.slang"
shader11 = "../crt/shaders/crt-royale/src/crt-royale-scanlines-horizontal-apply-mask.slang"
alias11 = "MASKED_SCANLINES"
filter_linear11 = "true" # This could just as easily be nearest neighbor.
scale_type11 = "source"
scale_type11 = "viewport"
scale11 = "1.0"
#float_framebuffer11 = "true"
srgb_framebuffer11 = "true"
# Pass 12: Blur the brightpass horizontally and combine it with the dimpass:
shader12 = "../crt/shaders/crt-royale/src/crt-royale-bloom-horizontal-reconstitute.slang"
filter_linear12 = "true"
scale_type12 = "source"
# Pass 12: Compute a brightpass. This will require reading the final mask.
shader12 = "../crt/shaders/crt-royale/src/crt-royale-brightpass.slang"
alias12 = "BRIGHTPASS"
filter_linear12 = "true" # This could just as easily be nearest neighbor.
scale_type12 = "viewport"
scale12 = "1.0"
srgb_framebuffer12 = "true"
# Pass 13: Compute curvature/AA:
shader13 = "../crt/shaders/crt-royale/src/crt-royale-geometry-aa-last-pass.slang"
filter_linear13 = "true"
scale_type13 = "viewport"
mipmap_input13 = "true"
texture_wrap_mode13 = "clamp_to_edge"
# Pass 13: Blur the brightpass vertically
shader13 = "../crt/shaders/crt-royale/src/crt-royale-bloom-vertical.slang"
filter_linear13 = "true" # This could just as easily be nearest neighbor.
scale_type13 = "source"
scale13 = "1.0"
srgb_framebuffer13 = "true"
# Pass 14: Blur the brightpass horizontally and combine it with the dimpass:
shader14 = "../crt/shaders/crt-royale/src/crt-royale-bloom-horizontal-reconstitute.slang"
filter_linear14 = "true"
scale_type14 = "source"
scale14 = "1.0"
srgb_framebuffer14 = "true"
# Pass 15: Compute curvature/AA:
shader15 = "../crt/shaders/crt-royale/src/crt-royale-geometry-aa-last-pass.slang"
filter_linear15 = "true"
scale_type15 = "viewport"
mipmap_input15 = "true"
texture_wrap_mode15 = "clamp_to_edge"
parameters = "quality"
quality = 0.0

View file

@ -1,50 +1,61 @@
shaders = 7
shaders = 9
shader0 = ../ntsc/shaders/ntsc-adaptive/ntsc-pass1.slang
scale_type0 = source
filter_linear0 = false
scale_x0 = 4.0
scale_y0 = 1.0
frame_count_mod0 = 2
float_framebuffer0 = true
shader0 = ../stock.slang
alias0 = PrePass0
shader1 = ../ntsc/shaders/ntsc-adaptive/ntsc-pass2.slang
filter_linear1 = false
scale_type1 = source
scale_x1 = 0.5
shader1 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass1.slang
alias1 = NPass1
scale_type_x1 = source
scale_type_y1 = source
scale_x1 = 4.0
scale_y1 = 1.0
float_framebuffer1 = true
filter_linear1 = false
shader2 = ../crt/shaders/phosphorlut/scanlines-interlace-linearize.slang
alias2 = firstPass
scale2 = 2.0
shader2 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass2.slang
filter_linear2 = true
float_framebuffer2 = true
scale_type2 = source
srgb_framebuffer2 = true
filter_linear2 = false
scale_x2 = 0.5
scale_y2 = 1.0
shader3 = ../blurs/shaders/royale/blur5fast-vertical.slang
scale_type3 = source
scale3 = 1.0
srgb_framebuffer3 = true
shader3 = ../crt/shaders/guest/advanced/ntsc/ntsc-pass3.slang
filter_linear3 = true
alias3 = blurPassV
scale_type3 = source
scale_x3 = 1.0
scale_y3 = 1.0
shader4 = ../blurs/shaders/royale/blur5fast-horizontal.slang
alias4 = blurPass
filter_linear4 = true
scale4 = 1.0
shader4 = ../crt/shaders/phosphorlut/scanlines-interlace-linearize.slang
alias4 = firstPass
scale4 = 2.0
scale_type4 = source
srgb_framebuffer4 = true
filter_linear4 = false
shader5 = ../crt/shaders/phosphorlut/phosphorlut-pass0.slang
alias5 = phosphorPass
filter_linear5 = true
shader5 = ../blurs/shaders/royale/blur5fast-vertical.slang
scale_type5 = source
scale_x5 = 4.0
scale_y5 = 2.0
scale5 = 1.0
srgb_framebuffer5 = true
filter_linear5 = true
alias5 = blurPassV
shader6 = ../crt/shaders/phosphorlut/phosphorlut-pass1.slang
shader6 = ../blurs/shaders/royale/blur5fast-horizontal.slang
alias6 = blurPass
filter_linear6 = true
scale6 = 1.0
scale_type6 = source
srgb_framebuffer6 = true
shader7 = ../crt/shaders/phosphorlut/phosphorlut-pass0.slang
alias7 = phosphorPass
filter_linear7 = true
scale_type7 = source
scale_x7 = 4.0
scale_y7 = 2.0
srgb_framebuffer7 = true
shader8 = ../crt/shaders/phosphorlut/phosphorlut-pass1.slang
filter_linear8 = true
parameters = "diffusion;PHOSPHOR_SCALE_X"
diffusion = 0.6