基于EasyX软渲染实现常见故障艺术

本文极大地参考了毛星云大师的《高品质后处理:十种故障艺术(Glitch Art)算法的总结与实现》一文,以及由他开发的 Unity 高品质后处理库 X-PostProcessing-Library源码,能力有限,但也希望本文能够给感兴趣的小伙伴们抛砖引玉,为国产 3A 游戏续星星之火。

怎么绘事呢?

故事的起因是群友的一句话让我想起了当年做 3D“软渲染”的冲动,想来 CodeBus 上已经有大佬给出了相对不错的实现,便重新拉取代码本地编译查看,却没料到性能比预想的还要差一些,就算是在此基础上费尽心思实现了导入 3D 模型导入进行渲染,在没有 PBR 等高性能需求的技术实现可能的情况下,“炫酷”这方面可能就多少差点意思——不过,既然 3D 软渲染提升的空间有限,那么实现 2D 软渲染应该还可以有相当大的操作可能。

EasyX 群聊天记录

想来近期工作研究的故障艺术效果(Glitch Art),便重新翻出了收藏夹中毛星云大师的文章,试着将应用于 GPU 的 shader 实现移植到 CPU 端。
本文着重探究在 CPU 端逐像素的图像处理逻辑,即模拟 shader 程序在片段着色器阶段的工作,这就导致其工作流并不一定是 CPU 这种相对线性的工作者所喜欢的,也就是说在实际运行的过程中,效率可能远不及 GPU 的并行处理。尽管如此,考虑到本文重点在于算法和编程思想的分享,而非高性能场景的实际应用,况且对于 60 帧的游戏 demo 而言,本文提及的大部分算法实现都还算可以勉强胜任。

GlitchProcessor 基类

我们首先需要理解 EasyX 处理图片的逻辑:在 EasyX 中,图片对象通过 IMAGE 类定义,通过 GetImageBuffer 函数可以获取指定 IMAGE 对象的色彩缓冲区,缓冲区中的元素为 DWORD 类型,那么我们就可以按位获取对应的 RGB 色彩分量。
而色彩缓冲区中的像素数据是逐行紧凑排布的,那么,对于在 EasyX 图片坐标系下 (x, y) 坐标的像素数据,就可以通过 buffer[y * width + x] 去获取。

在 Unity 或 OpenGL 等大多数图形抽象的实现中,纹理坐标系一般默认为左下角为坐标原点,这与 EasyX 继承自平台的左上角为坐标原点在 Y 轴方向上是相反的,但是考虑到故障效果等后处理效果在大部分情况下在空间和数据上都是坐标轴对称的,所以代码实现中并未对 Y 轴的反转做特殊处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class GlitchProcessor
{
public:
GlitchProcessor() = default;

GlitchProcessor(IMAGE* src)
{
src_image = src;
dst_image = new IMAGE(src->getwidth(), src->getheight());
}

~GlitchProcessor()
{
delete dst_image;
}

virtual void SetImage(IMAGE* src)
{
if (dst_image) delete dst_image;

src_image = src;
dst_image = new IMAGE(src->getwidth(), src->getheight());
}

IMAGE* GetImage()
{
return dst_image;
}

virtual void Process() = 0;

protected:
IMAGE* dst_image = nullptr;
IMAGE* src_image = nullptr;

protected:
// 是否可以开始处理
bool CanProcess()
{
return dst_image && GetImageBuffer(dst_image);
}

// 获取随机噪声
virtual double GetRandomNoise(double x, double y)
{
double val = sin(x * 12.9898 + y * 78.233) * 43758.5453;
return val - floor(val);
}

// 限定某值到区间内
double Clamp(double x, double min_val, double max_val)
{
return x < min_val ? min_val : (x > max_val ? max_val : x);
}
};

GlitchProcessor 类除去封装了必要的数据之外,还提供了一些工具方法,最关键的便是二维噪声函数 GetRandomNoise,函数的实现并不复杂但是包含了很多“魔法数字”,魔数在计算机图形学中并不少见,大部分都是与物理世界或测量结果有关的“经验公式”中的经验数字,此处的噪声实现取自毛星云大师的程序实现,我们只需知道 xy 的系数控制噪声在二维方向上的变化速度,而 43758.5453 则负责控制噪声波形的振幅,最后通过对结果取小数得到最终噪声,至于为什么是这几个数字便不得而知了。

f(x, y) = frac(sin(x * 12.9898 + y * 78.233) * 43758.5453)

这里有几个细节是斟酌后的结果,特殊说明一下:

为什么使用 double 而不是 float

阙石,float 会比 double 节省内存,并且在三角函数计算的收敛过程中会更加迅速;但本实验并非内存敏感场景,程序运行过程中内存占用主要来源于图片缓冲区而非处理过程;其次,经过现代编译器的优化后,double 进行常规运算的效率可能会高于 float;最后,为了避免类型转换每次都要在字面量后加 f 简直无法忍受!

为什么不对输入和输出的 IMAGE 指针添加 const 限定?

诚然,从程序设计角度讲,添加常量限定可以避免在缓冲区满天飞的处理过程中对缓冲区误操作,但是:首先是输入的图片,EasyX 的程序接口 GetImageBuffer 返回的是可读可写的缓冲区,函数原型从参数层面便要求了输入的图片对象必须不能为常量,尽管我们只是对输入的图片缓冲区进行了读取操作;其次是输出过程,考虑到多渲染 PASS,我们可以将一个处理器的输出作为另一个处理器的输入,通过流水线的方式实现多种故障效果的混合,为了能支持这种操作,所以我们便不能将输出的图片添加常量限定。

为什么使用噪声而不是随机数?

正如前文所述,我们实现了自己的噪声算法,而不是通过 rand() 函数获取随机数,这样做的考虑主要有以下几点:首先是噪声具有平滑性,从公式可以看出,整套算法是基于正弦函数的,所以在临近的采样点之间,噪声的输出是平滑的,而随机数不具备这样的特征;其次是噪声具有均匀性,随机数生成器只会在较大的范围内表现出统计学意义上的均匀,但是这个平均的表现受范围影响较大,我们无法控制其在任意范围内表现出稳定的均匀特性;最后是可预测性,噪声和随机数的本质区别可能就是在这里,噪声可以看做是“有规律的随机数”,诚然现在程序使用的随机也都是伪随机,但是想对其在应用层进行预测反而代价会很大,而通过噪声我们可以调节各个参数,或依照其实现特性(如正弦函数的周期性)对其进行规律性的调整。

Clamp 方法有什么用?

