V0.1.2, Updated and added some dithering algorithms to the program
This commit is contained in:
parent
bee56ff8ae
commit
8382078eb4
|
|
@ -5,10 +5,24 @@ struct RenderParameters {
|
||||||
float brightness;
|
float brightness;
|
||||||
float contrast;
|
float contrast;
|
||||||
float pixelScale;
|
float pixelScale;
|
||||||
int algorithm; // 0: None, 1: Bayer 8x8, 2: Bayer 4x4
|
int algorithm; // 0: None, 1: Bayer 2x2, 2: Bayer 4x4, 3: Bayer 8x8, 4: Cluster 4x4, 5: Cluster 8x8, 6: Blue Noise
|
||||||
int isGrayscale;
|
int isGrayscale;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bayer 2x2 Matrix
|
||||||
|
constant float bayer2x2[2][2] = {
|
||||||
|
{0.0/4.0, 2.0/4.0},
|
||||||
|
{3.0/4.0, 1.0/4.0}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bayer 4x4 Matrix
|
||||||
|
constant float bayer4x4[4][4] = {
|
||||||
|
{ 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0 },
|
||||||
|
{12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0 },
|
||||||
|
{ 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0 },
|
||||||
|
{15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0 }
|
||||||
|
};
|
||||||
|
|
||||||
// Bayer 8x8 Matrix
|
// Bayer 8x8 Matrix
|
||||||
constant float bayer8x8[8][8] = {
|
constant float bayer8x8[8][8] = {
|
||||||
{ 0.0/64.0, 32.0/64.0, 8.0/64.0, 40.0/64.0, 2.0/64.0, 34.0/64.0, 10.0/64.0, 42.0/64.0 },
|
{ 0.0/64.0, 32.0/64.0, 8.0/64.0, 40.0/64.0, 2.0/64.0, 34.0/64.0, 10.0/64.0, 42.0/64.0 },
|
||||||
|
|
@ -21,12 +35,36 @@ constant float bayer8x8[8][8] = {
|
||||||
{63.0/64.0, 31.0/64.0, 55.0/64.0, 23.0/64.0, 61.0/64.0, 29.0/64.0, 53.0/64.0, 21.0/64.0 }
|
{63.0/64.0, 31.0/64.0, 55.0/64.0, 23.0/64.0, 61.0/64.0, 29.0/64.0, 53.0/64.0, 21.0/64.0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bayer 4x4 Matrix
|
// Cluster 4x4 Matrix
|
||||||
constant float bayer4x4[4][4] = {
|
constant float cluster4x4[4][4] = {
|
||||||
{ 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0 },
|
{12.0/16.0, 5.0/16.0, 6.0/16.0, 13.0/16.0},
|
||||||
{12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0 },
|
{ 4.0/16.0, 0.0/16.0, 1.0/16.0, 7.0/16.0},
|
||||||
{ 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0 },
|
{11.0/16.0, 3.0/16.0, 2.0/16.0, 8.0/16.0},
|
||||||
{15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0 }
|
{15.0/16.0, 10.0/16.0, 9.0/16.0, 14.0/16.0}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cluster 8x8 Matrix
|
||||||
|
constant float cluster8x8[8][8] = {
|
||||||
|
{24.0/64.0, 10.0/64.0, 12.0/64.0, 26.0/64.0, 35.0/64.0, 47.0/64.0, 49.0/64.0, 37.0/64.0},
|
||||||
|
{ 8.0/64.0, 0.0/64.0, 2.0/64.0, 14.0/64.0, 45.0/64.0, 59.0/64.0, 61.0/64.0, 51.0/64.0},
|
||||||
|
{22.0/64.0, 6.0/64.0, 4.0/64.0, 20.0/64.0, 43.0/64.0, 57.0/64.0, 63.0/64.0, 53.0/64.0},
|
||||||
|
{30.0/64.0, 18.0/64.0, 16.0/64.0, 28.0/64.0, 33.0/64.0, 41.0/64.0, 55.0/64.0, 39.0/64.0},
|
||||||
|
{34.0/64.0, 46.0/64.0, 48.0/64.0, 36.0/64.0, 25.0/64.0, 11.0/64.0, 13.0/64.0, 27.0/64.0},
|
||||||
|
{44.0/64.0, 58.0/64.0, 60.0/64.0, 50.0/64.0, 9.0/64.0, 1.0/64.0, 3.0/64.0, 15.0/64.0},
|
||||||
|
{42.0/64.0, 56.0/64.0, 62.0/64.0, 52.0/64.0, 23.0/64.0, 7.0/64.0, 5.0/64.0, 21.0/64.0},
|
||||||
|
{32.0/64.0, 40.0/64.0, 54.0/64.0, 38.0/64.0, 31.0/64.0, 19.0/64.0, 17.0/64.0, 29.0/64.0}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Blue Noise 8x8 (Approx)
|
||||||
|
constant float blueNoise8x8[8][8] = {
|
||||||
|
{52.0/64.0, 21.0/64.0, 58.0/64.0, 10.0/64.0, 45.0/64.0, 33.0/64.0, 56.0/64.0, 17.0/64.0},
|
||||||
|
{ 4.0/64.0, 38.0/64.0, 28.0/64.0, 51.0/64.0, 5.0/64.0, 22.0/64.0, 40.0/64.0, 62.0/64.0},
|
||||||
|
{61.0/64.0, 12.0/64.0, 48.0/64.0, 14.0/64.0, 55.0/64.0, 36.0/64.0, 7.0/64.0, 31.0/64.0},
|
||||||
|
{32.0/64.0, 43.0/64.0, 2.0/64.0, 46.0/64.0, 25.0/64.0, 63.0/64.0, 19.0/64.0, 50.0/64.0},
|
||||||
|
{16.0/64.0, 53.0/64.0, 23.0/64.0, 60.0/64.0, 9.0/64.0, 47.0/64.0, 29.0/64.0, 6.0/64.0},
|
||||||
|
{44.0/64.0, 27.0/64.0, 39.0/64.0, 34.0/64.0, 54.0/64.0, 13.0/64.0, 59.0/64.0, 26.0/64.0},
|
||||||
|
{ 8.0/64.0, 57.0/64.0, 18.0/64.0, 1.0/64.0, 42.0/64.0, 30.0/64.0, 3.0/64.0, 49.0/64.0},
|
||||||
|
{35.0/64.0, 24.0/64.0, 0.0/64.0, 41.0/64.0, 15.0/64.0, 52.0/64.0, 20.0/64.0, 37.0/64.0}
|
||||||
};
|
};
|
||||||
|
|
||||||
kernel void ditherShader(texture2d<float, access::read> inputTexture [[texture(0)]],
|
kernel void ditherShader(texture2d<float, access::read> inputTexture [[texture(0)]],
|
||||||
|
|
@ -38,22 +76,21 @@ kernel void ditherShader(texture2d<float, access::read> inputTexture [[texture(0
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Pixelation (Downsampling)
|
// 1. Pixelation
|
||||||
float scale = max(1.0, params.pixelScale);
|
float scale = max(1.0, params.pixelScale);
|
||||||
uint2 sourceCoord = uint2(floor(float(gid.x) / scale) * scale, floor(float(gid.y) / scale) * scale);
|
uint2 sourceCoord = uint2(floor(float(gid.x) / scale) * scale, floor(float(gid.y) / scale) * scale);
|
||||||
|
|
||||||
// Clamp to texture bounds
|
|
||||||
sourceCoord.x = min(sourceCoord.x, inputTexture.get_width() - 1);
|
sourceCoord.x = min(sourceCoord.x, inputTexture.get_width() - 1);
|
||||||
sourceCoord.y = min(sourceCoord.y, inputTexture.get_height() - 1);
|
sourceCoord.y = min(sourceCoord.y, inputTexture.get_height() - 1);
|
||||||
|
|
||||||
float4 color = inputTexture.read(sourceCoord);
|
float4 color = inputTexture.read(sourceCoord);
|
||||||
|
|
||||||
// 2. Color Adjustment (Brightness & Contrast)
|
// 2. Color Adjustment
|
||||||
float3 rgb = color.rgb;
|
float3 rgb = color.rgb;
|
||||||
rgb = rgb + params.brightness;
|
rgb = rgb + params.brightness;
|
||||||
rgb = (rgb - 0.5) * params.contrast + 0.5;
|
rgb = (rgb - 0.5) * params.contrast + 0.5;
|
||||||
|
|
||||||
// Grayscale conversion (Luma)
|
// Grayscale
|
||||||
float luma = dot(rgb, float3(0.299, 0.587, 0.114));
|
float luma = dot(rgb, float3(0.299, 0.587, 0.114));
|
||||||
|
|
||||||
if (params.isGrayscale > 0) {
|
if (params.isGrayscale > 0) {
|
||||||
|
|
@ -61,24 +98,46 @@ kernel void ditherShader(texture2d<float, access::read> inputTexture [[texture(0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Dithering
|
// 3. Dithering
|
||||||
if (params.algorithm == 1) { // Bayer 8x8
|
float threshold = 0.5;
|
||||||
// Map current pixel to matrix coordinates
|
bool shouldDither = (params.algorithm > 0);
|
||||||
// We use the original gid (screen coordinates) for the matrix pattern to keep it stable across pixelation blocks?
|
|
||||||
// OR we use the sourceCoord (pixelated coordinates) to make the dither pattern scale with the pixels?
|
|
||||||
// Usually, dither is applied at screen resolution, but for "retro pixel art" look, the dither pattern usually matches the "big pixel" size.
|
|
||||||
// Let's try using the scaled coordinate index: sourceCoord / scale
|
|
||||||
|
|
||||||
uint x = uint(sourceCoord.x / scale) % 8;
|
if (shouldDither) {
|
||||||
uint y = uint(sourceCoord.y / scale) % 8;
|
uint x, y;
|
||||||
float threshold = bayer8x8[y][x];
|
|
||||||
|
|
||||||
// Apply threshold
|
switch (params.algorithm) {
|
||||||
rgb = (luma > threshold) ? float3(1.0) : float3(0.0);
|
case 1: // Bayer 2x2
|
||||||
|
x = uint(sourceCoord.x / scale) % 2;
|
||||||
} else if (params.algorithm == 2) { // Bayer 4x4
|
y = uint(sourceCoord.y / scale) % 2;
|
||||||
uint x = uint(sourceCoord.x / scale) % 4;
|
threshold = bayer2x2[y][x];
|
||||||
uint y = uint(sourceCoord.y / scale) % 4;
|
break;
|
||||||
float threshold = bayer4x4[y][x];
|
case 2: // Bayer 4x4
|
||||||
|
x = uint(sourceCoord.x / scale) % 4;
|
||||||
|
y = uint(sourceCoord.y / scale) % 4;
|
||||||
|
threshold = bayer4x4[y][x];
|
||||||
|
break;
|
||||||
|
case 3: // Bayer 8x8
|
||||||
|
x = uint(sourceCoord.x / scale) % 8;
|
||||||
|
y = uint(sourceCoord.y / scale) % 8;
|
||||||
|
threshold = bayer8x8[y][x];
|
||||||
|
break;
|
||||||
|
case 4: // Cluster 4x4
|
||||||
|
x = uint(sourceCoord.x / scale) % 4;
|
||||||
|
y = uint(sourceCoord.y / scale) % 4;
|
||||||
|
threshold = cluster4x4[y][x];
|
||||||
|
break;
|
||||||
|
case 5: // Cluster 8x8
|
||||||
|
x = uint(sourceCoord.x / scale) % 8;
|
||||||
|
y = uint(sourceCoord.y / scale) % 8;
|
||||||
|
threshold = cluster8x8[y][x];
|
||||||
|
break;
|
||||||
|
case 6: // Blue Noise 8x8
|
||||||
|
x = uint(sourceCoord.x / scale) % 8;
|
||||||
|
y = uint(sourceCoord.y / scale) % 8;
|
||||||
|
threshold = blueNoise8x8[y][x];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
rgb = (luma > threshold) ? float3(1.0) : float3(0.0);
|
rgb = (luma > threshold) ? float3(1.0) : float3(0.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,24 @@ import ImageIO
|
||||||
|
|
||||||
enum DitherAlgorithm: Int, CaseIterable, Identifiable {
|
enum DitherAlgorithm: Int, CaseIterable, Identifiable {
|
||||||
case noDither = 0
|
case noDither = 0
|
||||||
case bayer8x8 = 1
|
case bayer2x2 = 1
|
||||||
case bayer4x4 = 2
|
case bayer4x4 = 2
|
||||||
|
case bayer8x8 = 3
|
||||||
|
case cluster4x4 = 4
|
||||||
|
case cluster8x8 = 5
|
||||||
|
case blueNoise = 6
|
||||||
|
|
||||||
var id: Int { rawValue }
|
var id: Int { rawValue }
|
||||||
|
|
||||||
var name: String {
|
var name: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .noDither: return "No Dither"
|
case .noDither: return "No Dither"
|
||||||
case .bayer8x8: return "Bayer 8x8"
|
case .bayer2x2: return "Bayer 2x2 (Retro)"
|
||||||
case .bayer4x4: return "Bayer 4x4"
|
case .bayer4x4: return "Bayer 4x4 (Balanced)"
|
||||||
|
case .bayer8x8: return "Bayer 8x8 (Smooth)"
|
||||||
|
case .cluster4x4: return "Cluster 4x4 (Vintage)"
|
||||||
|
case .cluster8x8: return "Cluster 8x8 (Soft)"
|
||||||
|
case .blueNoise: return "Blue Noise / Organic (Best Quality)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +37,7 @@ class DitherViewModel {
|
||||||
var brightness: Double = 0.0
|
var brightness: Double = 0.0
|
||||||
var contrast: Double = 1.0
|
var contrast: Double = 1.0
|
||||||
var pixelScale: Double = 4.0
|
var pixelScale: Double = 4.0
|
||||||
var selectedAlgorithm: DitherAlgorithm = .bayer8x8
|
var selectedAlgorithm: DitherAlgorithm = .bayer4x4 // Default to Balanced
|
||||||
var isGrayscale: Bool = false
|
var isGrayscale: Bool = false
|
||||||
|
|
||||||
private let renderer = MetalImageRenderer()
|
private let renderer = MetalImageRenderer()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue