Chuyển đổi dữ liệu pixel từ glReadPixels của OpenGL sang CImage

Việc chuyển đổi dữ liệu đồ họa OpenGL hiển thị trên màn hình thành định dạng hình ảnh để in ấn là một yêu cầu phổ biến. Tuy nhiên, các hàm in ấn của MFC không hỗ trợ trực tiếp dữ liệu OpenGL. Do đó, cần phải chuyển đổi dữ liệu OpenGL sang định dạng GDI trước khi vẽ lên đối tượng pDC của máy in.

Hàm glReadPixels của OpenGL cho phép đọc một khối pixel từ bộ đệm khung (frame buffer). Một điểm đáng chú ý và có thể gây nhầm lẫn là hàm này bắt đầu đọc từ góc dưới bên trái của khối pixel, thay vì góc trên bên trái như nhiều lập trình viên mong đợi. Điều này có thể được xác nhận qua tài liệu chính thức của OpenGL. Trước khi gọi glReadPixels, cần thiết lập bộ đệm đọc bằng glReadBuffer(GL_BACK_LEFT). Việc thử sử dụng GL_FRONT_LEFT dường như không mang lại kết quả mong muốn về việc bắt đầu từ góc trên bên trái.

Sau khi đọc dữ liệu pixel, bước tiếp theo là lưu trữ chúng vào một đối tượng CImage. Dưới đây là cách thực hiện:

Khai báo trong tệp .h


GLubyte* m_pixelData = nullptr;
CImage m_imageOutput;
int m_imageWidth;
int m_imageHeight;

Hàm OnPreparePrinting

Trong hàm này, chúng ta chuẩn bị cho quá trình in và thiết lập bộ đệm đọc của OpenGL.


BOOL CYourView::OnPreparePrinting(CPrintInfo* pInfo)
{
    // Khởi tạo con trỏ dữ liệu pixel
    if (m_pixelData) {
        free(m_pixelData);
        m_pixelData = nullptr;
    }
    // Đặt bộ đệm đọc là BACK_LEFT
    glReadBuffer(GL_BACK_LEFT);
    // Lấy kích thước ảnh từ cửa sổ OpenGL
    // Giả sử m_imageWidth và m_imageHeight đã được thiết lập
    m_imageWidth = GetOpenGLWidth(); // Thay thế bằng hàm lấy chiều rộng thực tế
    m_imageHeight = GetOpenGLHeight(); // Thay thế bằng hàm lấy chiều cao thực tế
    return DoPreparePrinting(pInfo);
}

Hàm OnBeginPrinting

Tại đây, chúng ta cấp phát bộ nhớ cho dữ liệu pixel và thực hiện lệnh glReadPixels để lấy dữ liệu từ bộ đệm OpenGL. Sau đó, dữ liệu này được chuyển đổi và gán vào đối tượng CImage.