此方法用来限定某值到区间内,如果该值大小于区间最小值则返回最小值,大于区间最大值则返回最大值,在区间内则返回该值本身。在 shader 代码中,最终输出的颜色一般是不需要进行限定操作的,在标准化后的 RGB 色彩空间下,小于 0.0 或大于 1.0 的色彩值都是没有意义的,所以在片段着色器执行完毕后,色彩的各分量会被自动限定在合适的范围内;而在 CPU 处理色彩的过程中,过大或过小的值在进行转化时存在数据溢出的可能,所以我们需要提前将 RGB 各分量限定到合适的范围内,防止出现因为数据溢出而导致的像素色彩跳跃的噪点。

色彩溢出导致的噪点

正如上图所示,程序测试过程中所使用的图片来自 Pixiv:108638763

色彩分离故障(RGB Split Glitch)

色彩分离故障处理器的实现思路是对 RGB 三个色彩通道分别采取不同的 UV 偏移值进行采样,一般而言,三个通道中只有一个通道采用原始的 UV 值,另外两个通道进行随机抖动后采样。

色彩分离故障

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class RGBSplitGlitch : public GlitchProcessor
{
public:
RGBSplitGlitch() = default;
RGBSplitGlitch(IMAGE* src) : GlitchProcessor(src) {}
~RGBSplitGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
DWORD r, g, b;
size_t idx, split_amount;

split_amount = (size_t)(INDENSITY * GetRandomNoise(TIME, 2));

idx = y * width + (x + split_amount) % width;
r = (src_buffer[idx] & 0x00ff0000) >> 16;

idx = y * width + x;
g = (src_buffer[idx] & 0x0000ff00) >> 8;

idx = y * width + (x - split_amount) % width;
b = src_buffer[idx] & 0x000000ff;

idx = y * width + x;
dst_buffer[idx] = BGR(RGB(r, g, b));
}
}
}

private:
const double INDENSITY = 10; // 色彩分离强度
};

这里我们对偏移后的采样点坐标对宽度进行了取模,这是避免行像素索引的溢出,在稍后的故障特效处理器中,我们还将在避免索引溢出的同时,使用取模来控制动画效果。

说到动画效果的实现,必然就需要有时间的输入作为控制,我们定义了全局的计时器 TIME,用来记录程序从开始运行到现在以秒为单位的时间,这在 shader 中是很常见的处理思路。

错位图块故障(Image Block Glitch)

错位图块故障的生成思路如下:

  • 1. 基于 UV 和噪声函数生成随机强度的方格块:
1
double block = GetRandomNoise(floor((double)x / width * BLOCK_SIZE), floor((double)y / height * BLOCK_SIZE));

随机强度的均匀 Block 图块

  • 2. 将 Block 图块的强度值做强度的二次筛选,增加随机性:
1
double displace_noise = pow(block, 8) * pow(block, 3);

二次筛选

  • 3. 将二次筛选得到的图块强度值作为噪声强度的系数,分别对 G 和 B 颜色通道的采样点 UV 坐标进行偏移:
1
2
3
4
5
6
7
8
idx = y * width + x;
r = (src_buffer[idx] & 0x00ff0000) >> 16;

idx = y * width + (size_t)(x + (displace_noise * 0.05 * GetRandomNoise(7)) * width) % width;
g = (src_buffer[idx] & 0x0000ff00) >> 8;

idx = y * width + (size_t)(x - (displace_noise * 0.05 * GetRandomNoise(13)) * width) % width;
b = src_buffer[idx] & 0x000000ff;

错位图块故障

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class ImageBlockGlitch : public GlitchProcessor
{
public:
ImageBlockGlitch() = default;
ImageBlockGlitch(IMAGE* src) : GlitchProcessor(src) {}
~ImageBlockGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;

double block = GetRandomNoise(floor((double)x / width * BLOCK_SIZE), floor((double)y / height * BLOCK_SIZE));
double displace_noise = pow(block, 8) * pow(block, 3);

idx = y * width + x;
r = (src_buffer[idx] & 0x00ff0000) >> 16;

idx = y * width + (size_t)(x + (displace_noise * 0.05 * GetRandomNoise(7)) * width) % width;
g = (src_buffer[idx] & 0x0000ff00) >> 8;

idx = y * width + (size_t)(x - (displace_noise * 0.05 * GetRandomNoise(13)) * width) % width;
b = src_buffer[idx] & 0x000000ff;

idx = y * width + x;
dst_buffer[idx] = BGR(RGB(r, g, b));
}
}
}

private:
const double SPEED = 8; // 故障图块速度
const double BLOCK_SIZE = 5; // 故障图块比例

private:
double GetRandomNoise(double seed)
{
return GlitchProcessor::GetRandomNoise(seed, 1);
}

double GetRandomNoise(double seed_x, double seed_y)
{
double scale = floor(TIME * SPEED);
double val = sin(seed_x * scale * 17.13 + seed_y * scale * 3.71) * 43758.5453123;
return val - floor(val);
}
};

错位线条故障(Line Block Glitch)

错位线条故障的生成思路如下:

  • 1. 生成均匀宽度线条:
1
2
3
4
5
6
7
8
9
10
double Trunc(double x, double num_levels)
{
return floor(x * num_levels) / num_levels;
}

double strength = 0.5 + 0.5 * cos(TIME * FREQUENCY);
double scaled_time = TIME * strength;
double trunc_time = Trunc(scaled_time, 4);
double vec_val = Trunc((double)y / height, 8) + 100 * trunc_time;
double uv_trunc = GetRandomNoise(vec_val, vec_val);

均匀宽度的线条

  • 2. 将均匀线条转变为随机梯度的等宽线条:
1
double uv_random_trunc = 6 * Trunc(scaled_time, 24 * uv_trunc);

随机梯度的等宽线条

  • 3. 通过多次随机噪声将随机梯度的等宽线条变为随机梯度的非等宽线条:
1
2
3
4
5
6
7
vec_val = Trunc((double)y / height + uv_random_trunc, 8 * LINES_WIDTH);
double blockline_random = 0.5 * GetRandomNoise(vec_val, vec_val);
vec_val = Trunc((double)y / height + uv_random_trunc, 7);
blockline_random += 0.5 * GetRandomNoise(vec_val, vec_val);
blockline_random = blockline_random * 2 - 1;
blockline_random = blockline_random / abs(blockline_random) * max(0.0, min(1.0, (abs(blockline_random) - AMOUNT) / 0.4));
blockline_random = blockline_random * OFFSET;

随机梯度的非等宽线条

  • 4. 将上述生成的线条,作为 UV 的偏移量进行采样生成源色调的故障效果:
