由于本文章是对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,里面仅是一些头文件。
输出结果
你能看到那个白点吗?那是我们的起点。