[TinyRenderer] Chapter1 p1 Output Image

qiyuewuyi2333發表於2024-06-10

由於本文章是對TinyRenderer的模仿,所以並不打算引入外部庫。

那麼我們第一步需要解決的就是圖形輸出的問題,畢竟,如果連渲染的結果都看不到,那還叫什麼Renderer嘛。

由於不引入外部庫,所以選擇輸出的圖片格式應該越簡單越好,各種點陣圖就成為了我們的首選。
這裡我們選擇了生態較好的bmp點陣圖。
技術上,由於只使用C++,所以各種檔案流就成了我們構建圖片的唯一工具。

本章目標

輸出一張儲存了我們渲染結果的bmp點陣圖

需求:

  • 大小可以控制,也就是點陣圖的尺寸可控
  • 控制某個畫素點的顏色,精準更改set()
  • 對點陣圖進行上下反轉

實現

BMPImage.h

#ifndef BMP_IMAGE_H
#define BMP_IMAGE_H
#include <string>
#include <vector>

#pragma pack(push, 1)
struct BMPFileHeader
{
    uint16_t bfType;      // BMP檔案的型別,必須為"B"然後是"M"
    uint32_t bfSize;      // 檔案大小
    uint16_t bfReserved1; // 保留字,必須為0
    uint16_t bfReserved2; // 從檔案頭到實際點陣圖資料的偏移位元組數
    uint32_t bfOffBits;   // 資訊頭的大小
};

struct BMPInfoHeader
{
    uint32_t biSize;         // info head size
    int32_t biWidth;         // 影像寬度
    int32_t biHeight;        // 影像高度
    uint16_t biPlanes;       // 影像的位面數
    uint16_t biBitCount;     // 每個畫素的位數
    uint32_t biCompression;  // 壓縮型別
    uint32_t biSizeImage;    // 影像的大小,以位元組為單位
    int32_t biXPelsPerMeter; // 水平解析度
    int32_t biYPelsPerMeter; // 垂直解析度
    uint32_t biClrUsed;      // 點陣圖實際使用的顏色表中的顏色數
    uint32_t biClrImportant; // 點陣圖顯示過程中重要的顏色數
};
#pragma pack(pop)

/**
 * \brief custom the color format used
 */
enum ColorFormat
{
    RGB,
    CMYK
};

struct RGBPixel
{
    uint8_t red;
    uint8_t green;
    uint8_t blue;
    RGBPixel() : red(0), green(0), blue(0)
    {
    }
    RGBPixel(uint8_t red, uint8_t green, uint8_t blue) : red(red), green(green), blue(blue)
    {
    }
};

class BMPImage
{
  public:
    BMPImage() = delete;
    BMPImage(unsigned int width, unsigned int height, ColorFormat colorFormat = ColorFormat::RGB);
    void loadData(std::vector<char>&& userData);
    void generate(const std::string& fileName);
    void loadDataAndGenerate(std::vector<char>&& userData, const std::string& fileName);

    void set(int x, int y, RGBPixel pixel);
    void flipVertically();

  private:
    BMPFileHeader fileHeader;
    BMPInfoHeader infoHeader;

    ColorFormat colorFormat;
    std::vector<unsigned char> pixelData;
};

#endif

Important:

  • 在組織bmp檔案頭的部分,一定要使用預處理宏#pragma pack(push, 1)#pragma pack(pop),控制記憶體對齊方式為單位元組,否則會由於編譯器控制的記憶體對齊而導致檔案格式錯誤,從而不能正確輸出

BMPImage.cpp

#include "TinyRenderer/BMPImage.h"

#include <fstream>
#include <iostream>

BMPImage::BMPImage(unsigned width, unsigned height, ColorFormat colorFormat)
{
    int rowSize = (width * 3 + 3) & (~3); // Ensure row size is a multiple of 4 bytes
    int fileSize = rowSize * height + sizeof(BMPFileHeader) + sizeof(BMPInfoHeader);

    // Set BMP file header
    fileHeader.bfType = 0x4D42; // 'BM'
    fileHeader.bfSize = fileSize;
    fileHeader.bfReserved1 = 0;
    fileHeader.bfReserved2 = 0;
    fileHeader.bfOffBits = sizeof(BMPFileHeader) + sizeof(BMPInfoHeader);

    // Set BMP info header
    infoHeader.biSize = sizeof(BMPInfoHeader);
    infoHeader.biWidth = width;
    infoHeader.biHeight = height;
    infoHeader.biPlanes = 1;
    infoHeader.biBitCount = 24;
    infoHeader.biCompression = 0;
    infoHeader.biSizeImage = rowSize * height;
    infoHeader.biXPelsPerMeter = 0;
    infoHeader.biYPelsPerMeter = 0;
    infoHeader.biClrUsed = 0;
    infoHeader.biClrImportant = 0;

    // Initialize pixel data
    pixelData.resize(rowSize * height, 0);
}

// not important now
void BMPImage::loadData(std::vector<char>&& userData)
{
    // TODO: load image
}

void BMPImage::generate(const std::string& fileName)
{
    std::ofstream file(fileName, std::ios::out | std::ios::binary);
    if (!file)
    {
        std::cerr << "Error: Unable to open file for writing." << std::endl;
        return;
    }

    // Write headers
    file.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
    file.write(reinterpret_cast<const char*>(&infoHeader), sizeof(infoHeader));

    // Write pixel data
    file.write(reinterpret_cast<const char*>(pixelData.data()), pixelData.size());

    file.close();
}

void BMPImage::loadDataAndGenerate(std::vector<char>&& userData, const std::string& fileName)
{
}

void BMPImage::set(int x, int y, RGBPixel pixel)
{
    if (x < 0 || y < 0 || x >= infoHeader.biWidth || y >= infoHeader.biHeight)
    {
        throw std::out_of_range("Pixel coordinates are out of bounds");
    }
    int rowSize = (infoHeader.biWidth * 3 + 3) & (~3);
    int index = (infoHeader.biHeight - 1 - y) * rowSize + x * 3;
    pixelData[index] = pixel.blue;
    pixelData[index + 1] = pixel.green;
    pixelData[index + 2] = pixel.red;
}

void BMPImage::flipVertically()
{
    int width = infoHeader.biWidth;
    int height = infoHeader.biHeight;
    int rowSize = (width * 3 + 3) & (~3);

    for (int y = 0; y < height / 2; ++y)
    {
        int topIndex = y * rowSize;
        int bottomIndex = (height - 1 - y) * rowSize;
        for (int x = 0; x < rowSize; ++x)
        {
            std::swap(pixelData[topIndex + x], pixelData[bottomIndex + x]);
        }
    }
}

測試

main.cpp

#include "TinyRenderer/TinyRenderer.h"
#include "TinyRenderer/BMPImage.h"


int main()
{
    BMPImage image(100, 100, ColorFormat::RGB);
    RGBPixel white(255, 255, 255);
    image.set(22, 77, white);
    image.flipVertically();
    image.generate("test.bmp");
    std::cout << "Image Generated." << std::endl;
    return 0;
}

請忽略TinyRenderer/TinyRenderer.h,裡面僅是一些標頭檔案。

輸出結果

img

你能看到那個白點嗎?那是我們的起點。

相關文章