1
2
3
double normalized_x = (double)x / width;
normalized_x = max(0.0, min(1.0, normalized_x + 0.1 * blockline_random));
idx = y * width + (size_t)(normalized_x * width) % width;
  • 5. 将 RGB 色彩空间转化到 YUV 色彩空间,对色度和浓度进行偏移,再转回 RGB 色彩空间:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void RGBToYUV(double r, double g, double b, double* y, double* u, double* v)
{
*y = 0.299 * r + 0.587 * g + 0.114 * b;
*u = -0.14713 * r + -0.28886 * g + 0.436 * b;
*v = 0.615 * r + -0.51499 * g + -0.10001 * b;
}

void YUVToRGB(double y, double u, double v, double* r, double* g, double* b)
{
*r = y + v * 1.13983;
*g = y + -0.39465 * u + -0.58060 * v;
*b = y + u * 2.03211;
}

src_r = (src_buffer[idx] & 0x00ff0000) >> 16;
src_g = (src_buffer[idx] & 0x0000ff00) >> 8;
src_b = src_buffer[idx] & 0x000000ff;
RGBToYUV(src_r / 255, src_g / 255, src_b / 255, &src_y, &src_u, &src_v);
src_u /= 1.0 - 3.0 * abs(blockline_random) * max(0.0, min(1.0, 0.5 - blockline_random));
src_v += 0.125 * blockline_random * max(0.0, min(1.0, blockline_random - 0.5));
YUVToRGB(src_y, src_u, src_v, &dst_r, &dst_g, &dst_b);
  • 6. 将调色得到的故障效果像素颜色与源图片进行混叠,输出最终的颜色:
1
2
3
r = (DWORD)Clamp((src_r + (dst_r * 255 - src_r) * ALPHA), 0, 255);
g = (DWORD)Clamp((src_g + (dst_g * 255 - src_g) * ALPHA), 0, 255);
b = (DWORD)Clamp((src_b + (dst_b * 255 - src_b) * ALPHA), 0, 255);

错位线条故障

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class LineBlockGlitch : public GlitchProcessor
{
public:
LineBlockGlitch() = default;
LineBlockGlitch(IMAGE* src) : GlitchProcessor(src) {}
~LineBlockGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double src_y, src_u, src_v;
double src_r, src_g, src_b;
double dst_r, dst_g, dst_b;

// 生成随机强度梯度线条
double strength = 0.5 + 0.5 * cos(TIME * FREQUENCY);
double scaled_time = TIME * strength;
double trunc_time = Trunc(scaled_time, 4);
double vec_val = Trunc((double)y / height, 8) + 100 * trunc_time;
double uv_trunc = GetRandomNoise(vec_val, vec_val);
double uv_random_trunc = 6 * Trunc(scaled_time, 24 * uv_trunc);

// 生成随机非均匀宽度线条
vec_val = Trunc((double)y / height + uv_random_trunc, 8 * LINES_WIDTH);
double blockline_random = 0.5 * GetRandomNoise(vec_val, vec_val);
vec_val = Trunc((double)y / height + uv_random_trunc, 7);
blockline_random += 0.5 * GetRandomNoise(vec_val, vec_val);
blockline_random = blockline_random * 2 - 1;
blockline_random = blockline_random / abs(blockline_random) * max(0.0, min(1.0, (abs(blockline_random) - AMOUNT) / 0.4));
blockline_random = blockline_random * OFFSET;

// 生成源颜色的 BlockLine Glitch
double normalized_x = (double)x / width;
normalized_x = max(0.0, min(1.0, normalized_x + 0.1 * blockline_random));
idx = y * width + (size_t)(normalized_x * width) % width;

// 进行色彩偏移调整
src_r = (src_buffer[idx] & 0x00ff0000) >> 16;
src_g = (src_buffer[idx] & 0x0000ff00) >> 8;
src_b = src_buffer[idx] & 0x000000ff;
RGBToYUV(src_r / 255, src_g / 255, src_b / 255, &src_y, &src_u, &src_v);
src_u /= 1.0 - 3.0 * abs(blockline_random) * max(0.0, min(1.0, 0.5 - blockline_random));
src_v += 0.125 * blockline_random * max(0.0, min(1.0, blockline_random - 0.5));
YUVToRGB(src_y, src_u, src_v, &dst_r, &dst_g, &dst_b);

// 与源图片进行混叠
r = (DWORD)Clamp((src_r + (dst_r * 255 - src_r) * ALPHA), 0, 255);
g = (DWORD)Clamp((src_g + (dst_g * 255 - src_g) * ALPHA), 0, 255);
b = (DWORD)Clamp((src_b + (dst_b * 255 - src_b) * ALPHA), 0, 255);

dst_buffer[idx] = BGR(RGB(r, g, b));
}
}
}

private:
const double ALPHA = 0.85; // 混叠系数
const double OFFSET = 1; // 故障线条偏移
const double AMOUNT = 0.5; // 故障线条数量
const double FREQUENCY = 1; // 故障线条频率
const double LINES_WIDTH = 1; // 故障线条宽度

private:
double Trunc(double x, double num_levels)
{
return floor(x * num_levels) / num_levels;
}

void RGBToYUV(double r, double g, double b, double* y, double* u, double* v)
{
*y = 0.299 * r + 0.587 * g + 0.114 * b;
*u = -0.14713 * r + -0.28886 * g + 0.436 * b;
*v = 0.615 * r + -0.51499 * g + -0.10001 * b;
}

void YUVToRGB(double y, double u, double v, double* r, double* g, double* b)
{
*r = y + v * 1.13983;
*g = y + -0.39465 * u + -0.58060 * v;
*b = y + u * 2.03211;
}
};

图块抖动故障(Tile Jitter Glitch)

核心思路在于基于 UV 分层抖动,可以采用取余数的形式来对 UV 进行分层,对于层内的 UV 数值,使用三角函数进行抖动偏移后采样。

图块抖动故障

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class TileJitterGlitch : public GlitchProcessor
{
public:
TileJitterGlitch() = default;
TileJitterGlitch(IMAGE* src) : GlitchProcessor(src) {}
~TileJitterGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double offset_x = (double)x / width;
double offset_y = (double)y / height;

double strength = 0.5 + 0.5 * cos(TIME * FREQUENCY);

if ((int)((double)y / height * GetRandomNoise(TIME, 0.5) * SPLITTING_NUM) % 2 < 1)
{
offset_x += 1.0 / width * cos(TIME * SPEED) * OFFSET * strength;
offset_y += 1.0 / width * cos(TIME * SPEED) * OFFSET * strength * 0.25;
}

idx = (size_t)(offset_y * height) % height * width + (size_t)(offset_x * width) % width;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double SPEED = 4; // 故障图块速度
const double OFFSET = 20; // 故障图块偏移
const double FREQUENCY = 50; // 故障图块频率
const double SPLITTING_NUM = 8; // 故障图块数量
};

扫描线抖动故障(Scan Line Jitter Glitch)

扫描线抖动故障依然是相对简单的算法,可以对 UV 进行横向的基于噪声的扰动来实现。