void CYourView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)
{
    // Xác định số lượng byte cho mỗi pixel dựa trên định dạng
    GLint internalFormat;
    glGetIntegerv(GL_INTERNAL_FORMAT, &internalFormat);
    int bytesPerPixel = 0;
    switch(internalFormat) {
        case GL_RGB:
            bytesPerPixel = 3; // RGB
            break;
        case GL_RGBA:
            bytesPerPixel = 4; // RGBA
            break;
        case GL_ALPHA:
        case GL_LUMINANCE:
            bytesPerPixel = 1; // Alpha hoặc Luminance
            break;
        default:
            // Xử lý các định dạng khác hoặc mặc định là RGB
            bytesPerPixel = 3;
            break;
    }

    // Cấp phát bộ nhớ cho dữ liệu pixel
    size_t dataSize = static_cast(bytesPerPixel) * m_imageWidth * m_imageHeight;
    m_pixelData = (GLubyte*)malloc(dataSize);
    if (!m_pixelData) {
        // Xử lý lỗi cấp phát bộ nhớ
        return;
    }

    // Đọc pixel từ bộ đệm khung OpenGL
    // Lưu ý: glReadPixels đọc từ góc dưới bên trái
    glReadPixels(0, 0, m_imageWidth, m_imageHeight, GL_RGB, GL_UNSIGNED_BYTE, m_pixelData);

    // Tạo đối tượng CImage với độ sâu màu 24-bit (RGB)
    // Nếu định dạng đọc là RGBA, bạn có thể cần xử lý thêm kênh Alpha
    m_imageOutput.Create(m_imageWidth, m_imageHeight, 24);

    // Chuyển đổi dữ liệu pixel từ OpenGL sang định dạng CImage
    // Vòng lặp này xử lý việc đảo ngược thứ tự pixel theo chiều dọc do glReadPixels
    // và chuyển đổi thứ tự BGR sang RGB cho CImage
    int pixelIndex = m_imageHeight * m_imageWidth * bytesPerPixel;
    for (int y = 0; y < m_imageHeight; ++y) {
        for (int x = 0; x < m_imageWidth; ++x) {
            // Đọc các giá trị màu B, G, R từ dữ liệu OpenGL
            // Lưu ý: glReadPixels trả về GL_RGB (hoặc GL_RGBA), thứ tự byte có thể là R, G, B
            // nhưng CImage::SetPixelRGB mong đợi R, G, B.
            // Tuy nhiên, do glReadPixels đọc từ dưới lên, và chúng ta lặp từ trên xuống,
            // nên việc truy cập index theo cách này sẽ khớp với vị trí pixel mong muốn.
            GLubyte blueValue = m_pixelData[--pixelIndex];
            GLubyte greenValue = m_pixelData[--pixelIndex];
            GLubyte redValue = m_pixelData[--pixelIndex];
            
            // Đặt pixel vào đối tượng CImage
            // CImage::SetPixelRGB mong đợi R, G, B
            m_imageOutput.SetPixelRGB(x, y, redValue, greenValue, blueValue);
        }
    }
}

Hàm OnEndPrinting

Trong hàm này, chúng ta giải phóng bộ nhớ đã cấp phát và hủy đối tượng CImage.


void CYourView::OnEndPrinting(CDC* pDC, CPrintInfo* pInfo)
{
    if (m_pixelData) {
        free(m_pixelData);
        m_pixelData = nullptr;
    }
    m_imageOutput.Destroy();
}

Hàm OnPrint

Cuối cùng, trong hàm OnPrint, chúng ta sử dụng phương thức Draw của CImage để vẽ hình ảnh lên pDC của máy in.


void CYourView::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
    if (m_imageOutput.GetWidth() > 0 && m_imageOutput.GetHeight() > 0) {
        // Lấy kích thước ảnh
        int imageWidth = m_imageOutput.GetWidth();
        int imageHeight = m_imageOutput.GetHeight();

        // Vẽ ảnh lên DC của máy in, có thể điều chỉnh tỷ lệ
        // Ví dụ: vẽ với tỷ lệ 1:1
        m_imageOutput.Draw(pDC->m_hDC, 0, 0, imageWidth, imageHeight);
        
        // Hoặc vẽ với tỷ lệ tùy chỉnh, ví dụ gấp 4 lần
        // m_imageOutput.Draw(pDC->m_hDC, 0, 0, imageWidth * 4, imageHeight * 4);
    }
}

Lưu ý rằng vòng lặp xử lý dữ liệu pixel có vẻ hơi phức tạp với việc giảm chỉ số pixelIndex. Tuy nhiên, cách tiếp cận này là cần thiết để điều chỉnh thứ tự đọc pixel của glReadPixels (bắt đầu từ dưới lên) và đảm bảo dữ liệu được gán đúng vào vị trí tương ứng trong CImage.

Thẻ: OpenGL glReadPixels CImage MFC GDI

Đăng vào ngày 24 tháng 8 lúc 18:47