前言

来自 Tsoding 的视频《Graphics API is irrelevant》

Tsoding 的观点非常直白:如果你想做的只是把像素画到屏幕上,你根本不需要任何图形 API。CPU 可以直接计算出每个像素的颜色,然后以一种最简单的图像格式(比如 PPM)保存下来,再用任何图片查看器打开即可。

这就是“用 CPU 跑 Shader”的最原始形态:没有 GPU,没有着色器语言,只有 for 循环和 fputc。

PPM 格式:比 TXT 还简单的图像格式

  • P6(二进制):直接写入每个像素的 RGB 字节,文件更小,也更容易用代码生成。

我们要用的就是 P6 二进制格式。它的头部只有三行:P6、宽度 高度、255。

紧接着就是 宽度 × 高度 × 3 个字节,按 R, G, B, R, G, B, ... 顺序排列。

纯红色的 PPM

代码展示

#include <stdio.h>

int main()
{
    FILE *f = fopen("output.ppm", "wb");
    int w = 16 * 60;   // 960
    int h = 9 * 60;    // 540

    // PPM 头部(P6 二进制格式)
    fprintf(f, "P6\n");
    fprintf(f, "%d %d\n", w, h);
    fprintf(f, "255\n");

    // 像素数据:全部写成 R=255, G=0, B=0
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            fputc(0xFF, f);   // R
            fputc(0x00, f);   // G
            fputc(0x00, f);   // B
        }
    }

    fclose(f);
    return 0;
}

编译

g++ ppm.c -o ppm.exe
.\ppm.exe

运行之后会生成一个ppm文件,我用ffmpeg转成了jpg格式

ffmpeg -i output.ppm -pix_fmt yuvj420p output.jpg

只是一个纯红色的无聊图案(?

示例

一个棋盘格

当然我们可以通过判断当前索引是奇数还是偶数,决定当前像素的颜色


if((i/60+j/60)%2){
           fputc(0xFF,f);
           fputc(0x00,f);
           fputc(0x00,f);
       }else{
           fputc(0x00,f);
           fputc(0x00,f);
           fputc(0x00,f);
        }

这样就可以得到一个黑红相间的棋盘格

棋盘格

一个棋盘格视频

我们还可以让棋盘格移动,只需要做这样的修改

if(((i+10)/60+(j+10)/60)%2)

编译运行之后的棋盘格就会在X和Y方向偏移

然后,我们可以重复60次,将上述过程从0-60进行迭代,每一次迭代我们都创建一个单独的帧文件,共生成60个,代码如下

#include<stdio.h>

int main()
{
    char buf[256];   //在栈上分配生成的帧文件名
    for(int k=0;k<60;k++){
    snprintf(buf,sizeof(buf),"output-%02d.ppm",k);
    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 i=0;i<h;i++){
             for(int j=0;j<w;j++){    
                 if(((i+k)/60+(j+k)/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;
}

然后在终端进行编译

ffmpeg -framerate 30 -i output-%02d.ppm -c:v libx264 -pix_fmt yuv420p output.mp4

即可看见视频

纯红色到任意“CPU Shader”

生成纯红色当然没什么用,但你可以替换内层循环里的 fputc逻辑——比如根据 (i, j) 坐标计算渐变色、画圆、画棋盘格,甚至实现分形、噪声、光线步进(Ray Marching)……所有这些都不需要任何图形 API,纯粹是 CPU 上对每个像素执行一个数学函数。

同时更进一步的,如果要动画,可以在外层再加一维时间 t,每帧生成一个 PPM,然后用 FFmpeg 合成视频。

以上本人因时间问题还未实现(挖坑中)