OpenGL 蓝书学习(第四版)第七章

OpenGL 蓝书学习(第四版)第七章

固定管线像素图显示

画位图

#ifdef __APPLE__
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif
 
#include <numbers>
#include <ostream>
#include <iostream>

// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
/*
 * FIRE 位图数据,用于 glBitmap 渲染
 * 32 * 32 位图,黑色背景,白色火焰形状
 * 每个字节表示 8 个像素,1 表示白色,0 表示黑色
 * 共 128 字节 (32 * 32 / 8)
 * 从左到右,从上到下排列
 */
static constexpr GLubyte FIRE[128] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x07, 0xf0,
    0x0f, 0x00, 0x1f, 0xe0, 0x1f, 0x80, 0x1f, 0xc0, 0x0f, 0xc0, 0x3f, 0x80,
    0x07, 0xe0, 0x7e, 0x00, 0x03, 0xf0, 0xff, 0x80, 0x03, 0xf5, 0xff, 0xe0,
    0x07, 0xfd, 0xff, 0xf8, 0x1f, 0xfc, 0xff, 0xe8, 0xff, 0xe3, 0xbf, 0x70,
    0xde, 0x80, 0xb7, 0x00, 0x71, 0x10, 0x4a, 0x80, 0x03, 0x10, 0x4e, 0x40,
    0x02, 0x88, 0x8c, 0x20, 0x05, 0x05, 0x04, 0x40, 0x02, 0x82, 0x14, 0x40,
    0x02, 0x40, 0x10, 0x80, 0x02, 0x64, 0x1a, 0x80, 0x00, 0x92, 0x29, 0x00,
    0x00, 0xb0, 0x48, 0x00, 0x00, 0xc8, 0x90, 0x00, 0x00, 0x85, 0x10, 0x00,
    0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00
};

// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

void renderScene(){
    glClear(GL_COLOR_BUFFER_BIT);
    glColor3f(1.0F, 1.0F, 1.0F);
    for(int i = 0; i < 2; i ++ ){
        glRasterPos2i(0, i * 32);
        for(int j = 0; j < 3; j ++ ){
            glBitmap(32, 32, 0.0, 0.0, 32.0, 0.0, FIRE);
        }
    }
    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.25F, 0.25F, 0.25F, 1.0F);
}
 
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    const int mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    setupRc();
    glutMainLoop();
    return 0;
}

加载tga图片

#include <ostream>
#ifdef __APPLE__
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif
 
#include <numbers>
#include <iostream>
#include <fstream>
#include <filesystem>
 
// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
 
// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度

// Targa 图像文件头结构体
#pragma pack(push, 1)          // 关闭结构体对齐填充
struct TargaHeader {
    // 图像 ID 区长度,0 = 无
    GLubyte idLength;
    GLubyte colorMapType;  // 0 = 无调色板
    GLubyte dataTypeCode;     // 图像数据类型代码 2 = 未压缩真彩图(非 RLE)
    GLushort colorMapOrigin;  // 调色板起始索引
    GLushort colorMapLength;  // 调色板颜色的数量
    GLubyte colorMapDepth;  // 调色板每条颜色的位数
    GLushort xOrigin;   // 图像在屏幕上的 X 坐标
    GLushort yOrigin;   // 图像在屏幕上的 Y 坐标
    GLushort width;   // 图像宽度
    GLushort height;  // 图像高度
    // 每像素 24 位 = RGB 各 8 位
    GLubyte bitsPerPixel;
    // 0 表示自下而上存储
    GLubyte imageDescriptor;
};
#pragma pack(pop)             // 恢复结构体对齐填充

std::ostream& operator<<(std::ostream& os, const TargaHeader& header) {
    os << "TargaHeader { "
       << "idLength=" << static_cast<int>(header.idLength) << ", "
       << "colorMapType=" << static_cast<int>(header.colorMapType) << ", "
       << "dataTypeCode=" << static_cast<int>(header.dataTypeCode) << ", "
       << "colorMapOrigin=" << header.colorMapOrigin << ", "
       << "colorMapLength=" << header.colorMapLength << ", "
       << "colorMapDepth=" << static_cast<int>(header.colorMapDepth) << ", "
       << "xOrigin=" << header.xOrigin << ", "
       << "yOrigin=" << header.yOrigin << ", "
       << "width=" << header.width << ", "
       << "height=" << header.height << ", "
       << "bitsPerPixel=" << static_cast<int>(header.bitsPerPixel) << ", "
       << "imageDescriptor=" << static_cast<int>(header.imageDescriptor)
       << " }";
    return os;
}
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

void renderScene(){
    glClear(GL_COLOR_BUFFER_BIT);

    std::ifstream file("../fire.tga", std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open fire.tga" << std::endl;
        return;
    }
    TargaHeader header;
    std::cout << "Size of TargaHeader: " << sizeof(TargaHeader) << std::endl;
    file.read(reinterpret_cast<char*>(&header), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to read Targa header" << std::endl;
        return;
    }
    std::cout << "Targa header: " << header << std::endl;
    const std::size_t imageSize = header.width * header.height * (header.bitsPerPixel / 8);
    const auto imageData = std::make_unique<GLbyte[]>(imageSize);
    file.read(reinterpret_cast<char*>(imageData.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to read Targa image data" << std::endl;
        return;
    }
    std::cout << "Successfully read Targa image data" << std::endl;

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glDrawPixels(header.width, header.height, GL_BGR, GL_UNSIGNED_BYTE, imageData.get());

    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.25F, 0.25F, 0.25F, 1.0F);
}
 
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    const int mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    setupRc();
    glutMainLoop();
    return 0;
}

加载png图片

#ifdef __APPLE__
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

#include <ostream>
#include <numbers>
#include <iostream>
#include <fstream>
#include <filesystem>
 
// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
 
// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

void renderScene(){
    glClear(GL_COLOR_BUFFER_BIT);

    stbi_set_flip_vertically_on_load(1);  // PNG/JPG 行序是"自上而下",翻转为 OpenGL 习惯的"自下而上"
    int width, height, channels;
    using ImageData = std::unique_ptr<unsigned char[], decltype(&stbi_image_free)>;
    ImageData imageData(stbi_load("../image.png", &width, &height, &channels, 4), &stbi_image_free);
    if (!imageData) {
        std::cerr << "Failed to load image.png" << std::endl;
        return;
    }
    std::cout << "Successfully read image.png data"  << ", width=" << width << ", height=" << height << ", channels=" << channels << std::endl;

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glDrawPixels(width, height, GL_RGBA, GL_UNSIGNED_BYTE, imageData.get());
    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.25F, 0.25F, 0.25F, 1.0F);
}
 
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    const int mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    std::cout << "Current path: " << std::filesystem::current_path() << std::endl;
    setupRc();
    glutMainLoop();
    return 0;
}

8种特效

#ifdef __APPLE__
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif
 
#include <ostream>
#include <numbers>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <memory>   // std::unique_ptr / std::make_unique
#include <utility>  // std::pair / std::make_pair / std::move
 
// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
 
// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度
static GLint renderMode = 1;
static GLint mainWindowId = 0; // 主窗口 ID

// Targa 图像文件头结构体
#pragma pack(push, 1)          // 关闭结构体对齐填充
struct TargaHeader {
    // 图像 ID 区长度,0 = 无
    GLubyte idLength;
    GLubyte colorMapType;  // 0 = 无调色板
    GLubyte dataTypeCode;     // 图像数据类型代码 2 = 未压缩真彩图(非 RLE)
    GLushort colorMapOrigin;  // 调色板起始索引
    GLushort colorMapLength;  // 调色板颜色的数量
    GLubyte colorMapDepth;  // 调色板每条颜色的位数
    GLushort xOrigin;   // 图像在屏幕上的 X 坐标
    GLushort yOrigin;   // 图像在屏幕上的 Y 坐标
    GLushort width;   // 图像宽度
    GLushort height;  // 图像高度
    // 每像素 24 位 = RGB 各 8 位
    GLubyte bitsPerPixel;
    // 0 表示自下而上存储
    GLubyte imageDescriptor;
};
#pragma pack(pop)             // 恢复结构体对齐填充