扫描线抖动故障

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class ScanLineJitterGlitch : public GlitchProcessor
{
public:
ScanLineJitterGlitch() = default;
ScanLineJitterGlitch(IMAGE* src) : GlitchProcessor(src) {}
~ScanLineJitterGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double offset_x;

double strength = 0.5 + 0.5 * cos(TIME * FREQUENCY);
double jitter = GetRandomNoise((double)y / height, TIME) * 2 - 1;
jitter *= (abs(jitter) < THRESHOLD ? 0 : 1) * AMOUNT * strength;
offset_x = (double)x / width + jitter;

idx = y * width + (size_t)(offset_x * width) % width;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double AMOUNT = 0.5; // 扫描线数量
const double THRESHOLD = 0.9; // 抖动临界值
const double FREQUENCY = 2; // 扫描线频率
};

数字条纹故障(Digital Stripe Glitch)

数字条纹故障在毛星云大师给出的 Unity 版本实现中,分为 CPU 和 GPU 端两部分,核心思路是通过在 Runtime 层生成随机的噪声纹理贴图,然后传入 shader 中作为纹理输入之一进行采样,在本文所提及的 EasyX 实现中,同样分为生成噪声纹理和重采样两步:

  • 1. 基于随机数生成数字条纹贴图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 生成随机颜色条纹贴图
void ResetRandomColorImage()
{
static const int STRIP_LENGTH_RATIO = 75; // 百分制条带长度
static const int WIDTH = 20, HEIGHT = 20; // 迷你颜色贴图尺寸
IMAGE mini_color_image = IMAGE(WIDTH, HEIGHT); // 迷你颜色贴图
DWORD* buffer = GetImageBuffer(&mini_color_image);
if (random_color_image) delete random_color_image;
random_color_image = new IMAGE(src_image->getwidth(), src_image->getheight());
for (size_t y = 0; y < HEIGHT; ++y)
{
for (size_t x = 0; x < WIDTH; ++x)
{
if (rand() % 100 > STRIP_LENGTH_RATIO)
current_fill_color = RGB(rand() % 256, rand() % 256, rand() % 256);
buffer[y * WIDTH + x] = BGR(current_fill_color);
}
}
// 将迷你颜色条纹贴图缩放到源图片相同尺寸并绘制到颜色条纹贴图上
StretchBlt(GetImageHDC(random_color_image), 0, 0, src_image->getwidth(), src_image->getheight(),
GetImageHDC(&mini_color_image), 0, 0, WIDTH, HEIGHT, SRCCOPY);
}

这里我们先生成了一张分辨率较小的正方形贴图,随后通过 StretchBlt 将贴图强制拉伸到了与输入的源图片相同的尺寸,这样便将小贴图中的线段变为了矩形,且矩形的长宽比例与源图片一致。

随机颜色条纹贴图

  • 2. 将条纹图片采样得到的颜色值作为对源图片采样的 UV 偏移:
1
2
3
4
5
6
7
8
9
10
11
// 基础数据准备
idx = y * width + x;
noise_r = (noise_buffer[idx] & 0x00ff0000) >> 16;
noise_g = (noise_buffer[idx] & 0x0000ff00) >> 8;
noise_b = noise_buffer[idx] & 0x000000ff;
double threshold = 1.001 - INDENSITY * 1.001;

// UV 偏移
double uv_shift = pow((double)noise_r / 255, 3) < threshold ? 0 : 1;
offset_x = (double)x / width + (double)noise_g / 255 * uv_shift;
offset_y = (double)y / height + (double)noise_b / 255 * uv_shift;

