Portable Pixmap Format(PPM)格式是非常简单的图片/视频格式,它结构非常简单,不含任何压缩和优化处理,图片就是直接存储所有像素(P3:ASCII格式,P6:二进制格式),视频则是通过一组 PPM 图像创建。
因此可以用 C/C++ 直接写入,无需专门的图形处理库。但是 PPM 格式的图像查看有点麻烦,可以使用 ffmpeg 转换为 png 格式,生成视频的过程也需要依赖 ffmpeg。

例如 P3 格式的 PPM 文件是完全使用 ASCII 格式明文存储的,格式规定如下:

  • 第一行是魔数 P3
  • 第二行说明图像的宽度和高度
  • 第三行说明像素值的最大值(通常是255)
  • 第四行开始是 RGB 像素数据矩阵

示例文件如下

1
2
3
4
5
P3
4 2
255
255 0 0 0 255 0 0 0 255 255 255 0
255 255 255 0 0 0 128 128 128 255 0 255

P6 格式则是把数据存储从 ASCII 格式改成了二进制格式,除此之外和 P3 格式一样。

参考资料:

示例代码

编译命令见头部注释。

checker.c
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
// $ cc -o checker checker.c
// $ ./checker
// $ ffmpeg -i output-%02d.ppm -r 60 output.mp4
#include <stdio.h>

int main() {
char buf[256];
for (int i = 0; i < 60; ++i) {
snprintf(buf, sizeof(buf), "output-%02d.ppm", i);
const char *output_path = buf;
FILE *f = fopen(output_path, "wb");
int w = 16 * 60;
int h = 9 * 60;
fprintf(f, "P6\n");
fprintf(f, "%d %d\n", w, h);
fprintf(f, "255\n");
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
if (((x + i) / 60 + (y + i) / 60) % 2) {
fputc(0xFF, f);
fputc(0x00, f);
fputc(0x00, f);
}
else {
fputc(0x00, f);
fputc(0x00, f);
fputc(0x00, f);
}
}
}
fclose(f);
printf("Generated %s\n", output_path);
}
return 0;
}
plasma.cpp
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
// $ cc -O3 -o plasma plasma.cpp -lm
// $ ./plasma
// $ ffmpeg -i output-%03d.ppm -r 60 output.mp4

#include <math.h>
#include <stdio.h>

struct vec4 {
float x, y, z, w;

vec4(float x = 0, float y = 0, float z = 0, float w = 0)
: x(x), y(y), z(z), w(w) {}
};

struct vec2 {
float x, y;

vec2(float x = 0, float y = 0) : x(x), y(y) {}

vec2 yx() const { return vec2(y, x); }

vec4 xyyx() const { return vec4(x, y, y, x); }
};

vec2 operator*(const vec2 &a, float s) { return vec2(a.x * s, a.y * s); }

vec2 operator+(const vec2 &a, float s) { return vec2(a.x + s, a.y + s); }

vec2 operator*(float s, const vec2 &a) { return a * s; }

vec2 operator-(const vec2 &a, const vec2 &b) {
return vec2(a.x - b.x, a.y - b.y);
}

vec2 operator+(const vec2 &a, const vec2 &b) {
return vec2(a.x + b.x, a.y + b.y);
}

vec2 operator*(const vec2 &a, const vec2 &b) {
return vec2(a.x * b.x, a.y * b.y);
}

vec2 operator/(const vec2 &a, float s) { return vec2(a.x / s, a.y / s); }

float dot(const vec2 &a, const vec2 &b) { return a.x * b.x + a.y * b.y; }

vec2 abs(const vec2 &a) { return vec2(fabsf(a.x), fabsf(a.y)); }

vec2 &operator+=(vec2 &a, const vec2 &b) {
a = a + b;
return a;
}

vec2 &operator+=(vec2 &a, float s) {
a = a + s;
return a;
}

vec2 cos(const vec2 &a) { return vec2(cosf(a.x), cosf(a.y)); }

vec4 sin(const vec4 &a) {
return vec4(sinf(a.x), sinf(a.y), sinf(a.z), sinf(a.w));
}

vec4 exp(const vec4 &a) {
return vec4(expf(a.x), expf(a.y), expf(a.z), expf(a.w));
}

vec4 tanh(const vec4 &a) {
return vec4(tanhf(a.x), tanhf(a.y), tanhf(a.z), tanhf(a.w));
}

vec4 operator+(const vec4 &a, float s) {
return vec4(a.x + s, a.y + s, a.z + s, a.w + s);
}

vec4 operator*(const vec4 &a, float s) {
return vec4(a.x * s, a.y * s, a.z * s, a.w * s);
}

vec4 operator*(float s, const vec4 &a) { return a * s; }

vec4 operator+(const vec4 &a, const vec4 &b) {
return vec4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
}

vec4 &operator+=(vec4 &a, const vec4 &b) {
a = a + b;
return a;
}

vec4 operator-(float s, const vec4 &a) {
return vec4(s - a.x, s - a.y, s - a.z, s - a.w);
}

vec4 operator/(const vec4 &a, const vec4 &b) {
return vec4(a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w);
}

int main() {
char buf[256];
for (int i = 0; i < 240; ++i) {
snprintf(buf, sizeof(buf), "output-%03d.ppm", i);
const char *output_path = buf;
FILE *f = fopen(output_path, "wb");
int w = 16 * 60;
int h = 9 * 60;
fprintf(f, "P6\n");
fprintf(f, "%d %d\n", w, h);
fprintf(f, "255\n");
vec2 r = {(float)w, (float)h};
float t = ((float)i / 240) * 2 * M_PI;
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
vec4 o;
vec2 FC = {(float)x, (float)y};

// https://x.com/XorDev/status/1894123951401378051
vec2 p = (FC * 2. - r) / r.y, l, i;
vec2 v = p * (l += 4. - 4. * abs(.7 - dot(p, p)));
for (; i.y++ < 8.; o += (sin(v.xyyx()) + 1.) * abs(v.x - v.y))
v += cos(v.yx() * i.y + i + t) / i.y + .7;
o = tanh(5. * exp(l.x - 4. - p.y * vec4(-1, 1, 2, 0)) / o);

fputc(o.x * 255, f);
fputc(o.y * 255, f);
fputc(o.z * 255, f);
}
}
fclose(f);
printf("Generated %s (%3d/%3d)\n", output_path, i + 1, 240);
}
return 0;
}