std::ostream& operator<<(std::ostream& os, const TargaHeader& header) {
    os << "TargaHeader { "
       << "idLength=" << static_cast<int>(header.idLength) << ", "
       << "colorMapType=" << static_cast<int>(header.colorMapType) << ", "
       << "dataTypeCode=" << static_cast<int>(header.dataTypeCode) << ", "
       << "colorMapOrigin=" << header.colorMapOrigin << ", "
       << "colorMapLength=" << header.colorMapLength << ", "
       << "colorMapDepth=" << static_cast<int>(header.colorMapDepth) << ", "
       << "xOrigin=" << header.xOrigin << ", "
       << "yOrigin=" << header.yOrigin << ", "
       << "width=" << header.width << ", "
       << "height=" << header.height << ", "
       << "bitsPerPixel=" << static_cast<int>(header.bitsPerPixel) << ", "
       << "imageDescriptor=" << static_cast<int>(header.imageDescriptor)
       << " }";
    return os;
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>>;
const static std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> targaData = loadTarga();
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> {
    std::cout << "Loading Targa image..." << std::endl;
    std::ifstream file("../horse.tga", std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open horse.tga" << std::endl;
        throw std::runtime_error("Failed to open horse.tga");
    }
    TargaHeader header;
    std::cout << "Size of TargaHeader: " << sizeof(TargaHeader) << std::endl;
    file.read(reinterpret_cast<char*>(&header), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to read Targa header" << std::endl;
        throw std::runtime_error("Failed to read Targa header");
    }
    std::cout << "Targa header: " << header << std::endl;
    const std::size_t imageSize = header.width * header.height * (header.bitsPerPixel / 8);
    auto imageData = std::make_unique<GLbyte[]>(imageSize);   // 注意:不能是 const auto,unique_ptr 需要可移动
    file.read(reinterpret_cast<char*>(imageData.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to read Targa image data" << std::endl;
        throw std::runtime_error("Failed to read Targa image data");
    }
    std::cout << "Successfully read Targa image data" << std::endl;
    return std::make_pair(std::move(header), std::move(imageData));
}

void renderScene(){
    glClear(GL_COLOR_BUFFER_BIT);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glDrawPixels(targaData.first.width, targaData.first.height, GL_BGR, GL_UNSIGNED_BYTE, targaData.second.get());

    // 中心绘制一个圆
    constexpr int numSegments = 100;
    constexpr float radius = 50.0f;
    glColor3f(1.0f, 0.0f, 0.0f); // 红色
    glBegin(GL_LINE_LOOP);
    for (int i = 0; i < numSegments; ++i) {
        float angle = 2.0f * PI * i / numSegments;
        float x = 800.0F / 2 + radius * cos(angle);
        float y = 600.0F / 2 + radius * sin(angle);
        glVertex2f(x, y);
    }
    glEnd();

    switch(renderMode){
        case 0: 
            glPixelZoom(1.0F, 1.0F);
            glRasterPos2i(0, 0);
            glPixelTransferf(GL_RED_SCALE, 1.0F);
            glPixelTransferf(GL_GREEN_SCALE, 1.0F);
            glPixelTransferf(GL_BLUE_SCALE, 1.0F);
            glPixelTransferi(GL_MAP_COLOR, GL_FALSE);
            break;
        case 2:
            glPixelZoom(-1.0F, -1.0F);
            glRasterPos2i(targaData.first.width, targaData.first.height);
            break;
        case 3:
            // 放大
            GLint viewport[4];
            glGetIntegerv(GL_VIEWPORT, viewport);
            glPixelZoom(static_cast<GLfloat>(viewport[2]) / targaData.first.width,
                        static_cast<GLfloat>(viewport[3]) / targaData.first.height);
            glRasterPos2i(0, 0);
            break;
        case 4:
            // 红色通道显示
            glPixelTransferf(GL_RED_SCALE, 1.0F);
            glPixelTransferf(GL_GREEN_SCALE, 0.0F);
            glPixelTransferf(GL_BLUE_SCALE, 0.0F);
            break;
        case 5:
            // 绿色通道显示
            glPixelTransferf(GL_RED_SCALE, 0.0F);
            glPixelTransferf(GL_GREEN_SCALE, 1.0F);
            glPixelTransferf(GL_BLUE_SCALE, 0.0F);
            break;
        case 6:
            // 蓝色通道显示
            glPixelTransferf(GL_RED_SCALE, 0.0F);
            glPixelTransferf(GL_GREEN_SCALE, 0.0F);
            glPixelTransferf(GL_BLUE_SCALE, 1.0F);
            break;
        case 7: {
            // 下面三行解决过度饱和
            glPixelTransferf(GL_RED_SCALE, 0.3F);
            glPixelTransferf(GL_GREEN_SCALE, 0.59F);
            glPixelTransferf(GL_BLUE_SCALE, 0.11F);
            const auto newImageData = std::make_unique<GLbyte[]>(targaData.first.width * targaData.first.height);
            glReadPixels(0, 0, targaData.first.width, targaData.first.height, GL_LUMINANCE, GL_UNSIGNED_BYTE, newImageData.get());
            glPixelTransferf(GL_RED_SCALE, 1.0F);
            glPixelTransferf(GL_GREEN_SCALE, 1.0F);
            glPixelTransferf(GL_BLUE_SCALE, 1.0F);
            glDrawPixels(targaData.first.width, targaData.first.height, GL_LUMINANCE, GL_UNSIGNED_BYTE, newImageData.get());
            break;
        }
        case 8: {
            GLfloat invertMap[256] = {0.0F};
            for (int i = 0; i < 256; ++i) {
                invertMap[i] = 1.0F - (1.0F / 255.0F) * static_cast<GLfloat>(i);
            }
            glPixelMapfv(GL_PIXEL_MAP_R_TO_R, 256, invertMap);
            glPixelMapfv(GL_PIXEL_MAP_G_TO_G, 256, invertMap);
            glPixelMapfv(GL_PIXEL_MAP_B_TO_B, 256, invertMap);
            glPixelTransferi(GL_MAP_COLOR, GL_TRUE);
            break;
        }
        default:
            break;
    }
    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.0F, 0.0F, 0.0F, 0.0F);
}
void writeTarga(const std::string& filename, const TargaHeader& header, const GLbyte* imageData) {
    // 获取viewport的宽高
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    std::cout << std::format("Viewport: x={}, y={}, width={}, height={}", viewport[0], viewport[1], viewport[2], viewport[3]) << std::endl;
    const GLushort width = viewport[2];
    const GLushort height = viewport[3];
    const TargaHeader newHeader = {
        .idLength = header.idLength,
        .colorMapType = header.colorMapType,
        .dataTypeCode = header.dataTypeCode,
        .colorMapOrigin = header.colorMapOrigin,
        .colorMapLength = header.colorMapLength,
        .colorMapDepth = header.colorMapDepth,
        .xOrigin = header.xOrigin,
        .yOrigin = header.yOrigin,
        .width = width,
        .height = height,
        .bitsPerPixel = header.bitsPerPixel,
        .imageDescriptor = header.imageDescriptor,
    };

    std::ofstream file(filename, std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open " << filename << " for writing" << std::endl;
        throw std::runtime_error("Failed to open file for writing");
    }
    file.write(reinterpret_cast<const char*>(&newHeader), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to write Targa header to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa header");
    }
    const std::size_t imageSize = newHeader.width * newHeader.height * (newHeader.bitsPerPixel / 8);
    const std::unique_ptr<GLbyte[]> imageBuffer = std::make_unique<GLbyte[]>(imageSize);

    glPixelStorei(GL_PACK_ALIGNMENT, 1);      // ← 注意是 PACK,读像素时用
    // 获取当前读的是前缓冲区还是后缓冲区
    GLint currentBuffer;
    glGetIntegerv(GL_READ_BUFFER, &currentBuffer); // 获取当前像素存储模式
    // 16进制输出当前读缓冲区
    std::cout << std::format("Current read buffer: 0x{:X}", currentBuffer) << std::endl;
    glReadBuffer(GL_FRONT);
    glReadPixels(0, 0, newHeader.width, newHeader.height, GL_BGR, GL_UNSIGNED_BYTE, imageBuffer.get());
    glReadBuffer(currentBuffer); // 恢复之前的读缓冲区
    file.write(reinterpret_cast<const char*>(imageBuffer.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to write Targa image data to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa image data");
    }
}
void processMenu(const int option){
    std::cout << std::format("Menu option selected: {}", option) << std::endl;
    glutSetWindow(mainWindowId);
    switch(option){
        case 1:
            std::cout << "Option 1 selected" << std::endl;
            writeTarga("output.tga", targaData.first, targaData.second.get());
            break;
        default:
            renderMode = option;
            break;
    }
    glutPostRedisplay();
}
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    std::cout << "current path: " << std::filesystem::current_path() << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    glutCreateMenu(processMenu);
    glutAddMenuEntry("重置", 0);
    glutAddMenuEntry("保存", 1);
    glutAddMenuEntry("旋转180度", 2);
    glutAddMenuEntry("放大", 3);
    glutAddMenuEntry("红色", 4);
    glutAddMenuEntry("绿色", 5);
    glutAddMenuEntry("蓝色", 6);
    glutAddMenuEntry("黑白", 7);
    glutAddMenuEntry("颜色反转", 8);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    setupRc();
    glutMainLoop();
    return 0;
}

保存为png

#ifdef __APPLE__
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif
 
#include <ostream>
#include <numbers>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <memory>   // std::unique_ptr / std::make_unique
#include <utility>  // std::pair / std::make_pair / std::move

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
 
// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
 
// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度
static GLint renderMode = 1;
static GLint mainWindowId = 0; // 主窗口 ID

// Targa 图像文件头结构体
#pragma pack(push, 1)          // 关闭结构体对齐填充
struct TargaHeader {
    // 图像 ID 区长度,0 = 无
    GLubyte idLength;
    GLubyte colorMapType;  // 0 = 无调色板
    GLubyte dataTypeCode;     // 图像数据类型代码 2 = 未压缩真彩图(非 RLE)
    GLushort colorMapOrigin;  // 调色板起始索引
    GLushort colorMapLength;  // 调色板颜色的数量
    GLubyte colorMapDepth;  // 调色板每条颜色的位数
    GLushort xOrigin;   // 图像在屏幕上的 X 坐标
    GLushort yOrigin;   // 图像在屏幕上的 Y 坐标
    GLushort width;   // 图像宽度
    GLushort height;  // 图像高度
    // 每像素 24 位 = RGB 各 8 位
    GLubyte bitsPerPixel;
    // 0 表示自下而上存储
    GLubyte imageDescriptor;
};
#pragma pack(pop)             // 恢复结构体对齐填充

std::ostream& operator<<(std::ostream& os, const TargaHeader& header) {
    os << "TargaHeader { "
       << "idLength=" << static_cast<int>(header.idLength) << ", "
       << "colorMapType=" << static_cast<int>(header.colorMapType) << ", "
       << "dataTypeCode=" << static_cast<int>(header.dataTypeCode) << ", "
       << "colorMapOrigin=" << header.colorMapOrigin << ", "
       << "colorMapLength=" << header.colorMapLength << ", "
       << "colorMapDepth=" << static_cast<int>(header.colorMapDepth) << ", "
       << "xOrigin=" << header.xOrigin << ", "
       << "yOrigin=" << header.yOrigin << ", "
       << "width=" << header.width << ", "
       << "height=" << header.height << ", "
       << "bitsPerPixel=" << static_cast<int>(header.bitsPerPixel) << ", "
       << "imageDescriptor=" << static_cast<int>(header.imageDescriptor)
       << " }";
    return os;
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>>;
const static std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> targaData = loadTarga();
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> {
    std::cout << "Loading Targa image..." << std::endl;
    std::ifstream file("../horse.tga", std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open horse.tga" << std::endl;
        throw std::runtime_error("Failed to open horse.tga");
    }
    TargaHeader header;
    std::cout << "Size of TargaHeader: " << sizeof(TargaHeader) << std::endl;
    file.read(reinterpret_cast<char*>(&header), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to read Targa header" << std::endl;
        throw std::runtime_error("Failed to read Targa header");
    }
    std::cout << "Targa header: " << header << std::endl;
    const std::size_t imageSize = header.width * header.height * (header.bitsPerPixel / 8);
    auto imageData = std::make_unique<GLbyte[]>(imageSize);   // 注意:不能是 const auto,unique_ptr 需要可移动
    file.read(reinterpret_cast<char*>(imageData.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to read Targa image data" << std::endl;
        throw std::runtime_error("Failed to read Targa image data");
    }
    std::cout << "Successfully read Targa image data" << std::endl;
    return std::make_pair(std::move(header), std::move(imageData));
}

void renderScene(){
    glClear(GL_COLOR_BUFFER_BIT);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glDrawPixels(targaData.first.width, targaData.first.height, GL_BGR, GL_UNSIGNED_BYTE, targaData.second.get());

    // 中心绘制一个圆
    constexpr int numSegments = 100;
    constexpr float radius = 50.0f;
    glColor3f(1.0f, 0.0f, 0.0f); // 红色
    glBegin(GL_LINE_LOOP);
    for (int i = 0; i < numSegments; ++i) {
        float angle = 2.0f * PI * i / numSegments;
        float x = 800.0F / 2 + radius * cos(angle);
        float y = 600.0F / 2 + radius * sin(angle);
        glVertex2f(x, y);
    }
    glEnd();

    switch(renderMode){
        case 0: 
            glPixelZoom(1.0F, 1.0F);
            glRasterPos2i(0, 0);
            glPixelTransferf(GL_RED_SCALE, 1.0F);
            glPixelTransferf(GL_GREEN_SCALE, 1.0F);
            glPixelTransferf(GL_BLUE_SCALE, 1.0F);
            glPixelTransferi(GL_MAP_COLOR, GL_FALSE);
            break;
        case 2:
            glPixelZoom(-1.0F, -1.0F);
            glRasterPos2i(targaData.first.width, targaData.first.height);
            break;
        case 3:
            // 放大
            GLint viewport[4];
            glGetIntegerv(GL_VIEWPORT, viewport);
            glPixelZoom(static_cast<GLfloat>(viewport[2]) / targaData.first.width,
                        static_cast<GLfloat>(viewport[3]) / targaData.first.height);
            glRasterPos2i(0, 0);
            break;
        case 4:
            // 红色通道显示
            glPixelTransferf(GL_RED_SCALE, 1.0F);
            glPixelTransferf(GL_GREEN_SCALE, 0.0F);
            glPixelTransferf(GL_BLUE_SCALE, 0.0F);
            break;
        case 5:
            // 绿色通道显示
            glPixelTransferf(GL_RED_SCALE, 0.0F);
            glPixelTransferf(GL_GREEN_SCALE, 1.0F);
            glPixelTransferf(GL_BLUE_SCALE, 0.0F);
            break;
        case 6:
            // 蓝色通道显示
            glPixelTransferf(GL_RED_SCALE, 0.0F);
            glPixelTransferf(GL_GREEN_SCALE, 0.0F);
            glPixelTransferf(GL_BLUE_SCALE, 1.0F);
            break;
        case 7: {
            // 下面三行解决过度饱和
            glPixelTransferf(GL_RED_SCALE, 0.3F);
            glPixelTransferf(GL_GREEN_SCALE, 0.59F);
            glPixelTransferf(GL_BLUE_SCALE, 0.11F);
            const auto newImageData = std::make_unique<GLbyte[]>(targaData.first.width * targaData.first.height);
            glReadPixels(0, 0, targaData.first.width, targaData.first.height, GL_LUMINANCE, GL_UNSIGNED_BYTE, newImageData.get());
            glPixelTransferf(GL_RED_SCALE, 1.0F);
            glPixelTransferf(GL_GREEN_SCALE, 1.0F);
            glPixelTransferf(GL_BLUE_SCALE, 1.0F);
            glDrawPixels(targaData.first.width, targaData.first.height, GL_LUMINANCE, GL_UNSIGNED_BYTE, newImageData.get());
            break;
        }
        case 8: {
            GLfloat invertMap[256] = {0.0F};
            for (int i = 0; i < 256; ++i) {
                invertMap[i] = 1.0F - (1.0F / 255.0F) * static_cast<GLfloat>(i);
            }
            glPixelMapfv(GL_PIXEL_MAP_R_TO_R, 256, invertMap);
            glPixelMapfv(GL_PIXEL_MAP_G_TO_G, 256, invertMap);
            glPixelMapfv(GL_PIXEL_MAP_B_TO_B, 256, invertMap);
            glPixelTransferi(GL_MAP_COLOR, GL_TRUE);
            break;
        }
        default:
            break;
    }
    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.0F, 0.0F, 0.0F, 0.0F);
}
void writePng(const std::string& filename, const int width, const int height) {
    glPixelTransferi(GL_MAP_COLOR, GL_FALSE); // 禁用颜色映射
    // openGL 的 glReadPixels 读取的图像是自下而上的,而 PNG 是自上而下的,所以需要翻转图像
    stbi_flip_vertically_on_write(1);
    
    glPixelStorei(GL_PACK_ALIGNMENT, 1); // 设置像素存储模式,确保行对齐为1
    // 获取当前读的是前缓冲区还是后缓冲区
    GLint currentBuffer;
    glGetIntegerv(GL_READ_BUFFER, &currentBuffer); // 获取当前像素存储模式
    // 16进制输出当前读缓冲区
    std::cout << std::format("Current read buffer: 0x{:X}", currentBuffer) << std::endl;
    glReadBuffer(GL_FRONT);
    const std::size_t imageSize = width * height * 3; // 3 bytes per pixel for RGB
    const auto flippedImageData = std::make_unique<GLbyte[]>(imageSize);
    glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, flippedImageData.get());
    glReadBuffer(currentBuffer); // 恢复之前的读缓冲区

    if (!stbi_write_png(filename.c_str(), width, height, 3, flippedImageData.get(), width * 3)) {
        std::cerr << "Failed to write PNG file: " << filename << std::endl;
        throw std::runtime_error("Failed to write PNG file");
    }
}
void writeTarga(const std::string& filename, const TargaHeader& header) {
    glPixelTransferi(GL_MAP_COLOR, GL_FALSE); // 禁用颜色映射
    // 获取viewport的宽高
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    std::cout << std::format("Viewport: x={}, y={}, width={}, height={}", viewport[0], viewport[1], viewport[2], viewport[3]) << std::endl;
    const GLushort width = viewport[2];
    const GLushort height = viewport[3];
    const TargaHeader newHeader = {
        .idLength = header.idLength,
        .colorMapType = header.colorMapType,
        .dataTypeCode = header.dataTypeCode,
        .colorMapOrigin = header.colorMapOrigin,
        .colorMapLength = header.colorMapLength,
        .colorMapDepth = header.colorMapDepth,
        .xOrigin = header.xOrigin,
        .yOrigin = header.yOrigin,
        .width = width,
        .height = height,
        .bitsPerPixel = header.bitsPerPixel,
        .imageDescriptor = header.imageDescriptor,
    };

    std::ofstream file(filename, std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open " << filename << " for writing" << std::endl;
        throw std::runtime_error("Failed to open file for writing");
    }
    file.write(reinterpret_cast<const char*>(&newHeader), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to write Targa header to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa header");
    }
    const std::size_t imageSize = 
        // renderMode == 8 ? newHeader.width * newHeader.height
        newHeader.width * newHeader.height * (newHeader.bitsPerPixel / 8);
    const std::unique_ptr<GLbyte[]> imageBuffer = std::make_unique<GLbyte[]>(imageSize);

    glPixelStorei(GL_PACK_ALIGNMENT, 1);      // ← 注意是 PACK,读像素时用
    // 获取当前读的是前缓冲区还是后缓冲区
    GLint currentBuffer;
    glGetIntegerv(GL_READ_BUFFER, &currentBuffer); // 获取当前像素存储模式
    // 16进制输出当前读缓冲区
    std::cout << std::format("Current read buffer: 0x{:X}", currentBuffer) << std::endl;
    glReadBuffer(GL_FRONT);
    glReadPixels(0, 0, newHeader.width, newHeader.height, GL_BGR, GL_UNSIGNED_BYTE, imageBuffer.get());
    glReadBuffer(currentBuffer); // 恢复之前的读缓冲区
    file.write(reinterpret_cast<const char*>(imageBuffer.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to write Targa image data to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa image data");
    }
}
void processMenu(const int option){
    std::cout << std::format("Menu option selected: {}", option) << std::endl;
    glutSetWindow(mainWindowId);
    switch(option){
        case 1:
            std::cout << "Option 1 selected" << std::endl;
            // writeTarga("output.tga", targaData.first, targaData.second.get());
            writePng("output.png", targaData.first.width, targaData.first.height);
            break;
        default:
            renderMode = option;
            break;
    }
    glutPostRedisplay();
}
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    std::cout << "current path: " << std::filesystem::current_path() << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    glutCreateMenu(processMenu);
    glutAddMenuEntry("重置", 0);
    glutAddMenuEntry("保存", 1);
    glutAddMenuEntry("旋转180度", 2);
    glutAddMenuEntry("放大", 3);
    glutAddMenuEntry("红色", 4);
    glutAddMenuEntry("绿色", 5);
    glutAddMenuEntry("蓝色", 6);
    glutAddMenuEntry("黑白", 7);
    glutAddMenuEntry("颜色反转", 8);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    setupRc();
    glutMainLoop();
    return 0;
}

扩展查询

#ifdef __APPLE__
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif
 
#include <ostream>
#include <numbers>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <memory>   // std::unique_ptr / std::make_unique
#include <utility>  // std::pair / std::make_pair / std::move

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
 
// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
 
// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度
static GLint renderMode = 1;
static GLint mainWindowId = 0; // 主窗口 ID

// Targa 图像文件头结构体
#pragma pack(push, 1)          // 关闭结构体对齐填充
struct TargaHeader {
    // 图像 ID 区长度,0 = 无
    GLubyte idLength;
    GLubyte colorMapType;  // 0 = 无调色板
    GLubyte dataTypeCode;     // 图像数据类型代码 2 = 未压缩真彩图(非 RLE)
    GLushort colorMapOrigin;  // 调色板起始索引
    GLushort colorMapLength;  // 调色板颜色的数量
    GLubyte colorMapDepth;  // 调色板每条颜色的位数
    GLushort xOrigin;   // 图像在屏幕上的 X 坐标
    GLushort yOrigin;   // 图像在屏幕上的 Y 坐标
    GLushort width;   // 图像宽度
    GLushort height;  // 图像高度
    // 每像素 24 位 = RGB 各 8 位
    GLubyte bitsPerPixel;
    // 0 表示自下而上存储
    GLubyte imageDescriptor;
};
#pragma pack(pop)             // 恢复结构体对齐填充

std::ostream& operator<<(std::ostream& os, const TargaHeader& header) {
    os << "TargaHeader { "
       << "idLength=" << static_cast<int>(header.idLength) << ", "
       << "colorMapType=" << static_cast<int>(header.colorMapType) << ", "
       << "dataTypeCode=" << static_cast<int>(header.dataTypeCode) << ", "
       << "colorMapOrigin=" << header.colorMapOrigin << ", "
       << "colorMapLength=" << header.colorMapLength << ", "
       << "colorMapDepth=" << static_cast<int>(header.colorMapDepth) << ", "
       << "xOrigin=" << header.xOrigin << ", "
       << "yOrigin=" << header.yOrigin << ", "
       << "width=" << header.width << ", "
       << "height=" << header.height << ", "
       << "bitsPerPixel=" << static_cast<int>(header.bitsPerPixel) << ", "
       << "imageDescriptor=" << static_cast<int>(header.imageDescriptor)
       << " }";
    return os;
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>>;
const static std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> targaData = loadTarga();
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> {
    std::cout << "Loading Targa image..." << std::endl;
    std::ifstream file("../horse.tga", std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open horse.tga" << std::endl;
        throw std::runtime_error("Failed to open horse.tga");
    }
    TargaHeader header;
    std::cout << "Size of TargaHeader: " << sizeof(TargaHeader) << std::endl;
    file.read(reinterpret_cast<char*>(&header), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to read Targa header" << std::endl;
        throw std::runtime_error("Failed to read Targa header");
    }
    std::cout << "Targa header: " << header << std::endl;
    const std::size_t imageSize = header.width * header.height * (header.bitsPerPixel / 8);
    auto imageData = std::make_unique<GLbyte[]>(imageSize);   // 注意:不能是 const auto,unique_ptr 需要可移动
    file.read(reinterpret_cast<char*>(imageData.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to read Targa image data" << std::endl;
        throw std::runtime_error("Failed to read Targa image data");
    }
    std::cout << "Successfully read Targa image data" << std::endl;
    return std::make_pair(std::move(header), std::move(imageData));
}

void renderScene(){
    glClear(GL_COLOR_BUFFER_BIT);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glDrawPixels(targaData.first.width, targaData.first.height, GL_BGR, GL_UNSIGNED_BYTE, targaData.second.get());

    // 中心绘制一个圆
    constexpr int numSegments = 100;
    constexpr float radius = 50.0f;
    glColor3f(1.0f, 0.0f, 0.0f); // 红色
    glBegin(GL_LINE_LOOP);
    for (int i = 0; i < numSegments; ++i) {
        float angle = 2.0f * PI * i / numSegments;
        float x = 800.0F / 2 + radius * cos(angle);
        float y = 600.0F / 2 + radius * sin(angle);
        glVertex2f(x, y);
    }
    glEnd();

    switch(renderMode){
        case 0: 
            glPixelZoom(1.0F, 1.0F);
            glRasterPos2i(0, 0);
            glPixelTransferf(GL_RED_SCALE, 1.0F);
            glPixelTransferf(GL_GREEN_SCALE, 1.0F);
            glPixelTransferf(GL_BLUE_SCALE, 1.0F);
            glPixelTransferi(GL_MAP_COLOR, GL_FALSE);
            break;
        case 2:
            glPixelZoom(-1.0F, -1.0F);
            glRasterPos2i(targaData.first.width, targaData.first.height);
            break;
        case 3:
            // 放大
            GLint viewport[4];
            glGetIntegerv(GL_VIEWPORT, viewport);
            glPixelZoom(static_cast<GLfloat>(viewport[2]) / targaData.first.width,
                        static_cast<GLfloat>(viewport[3]) / targaData.first.height);
            glRasterPos2i(0, 0);
            break;
        case 4:
            // 红色通道显示
            glPixelTransferf(GL_RED_SCALE, 1.0F);
            glPixelTransferf(GL_GREEN_SCALE, 0.0F);
            glPixelTransferf(GL_BLUE_SCALE, 0.0F);
            break;
        case 5:
            // 绿色通道显示
            glPixelTransferf(GL_RED_SCALE, 0.0F);
            glPixelTransferf(GL_GREEN_SCALE, 1.0F);
            glPixelTransferf(GL_BLUE_SCALE, 0.0F);
            break;
        case 6:
            // 蓝色通道显示
            glPixelTransferf(GL_RED_SCALE, 0.0F);
            glPixelTransferf(GL_GREEN_SCALE, 0.0F);
            glPixelTransferf(GL_BLUE_SCALE, 1.0F);
            break;
        case 7: {
            // 下面三行解决过度饱和
            glPixelTransferf(GL_RED_SCALE, 0.3F);
            glPixelTransferf(GL_GREEN_SCALE, 0.59F);
            glPixelTransferf(GL_BLUE_SCALE, 0.11F);
            const auto newImageData = std::make_unique<GLbyte[]>(targaData.first.width * targaData.first.height);
            glReadPixels(0, 0, targaData.first.width, targaData.first.height, GL_LUMINANCE, GL_UNSIGNED_BYTE, newImageData.get());
            glPixelTransferf(GL_RED_SCALE, 1.0F);
            glPixelTransferf(GL_GREEN_SCALE, 1.0F);
            glPixelTransferf(GL_BLUE_SCALE, 1.0F);
            glDrawPixels(targaData.first.width, targaData.first.height, GL_LUMINANCE, GL_UNSIGNED_BYTE, newImageData.get());
            break;
        }
        case 8: {
            GLfloat invertMap[256] = {0.0F};
            for (int i = 0; i < 256; ++i) {
                invertMap[i] = 1.0F - (1.0F / 255.0F) * static_cast<GLfloat>(i);
            }
            glPixelMapfv(GL_PIXEL_MAP_R_TO_R, 256, invertMap);
            glPixelMapfv(GL_PIXEL_MAP_G_TO_G, 256, invertMap);
            glPixelMapfv(GL_PIXEL_MAP_B_TO_B, 256, invertMap);
            glPixelTransferi(GL_MAP_COLOR, GL_TRUE);
            break;
        }
        default:
            break;
    }
    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.0F, 0.0F, 0.0F, 0.0F);
}
void writePng(const std::string& filename, const int width, const int height) {
    // openGL 的 glReadPixels 读取的图像是自下而上的,而 PNG 是自上而下的,所以需要翻转图像
    stbi_flip_vertically_on_write(1);
    
    glPixelStorei(GL_PACK_ALIGNMENT, 1); // 设置像素存储模式,确保行对齐为1
    // 获取当前读的是前缓冲区还是后缓冲区
    GLint currentBuffer;
    glGetIntegerv(GL_READ_BUFFER, &currentBuffer); // 获取当前像素存储模式
    // 16进制输出当前读缓冲区
    std::cout << std::format("Current read buffer: 0x{:X}", currentBuffer) << std::endl;
    glReadBuffer(GL_FRONT);
    const std::size_t imageSize = width * height * 3; // 3 bytes per pixel for RGB
    const auto flippedImageData = std::make_unique<GLbyte[]>(imageSize);
    glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, flippedImageData.get());
    glReadBuffer(currentBuffer); // 恢复之前的读缓冲区

    if (!stbi_write_png(filename.c_str(), width, height, 3, flippedImageData.get(), width * 3)) {
        std::cerr << "Failed to write PNG file: " << filename << std::endl;
        throw std::runtime_error("Failed to write PNG file");
    }
}
void writeTarga(const std::string& filename, const TargaHeader& header) {
    glPixelTransferi(GL_MAP_COLOR, GL_FALSE); // 禁用颜色映射
    // 获取viewport的宽高
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    std::cout << std::format("Viewport: x={}, y={}, width={}, height={}", viewport[0], viewport[1], viewport[2], viewport[3]) << std::endl;
    const GLushort width = viewport[2];
    const GLushort height = viewport[3];
    const TargaHeader newHeader = {
        .idLength = header.idLength,
        .colorMapType = header.colorMapType,
        .dataTypeCode = header.dataTypeCode,
        .colorMapOrigin = header.colorMapOrigin,
        .colorMapLength = header.colorMapLength,
        .colorMapDepth = header.colorMapDepth,
        .xOrigin = header.xOrigin,
        .yOrigin = header.yOrigin,
        .width = width,
        .height = height,
        .bitsPerPixel = header.bitsPerPixel,
        .imageDescriptor = header.imageDescriptor,
    };

    std::ofstream file(filename, std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open " << filename << " for writing" << std::endl;
        throw std::runtime_error("Failed to open file for writing");
    }
    file.write(reinterpret_cast<const char*>(&newHeader), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to write Targa header to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa header");
    }
    const std::size_t imageSize = 
        // renderMode == 8 ? newHeader.width * newHeader.height
        newHeader.width * newHeader.height * (newHeader.bitsPerPixel / 8);
    const std::unique_ptr<GLbyte[]> imageBuffer = std::make_unique<GLbyte[]>(imageSize);

    glPixelStorei(GL_PACK_ALIGNMENT, 1);      // ← 注意是 PACK,读像素时用
    // 获取当前读的是前缓冲区还是后缓冲区
    GLint currentBuffer;
    glGetIntegerv(GL_READ_BUFFER, &currentBuffer); // 获取当前像素存储模式
    // 16进制输出当前读缓冲区
    std::cout << std::format("Current read buffer: 0x{:X}", currentBuffer) << std::endl;
    glReadBuffer(GL_FRONT);
    glReadPixels(0, 0, newHeader.width, newHeader.height, GL_BGR, GL_UNSIGNED_BYTE, imageBuffer.get());
    glReadBuffer(currentBuffer); // 恢复之前的读缓冲区
    file.write(reinterpret_cast<const char*>(imageBuffer.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to write Targa image data to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa image data");
    }
}
void processMenu(const int option){
    std::cout << std::format("Menu option selected: {}", option) << std::endl;
    glutSetWindow(mainWindowId);
    switch(option){
        case 1:
            std::cout << "Option 1 selected" << std::endl;
            writeTarga("output.tga", targaData.first);
            break;
        default:
            renderMode = option;
            break;
    }
    glutPostRedisplay();
}
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    std::cout << "current path: " << std::filesystem::current_path() << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    glutCreateMenu(processMenu);
    glutAddMenuEntry("重置", 0);
    glutAddMenuEntry("保存", 1);
    glutAddMenuEntry("旋转180度", 2);
    glutAddMenuEntry("放大", 3);
    glutAddMenuEntry("红色", 4);
    glutAddMenuEntry("绿色", 5);
    glutAddMenuEntry("蓝色", 6);
    glutAddMenuEntry("黑白", 7);
    glutAddMenuEntry("颜色反转", 8);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    setupRc();
    std::cout << "GL_ARB_imaging supported: " << glutExtensionSupported("GL_ARB_imaging") << std::endl;
    glutMainLoop();
    return 0;
}

图像处理

#include <OpenGL/glext.h>
#ifdef __APPLE__
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif
 
#include <ostream>
#include <numbers>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <memory>   // std::unique_ptr / std::make_unique
#include <utility>  // std::pair / std::make_pair / std::move

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
 
// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
 
// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度

static GLint renderMode = 0;
static GLint mainWindowId = 0; // 主窗口 ID
static GLboolean isHistogram = GL_FALSE; // 是否显示直方图

// Targa 图像文件头结构体
#pragma pack(push, 1)          // 关闭结构体对齐填充
struct TargaHeader {
    // 图像 ID 区长度,0 = 无
    GLubyte idLength;
    GLubyte colorMapType;  // 0 = 无调色板
    GLubyte dataTypeCode;     // 图像数据类型代码 2 = 未压缩真彩图(非 RLE)
    GLushort colorMapOrigin;  // 调色板起始索引
    GLushort colorMapLength;  // 调色板颜色的数量
    GLubyte colorMapDepth;  // 调色板每条颜色的位数
    GLushort xOrigin;   // 图像在屏幕上的 X 坐标
    GLushort yOrigin;   // 图像在屏幕上的 Y 坐标
    GLushort width;   // 图像宽度
    GLushort height;  // 图像高度
    // 每像素 24 位 = RGB 各 8 位
    GLubyte bitsPerPixel;
    // 0 表示自下而上存储
    GLubyte imageDescriptor;
};
#pragma pack(pop)             // 恢复结构体对齐填充

std::ostream& operator<<(std::ostream& os, const TargaHeader& header) {
    os << "TargaHeader { "
       << "idLength=" << static_cast<int>(header.idLength) << ", "
       << "colorMapType=" << static_cast<int>(header.colorMapType) << ", "
       << "dataTypeCode=" << static_cast<int>(header.dataTypeCode) << ", "
       << "colorMapOrigin=" << header.colorMapOrigin << ", "
       << "colorMapLength=" << header.colorMapLength << ", "
       << "colorMapDepth=" << static_cast<int>(header.colorMapDepth) << ", "
       << "xOrigin=" << header.xOrigin << ", "
       << "yOrigin=" << header.yOrigin << ", "
       << "width=" << header.width << ", "
       << "height=" << header.height << ", "
       << "bitsPerPixel=" << static_cast<int>(header.bitsPerPixel) << ", "
       << "imageDescriptor=" << static_cast<int>(header.imageDescriptor)
       << " }";
    return os;
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>>;
const static std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> targaData = loadTarga();
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> {
    std::cout << "Loading Targa image..." << std::endl;
    std::ifstream file("../horse.tga", std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open horse.tga" << std::endl;
        throw std::runtime_error("Failed to open horse.tga");
    }
    TargaHeader header;
    std::cout << "Size of TargaHeader: " << sizeof(TargaHeader) << std::endl;
    file.read(reinterpret_cast<char*>(&header), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to read Targa header" << std::endl;
        throw std::runtime_error("Failed to read Targa header");
    }
    std::cout << "Targa header: " << header << std::endl;
    const std::size_t imageSize = header.width * header.height * (header.bitsPerPixel / 8);
    auto imageData = std::make_unique<GLbyte[]>(imageSize);   // 注意:不能是 const auto,unique_ptr 需要可移动
    file.read(reinterpret_cast<char*>(imageData.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to read Targa image data" << std::endl;
        throw std::runtime_error("Failed to read Targa image data");
    }
    std::cout << "Successfully read Targa image data" << std::endl;
    return std::make_pair(std::move(header), std::move(imageData));
}

void renderScene(){

    // 清除颜色缓冲区
    glClear(GL_COLOR_BUFFER_BIT);

    // 光栅(Raster)位置设置
    glRasterPos2i(0, 0);
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    glPixelZoom(viewport[2] / static_cast<GLfloat>(targaData.first.width), viewport[3] / static_cast<GLfloat>(targaData.first.height)); // 设置像素缩放因子为默认值

    static constexpr GLfloat lumMat[16] = {
        0.30f, 0.30f, 0.30f, 0.0f,
        0.59f, 0.59f, 0.59f, 0.0f,
        0.11f, 0.11f, 0.11f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f
    };

    if (isHistogram) {
        glMatrixMode(GL_COLOR);
        glLoadMatrixf(lumMat);
        glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
        glHistogram(GL_HISTOGRAM, 256, GL_LUMINANCE, GL_FALSE);
        glEnable(GL_HISTOGRAM);
    }

    switch (renderMode) {
        case 5: {
            static constexpr GLfloat sharpen[3 * 3] = {
                0.0f, -1.0f, 0.0f,
                -1.0f, 5.0f, -1.0f,
                0.0f, -1.0f, 0.0f
            };
            glConvolutionFilter2D(GL_CONVOLUTION_2D, GL_RGB, 3, 3, GL_LUMINANCE, GL_FLOAT, sharpen);
            glEnable(GL_CONVOLUTION_2D);
            break;
        }
        case 4: {
            static constexpr GLfloat emboss[3 * 3] = {
                2.0f, 0.0f, 0.0f,
                0.0f, -1.0f, 0.0f,
                0.0f, 0.0f, -1.0f
            };
            glConvolutionFilter2D(GL_CONVOLUTION_2D, GL_RGB, 3, 3, GL_LUMINANCE, GL_FLOAT, emboss);
            glEnable(GL_CONVOLUTION_2D);
            glMatrixMode(GL_COLOR); // 恢复模型视图矩阵模式
            glLoadMatrixf(lumMat);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 3: { // 反转图像
            static GLubyte invertTable[256][3]{};
            for (int i = 0; i < 256; ++i) {
                invertTable[i][0] = 255 - i;
                invertTable[i][1] = 255 - i;
                invertTable[i][2] = 255 - i;
            }
            glColorTable(GL_COLOR_TABLE, GL_RGB, 256, GL_RGB, GL_UNSIGNED_BYTE, invertTable);
            glColorTable(GL_PROXY_COLOR_TABLE, GL_RGB, 256, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
            GLint width;
            glGetColorTableParameteriv(GL_PROXY_COLOR_TABLE, GL_COLOR_TABLE_WIDTH, &width);
            std::cout << "Proxy color table width: " << width << std::endl;
            glEnable(GL_COLOR_TABLE);
            break;
        }
        case 2: { // 亮化图片
            glMatrixMode(GL_COLOR);
            glScalef(1.25F, 1.25F, 1.25F);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 1: { // 暗化图片
            glMatrixMode(GL_COLOR);
            glScalef(0.8F, 0.8F, 0.8F);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        default:
            // 默认渲染模式的处理逻辑
            break;
    }
    glDrawPixels(targaData.first.width, targaData.first.height, GL_BGR, GL_UNSIGNED_BYTE, targaData.second.get());
    if (isHistogram) {
        std::cout << "Displaying histogram..." << std::endl;
        static GLint histogram[256];
        glGetHistogram(GL_HISTOGRAM, GL_TRUE, GL_LUMINANCE, GL_INT, histogram);
        GLint largest = 0;
        for(GLint i = 0; i < 256; ++i) {
            if (histogram[i] > largest) {
                largest = histogram[i];
            }
        }
        glColor3f(1.0F, 1.0F, 1.0F);
        glBegin(GL_LINE_STRIP);
        for (GLint i = 0; i < 256; ++i) {
            // glVertex2f(static_cast<GLfloat>(i) / 255.0F, static_cast<GLfloat>(histogram[i]) / static_cast<GLfloat>(largest));
            glVertex2f(static_cast<GLfloat>(i), static_cast<GLfloat>(histogram[i]) / static_cast<GLfloat>(largest) * 128.0F);
        }
        glEnd();
        // 不要在这里清 isHistogram:GLUT 常在菜单关闭后再重绘一次,
        // 若此刻清掉标志,下一帧 glClear + glDrawPixels 就会把直方图覆盖掉。
        glDisable(GL_HISTOGRAM);
    }
    glMatrixMode(GL_COLOR); // 恢复模型视图矩阵模式
    glLoadIdentity(); // 重置颜色矩阵
    glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
    glDisable(GL_CONVOLUTION_2D);
    glDisable(GL_COLOR_TABLE); // 禁用颜色表
    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.0F, 0.0F, 0.0F, 0.0F);
}
void writePng(const std::string& filename, const int width, const int height) {
    // openGL 的 glReadPixels 读取的图像是自下而上的,而 PNG 是自上而下的,所以需要翻转图像
    stbi_flip_vertically_on_write(1);
    
    glPixelStorei(GL_PACK_ALIGNMENT, 1); // 设置像素存储模式,确保行对齐为1
    // 获取当前读的是前缓冲区还是后缓冲区
    GLint currentBuffer;
    glGetIntegerv(GL_READ_BUFFER, &currentBuffer); // 获取当前像素存储模式
    // 16进制输出当前读缓冲区
    std::cout << std::format("Current read buffer: 0x{:X}", currentBuffer) << std::endl;
    glReadBuffer(GL_FRONT);
    const std::size_t imageSize = width * height * 3; // 3 bytes per pixel for RGB
    const auto flippedImageData = std::make_unique<GLbyte[]>(imageSize);
    glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, flippedImageData.get());
    glReadBuffer(currentBuffer); // 恢复之前的读缓冲区

    if (!stbi_write_png(filename.c_str(), width, height, 3, flippedImageData.get(), width * 3)) {
        std::cerr << "Failed to write PNG file: " << filename << std::endl;
        throw std::runtime_error("Failed to write PNG file");
    }
}
void writeTarga(const std::string& filename, const TargaHeader& header) {
    glPixelTransferi(GL_MAP_COLOR, GL_FALSE); // 禁用颜色映射
    // 获取viewport的宽高
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    std::cout << std::format("Viewport: x={}, y={}, width={}, height={}", viewport[0], viewport[1], viewport[2], viewport[3]) << std::endl;
    const GLushort width = viewport[2];
    const GLushort height = viewport[3];
    const TargaHeader newHeader = {
        .idLength = header.idLength,
        .colorMapType = header.colorMapType,
        .dataTypeCode = header.dataTypeCode,
        .colorMapOrigin = header.colorMapOrigin,
        .colorMapLength = header.colorMapLength,
        .colorMapDepth = header.colorMapDepth,
        .xOrigin = header.xOrigin,
        .yOrigin = header.yOrigin,
        .width = width,
        .height = height,
        .bitsPerPixel = header.bitsPerPixel,
        .imageDescriptor = header.imageDescriptor,
    };

    std::ofstream file(filename, std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open " << filename << " for writing" << std::endl;
        throw std::runtime_error("Failed to open file for writing");
    }
    file.write(reinterpret_cast<const char*>(&newHeader), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to write Targa header to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa header");
    }
    const std::size_t imageSize = 
        // renderMode == 8 ? newHeader.width * newHeader.height
        newHeader.width * newHeader.height * (newHeader.bitsPerPixel / 8);
    const std::unique_ptr<GLbyte[]> imageBuffer = std::make_unique<GLbyte[]>(imageSize);

    glPixelStorei(GL_PACK_ALIGNMENT, 1);      // ← 注意是 PACK,读像素时用
    // 获取当前读的是前缓冲区还是后缓冲区
    GLint currentBuffer;
    glGetIntegerv(GL_READ_BUFFER, &currentBuffer); // 获取当前像素存储模式
    // 16进制输出当前读缓冲区
    std::cout << std::format("Current read buffer: 0x{:X}", currentBuffer) << std::endl;
    glReadBuffer(GL_FRONT);
    glReadPixels(0, 0, newHeader.width, newHeader.height, GL_BGR, GL_UNSIGNED_BYTE, imageBuffer.get());
    glReadBuffer(currentBuffer); // 恢复之前的读缓冲区
    file.write(reinterpret_cast<const char*>(imageBuffer.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to write Targa image data to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa image data");
    }
}
void processMenu(const int option){
    std::cout << std::format("Menu option selected: {}", option) << std::endl;
    glutSetWindow(mainWindowId);
    renderMode = option;
    isHistogram = (option == 6) ? GL_TRUE : GL_FALSE;   // 只有直方图模式开启,切到其它模式自动关闭
    glutPostRedisplay();
}
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    std::cout << "current path: " << std::filesystem::current_path() << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    glutCreateMenu(processMenu);
    glutAddMenuEntry("重置", 0);
    glutAddMenuEntry("暗化图片", 1);
    glutAddMenuEntry("亮化图片", 2);
    glutAddMenuEntry("反转图片", 3);
    glutAddMenuEntry("浮雕化图片", 4);
    glutAddMenuEntry("锐化图片", 5);
    glutAddMenuEntry("直方图均衡化", 6);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    setupRc();
    std::cout << "GL_ARB_imaging supported: " << glutExtensionSupported("GL_ARB_imaging") << std::endl;
    glutMainLoop();
    return 0;
}

图片blur

#ifdef __APPLE__
#include <OpenGL/glext.h>
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif
 
#include <ostream>
#include <numbers>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <array>     // std::array(高斯核)
#include <format>    // std::format
#include <stdexcept> // std::runtime_error
#include <memory>   // std::unique_ptr / std::make_unique
#include <utility>  // std::pair / std::make_pair / std::move

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
 
// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
 
// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度

static GLint renderMode = 0;
static GLint mainWindowId = 0; // 主窗口 ID
static GLboolean isHistogram = GL_FALSE; // 是否显示直方图

// Targa 图像文件头结构体
#pragma pack(push, 1)          // 关闭结构体对齐填充
struct TargaHeader {
    // 图像 ID 区长度,0 = 无
    GLubyte idLength;
    GLubyte colorMapType;  // 0 = 无调色板
    GLubyte dataTypeCode;     // 图像数据类型代码 2 = 未压缩真彩图(非 RLE)
    GLushort colorMapOrigin;  // 调色板起始索引
    GLushort colorMapLength;  // 调色板颜色的数量
    GLubyte colorMapDepth;  // 调色板每条颜色的位数
    GLushort xOrigin;   // 图像在屏幕上的 X 坐标
    GLushort yOrigin;   // 图像在屏幕上的 Y 坐标
    GLushort width;   // 图像宽度
    GLushort height;  // 图像高度
    // 每像素 24 位 = RGB 各 8 位
    GLubyte bitsPerPixel;
    // 0 表示自下而上存储
    GLubyte imageDescriptor;
};
#pragma pack(pop)             // 恢复结构体对齐填充

std::ostream& operator<<(std::ostream& os, const TargaHeader& header) {
    os << "TargaHeader { "
       << "idLength=" << static_cast<int>(header.idLength) << ", "
       << "colorMapType=" << static_cast<int>(header.colorMapType) << ", "
       << "dataTypeCode=" << static_cast<int>(header.dataTypeCode) << ", "
       << "colorMapOrigin=" << header.colorMapOrigin << ", "
       << "colorMapLength=" << header.colorMapLength << ", "
       << "colorMapDepth=" << static_cast<int>(header.colorMapDepth) << ", "
       << "xOrigin=" << header.xOrigin << ", "
       << "yOrigin=" << header.yOrigin << ", "
       << "width=" << header.width << ", "
       << "height=" << header.height << ", "
       << "bitsPerPixel=" << static_cast<int>(header.bitsPerPixel) << ", "
       << "imageDescriptor=" << static_cast<int>(header.imageDescriptor)
       << " }";
    return os;
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>>;
const static std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> targaData = loadTarga();
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> {
    std::cout << "Loading Targa image..." << std::endl;
    std::ifstream file("../horse.tga", std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open horse.tga" << std::endl;
        throw std::runtime_error("Failed to open horse.tga");
    }
    TargaHeader header;
    std::cout << "Size of TargaHeader: " << sizeof(TargaHeader) << std::endl;
    file.read(reinterpret_cast<char*>(&header), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to read Targa header" << std::endl;
        throw std::runtime_error("Failed to read Targa header");
    }
    std::cout << "Targa header: " << header << std::endl;
    const std::size_t imageSize = header.width * header.height * (header.bitsPerPixel / 8);
    auto imageData = std::make_unique<GLbyte[]>(imageSize);   // 注意:不能是 const auto,unique_ptr 需要可移动
    file.read(reinterpret_cast<char*>(imageData.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to read Targa image data" << std::endl;
        throw std::runtime_error("Failed to read Targa image data");
    }
    std::cout << "Successfully read Targa image data" << std::endl;
    return std::make_pair(std::move(header), std::move(imageData));
}

void renderScene(){

    // 清除颜色缓冲区
    glClear(GL_COLOR_BUFFER_BIT);

    // 光栅(Raster)位置设置
    glRasterPos2i(0, 0);
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    glPixelZoom(viewport[2] / static_cast<GLfloat>(targaData.first.width), viewport[3] / static_cast<GLfloat>(targaData.first.height)); // 设置像素缩放因子为默认值

    static constexpr GLfloat lumMat[16] = {
        0.30f, 0.30f, 0.30f, 0.0f,
        0.59f, 0.59f, 0.59f, 0.0f,
        0.11f, 0.11f, 0.11f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f
    };

    if (isHistogram) {
        glMatrixMode(GL_COLOR);
        glLoadMatrixf(lumMat);
        glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
        glHistogram(GL_HISTOGRAM, 256, GL_LUMINANCE, GL_FALSE);
        glEnable(GL_HISTOGRAM);
    }

    switch (renderMode) {
        case 5: {
            static constexpr GLfloat sharpen[3 * 3] = {
                0.0f, -1.0f, 0.0f,
                -1.0f, 5.0f, -1.0f,
                0.0f, -1.0f, 0.0f
            };
            glConvolutionFilter2D(GL_CONVOLUTION_2D, GL_RGB, 3, 3, GL_LUMINANCE, GL_FLOAT, sharpen);
            glEnable(GL_CONVOLUTION_2D);
            break;
        }
        case 4: {
            static constexpr GLfloat emboss[3 * 3] = {
                2.0f, 0.0f, 0.0f,
                0.0f, -1.0f, 0.0f,
                0.0f, 0.0f, -1.0f
            };
            glConvolutionFilter2D(GL_CONVOLUTION_2D, GL_RGB, 3, 3, GL_LUMINANCE, GL_FLOAT, emboss);
            glEnable(GL_CONVOLUTION_2D);
            glMatrixMode(GL_COLOR); // 恢复模型视图矩阵模式
            glLoadMatrixf(lumMat);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 3: { // 反转图像
            // 反转表的生成与代理查询只做一次:它们是静态开销,每帧重复既浪费又会刷屏
            static GLubyte invertTable[256][3]{};
            static bool invertTableReady = false;
            if (!invertTableReady) {
                for (int i = 0; i < 256; ++i) {
                    invertTable[i][0] = static_cast<GLubyte>(255 - i);
                    invertTable[i][1] = static_cast<GLubyte>(255 - i);
                    invertTable[i][2] = static_cast<GLubyte>(255 - i);
                }
                glColorTable(GL_PROXY_COLOR_TABLE, GL_RGB, 256, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
                GLint width = 0;
                glGetColorTableParameteriv(GL_PROXY_COLOR_TABLE, GL_COLOR_TABLE_WIDTH, &width);
                std::cout << "Proxy color table width: " << width << " (仅首次查询)" << std::endl;
                invertTableReady = true;
            }
            glColorTable(GL_COLOR_TABLE, GL_RGB, 256, GL_RGB, GL_UNSIGNED_BYTE, invertTable);
            glEnable(GL_COLOR_TABLE);
            break;
        }
        case 2: { // 亮化图片
            glMatrixMode(GL_COLOR);
            glScalef(1.25F, 1.25F, 1.25F);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 1: { // 暗化图片
            glMatrixMode(GL_COLOR);
            glScalef(0.8F, 0.8F, 0.8F);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 7: { // 模糊:高斯模糊(9×9,σ=2.5)—— 调 kSigma / kRadius 即可改模糊强度
            constexpr int   kRadius = 4;      // 半径:2 → 5×5;4 → 9×9(越大越糊)
            constexpr float kSigma  = 5.5F;   // 标准差:越大越糊
            constexpr int   kSize   = kRadius * 2 + 1;

            // 按二维高斯公式生成权重并归一化(只算一次),不必再手写几十个数字
            static const std::array<GLfloat, kSize * kSize> gaussian = [] {
                std::array<GLfloat, kSize * kSize> weights{};
                GLfloat sum = 0.0F;
                for (int y = -kRadius; y <= kRadius; ++y) {
                    for (int x = -kRadius; x <= kRadius; ++x) {
                        const GLfloat w = std::exp(-static_cast<GLfloat>(x * x + y * y) /
                                                   (2.0F * kSigma * kSigma));
                        weights[(y + kRadius) * kSize + (x + kRadius)] = w;
                        sum += w;
                    }
                }
                for (GLfloat& w : weights) {
                    w /= sum;                 // 归一化:总权重为 1,整体亮度不变
                }
                return weights;
            }();

            // 合法边界模式只有 GL_REDUCE(默认,输出会缩小)、GL_CONSTANT_BORDER、GL_REPLICATE_BORDER;
            // 误用 GL_REPEAT 在 macOS 上不报错,但第二次绘制会段错误
            glConvolutionParameteri(GL_CONVOLUTION_2D, GL_CONVOLUTION_BORDER_MODE, GL_REPLICATE_BORDER);
            glConvolutionFilter2D(GL_CONVOLUTION_2D, GL_RGB, kSize, kSize, GL_LUMINANCE, GL_FLOAT, gaussian.data());
            glEnable(GL_CONVOLUTION_2D);
            break;
        }
        default:
            // 默认渲染模式的处理逻辑
            break;
    }
    glDrawPixels(targaData.first.width, targaData.first.height, GL_BGR, GL_UNSIGNED_BYTE, targaData.second.get());
    if (isHistogram) {
        std::cout << "Displaying histogram..." << std::endl;
        static GLint histogram[256];
        glGetHistogram(GL_HISTOGRAM, GL_TRUE, GL_LUMINANCE, GL_INT, histogram);
        GLint largest = 0;
        for(GLint i = 0; i < 256; ++i) {
            if (histogram[i] > largest) {
                largest = histogram[i];
            }
        }
        glColor3f(1.0F, 1.0F, 1.0F);
        glBegin(GL_LINE_STRIP);
        for (GLint i = 0; i < 256; ++i) {
            // glVertex2f(static_cast<GLfloat>(i) / 255.0F, static_cast<GLfloat>(histogram[i]) / static_cast<GLfloat>(largest));
            glVertex2f(static_cast<GLfloat>(i), static_cast<GLfloat>(histogram[i]) / static_cast<GLfloat>(largest) * 128.0F);
        }
        glEnd();
        // 不要在这里清 isHistogram:GLUT 常在菜单关闭后再重绘一次,
        // 若此刻清掉标志,下一帧 glClear + glDrawPixels 就会把直方图覆盖掉。
        glDisable(GL_HISTOGRAM);
    }
    glMatrixMode(GL_COLOR); // 恢复模型视图矩阵模式
    glLoadIdentity(); // 重置颜色矩阵
    glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
    glDisable(GL_CONVOLUTION_2D);
    glDisable(GL_COLOR_TABLE); // 禁用颜色表
    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.0F, 0.0F, 0.0F, 0.0F);
}
// 截屏辅助:glReadPixels 同样会经过 pixel transfer,
// 构造时把映射与通道系数复位为恒等,析构时自动还原之前的状态。
class PixelTransferGuard {
public:
    PixelTransferGuard() {
        glGetBooleanv(GL_MAP_COLOR, &mapColor_);
        glGetFloatv(GL_RED_SCALE, &redScale_);
        glGetFloatv(GL_GREEN_SCALE, &greenScale_);
        glGetFloatv(GL_BLUE_SCALE, &blueScale_);
        glPixelTransferi(GL_MAP_COLOR, GL_FALSE);
        glPixelTransferf(GL_RED_SCALE, 1.0F);
        glPixelTransferf(GL_GREEN_SCALE, 1.0F);
        glPixelTransferf(GL_BLUE_SCALE, 1.0F);
    }
    ~PixelTransferGuard() {
        glPixelTransferi(GL_MAP_COLOR, mapColor_ ? GL_TRUE : GL_FALSE);
        glPixelTransferf(GL_RED_SCALE, redScale_);
        glPixelTransferf(GL_GREEN_SCALE, greenScale_);
        glPixelTransferf(GL_BLUE_SCALE, blueScale_);
    }
    PixelTransferGuard(const PixelTransferGuard&) = delete;
    PixelTransferGuard& operator=(const PixelTransferGuard&) = delete;

private:
    GLboolean mapColor_ = GL_FALSE;
    GLfloat redScale_ = 1.0F;
    GLfloat greenScale_ = 1.0F;
    GLfloat blueScale_ = 1.0F;
};

// 截屏读取:把"从当前窗口前缓冲取像素"这件事收拢到一处。
// 构造时复位 pixel transfer、打开 PACK 对齐、切到 GL_FRONT;析构时自动还原读缓冲区。
class ScreenshotReader {
public:
    ScreenshotReader() {
        glPixelStorei(GL_PACK_ALIGNMENT, 1);
        glGetIntegerv(GL_READ_BUFFER, &previousBuffer_);
        std::cout << std::format("Current read buffer: 0x{:X}", previousBuffer_) << std::endl;
        glReadBuffer(GL_FRONT);
    }
    ~ScreenshotReader() { glReadBuffer(previousBuffer_); }
    ScreenshotReader(const ScreenshotReader&) = delete;
    ScreenshotReader& operator=(const ScreenshotReader&) = delete;

private:
    PixelTransferGuard transferGuard_;          // 复位 pixel transfer(读完自动还原)
    GLint previousBuffer_ = GL_BACK;            // 读完把读缓冲区切回去
};

void readFrontBuffer(const int width, const int height, const GLenum format, GLbyte* out) {
    const ScreenshotReader reader;              // 读前准备 + 读完还原
    glReadPixels(0, 0, width, height, format, GL_UNSIGNED_BYTE, out);
}

void writePng(const std::string& filename, const int width, const int height) {
    // openGL 的 glReadPixels 读取的图像是自下而上的,而 PNG 是自上而下的,所以需要翻转图像
    stbi_flip_vertically_on_write(1);
    
    const std::size_t imageSize = width * height * 3; // 3 bytes per pixel for RGB
    const auto flippedImageData = std::make_unique<GLbyte[]>(imageSize);
    readFrontBuffer(width, height, GL_RGB, flippedImageData.get());   // 像素在内存里是自下而上的

    if (!stbi_write_png(filename.c_str(), width, height, 3, flippedImageData.get(), width * 3)) {
        std::cerr << "Failed to write PNG file: " << filename << std::endl;
        throw std::runtime_error("Failed to write PNG file");
    }
}
void writeTarga(const std::string& filename, const TargaHeader& header) {
    // 获取viewport的宽高
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    std::cout << std::format("Viewport: x={}, y={}, width={}, height={}", viewport[0], viewport[1], viewport[2], viewport[3]) << std::endl;
    const GLushort width = viewport[2];
    const GLushort height = viewport[3];
    const TargaHeader newHeader = {
        .idLength = header.idLength,
        .colorMapType = header.colorMapType,
        .dataTypeCode = header.dataTypeCode,
        .colorMapOrigin = header.colorMapOrigin,
        .colorMapLength = header.colorMapLength,
        .colorMapDepth = header.colorMapDepth,
        .xOrigin = header.xOrigin,
        .yOrigin = header.yOrigin,
        .width = width,
        .height = height,
        .bitsPerPixel = header.bitsPerPixel,
        .imageDescriptor = header.imageDescriptor,
    };

    std::ofstream file(filename, std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open " << filename << " for writing" << std::endl;
        throw std::runtime_error("Failed to open file for writing");
    }
    file.write(reinterpret_cast<const char*>(&newHeader), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to write Targa header to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa header");
    }
    const std::size_t imageSize = newHeader.width * newHeader.height * (newHeader.bitsPerPixel / 8);
    const std::unique_ptr<GLbyte[]> imageBuffer = std::make_unique<GLbyte[]>(imageSize);

    readFrontBuffer(newHeader.width, newHeader.height, GL_BGR, imageBuffer.get());
    file.write(reinterpret_cast<const char*>(imageBuffer.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to write Targa image data to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa image data");
    }
}
void processMenu(const int option){
    std::cout << std::format("Menu option selected: {}", option) << std::endl;
    glutSetWindow(mainWindowId);

    if (option >= 10) {   // 10 及以上是"动作",与 0..9 的渲染模式分开,避免编号撞车
        GLint viewport[4];
        glGetIntegerv(GL_VIEWPORT, viewport);
        switch (option) {
            case 10: writeTarga("output.tga", targaData.first); break;
            case 11: writePng("output.png", viewport[2], viewport[3]); break;
            default: break;
        }
        return;           // 动作不改变当前渲染模式
    }

    renderMode = option;
    isHistogram = (option == 6) ? GL_TRUE : GL_FALSE;   // 只有直方图模式开启,切到其它模式自动关闭
    glutPostRedisplay();
}
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    std::cout << "current path: " << std::filesystem::current_path() << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    glutCreateMenu(processMenu);
    glutAddMenuEntry("重置", 0);
    glutAddMenuEntry("暗化图片", 1);
    glutAddMenuEntry("亮化图片", 2);
    glutAddMenuEntry("反转图片", 3);
    glutAddMenuEntry("浮雕化图片", 4);
    glutAddMenuEntry("锐化图片", 5);
    glutAddMenuEntry("直方图均衡化", 6);
    glutAddMenuEntry("blur图片", 7);
    glutAddMenuEntry("保存 TGA", 10);
    glutAddMenuEntry("保存 PNG", 11);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    setupRc();
    std::cout << "GL_ARB_imaging supported: " << glutExtensionSupported("GL_ARB_imaging") << std::endl;
    glutMainLoop();
    return 0;
}

高斯模糊

#ifdef __APPLE__
#include <OpenGL/glext.h>
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif
 
#include <ostream>
#include <numbers>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <array>     // std::array(高斯核)
#include <format>    // std::format
#include <stdexcept> // std::runtime_error
#include <memory>   // std::unique_ptr / std::make_unique
#include <utility>  // std::pair / std::make_pair / std::move

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
 
// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
 
// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度

static GLint renderMode = 0;
static GLint mainWindowId = 0; // 主窗口 ID
static GLboolean isHistogram = GL_FALSE; // 是否显示直方图

// Targa 图像文件头结构体
#pragma pack(push, 1)          // 关闭结构体对齐填充
struct TargaHeader {
    // 图像 ID 区长度,0 = 无
    GLubyte idLength;
    GLubyte colorMapType;  // 0 = 无调色板
    GLubyte dataTypeCode;     // 图像数据类型代码 2 = 未压缩真彩图(非 RLE)
    GLushort colorMapOrigin;  // 调色板起始索引
    GLushort colorMapLength;  // 调色板颜色的数量
    GLubyte colorMapDepth;  // 调色板每条颜色的位数
    GLushort xOrigin;   // 图像在屏幕上的 X 坐标
    GLushort yOrigin;   // 图像在屏幕上的 Y 坐标
    GLushort width;   // 图像宽度
    GLushort height;  // 图像高度
    // 每像素 24 位 = RGB 各 8 位
    GLubyte bitsPerPixel;
    // 0 表示自下而上存储
    GLubyte imageDescriptor;
};
#pragma pack(pop)             // 恢复结构体对齐填充

std::ostream& operator<<(std::ostream& os, const TargaHeader& header) {
    os << "TargaHeader { "
       << "idLength=" << static_cast<int>(header.idLength) << ", "
       << "colorMapType=" << static_cast<int>(header.colorMapType) << ", "
       << "dataTypeCode=" << static_cast<int>(header.dataTypeCode) << ", "
       << "colorMapOrigin=" << header.colorMapOrigin << ", "
       << "colorMapLength=" << header.colorMapLength << ", "
       << "colorMapDepth=" << static_cast<int>(header.colorMapDepth) << ", "
       << "xOrigin=" << header.xOrigin << ", "
       << "yOrigin=" << header.yOrigin << ", "
       << "width=" << header.width << ", "
       << "height=" << header.height << ", "
       << "bitsPerPixel=" << static_cast<int>(header.bitsPerPixel) << ", "
       << "imageDescriptor=" << static_cast<int>(header.imageDescriptor)
       << " }";
    return os;
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>>;
const static std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> targaData = loadTarga();
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> {
    std::cout << "Loading Targa image..." << std::endl;
    std::ifstream file("../horse.tga", std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open horse.tga" << std::endl;
        throw std::runtime_error("Failed to open horse.tga");
    }
    TargaHeader header;
    std::cout << "Size of TargaHeader: " << sizeof(TargaHeader) << std::endl;
    file.read(reinterpret_cast<char*>(&header), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to read Targa header" << std::endl;
        throw std::runtime_error("Failed to read Targa header");
    }
    std::cout << "Targa header: " << header << std::endl;
    const std::size_t imageSize = header.width * header.height * (header.bitsPerPixel / 8);
    auto imageData = std::make_unique<GLbyte[]>(imageSize);   // 注意:不能是 const auto,unique_ptr 需要可移动
    file.read(reinterpret_cast<char*>(imageData.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to read Targa image data" << std::endl;
        throw std::runtime_error("Failed to read Targa image data");
    }
    std::cout << "Successfully read Targa image data" << std::endl;
    return std::make_pair(std::move(header), std::move(imageData));
}

void renderScene(){

    // 清除颜色缓冲区
    glClear(GL_COLOR_BUFFER_BIT);

    // 光栅(Raster)位置设置
    glRasterPos2i(0, 0);
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    glPixelZoom(viewport[2] / static_cast<GLfloat>(targaData.first.width), viewport[3] / static_cast<GLfloat>(targaData.first.height)); // 设置像素缩放因子为默认值

    static constexpr GLfloat lumMat[16] = {
        0.30f, 0.30f, 0.30f, 0.0f,
        0.59f, 0.59f, 0.59f, 0.0f,
        0.11f, 0.11f, 0.11f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f
    };

    if (isHistogram) {
        glMatrixMode(GL_COLOR);
        glLoadMatrixf(lumMat);
        glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
        glHistogram(GL_HISTOGRAM, 256, GL_LUMINANCE, GL_FALSE);
        glEnable(GL_HISTOGRAM);
    }

    switch (renderMode) {
        case 5: {
            static constexpr GLfloat sharpen[3 * 3] = {
                0.0f, -1.0f, 0.0f,
                -1.0f, 5.0f, -1.0f,
                0.0f, -1.0f, 0.0f
            };
            glConvolutionFilter2D(GL_CONVOLUTION_2D, GL_RGB, 3, 3, GL_LUMINANCE, GL_FLOAT, sharpen);
            glEnable(GL_CONVOLUTION_2D);
            break;
        }
        case 4: {
            static constexpr GLfloat emboss[3 * 3] = {
                2.0f, 0.0f, 0.0f,
                0.0f, -1.0f, 0.0f,
                0.0f, 0.0f, -1.0f
            };
            glConvolutionFilter2D(GL_CONVOLUTION_2D, GL_RGB, 3, 3, GL_LUMINANCE, GL_FLOAT, emboss);
            glEnable(GL_CONVOLUTION_2D);
            glMatrixMode(GL_COLOR); // 恢复模型视图矩阵模式
            glLoadMatrixf(lumMat);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 3: { // 反转图像
            // 反转表的生成与代理查询只做一次:它们是静态开销,每帧重复既浪费又会刷屏
            static GLubyte invertTable[256][3]{};
            static bool invertTableReady = false;
            if (!invertTableReady) {
                for (int i = 0; i < 256; ++i) {
                    invertTable[i][0] = static_cast<GLubyte>(255 - i);
                    invertTable[i][1] = static_cast<GLubyte>(255 - i);
                    invertTable[i][2] = static_cast<GLubyte>(255 - i);
                }
                glColorTable(GL_PROXY_COLOR_TABLE, GL_RGB, 256, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
                GLint width = 0;
                glGetColorTableParameteriv(GL_PROXY_COLOR_TABLE, GL_COLOR_TABLE_WIDTH, &width);
                std::cout << "Proxy color table width: " << width << " (仅首次查询)" << std::endl;
                invertTableReady = true;
            }
            glColorTable(GL_COLOR_TABLE, GL_RGB, 256, GL_RGB, GL_UNSIGNED_BYTE, invertTable);
            glEnable(GL_COLOR_TABLE);
            break;
        }
        case 2: { // 亮化图片
            glMatrixMode(GL_COLOR);
            glScalef(1.25F, 1.25F, 1.25F);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 1: { // 暗化图片
            glMatrixMode(GL_COLOR);
            glScalef(0.8F, 0.8F, 0.8F);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 7: { // 模糊:可分离高斯模糊(横竖各一趟 1D)—— 调 kRadius / kSigma 改强度
            constexpr int   kRadius = 4;      // 半径:4 → 1D 长度 9(越大越糊)
            constexpr float kSigma  = 5.5F;   // 标准差:越大越糊(超过半径后趋于方波)
            constexpr int   kSize   = kRadius * 2 + 1;

            // 可分离滤波只需要一维权重(行、列共用同一组),按高斯公式生成并归一化
            static const std::array<GLfloat, kSize> kernel1D = [] {
                std::array<GLfloat, kSize> weights{};
                GLfloat sum = 0.0F;
                for (int i = -kRadius; i <= kRadius; ++i) {
                    const GLfloat w = std::exp(-static_cast<GLfloat>(i * i) /
                                               (2.0F * kSigma * kSigma));
                    weights[i + kRadius] = w;
                    sum += w;
                }
                for (GLfloat& w : weights) {
                    w /= sum;                 // 归一化:总权重为 1,整体亮度不变
                }
                return weights;
            }();

            // 先横向、再纵向各做一次 1D 卷积,等价于 (2r+1)×(2r+1) 的二维高斯,
            // 但每像素乘加从 kSize² = 81 降到 2×kSize = 18
            // 注意:format/type 只有一对,row 与 column 共用
            // 合法边界模式只有 GL_REDUCE(默认,输出会缩小)、GL_CONSTANT_BORDER、GL_REPLICATE_BORDER;
            // 误用 GL_REPEAT 在 macOS 上不报错,但第二次绘制会段错误
            glConvolutionParameteri(GL_CONVOLUTION_2D, GL_CONVOLUTION_BORDER_MODE, GL_REPLICATE_BORDER);
            glConvolutionParameteri(GL_SEPARABLE_2D,     GL_CONVOLUTION_BORDER_MODE, GL_REPLICATE_BORDER);
            glSeparableFilter2D(GL_SEPARABLE_2D, GL_RGB, kSize, kSize,
                                GL_LUMINANCE, GL_FLOAT,
                                kernel1D.data(),   // row:水平权重
                                kernel1D.data());  // column:垂直权重
            glEnable(GL_SEPARABLE_2D);
            break;
        }
        default:
            // 默认渲染模式的处理逻辑
            break;
    }
    glDrawPixels(targaData.first.width, targaData.first.height, GL_BGR, GL_UNSIGNED_BYTE, targaData.second.get());
    if (isHistogram) {
        std::cout << "Displaying histogram..." << std::endl;
        static GLint histogram[256];
        glGetHistogram(GL_HISTOGRAM, GL_TRUE, GL_LUMINANCE, GL_INT, histogram);
        GLint largest = 0;
        for(GLint i = 0; i < 256; ++i) {
            if (histogram[i] > largest) {
                largest = histogram[i];
            }
        }
        glColor3f(1.0F, 1.0F, 1.0F);
        glBegin(GL_LINE_STRIP);
        for (GLint i = 0; i < 256; ++i) {
            // glVertex2f(static_cast<GLfloat>(i) / 255.0F, static_cast<GLfloat>(histogram[i]) / static_cast<GLfloat>(largest));
            glVertex2f(static_cast<GLfloat>(i), static_cast<GLfloat>(histogram[i]) / static_cast<GLfloat>(largest) * 128.0F);
        }
        glEnd();
        // 不要在这里清 isHistogram:GLUT 常在菜单关闭后再重绘一次,
        // 若此刻清掉标志,下一帧 glClear + glDrawPixels 就会把直方图覆盖掉。
        glDisable(GL_HISTOGRAM);
    }
    glMatrixMode(GL_COLOR); // 恢复模型视图矩阵模式
    glLoadIdentity(); // 重置颜色矩阵
    glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
    glDisable(GL_CONVOLUTION_2D);
    glDisable(GL_SEPARABLE_2D);   // 可分离滤波是独立的 enable 位,必须一起关
    glDisable(GL_COLOR_TABLE); // 禁用颜色表
    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.0F, 0.0F, 0.0F, 0.0F);
}
// 截屏辅助:glReadPixels 同样会经过 pixel transfer,
// 构造时把映射与通道系数复位为恒等,析构时自动还原之前的状态。
class PixelTransferGuard {
public:
    PixelTransferGuard() {
        glGetBooleanv(GL_MAP_COLOR, &mapColor_);
        glGetFloatv(GL_RED_SCALE, &redScale_);
        glGetFloatv(GL_GREEN_SCALE, &greenScale_);
        glGetFloatv(GL_BLUE_SCALE, &blueScale_);
        glPixelTransferi(GL_MAP_COLOR, GL_FALSE);
        glPixelTransferf(GL_RED_SCALE, 1.0F);
        glPixelTransferf(GL_GREEN_SCALE, 1.0F);
        glPixelTransferf(GL_BLUE_SCALE, 1.0F);
    }
    ~PixelTransferGuard() {
        glPixelTransferi(GL_MAP_COLOR, mapColor_ ? GL_TRUE : GL_FALSE);
        glPixelTransferf(GL_RED_SCALE, redScale_);
        glPixelTransferf(GL_GREEN_SCALE, greenScale_);
        glPixelTransferf(GL_BLUE_SCALE, blueScale_);
    }
    PixelTransferGuard(const PixelTransferGuard&) = delete;
    PixelTransferGuard& operator=(const PixelTransferGuard&) = delete;

private:
    GLboolean mapColor_ = GL_FALSE;
    GLfloat redScale_ = 1.0F;
    GLfloat greenScale_ = 1.0F;
    GLfloat blueScale_ = 1.0F;
};

// 截屏读取:把"从当前窗口前缓冲取像素"这件事收拢到一处。
// 构造时复位 pixel transfer、打开 PACK 对齐、切到 GL_FRONT;析构时自动还原读缓冲区。
class ScreenshotReader {
public:
    ScreenshotReader() {
        glPixelStorei(GL_PACK_ALIGNMENT, 1);
        glGetIntegerv(GL_READ_BUFFER, &previousBuffer_);
        std::cout << std::format("Current read buffer: 0x{:X}", previousBuffer_) << std::endl;
        glReadBuffer(GL_FRONT);
    }
    ~ScreenshotReader() { glReadBuffer(previousBuffer_); }
    ScreenshotReader(const ScreenshotReader&) = delete;
    ScreenshotReader& operator=(const ScreenshotReader&) = delete;

private:
    PixelTransferGuard transferGuard_;          // 复位 pixel transfer(读完自动还原)
    GLint previousBuffer_ = GL_BACK;            // 读完把读缓冲区切回去
};

void readFrontBuffer(const int width, const int height, const GLenum format, GLbyte* out) {
    const ScreenshotReader reader;              // 读前准备 + 读完还原
    glReadPixels(0, 0, width, height, format, GL_UNSIGNED_BYTE, out);
}

void writePng(const std::string& filename, const int width, const int height) {
    // openGL 的 glReadPixels 读取的图像是自下而上的,而 PNG 是自上而下的,所以需要翻转图像
    stbi_flip_vertically_on_write(1);
    
    const std::size_t imageSize = width * height * 3; // 3 bytes per pixel for RGB
    const auto flippedImageData = std::make_unique<GLbyte[]>(imageSize);
    readFrontBuffer(width, height, GL_RGB, flippedImageData.get());   // 像素在内存里是自下而上的

    if (!stbi_write_png(filename.c_str(), width, height, 3, flippedImageData.get(), width * 3)) {
        std::cerr << "Failed to write PNG file: " << filename << std::endl;
        throw std::runtime_error("Failed to write PNG file");
    }
}
void writeTarga(const std::string& filename, const TargaHeader& header) {
    // 获取viewport的宽高
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    std::cout << std::format("Viewport: x={}, y={}, width={}, height={}", viewport[0], viewport[1], viewport[2], viewport[3]) << std::endl;
    const GLushort width = viewport[2];
    const GLushort height = viewport[3];
    const TargaHeader newHeader = {
        .idLength = header.idLength,
        .colorMapType = header.colorMapType,
        .dataTypeCode = header.dataTypeCode,
        .colorMapOrigin = header.colorMapOrigin,
        .colorMapLength = header.colorMapLength,
        .colorMapDepth = header.colorMapDepth,
        .xOrigin = header.xOrigin,
        .yOrigin = header.yOrigin,
        .width = width,
        .height = height,
        .bitsPerPixel = header.bitsPerPixel,
        .imageDescriptor = header.imageDescriptor,
    };

    std::ofstream file(filename, std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open " << filename << " for writing" << std::endl;
        throw std::runtime_error("Failed to open file for writing");
    }
    file.write(reinterpret_cast<const char*>(&newHeader), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to write Targa header to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa header");
    }
    const std::size_t imageSize = newHeader.width * newHeader.height * (newHeader.bitsPerPixel / 8);
    const std::unique_ptr<GLbyte[]> imageBuffer = std::make_unique<GLbyte[]>(imageSize);

    readFrontBuffer(newHeader.width, newHeader.height, GL_BGR, imageBuffer.get());
    file.write(reinterpret_cast<const char*>(imageBuffer.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to write Targa image data to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa image data");
    }
}
void processMenu(const int option){
    std::cout << std::format("Menu option selected: {}", option) << std::endl;
    glutSetWindow(mainWindowId);

    if (option >= 10) {   // 10 及以上是"动作",与 0..9 的渲染模式分开,避免编号撞车
        GLint viewport[4];
        glGetIntegerv(GL_VIEWPORT, viewport);
        switch (option) {
            case 10: writeTarga("output.tga", targaData.first); break;
            case 11: writePng("output.png", viewport[2], viewport[3]); break;
            default: break;
        }
        return;           // 动作不改变当前渲染模式
    }

    renderMode = option;
    isHistogram = (option == 6) ? GL_TRUE : GL_FALSE;   // 只有直方图模式开启,切到其它模式自动关闭
    glutPostRedisplay();
}
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    std::cout << "current path: " << std::filesystem::current_path() << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    glutCreateMenu(processMenu);
    glutAddMenuEntry("重置", 0);
    glutAddMenuEntry("暗化图片", 1);
    glutAddMenuEntry("亮化图片", 2);
    glutAddMenuEntry("反转图片", 3);
    glutAddMenuEntry("浮雕化图片", 4);
    glutAddMenuEntry("锐化图片", 5);
    glutAddMenuEntry("直方图均衡化", 6);
    glutAddMenuEntry("blur图片", 7);
    glutAddMenuEntry("保存 TGA", 10);
    glutAddMenuEntry("保存 PNG", 11);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    setupRc();
    std::cout << "GL_ARB_imaging supported: " << glutExtensionSupported("GL_ARB_imaging") << std::endl;
    glutMainLoop();
    return 0;
}

几种边界模式

#ifdef __APPLE__
#include <OpenGL/glext.h>
#include <GLUT/glut.h>
#include <GLKit/GLKMath.h>
#include <OpenGL/gl.h>
#include <OpenGL/gltypes.h>
#include <OpenGL/glu.h>
#else
extern "C" {
#include <GL/freeglut.h>
}
#endif
 
#include <ostream>
#include <numbers>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <array>     // std::array(高斯核)
#include <format>    // std::format
#include <stdexcept> // std::runtime_error
#include <memory>   // std::unique_ptr / std::make_unique
#include <utility>  // std::pair / std::make_pair / std::move

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
 
// 常量
static constexpr GLfloat PI = std::numbers::pi_v<GLfloat>;
 
// 变量
static GLfloat xRot = 0.0F; // x旋转角度
static GLfloat yRot = 0.0F; // y旋转角度
static GLfloat zRot = 0.0F; // z旋转角度

static GLint renderMode = 0;
static GLint mainWindowId = 0; // 主窗口 ID
static GLboolean isHistogram = GL_FALSE; // 是否显示直方图

// Targa 图像文件头结构体
#pragma pack(push, 1)          // 关闭结构体对齐填充
struct TargaHeader {
    // 图像 ID 区长度,0 = 无
    GLubyte idLength;
    GLubyte colorMapType;  // 0 = 无调色板
    GLubyte dataTypeCode;     // 图像数据类型代码 2 = 未压缩真彩图(非 RLE)
    GLushort colorMapOrigin;  // 调色板起始索引
    GLushort colorMapLength;  // 调色板颜色的数量
    GLubyte colorMapDepth;  // 调色板每条颜色的位数
    GLushort xOrigin;   // 图像在屏幕上的 X 坐标
    GLushort yOrigin;   // 图像在屏幕上的 Y 坐标
    GLushort width;   // 图像宽度
    GLushort height;  // 图像高度
    // 每像素 24 位 = RGB 各 8 位
    GLubyte bitsPerPixel;
    // 0 表示自下而上存储
    GLubyte imageDescriptor;
};
#pragma pack(pop)             // 恢复结构体对齐填充

std::ostream& operator<<(std::ostream& os, const TargaHeader& header) {
    os << "TargaHeader { "
       << "idLength=" << static_cast<int>(header.idLength) << ", "
       << "colorMapType=" << static_cast<int>(header.colorMapType) << ", "
       << "dataTypeCode=" << static_cast<int>(header.dataTypeCode) << ", "
       << "colorMapOrigin=" << header.colorMapOrigin << ", "
       << "colorMapLength=" << header.colorMapLength << ", "
       << "colorMapDepth=" << static_cast<int>(header.colorMapDepth) << ", "
       << "xOrigin=" << header.xOrigin << ", "
       << "yOrigin=" << header.yOrigin << ", "
       << "width=" << header.width << ", "
       << "height=" << header.height << ", "
       << "bitsPerPixel=" << static_cast<int>(header.bitsPerPixel) << ", "
       << "imageDescriptor=" << static_cast<int>(header.imageDescriptor)
       << " }";
    return os;
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>>;
const static std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> targaData = loadTarga();
 
void changeSize(const int width, const int height){
    std::cout << std::format("changeSize: width={}, height={}", width, height) << "\n";
    if(height == 0) {
        throw std::runtime_error("Height cannot be zero.");
    }
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
// 键盘回调:按 ESC 或 'q' 退出
void keyboard(const unsigned char key, const int x, const int y) {
    std::cout << std::format("keyboard: key={}, x={}, y={}", key, x, y) << "\n";
    switch (key) {
        case 27:    // ESC 键
        case 'q':
        case 'Q':
            std::cout << "keyboard close..." << "\n";
            exit(0);
            break;
        case 'a':
        case 'A':
            break;
        case 'd':
        case 'D':
            break;
        default:
            break;
    }
}

void specialKeys(const int key, const int x, const int y){
    switch (key) {
        case GLUT_KEY_UP:
            break;
        case GLUT_KEY_DOWN:
            break;
        case GLUT_KEY_LEFT:
            break;
        case GLUT_KEY_RIGHT:
            break;
        default:
            return; // 不处理其他按键
    }
}

auto loadTarga() -> std::pair<TargaHeader, std::unique_ptr<GLbyte[]>> {
    std::cout << "Loading Targa image..." << std::endl;
    std::ifstream file("../horse.tga", std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open horse.tga" << std::endl;
        throw std::runtime_error("Failed to open horse.tga");
    }
    TargaHeader header;
    std::cout << "Size of TargaHeader: " << sizeof(TargaHeader) << std::endl;
    file.read(reinterpret_cast<char*>(&header), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to read Targa header" << std::endl;
        throw std::runtime_error("Failed to read Targa header");
    }
    std::cout << "Targa header: " << header << std::endl;
    const std::size_t imageSize = header.width * header.height * (header.bitsPerPixel / 8);
    auto imageData = std::make_unique<GLbyte[]>(imageSize);   // 注意:不能是 const auto,unique_ptr 需要可移动
    file.read(reinterpret_cast<char*>(imageData.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to read Targa image data" << std::endl;
        throw std::runtime_error("Failed to read Targa image data");
    }
    std::cout << "Successfully read Targa image data" << std::endl;
    return std::make_pair(std::move(header), std::move(imageData));
}

// CONSTANT 边界需要一个"边框常量色"。这里取图像四条边的平均色:
// 边界像素向外融合时朝"自身边缘的平均色"过渡,而不是被默认的黑色压暗。
// 注意:targaData 的内存顺序是 BGR(绘制时用 GL_BGR),取用时要把 B、R 换回来。
std::array<GLfloat, 4> borderAverageColor() {
    static const std::array<GLfloat, 4> cached = [] {
        const int w = targaData.first.width;
        const int h = targaData.first.height;
        const auto* px = reinterpret_cast<const unsigned char*>(targaData.second.get());
        auto sample = [&](const int x, const int y) {
            const std::size_t i = (static_cast<std::size_t>(y) * w + x) * 3;
            return std::array<double, 3>{
                static_cast<double>(px[i + 2]),   // 数据是 BGR → 换回 R
                static_cast<double>(px[i + 1]),   //            G
                static_cast<double>(px[i]),       //            B
            };
        };
        std::array<double, 3> sum{0.0, 0.0, 0.0};
        long count = 0;
        for (int x = 0; x < w; ++x) {
            for (const int y : {0, h - 1}) {
                const auto c = sample(x, y);
                for (int k = 0; k < 3; ++k) sum[k] += c[k];
                ++count;
            }
        }
        for (int y = 0; y < h; ++y) {
            for (const int x : {0, w - 1}) {
                const auto c = sample(x, y);
                for (int k = 0; k < 3; ++k) sum[k] += c[k];
                ++count;
            }
        }
        return std::array<GLfloat, 4>{
            static_cast<GLfloat>(sum[0] / count / 255.0),
            static_cast<GLfloat>(sum[1] / count / 255.0),
            static_cast<GLfloat>(sum[2] / count / 255.0),
            1.0F,
        };
    }();
    return cached;
}

void renderScene(){

    // 清除颜色缓冲区
    glClear(GL_COLOR_BUFFER_BIT);

    // 光栅(Raster)位置设置
    glRasterPos2i(0, 0);
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    glPixelZoom(viewport[2] / static_cast<GLfloat>(targaData.first.width), viewport[3] / static_cast<GLfloat>(targaData.first.height)); // 设置像素缩放因子为默认值

    static constexpr GLfloat lumMat[16] = {
        0.30f, 0.30f, 0.30f, 0.0f,
        0.59f, 0.59f, 0.59f, 0.0f,
        0.11f, 0.11f, 0.11f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f
    };

    if (isHistogram) {
        glMatrixMode(GL_COLOR);
        glLoadMatrixf(lumMat);
        glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
        glHistogram(GL_HISTOGRAM, 256, GL_LUMINANCE, GL_FALSE);
        glEnable(GL_HISTOGRAM);
    }

    switch (renderMode) {
        case 5: {
            static constexpr GLfloat sharpen[3 * 3] = {
                0.0f, -1.0f, 0.0f,
                -1.0f, 5.0f, -1.0f,
                0.0f, -1.0f, 0.0f
            };
            glConvolutionFilter2D(GL_CONVOLUTION_2D, GL_RGB, 3, 3, GL_LUMINANCE, GL_FLOAT, sharpen);
            glEnable(GL_CONVOLUTION_2D);
            break;
        }
        case 4: {
            static constexpr GLfloat emboss[3 * 3] = {
                2.0f, 0.0f, 0.0f,
                0.0f, -1.0f, 0.0f,
                0.0f, 0.0f, -1.0f
            };
            glConvolutionFilter2D(GL_CONVOLUTION_2D, GL_RGB, 3, 3, GL_LUMINANCE, GL_FLOAT, emboss);
            glEnable(GL_CONVOLUTION_2D);
            glMatrixMode(GL_COLOR); // 恢复模型视图矩阵模式
            glLoadMatrixf(lumMat);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 3: { // 反转图像
            // 反转表的生成与代理查询只做一次:它们是静态开销,每帧重复既浪费又会刷屏
            static GLubyte invertTable[256][3]{};
            static bool invertTableReady = false;
            if (!invertTableReady) {
                for (int i = 0; i < 256; ++i) {
                    invertTable[i][0] = static_cast<GLubyte>(255 - i);
                    invertTable[i][1] = static_cast<GLubyte>(255 - i);
                    invertTable[i][2] = static_cast<GLubyte>(255 - i);
                }
                glColorTable(GL_PROXY_COLOR_TABLE, GL_RGB, 256, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
                GLint width = 0;
                glGetColorTableParameteriv(GL_PROXY_COLOR_TABLE, GL_COLOR_TABLE_WIDTH, &width);
                std::cout << "Proxy color table width: " << width << " (仅首次查询)" << std::endl;
                invertTableReady = true;
            }
            glColorTable(GL_COLOR_TABLE, GL_RGB, 256, GL_RGB, GL_UNSIGNED_BYTE, invertTable);
            glEnable(GL_COLOR_TABLE);
            break;
        }
        case 2: { // 亮化图片
            glMatrixMode(GL_COLOR);
            glScalef(1.25F, 1.25F, 1.25F);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 1: { // 暗化图片
            glMatrixMode(GL_COLOR);
            glScalef(0.8F, 0.8F, 0.8F);
            glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
            break;
        }
        case 7: { // 模糊:可分离高斯模糊(横竖各一趟 1D)—— 调 kRadius / kSigma 改强度
            /*
                kRadius = 1
                0.3679  0.6065  0.3679
                0.6065  1.0000  0.6065
                0.3679  0.6065  0.3679
                // 归一化,为高斯卷积核
                0.0751  0.1238  0.0751
                0.1238  0.2042  0.1238
                0.0751  0.1238  0.0751
            */
            // kRadius: 高斯卷积核半径, kRadius = 3 * kSigma
            constexpr int   kRadius = 5;      // 半径:4 → 1D 长度 9(越大越糊)
            constexpr float kSigma  = kRadius == 1 ? 0.7071F : static_cast<float>(kRadius) / 3.0F;   // 标准差:越大越糊(超过半径后趋于方波)
            constexpr int   kSize   = kRadius * 2 + 1;
            /*
                w(i) = e^(−i²/(2σ²))
                w(i) = w(i) / Σ w(j)
            */
            // 可分离滤波只需要一维权重(行、列共用同一组),按高斯公式生成并归一化
            static const std::array<GLfloat, kSize> kernel1D = [] {
                std::array<GLfloat, kSize> weights{};
                GLfloat sum = 0.0F;
                for (int i = -kRadius; i <= kRadius; ++i) {
                    const GLfloat w = std::exp(-static_cast<GLfloat>(i * i) /
                                               (2.0F * kSigma * kSigma));
                    weights[i + kRadius] = w;
                    sum += w;
                }
                for (GLfloat& w : weights) {
                    w /= sum;                 // 归一化:总权重为 1,整体亮度不变
                }
                return weights;
            }();
            // 矩阵乘法 kernel1D(列)* kernel1D(行) = 2D 高斯卷积核
            // 先横向、再纵向各做一次 1D 卷积,等价于 (2r+1)×(2r+1) 的二维高斯,
            // 但每像素乘加从 kSize² = 81 降到 2×kSize = 18
            // 注意:format/type 只有一对,row 与 column 共用
            // 合法边界模式只有 GL_REDUCE(默认,输出会缩小)、GL_CONSTANT_BORDER、GL_REPLICATE_BORDER;
            // 误用 GL_REPEAT 在 macOS 上不报错,但第二次绘制会段错误
            // 边界模式是"枚举",必须用 glConvolutionParameteri;颜色是"浮点数组",用 glConvolutionParameterfv
            // CONSTANT:图像外按一个常量色参与卷积 —— 尺寸不变、边界也参与模糊
            const std::array<GLfloat, 4> borderColor = borderAverageColor();
            std::cout << std::format("CONSTANT 边框色 = ({:.3F}, {:.3F}, {:.3F})\n",
                                     borderColor[0], borderColor[1], borderColor[2]);
            glConvolutionParameteri(GL_CONVOLUTION_2D, GL_CONVOLUTION_BORDER_MODE, GL_CONSTANT_BORDER);
            glConvolutionParameteri(GL_SEPARABLE_2D,   GL_CONVOLUTION_BORDER_MODE, GL_CONSTANT_BORDER);
            glConvolutionParameterfv(GL_CONVOLUTION_2D, GL_CONVOLUTION_BORDER_COLOR, borderColor.data());
            glConvolutionParameterfv(GL_SEPARABLE_2D,   GL_CONVOLUTION_BORDER_COLOR, borderColor.data());
            glSeparableFilter2D(GL_SEPARABLE_2D, GL_RGB, kSize, kSize,
                                GL_LUMINANCE, GL_FLOAT,
                                kernel1D.data(),   // row:水平权重
                                kernel1D.data());  // column:垂直权重
            glEnable(GL_SEPARABLE_2D);
            break;
        }
        default:
            // 默认渲染模式的处理逻辑
            break;
    }
    glDrawPixels(targaData.first.width, targaData.first.height, GL_BGR, GL_UNSIGNED_BYTE, targaData.second.get());
    if (isHistogram) {
        std::cout << "Displaying histogram..." << std::endl;
        static GLint histogram[256];
        glGetHistogram(GL_HISTOGRAM, GL_TRUE, GL_LUMINANCE, GL_INT, histogram);
        GLint largest = 0;
        for(GLint i = 0; i < 256; ++i) {
            if (histogram[i] > largest) {
                largest = histogram[i];
            }
        }
        glColor3f(1.0F, 1.0F, 1.0F);
        glBegin(GL_LINE_STRIP);
        for (GLint i = 0; i < 256; ++i) {
            // glVertex2f(static_cast<GLfloat>(i) / 255.0F, static_cast<GLfloat>(histogram[i]) / static_cast<GLfloat>(largest));
            glVertex2f(static_cast<GLfloat>(i), static_cast<GLfloat>(histogram[i]) / static_cast<GLfloat>(largest) * 128.0F);
        }
        glEnd();
        // 不要在这里清 isHistogram:GLUT 常在菜单关闭后再重绘一次,
        // 若此刻清掉标志,下一帧 glClear + glDrawPixels 就会把直方图覆盖掉。
        glDisable(GL_HISTOGRAM);
    }
    glMatrixMode(GL_COLOR); // 恢复模型视图矩阵模式
    glLoadIdentity(); // 重置颜色矩阵
    glMatrixMode(GL_MODELVIEW); // 恢复模型视图矩阵模式
    glDisable(GL_CONVOLUTION_2D);
    glDisable(GL_SEPARABLE_2D);   // 可分离滤波是独立的 enable 位,必须一起关
    glDisable(GL_COLOR_TABLE); // 禁用颜色表
    glutSwapBuffers();
}
 
void setupRc(){
    glClearColor(0.0F, 0.0F, 0.0F, 0.0F);
}
// 截屏辅助:glReadPixels 同样会经过 pixel transfer,
// 构造时把映射与通道系数复位为恒等,析构时自动还原之前的状态。
class PixelTransferGuard {
public:
    PixelTransferGuard() {
        glGetBooleanv(GL_MAP_COLOR, &mapColor_);
        glGetFloatv(GL_RED_SCALE, &redScale_);
        glGetFloatv(GL_GREEN_SCALE, &greenScale_);
        glGetFloatv(GL_BLUE_SCALE, &blueScale_);
        glPixelTransferi(GL_MAP_COLOR, GL_FALSE);
        glPixelTransferf(GL_RED_SCALE, 1.0F);
        glPixelTransferf(GL_GREEN_SCALE, 1.0F);
        glPixelTransferf(GL_BLUE_SCALE, 1.0F);
    }
    ~PixelTransferGuard() {
        glPixelTransferi(GL_MAP_COLOR, mapColor_ ? GL_TRUE : GL_FALSE);
        glPixelTransferf(GL_RED_SCALE, redScale_);
        glPixelTransferf(GL_GREEN_SCALE, greenScale_);
        glPixelTransferf(GL_BLUE_SCALE, blueScale_);
    }
    PixelTransferGuard(const PixelTransferGuard&) = delete;
    PixelTransferGuard& operator=(const PixelTransferGuard&) = delete;

private:
    GLboolean mapColor_ = GL_FALSE;
    GLfloat redScale_ = 1.0F;
    GLfloat greenScale_ = 1.0F;
    GLfloat blueScale_ = 1.0F;
};

// 截屏读取:把"从当前窗口前缓冲取像素"这件事收拢到一处。
// 构造时复位 pixel transfer、打开 PACK 对齐、切到 GL_FRONT;析构时自动还原读缓冲区。
class ScreenshotReader {
public:
    ScreenshotReader() {
        glPixelStorei(GL_PACK_ALIGNMENT, 1);
        glGetIntegerv(GL_READ_BUFFER, &previousBuffer_);
        std::cout << std::format("Current read buffer: 0x{:X}", previousBuffer_) << std::endl;
        glReadBuffer(GL_FRONT);
    }
    ~ScreenshotReader() { glReadBuffer(previousBuffer_); }
    ScreenshotReader(const ScreenshotReader&) = delete;
    ScreenshotReader& operator=(const ScreenshotReader&) = delete;

private:
    PixelTransferGuard transferGuard_;          // 复位 pixel transfer(读完自动还原)
    GLint previousBuffer_ = GL_BACK;            // 读完把读缓冲区切回去
};

void readFrontBuffer(const int width, const int height, const GLenum format, GLbyte* out) {
    const ScreenshotReader reader;              // 读前准备 + 读完还原
    glReadPixels(0, 0, width, height, format, GL_UNSIGNED_BYTE, out);
}

void writePng(const std::string& filename, const int width, const int height) {
    // openGL 的 glReadPixels 读取的图像是自下而上的,而 PNG 是自上而下的,所以需要翻转图像
    stbi_flip_vertically_on_write(1);
    
    const std::size_t imageSize = width * height * 3; // 3 bytes per pixel for RGB
    const auto flippedImageData = std::make_unique<GLbyte[]>(imageSize);
    readFrontBuffer(width, height, GL_RGB, flippedImageData.get());   // 像素在内存里是自下而上的

    if (!stbi_write_png(filename.c_str(), width, height, 3, flippedImageData.get(), width * 3)) {
        std::cerr << "Failed to write PNG file: " << filename << std::endl;
        throw std::runtime_error("Failed to write PNG file");
    }
}
void writeTarga(const std::string& filename, const TargaHeader& header) {
    // 获取viewport的宽高
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    std::cout << std::format("Viewport: x={}, y={}, width={}, height={}", viewport[0], viewport[1], viewport[2], viewport[3]) << std::endl;
    const GLushort width = viewport[2];
    const GLushort height = viewport[3];
    const TargaHeader newHeader = {
        .idLength = header.idLength,
        .colorMapType = header.colorMapType,
        .dataTypeCode = header.dataTypeCode,
        .colorMapOrigin = header.colorMapOrigin,
        .colorMapLength = header.colorMapLength,
        .colorMapDepth = header.colorMapDepth,
        .xOrigin = header.xOrigin,
        .yOrigin = header.yOrigin,
        .width = width,
        .height = height,
        .bitsPerPixel = header.bitsPerPixel,
        .imageDescriptor = header.imageDescriptor,
    };

    std::ofstream file(filename, std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open " << filename << " for writing" << std::endl;
        throw std::runtime_error("Failed to open file for writing");
    }
    file.write(reinterpret_cast<const char*>(&newHeader), sizeof(TargaHeader));
    if (!file) {
        std::cerr << "Failed to write Targa header to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa header");
    }
    const std::size_t imageSize = newHeader.width * newHeader.height * (newHeader.bitsPerPixel / 8);
    const std::unique_ptr<GLbyte[]> imageBuffer = std::make_unique<GLbyte[]>(imageSize);

    readFrontBuffer(newHeader.width, newHeader.height, GL_BGR, imageBuffer.get());
    file.write(reinterpret_cast<const char*>(imageBuffer.get()), imageSize);
    if (!file) {
        std::cerr << "Failed to write Targa image data to " << filename << std::endl;
        throw std::runtime_error("Failed to write Targa image data");
    }
}
void processMenu(const int option){
    std::cout << std::format("Menu option selected: {}", option) << std::endl;
    glutSetWindow(mainWindowId);

    if (option >= 10) {   // 10 及以上是"动作",与 0..9 的渲染模式分开,避免编号撞车
        GLint viewport[4];
        glGetIntegerv(GL_VIEWPORT, viewport);
        switch (option) {
            case 10: writeTarga("output.tga", targaData.first); break;
            case 11: writePng("output.png", viewport[2], viewport[3]); break;
            default: break;
        }
        return;           // 动作不改变当前渲染模式
    }

    renderMode = option;
    isHistogram = (option == 6) ? GL_TRUE : GL_FALSE;   // 只有直方图模式开启,切到其它模式自动关闭
    glutPostRedisplay();
}
auto main(const int argc, const char** argv) -> int {
    glutInit(const_cast<int*>(&argc), const_cast<char**>(argv));
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(800, 600);
    mainWindowId = glutCreateWindow("OpenGL Window");
    std::cout << "Main window ID: " << mainWindowId << std::endl;
    // 验证 MSAA 是否真正生效(macOS 的 GLUT 拿不到多重采样像素格式时会静默降级)
    GLint sampleBuffers = 0, samples = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
    glGetIntegerv(GL_SAMPLES, &samples);
    std::cout << std::format("Renderer: {}, sample buffers: {}, samples: {}",
        reinterpret_cast<const char*>(glGetString(GL_RENDERER)), sampleBuffers, samples) << std::endl;
    std::cout << "current path: " << std::filesystem::current_path() << std::endl;
    glutReshapeFunc(changeSize);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(specialKeys);
    glutDisplayFunc(renderScene);
    glutCreateMenu(processMenu);
    glutAddMenuEntry("重置", 0);
    glutAddMenuEntry("暗化图片", 1);
    glutAddMenuEntry("亮化图片", 2);
    glutAddMenuEntry("反转图片", 3);
    glutAddMenuEntry("浮雕化图片", 4);
    glutAddMenuEntry("锐化图片", 5);
    glutAddMenuEntry("直方图均衡化", 6);
    glutAddMenuEntry("blur图片", 7);
    glutAddMenuEntry("保存 TGA", 10);
    glutAddMenuEntry("保存 PNG", 11);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    setupRc();
    std::cout << "GL_ARB_imaging supported: " << glutExtensionSupported("GL_ARB_imaging") << std::endl;
    glutMainLoop();
    return 0;
}
posted @ 2026-09-12 18:21  爱情丶眨眼而去  阅读(12)  评论(0)    收藏  举报