数字条纹故障

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class DigitalStripeGlitch : public GlitchProcessor
{
public:
DigitalStripeGlitch() = default;
DigitalStripeGlitch(IMAGE* src)
: GlitchProcessor(src) { ResetRandomColorImage(); }
~DigitalStripeGlitch() = default;

void SetImage(IMAGE* src)
{
GlitchProcessor::SetImage(src);
ResetRandomColorImage();
}

void Process()
{
if (!CanProcess()) return;

if ((size_t)(TIME * 1000) % 5 == 0)
ResetRandomColorImage();

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);
DWORD* noise_buffer = GetImageBuffer(random_color_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double offset_x, offset_y;
DWORD noise_r, noise_g, noise_b;

// 基础数据准备
idx = y * width + x;
noise_r = (noise_buffer[idx] & 0x00ff0000) >> 16;
noise_g = (noise_buffer[idx] & 0x0000ff00) >> 8;
noise_b = noise_buffer[idx] & 0x000000ff;
double threshold = 1.001 - INDENSITY * 1.001;

// UV 偏移
double uv_shift = pow((double)noise_r / 255, 3) < threshold ? 0 : 1;
offset_x = (double)x / width + (double)noise_g / 255 * uv_shift;
offset_y = (double)y / height + (double)noise_b / 255 * uv_shift;

idx = ((size_t)(offset_y * height) % height) * width + (size_t)(offset_x * width) % width;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double INDENSITY = 0.25; // 条纹强度
IMAGE* random_color_image = nullptr;
COLORREF current_fill_color = RGB(255, 255, 255);

private:
// 生成随机颜色条纹贴图
void ResetRandomColorImage()
{
static const int STRIP_LENGTH_RATIO = 75; // 百分制条带长度
static const int WIDTH = 20, HEIGHT = 20; // 迷你颜色贴图尺寸
IMAGE mini_color_image = IMAGE(WIDTH, HEIGHT); // 迷你颜色贴图
DWORD* buffer = GetImageBuffer(&mini_color_image);
if (random_color_image) delete random_color_image;
random_color_image = new IMAGE(src_image->getwidth(), src_image->getheight());
for (size_t y = 0; y < HEIGHT; ++y)
{
for (size_t x = 0; x < WIDTH; ++x)
{
if (rand() % 100 > STRIP_LENGTH_RATIO)
current_fill_color = RGB(rand() % 256, rand() % 256, rand() % 256);
buffer[y * WIDTH + x] = BGR(current_fill_color);
}
}
// 将迷你颜色条纹贴图缩放到源图片相同尺寸并绘制到颜色条纹贴图上
StretchBlt(GetImageHDC(random_color_image), 0, 0, src_image->getwidth(), src_image->getheight(),
GetImageHDC(&mini_color_image), 0, 0, WIDTH, HEIGHT, SRCCOPY);
}
};

模拟噪点故障(Analog Noise Glitch)

模拟噪点故障的实现思路如下:

  • 1. 使用噪声扰动源图片的颜色值,即根据噪声对 UV 进行偏移后采样:
1
2
3
4
5
6
7
noise_x = GetRandomNoise(TIME * SPEED + (double)x / width / -213, TIME * SPEED + (double)y / height / 5.53);
noise_y = GetRandomNoise(TIME * SPEED - (double)x / width / 213, TIME * SPEED - (double)y / height / -5.53);
noise_z = GetRandomNoise(TIME * SPEED + (double)x / width / 213, TIME * SPEED + (double)y / height / 5.53);

noise_r += 0.9 * noise_x - 0.45;
noise_g += 0.9 * noise_y - 0.45;
noise_b += 0.9 * noise_z - 0.45;

需要注意,上段代码最后对扰动后得到的数值进行了调整,这样做是为了确保扰动后的画面平均亮度与源图片相同,不能忽略,但可以对两个数值进行等比缩放实现不同的效果。

  • 2. 加入灰度抖动,当某一刻的随机强度值大于亮度抖动阈值时,将采样得到的 RGB 颜色向着对应的灰度插值,从而使画面呈现黑白灰度表现:
1
2
3
4
5
6
7
double luminance = noise_r * 0.22 + noise_g * 0.707 * noise_b * 0.071;
if (GetRandomNoise(TIME * SPEED, TIME * SPEED) > LUMINANCE_JITTER_THRESHOLD)
noise_r = luminance, noise_g = luminance, noise_b = luminance;

r = (DWORD)Clamp((r + (noise_r * 255 - r) * FADING), 0, 255);
g = (DWORD)Clamp((g + (noise_g * 255 - g) * FADING), 0, 255);
b = (DWORD)Clamp((b + (noise_b * 255 - b) * FADING), 0, 255);

模拟噪点故障

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class AnalogNoiseGlitch : public GlitchProcessor
{
public:
AnalogNoiseGlitch() = default;
AnalogNoiseGlitch(IMAGE* src) : GlitchProcessor(src) { }
~AnalogNoiseGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double noise_r, noise_g, noise_b;
double noise_x, noise_y, noise_z;

idx = y * width + x;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;
noise_r = (double)r / 255, noise_g = (double)g / 255, noise_b = (double)b / 255;

double luminance = noise_r * 0.22 + noise_g * 0.707 * noise_b * 0.071;
if (GetRandomNoise(TIME * SPEED, TIME * SPEED) > LUMINANCE_JITTER_THRESHOLD)
noise_r = luminance, noise_g = luminance, noise_b = luminance;

noise_x = GetRandomNoise(TIME * SPEED + (double)x / width / -213, TIME * SPEED + (double)y / height / 5.53);
noise_y = GetRandomNoise(TIME * SPEED - (double)x / width / 213, TIME * SPEED - (double)y / height / -5.53);
noise_z = GetRandomNoise(TIME * SPEED + (double)x / width / 213, TIME * SPEED + (double)y / height / 5.53);

noise_r += 0.9 * noise_x - 0.45;
noise_g += 0.9 * noise_y - 0.45;
noise_b += 0.9 * noise_z - 0.45;

r = (DWORD)Clamp((r + (noise_r * 255 - r) * FADING), 0, 255);
g = (DWORD)Clamp((g + (noise_g * 255 - g) * FADING), 0, 255);
b = (DWORD)Clamp((b + (noise_b * 255 - b) * FADING), 0, 255);

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double SPEED = 0.25; // 噪点速度
const double FADING = 0.5; // 混叠系数
const double LUMINANCE_JITTER_THRESHOLD = 0.9; // 亮度抖动阈值
};

屏幕跳跃故障(Screen Jump Glitch)

实现思路在于取经过时间校正后的 UV 小数部分,并与源 UV 插值,得到均匀梯度式扰动屏幕空间 UV,再用此 UV 进行采样即可得到画面跳动的效果。

屏幕跳跃故障

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class ScreenJumpGlitch : public GlitchProcessor
{
public:
ScreenJumpGlitch() = default;
ScreenJumpGlitch(IMAGE* src) : GlitchProcessor(src) { }
~ScreenJumpGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;

double normalized_y = (double)y / height;
double jump_time = TIME * INDENSITY * 9.8;
double dst = normalized_y + jump_time - floor(normalized_y + jump_time);
double jump = normalized_y + (dst - normalized_y) * INDENSITY;

idx = (size_t)(jump * height) % height * width + x;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double INDENSITY = 0.25; // 跳跃强度
};

屏幕抖动故障(Screen Shake Glitch)

屏幕抖动故障与屏幕跳跃故障实现思路相似,都是通过对 UV 的扰动实现,但不同的是屏幕抖动故障需要通过噪声函数来进行随机扰动,而不是均匀梯度式的形式。

屏幕抖动故障

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class ScreenShakeGlitch : public GlitchProcessor
{
public:
ScreenShakeGlitch() = default;
ScreenShakeGlitch(IMAGE* src) : GlitchProcessor(src) { }
~ScreenShakeGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double offset_y;

double shake = (GetRandomNoise(TIME, 2) - 0.5) * INDENSITY;
offset_y = (double)y / height + shake;

idx = (size_t)(offset_y * height) % height * width + x;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double INDENSITY = 0.1; // 抖动强度
};

帧率感人,没有办法了吗?

我们可以注意到,这种几乎 1:1 还原 shader 实现思路在某些效果下可能帧率会下降严重,逐像素地执行相同的处理逻辑似乎让我们反其道而行之陷入了 CPU 的劣势陷阱,那么,有哪些可能的优化有希望可以提升程序性能呢?

接下来的论述仅从理论角度分析可能性,实际效果有待验证。

  • 1. 提高缓存命中

这个在现有实现思路保持不变的情况下似乎很难做到,但是我们可以知道缓存命中率低确实在影响着我们的程序:虽然我们通过双循环在逐像素处理时顺序访问了我们的色彩缓冲区,但是在实际处理的过程中,尤其是进行 UV 偏移,或者是多缓冲区输入时,我们在这个较大的缓冲区内部进行了随机访问,这就极有可能导致缓存命中率低;如果非要说在现有基础上优化,那便是尽可能将 UV 偏移限定在水平方向上,因为像素数据在缓冲区内按行排布,临近访问的命中率可能会更高一些,不过这种优化的空间极小。

  • 2. 更大粒度的像素块处理

如果是屏幕跳跃故障或者屏幕抖动故障这种涉及规则矩形图片偏移绘制的效果,完全可以通过图片裁剪位移显示,而不是需要逐像素进行处理,即便是需要对区域图片进行色彩混叠的故障效果,也可以借助三元光栅操作码提升性能,这同样是内存友好的。

  • 3. 用空间换时间

对于贴图或静态画面,我们可以预先将噪声帧预先计算并缓存在数组中,以序列帧动画的方式播放出来;即便是对全屏后处理的动态效果,逐像素处理时的一半左右的工作,如用于生成噪声贴图的工作,也可以预先计算并缓存,随后通过三元光栅操作码或在运行时逐像素操作时只进行混叠,这样也可以较大地提升程序运行时性能——毕竟如果是性能敏感的游戏场景,玩家对于初始化时的等待远比运行时的卡顿包容度高。

程序完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
#include <graphics.h>

#include <vector>
#include <string>

#define RES_PATH "resources\\image.jpg"

static double TIME; // 程序运行时间(秒)

/*

* =========== 故障效果处理器基类 ===========

* 使用带参构造函数或 SetImage 函数设置源图片
处理器只会持有引用而不会拷贝源图片数据
请确保处理器调用 Process 函数期间源图片数据可用

* 调用 Process 逐像素处理后通过 GetImage 获取处理后的目标图片
不要使用 Resize 等函数对目标图片进行写入操作以免使缓冲区失效

*/
class GlitchProcessor
{
public:
GlitchProcessor() = default;

GlitchProcessor(IMAGE* src)
{
src_image = src;
dst_image = new IMAGE(src->getwidth(), src->getheight());
}

~GlitchProcessor()
{
delete dst_image;
}

virtual void SetImage(IMAGE* src)
{
if (dst_image) delete dst_image;

src_image = src;
dst_image = new IMAGE(src->getwidth(), src->getheight());
}

IMAGE* GetImage()
{
return dst_image;
}

virtual void Process() = 0;

protected:
IMAGE* dst_image = nullptr;
IMAGE* src_image = nullptr;

protected:
// 是否可以开始处理
bool CanProcess()
{
return dst_image && GetImageBuffer(dst_image);
}

// 获取随机噪声
virtual double GetRandomNoise(double x, double y)
{
double val = sin(x * 12.9898 + y * 78.233) * 43758.5453;
return val - floor(val);
}

// 限定某值到区间内
double Clamp(double x, double min_val, double max_val)
{
return x < min_val ? min_val : (x > max_val ? max_val : x);
}
};


/*

* 色彩分离故障

*/
class RGBSplitGlitch : public GlitchProcessor
{
public:
RGBSplitGlitch() = default;
RGBSplitGlitch(IMAGE* src) : GlitchProcessor(src) {}
~RGBSplitGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
DWORD r, g, b;
size_t idx, split_amount;

split_amount = (size_t)(INDENSITY * GetRandomNoise(TIME, 2));

idx = y * width + (x + split_amount) % width;
r = (src_buffer[idx] & 0x00ff0000) >> 16;

idx = y * width + x;
g = (src_buffer[idx] & 0x0000ff00) >> 8;

idx = y * width + (x - split_amount) % width;
b = src_buffer[idx] & 0x000000ff;

idx = y * width + x;
dst_buffer[idx] = BGR(RGB(r, g, b));
}
}
}

private:
const double INDENSITY = 10; // 色彩分离强度
};


/*

* 错位图块故障

*/
class ImageBlockGlitch : public GlitchProcessor
{
public:
ImageBlockGlitch() = default;
ImageBlockGlitch(IMAGE* src) : GlitchProcessor(src) {}
~ImageBlockGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;

double block = GetRandomNoise(floor((double)x / width * BLOCK_SIZE), floor((double)y / height * BLOCK_SIZE));
double displace_noise = pow(block, 8) * pow(block, 3);

idx = y * width + x;
r = (src_buffer[idx] & 0x00ff0000) >> 16;

idx = y * width + (size_t)(x + (displace_noise * 0.05 * GetRandomNoise(7)) * width) % width;
g = (src_buffer[idx] & 0x0000ff00) >> 8;

idx = y * width + (size_t)(x - (displace_noise * 0.05 * GetRandomNoise(13)) * width) % width;
b = src_buffer[idx] & 0x000000ff;

idx = y * width + x;
dst_buffer[idx] = BGR(RGB(r, g, b));
}
}
}

private:
const double SPEED = 8; // 故障图块速度
const double BLOCK_SIZE = 5; // 故障图块比例

private:
double GetRandomNoise(double seed)
{
return GlitchProcessor::GetRandomNoise(seed, 1);
}

double GetRandomNoise(double seed_x, double seed_y)
{
double scale = floor(TIME * SPEED);
double val = sin(seed_x * scale * 17.13 + seed_y * scale * 3.71) * 43758.5453123;
return val - floor(val);
}
};


/*

* 错位线条故障

*/
class LineBlockGlitch : public GlitchProcessor
{
public:
LineBlockGlitch() = default;
LineBlockGlitch(IMAGE* src) : GlitchProcessor(src) {}
~LineBlockGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double src_y, src_u, src_v;
double src_r, src_g, src_b;
double dst_r, dst_g, dst_b;

// 生成随机强度梯度线条
double strength = 0.5 + 0.5 * cos(TIME * FREQUENCY);
double scaled_time = TIME * strength;
double trunc_time = Trunc(scaled_time, 4);
double vec_val = Trunc((double)y / height, 8) + 100 * trunc_time;
double uv_trunc = GetRandomNoise(vec_val, vec_val);
double uv_random_trunc = 6 * Trunc(scaled_time, 24 * uv_trunc);

// 生成随机非均匀宽度线条
vec_val = Trunc((double)y / height + uv_random_trunc, 8 * LINES_WIDTH);
double blockline_random = 0.5 * GetRandomNoise(vec_val, vec_val);
vec_val = Trunc((double)y / height + uv_random_trunc, 7);
blockline_random += 0.5 * GetRandomNoise(vec_val, vec_val);
blockline_random = blockline_random * 2 - 1;
blockline_random = blockline_random / abs(blockline_random) * max(0.0, min(1.0, (abs(blockline_random) - AMOUNT) / 0.4));
blockline_random = blockline_random * OFFSET;

// 生成源颜色的 BlockLine Glitch
double normalized_x = (double)x / width;
normalized_x = max(0.0, min(1.0, normalized_x + 0.1 * blockline_random));
idx = y * width + (size_t)(normalized_x * width) % width;

// 进行色彩偏移调整
src_r = (src_buffer[idx] & 0x00ff0000) >> 16;
src_g = (src_buffer[idx] & 0x0000ff00) >> 8;
src_b = src_buffer[idx] & 0x000000ff;
RGBToYUV(src_r / 255, src_g / 255, src_b / 255, &src_y, &src_u, &src_v);
src_u /= 1.0 - 3.0 * abs(blockline_random) * max(0.0, min(1.0, 0.5 - blockline_random));
src_v += 0.125 * blockline_random * max(0.0, min(1.0, blockline_random - 0.5));
YUVToRGB(src_y, src_u, src_v, &dst_r, &dst_g, &dst_b);

// 与源图片进行混叠
r = (DWORD)Clamp((src_r + (dst_r * 255 - src_r) * ALPHA), 0, 255);
g = (DWORD)Clamp((src_g + (dst_g * 255 - src_g) * ALPHA), 0, 255);
b = (DWORD)Clamp((src_b + (dst_b * 255 - src_b) * ALPHA), 0, 255);

dst_buffer[idx] = BGR(RGB(r, g, b));
}
}
}

private:
const double ALPHA = 0.85; // 混叠系数
const double OFFSET = 1; // 故障线条偏移
const double AMOUNT = 0.5; // 故障线条数量
const double FREQUENCY = 1; // 故障线条频率
const double LINES_WIDTH = 1; // 故障线条宽度

private:
double Trunc(double x, double num_levels)
{
return floor(x * num_levels) / num_levels;
}

void RGBToYUV(double r, double g, double b, double* y, double* u, double* v)
{
*y = 0.299 * r + 0.587 * g + 0.114 * b;
*u = -0.14713 * r + -0.28886 * g + 0.436 * b;
*v = 0.615 * r + -0.51499 * g + -0.10001 * b;
}

void YUVToRGB(double y, double u, double v, double* r, double* g, double* b)
{
*r = y + v * 1.13983;
*g = y + -0.39465 * u + -0.58060 * v;
*b = y + u * 2.03211;
}
};


/*

* 图块抖动故障

*/
class TileJitterGlitch : public GlitchProcessor
{
public:
TileJitterGlitch() = default;
TileJitterGlitch(IMAGE* src) : GlitchProcessor(src) {}
~TileJitterGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double offset_x = (double)x / width;
double offset_y = (double)y / height;

double strength = 0.5 + 0.5 * cos(TIME * FREQUENCY);

if ((int)((double)y / height * GetRandomNoise(TIME, 0.5) * SPLITTING_NUM) % 2 < 1)
{
offset_x += 1.0 / width * cos(TIME * SPEED) * OFFSET * strength;
offset_y += 1.0 / width * cos(TIME * SPEED) * OFFSET * strength * 0.25;
}

idx = (size_t)(offset_y * height) % height * width + (size_t)(offset_x * width) % width;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double SPEED = 4; // 故障图块速度
const double OFFSET = 20; // 故障图块偏移
const double FREQUENCY = 50; // 故障图块频率
const double SPLITTING_NUM = 8; // 故障图块数量
};


/*

* 扫描线抖动故障

*/
class ScanLineJitterGlitch : public GlitchProcessor
{
public:
ScanLineJitterGlitch() = default;
ScanLineJitterGlitch(IMAGE* src) : GlitchProcessor(src) {}
~ScanLineJitterGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double offset_x;

double strength = 0.5 + 0.5 * cos(TIME * FREQUENCY);
double jitter = GetRandomNoise((double)y / height, TIME) * 2 - 1;
jitter *= (abs(jitter) < THRESHOLD ? 0 : 1) * AMOUNT * strength;
offset_x = (double)x / width + jitter;

idx = y * width + (size_t)(offset_x * width) % width;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double AMOUNT = 0.5; // 扫描线数量
const double THRESHOLD = 0.9; // 抖动临界值
const double FREQUENCY = 2; // 扫描线频率
};


/*

* 数字条纹故障

*/
class DigitalStripeGlitch : public GlitchProcessor
{
public:
DigitalStripeGlitch() = default;
DigitalStripeGlitch(IMAGE* src)
: GlitchProcessor(src) { ResetRandomColorImage(); }
~DigitalStripeGlitch() = default;

void SetImage(IMAGE* src)
{
GlitchProcessor::SetImage(src);
ResetRandomColorImage();
}

void Process()
{
if (!CanProcess()) return;

if ((size_t)(TIME * 1000) % 5 == 0)
ResetRandomColorImage();

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);
DWORD* noise_buffer = GetImageBuffer(random_color_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double offset_x, offset_y;
DWORD noise_r, noise_g, noise_b;

// 基础数据准备
idx = y * width + x;
noise_r = (noise_buffer[idx] & 0x00ff0000) >> 16;
noise_g = (noise_buffer[idx] & 0x0000ff00) >> 8;
noise_b = noise_buffer[idx] & 0x000000ff;
double threshold = 1.001 - INDENSITY * 1.001;

// UV 偏移
double uv_shift = pow((double)noise_r / 255, 3) < threshold ? 0 : 1;
offset_x = (double)x / width + (double)noise_g / 255 * uv_shift;
offset_y = (double)y / height + (double)noise_b / 255 * uv_shift;

idx = ((size_t)(offset_y * height) % height) * width + (size_t)(offset_x * width) % width;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double INDENSITY = 0.25; // 条纹强度
IMAGE* random_color_image = nullptr;
COLORREF current_fill_color = RGB(255, 255, 255);

private:
// 生成随机颜色条纹贴图
void ResetRandomColorImage()
{
static const int STRIP_LENGTH_RATIO = 75; // 百分制条带长度
static const int WIDTH = 20, HEIGHT = 20; // 迷你颜色贴图尺寸
IMAGE mini_color_image = IMAGE(WIDTH, HEIGHT); // 迷你颜色贴图
DWORD* buffer = GetImageBuffer(&mini_color_image);
if (random_color_image) delete random_color_image;
random_color_image = new IMAGE(src_image->getwidth(), src_image->getheight());
for (size_t y = 0; y < HEIGHT; ++y)
{
for (size_t x = 0; x < WIDTH; ++x)
{
if (rand() % 100 > STRIP_LENGTH_RATIO)
current_fill_color = RGB(rand() % 256, rand() % 256, rand() % 256);
buffer[y * WIDTH + x] = BGR(current_fill_color);
}
}
// 将迷你颜色条纹贴图缩放到源图片相同尺寸并绘制到颜色条纹贴图上
StretchBlt(GetImageHDC(random_color_image), 0, 0, src_image->getwidth(), src_image->getheight(),
GetImageHDC(&mini_color_image), 0, 0, WIDTH, HEIGHT, SRCCOPY);
}
};


/*

* 模拟噪点故障

*/
class AnalogNoiseGlitch : public GlitchProcessor
{
public:
AnalogNoiseGlitch() = default;
AnalogNoiseGlitch(IMAGE* src) : GlitchProcessor(src) { }
~AnalogNoiseGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double noise_r, noise_g, noise_b;
double noise_x, noise_y, noise_z;

idx = y * width + x;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;
noise_r = (double)r / 255, noise_g = (double)g / 255, noise_b = (double)b / 255;

double luminance = noise_r * 0.22 + noise_g * 0.707 * noise_b * 0.071;
if (GetRandomNoise(TIME * SPEED, TIME * SPEED) > LUMINANCE_JITTER_THRESHOLD)
noise_r = luminance, noise_g = luminance, noise_b = luminance;

noise_x = GetRandomNoise(TIME * SPEED + (double)x / width / -213, TIME * SPEED + (double)y / height / 5.53);
noise_y = GetRandomNoise(TIME * SPEED - (double)x / width / 213, TIME * SPEED - (double)y / height / -5.53);
noise_z = GetRandomNoise(TIME * SPEED + (double)x / width / 213, TIME * SPEED + (double)y / height / 5.53);

noise_r += 0.9 * noise_x - 0.45;
noise_g += 0.9 * noise_y - 0.45;
noise_b += 0.9 * noise_z - 0.45;

r = (DWORD)Clamp((r + (noise_r * 255 - r) * FADING), 0, 255);
g = (DWORD)Clamp((g + (noise_g * 255 - g) * FADING), 0, 255);
b = (DWORD)Clamp((b + (noise_b * 255 - b) * FADING), 0, 255);

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double SPEED = 0.25; // 噪点速度
const double FADING = 0.5; // 混叠系数
const double LUMINANCE_JITTER_THRESHOLD = 0.9; // 亮度抖动阈值
};


/*

* 屏幕跳跃故障

*/
class ScreenJumpGlitch : public GlitchProcessor
{
public:
ScreenJumpGlitch() = default;
ScreenJumpGlitch(IMAGE* src) : GlitchProcessor(src) { }
~ScreenJumpGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;

double normalized_y = (double)y / height;
double jump_time = TIME * INDENSITY * 9.8;
double dst = normalized_y + jump_time - floor(normalized_y + jump_time);
double jump = normalized_y + (dst - normalized_y) * INDENSITY;

idx = (size_t)(jump * height) % height * width + x;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double INDENSITY = 0.25; // 跳跃强度
};


/*

* 屏幕抖动故障

*/
class ScreenShakeGlitch : public GlitchProcessor
{
public:
ScreenShakeGlitch() = default;
ScreenShakeGlitch(IMAGE* src) : GlitchProcessor(src) { }
~ScreenShakeGlitch() = default;

void Process()
{
if (!CanProcess()) return;

size_t width = src_image->getwidth();
size_t height = src_image->getheight();
DWORD* src_buffer = GetImageBuffer(src_image);
DWORD* dst_buffer = GetImageBuffer(dst_image);

for (size_t y = 0; y < height; ++y)
{
for (size_t x = 0; x < width; ++x)
{
size_t idx;
DWORD r, g, b;
double offset_y;

double shake = (GetRandomNoise(TIME, 2) - 0.5) * INDENSITY;
offset_y = (double)y / height + shake;

idx = (size_t)(offset_y * height) % height * width + x;
r = (src_buffer[idx] & 0x00ff0000) >> 16;
g = (src_buffer[idx] & 0x0000ff00) >> 8;
b = src_buffer[idx] & 0x000000ff;

dst_buffer[y * width + x] = BGR(RGB(r, g, b));
}
}
}

private:
const double INDENSITY = 0.1; // 抖动强度
};


int main(int argc, char** argv)
{
// 加载源图片
IMAGE image_src;
loadimage(&image_src, _T(RES_PATH));

// 检查加载结果
if (!GetImageBuffer(&image_src))
return -1;

// 初始化绘图设备和窗口标题
initgraph(image_src.getwidth(), image_src.getheight()/*, SHOWCONSOLE*/);
SetWindowText(GetHWnd(), _T("EasyX Glitch Art"));

// 创建渲染器列表
std::vector<std::pair<std::wstring, GlitchProcessor*>> processor_list =
{
{ _T("色彩分离故障"), new RGBSplitGlitch(&image_src) },
{ _T("错位图块故障"), new ImageBlockGlitch(&image_src) },
{ _T("错位线条故障"), new LineBlockGlitch(&image_src) },
{ _T("图块抖动故障"), new TileJitterGlitch(&image_src) },
{ _T("扫描线抖动故障"), new ScanLineJitterGlitch(&image_src) },
{ _T("数字条纹故障"), new DigitalStripeGlitch(&image_src) },
{ _T("模拟噪点故障"), new AnalogNoiseGlitch(&image_src) },
{ _T("屏幕跳跃故障"), new ScreenJumpGlitch(&image_src) },
{ _T("屏幕抖动故障"), new ScreenShakeGlitch(&image_src) },
};

// 指定当前渲染器
GlitchProcessor* current_processor = processor_list[0].second;

// 记录程序开始时间
const DWORD INIT_TICK_COUNT = GetTickCount();

BeginBatchDraw();
setbkmode(TRANSPARENT);

// 窗口主循环
while (true)
{
// 处理按键切换处理器事件
static ExMessage msg;
while (peekmessage(&msg))
{
if (msg.message == WM_KEYDOWN
&& msg.vkcode >= 0x30 && msg.vkcode <= 0x39
&& msg.vkcode - 0x30 < processor_list.size())
current_processor = processor_list[msg.vkcode - 0x30].second;
}

// 更新处理器全局变量
TIME = (GetTickCount() - INIT_TICK_COUNT) / 1000.0;

// 使用当前处理器进行处理并记录时间
const DWORD frame_begin = GetTickCount();
current_processor->Process();
putimage(0, 0, current_processor->GetImage());
DWORD frame_end = GetTickCount();

// 绘制左侧面板底色
static const int PANEL_WIDTH = 225;
setfillcolor(RGB(210, 210, 210));
fillrectangle(0, 0, PANEL_WIDTH, image_src.getheight());

// 计算处理器帧率并显示
size_t frame_rate = 1000 / max((frame_end - frame_begin), 1);
settextcolor(RGB(15, 15, 15));
outtextxy(15, 15, (_T("帧率:") + std::to_wstring(frame_rate).append(_T(" FPS"))).c_str());

// 显示全部可用处理器信息
setlinecolor(RGB(155, 155, 155)); line(5, 45, PANEL_WIDTH - 5, 45);
outtextxy(15, 65, _T("* 数字键切换故障效果"));
for (int i = 0; i < processor_list.size(); ++i)
{
const auto& processor_info = processor_list[i];
(current_processor == processor_info.second) ?
settextcolor(RGB(195, 115, 25)) : settextcolor(RGB(25, 25, 95));
std::wstring text = _T("[ ") + std::to_wstring(i) + _T(" ] ") + processor_info.first;
outtextxy(15, 100 + i * 25, text.c_str());
}

FlushBatchDraw();

// 稳定实际显示帧率
frame_end = GetTickCount();
if (frame_end - frame_begin < 1000 / 60)
Sleep(1000 / 60 - (frame_end - frame_begin));
}

EndBatchDraw();

// 释放渲染器资源
for (auto processor : processor_list)
delete processor.second;

return 0;
}
作者

Voidmatrix

发布于

2023-08-08

更新于

2023-08-08

许可协议

评论