Skip to content

Latest commit

 

History

History
406 lines (336 loc) · 11.8 KB

File metadata and controls

406 lines (336 loc) · 11.8 KB

魔改一下LibGraphics

[toc]

操作像素

做到这里,你的LibGraphics已经可以胜任大多数C大程项目了。但为了方便地画出三维图形,还是得再改一改。

LibGraphics本身只是对Win32 API的封装,即使是非常基本的图形操作,都要调用Win32对应的接口。

  • LibGraphicsDrawLine函数,最终调用了Win32LineTo函数。
// graphics.c
void DrawLine(double dx, double dy)
{ 
    // ... 
}
// ||
// \/
static void DisplayLine(double x, double y, double dx, double dy)
{
    // ...
    if (regionState == NoRegion) {
		// ...
        LineTo(osdc, x1, y1); // 【属于Win32系列】
    } else {
        AddSegment(x0, y0, x1, y1);
    }
}

但我们要实现的软3D渲染流程希望尽可能地用CPU去模拟GPU的工作过程,因此**操作 帧缓冲(Frame Buffer) 以 操作 屏幕内容(物理意义,比如你的显示屏)**这个概念是必要的。

说白了就是要操作窗口中的像素。我们可以用Win32HBITMAP结构,以操作内存的方式操作osdc(忘记osdc的作用请点击此链接)

操作内存就是模拟操作帧缓冲,操作窗口内容就是模拟操作屏幕内容。

修改LibGraphics 与 编写LibGraphics3D库

  • 在项目根目录中新建libg3D文件夹。
  • 在 VS 2019 - 解决方案资源管理器 - <项目名>/源文件 中,添加一个筛选器(就是虚拟的文件夹),命名为libg3D
  • libg3D文件夹中添加graphics3D.hgraphics3D.c文件,并将graphics3D.c添加到 <项目名>/源文件/libg3D 中。
  • libgraphics文件夹中添加win32Export.h
    • 这个的作用后面再讲。
  • 至此,磁盘中的项目根目录如下:
+ <项目名>
|__+ libragphics
|  |__ win32Export.h 
|  |__ graphics.h
|  |__ graphics.c
|  |__ ...
|__+ libg3D
|  |__ graphics3D.h
|  |__ graphics3D.c
|__ main.c
|__ <项目名>.sln
|__ <项目名>.vcxproj
|__ ...
  • VS 2019中的解决方案资源管理器如下:
+ <项目名>
|__+ 头文件
|__+ 源文件
|  |__ main.c
|  |__+ libgraphics
|  |  |__ graphics.c
|  |  |__ genlib.c
|  |  |__ ...
|  |__+ libg3D
|     |__ graphics3D.c
|__ ...
  • graphics3D.h中声明以下接口:
#ifndef _GRAPHICS3D_H
#define _GRAPHICS3D_H

// 底层图形组件
enum LIBG3D_PRIMITIVE
{
	LIBG3D_POINTS,
	LIBG3D_LINES,
	LIBG3D_TRIANGLES
};

// 错误码
enum LIBG3D_ERROR
{
	LIBG3D_NO_ERR,
	LIBG3D_ILLEGAL_VAL,
	LIBG3D_NULL_MVP_MAT
};

/// <summary>
/// 获取窗口宽度,以像素为单位,而不是LibGraphics那个奇怪的double类型数值
/// </summary>
/// <returns>窗口宽度(以像素为单位)</returns>
int libg3DGetWindowPixelWidth();

/// <summary>
/// 获取窗口高度,以像素为单位,而不是LibGraphics那个奇怪的double类型数值
/// </summary>
/// <returns>窗口高度(以像素为单位)</returns>
int libg3DGetWindowPixelHeight();

/// <summary>
/// 设置当前视口尺寸
/// </summary>
/// <param name="x">距离窗口左下的横坐标</param>
/// <param name="y">距离窗口左下的纵坐标</param>
/// <param name="width">宽度</param>
/// <param name="height">高度</param>
void libg3DViewport(int x, int y, int width, int height);

/// <summary>
/// 使用某个颜色填充视口
/// </summary>
/// <param name="r">红</param>
/// <param name="g">绿</param>
/// <param name="b">蓝</param>
void libg3DClearWithColor(float r, float g, float b);

#endif // !_GRAPHICS3D_H
  • 要在graphics3D.c实现上面的接口,我们需要一个与osdc绑定的HBITMAP结构。但如你所见osdcgraphics.c的静态全局变量,不能在graphics3D.c中访问。
// graphics.c
// ...
static HWND consoleWindow, graphicsWindow;
static HDC gdc, osdc; // 静态全局变量
static HBITMAP osBits;
static HPEN drawPen, erasePen, nullPen;
// ...
  • 要解决这个问题,有多种方法。

  • 方法一是把静态全局的osdc修改成全局osdc,再在graphics3D.c中用extern关键字声明一个osdc

  • 方法二看起来更加中二(Doge),我们就用方法二:

    • 鲁迅说过:

      • 如果一个软件工程问题不能靠加一层封装来解决,那就加两层!

    • 现在的LibGraphicsLibGraphcis3D是这样的:

-----+------------------------------------
应用层| main.c ------------+
-----+---|---------------+-\--------------
     |  \|/              | _\|
接口层| extgraph.h        | graphgics3D.h
-----+---|---------------+---|------------
     |   |               |   |
     |  \|/              |  \|/
实现层| graphics.c (osdc) | graphics3D.c
-----+-------------------+----------------
    • 我们给它加一层:
-----+--------------------------------------
应用层| main.c --------------+
-----+---|-----------------+-\--------------
     |  \|/                | _\|
接口层| extgraph.h          | graphgics3D.h
-----+---|-----------------+---|------------
     |   |                     |
共享层|   | win32Export.h<------|------+
-----+---|----------|------+---|------|----
     |   |          |      |   |      |
     |  \|/        \|/     |  \|/     |
实现层| graphics.c (osdc)   | graphics3D.c
-----+---------------------+----------------
    • 通过共享层的win32Export.h,把graphics.c里面的部分Win32变量暴露出来。
      • 避免全局变量可能造成的命名混乱:比如存在多个osdc的问题(毕竟这是个很常见的名字)。
      • 避免内部数据被暴露给应用层:比如让graphics3D.c直接include extgraph.h,通过extgraph.h共享数据。这样将导致main.c也可以访问osdc,给了用户乱搞的自由。
      • 其他好处编不下去了。。。
    • win32Export.h与其函数的实现如下:
// win32Export.h
#ifndef _WIN32_EXPORT_H
#define _WIN32_EXPORT_H

#include <Windows.h>

// 用于调用InvalidateRect()
HWND GetLibgWindow();

// 用于创建HBITMAP
HDC GetLibgOSDC();

// 用于libg3DGetWindowPixelWidth()
int GetLibgWindowPixelWidth();

// 用于libg3DGetWindowPixelHeight()
int GetLibgWindowPixelHeight();

#endif // !_WIN32_EXPORT_H

// graphics.c
// +
HWND GetLibgWindow() { return graphicsWindow; }

HDC GetLibgOSDC() { return osdc; }

int GetLibgWindowPixelWidth() { return pixelWidth; }

int GetLibgWindowPixelHeight() { return pixelHeight; }
  • 终于,我们可以在graphics3D.c中实现graphics3D.h的接口了:
#include "graphics3D.h"
#include "../libgraphics/win32Export.h"

// WIN32元素
static HBITMAP oldBitmap = NULL, bitmap = NULL;

// 帧缓冲结构
//   一个窗口就由一个帧缓冲操控。
struct FrameBuffer
{
	int windowW, windowH; // 窗口大小
	int viewportX, viewportY; // 视口距离窗口左下角的偏移
	int viewportW, viewportH; // 视口大小
	unsigned char* colorAttachment; // 帧缓冲的颜色缓冲(还有其他缓冲,后面再说)
};

static struct FrameBuffer frameBuffer = { 0 };

int libg3DGetWindowPixelWidth()
{
	return GetLibgWindowPixelWidth();
}
int libg3DGetWindowPixelHeight()
{
	return GetLibgWindowPixelHeight();
}

void libg3DViewport(int x, int y, int width, int height)
{
	// 保证初始化只进行一次
	if (frameBuffer.colorAttachment == NULL)
	{
		// 获取graphics.c创建的窗口尺寸
		frameBuffer.windowW = GetLibgWindowPixelWidth();
		frameBuffer.windowH = GetLibgWindowPixelHeight();
		// 新建一个与osdc适配的bitmap对象,
		// BITMAPINFO 详见 https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo
		BITMAPINFO bitmapInfo = { {
				sizeof(BITMAPINFOHEADER),
				frameBuffer.windowW, frameBuffer.windowH,
				1,
				32, // 一个像素占4个byte,32个bit
				BI_RGB,
				frameBuffer.windowW * frameBuffer.windowH * 4,
				0, 0, 0, 0
			}
		};
		LPVOID ptr = NULL;
		bitmap = CreateDIBSection(GetLibgOSDC(), &bitmapInfo, DIB_RGB_COLORS, &ptr, 0, 0);
		// 将bitmap设置为当前osdc的bitmap,操作bitmap即操作osdc
		oldBitmap = (HBITMAP)SelectObject(GetLibgOSDC(), bitmap);
		// 将frameBuffer的colorAttachment指向ptr,操作frameBuffer即操作bitmap
		frameBuffer.colorAttachment = (unsigned char*)ptr;
		frameBuffer.viewportH = frameBuffer.windowH;
		frameBuffer.viewportW = frameBuffer.windowW;
	}

	// 改变frameBuffer的viewport相关变量
	//   各种边界检查
	frameBuffer.viewportX = x < 0 ? 0 : x;
	frameBuffer.viewportX = frameBuffer.viewportX >= frameBuffer.windowW - 1
		? frameBuffer.windowW - 1 : frameBuffer.viewportX;
	frameBuffer.viewportY = y < 0 ? 0 : y;
	frameBuffer.viewportY = frameBuffer.viewportY >= frameBuffer.windowH - 1
		? frameBuffer.windowH - 1 : frameBuffer.viewportY;
	int tmp;
	tmp = frameBuffer.viewportX + width;
	frameBuffer.viewportW = (tmp > frameBuffer.windowW ? frameBuffer.windowW : tmp)
		- frameBuffer.viewportX;
	tmp = frameBuffer.viewportY + height;
	frameBuffer.viewportH = (tmp > frameBuffer.windowH ? frameBuffer.windowH : tmp)
		- frameBuffer.viewportY;
}

void libg3DClearWithColor(unsigned char r, unsigned char g, unsigned char b)
{
	// 步长,每跨一步相当于往上一行
	int stride = frameBuffer.windowW * 4;
	// 行起始指针,指向当前viewport 第N行 的 第一个像素
	unsigned char* rowPtr = frameBuffer.colorAttachment
		+ frameBuffer.viewportY * stride
		+ frameBuffer.viewportX * 4;
	for (int row = 0; row < frameBuffer.viewportH; ++row)
	{
		// 像素起始指针,指向 rowPtr所在行 的 第N个 像素
		unsigned char* pixPtr = rowPtr;
		for (int col = 0; col < frameBuffer.viewportW; ++col)
		{
			// 逐像素赋颜色值
			// 注意:WIN32的BITMAP采用BGRA布局,而不是RGBA
			//   一个像素占4个byte,32个bit
			pixPtr[0] = b;
			pixPtr[1] = g;
			pixPtr[2] = r;
			pixPtr[3] = 255; // 透明度满格

			pixPtr += 4; // 往右一列
		}
		rowPtr += stride; // 往上一行
	}

	// 将整个viewport标记为需要更新(不是需要擦除)
	{
		// 注意:viewportRECT使用Win32坐标系,
		//   y轴反向,同时原点在窗口左上角
		RECT viewportRECT = {
		frameBuffer.viewportX, // 左
		frameBuffer.windowH - frameBuffer.viewportY - frameBuffer.viewportH, // 上
		frameBuffer.viewportX + frameBuffer.viewportW, // 右
		frameBuffer.windowH - frameBuffer.viewportY // 下
		};
		InvalidateRect(GetLibgWindow(), &viewportRECT, FALSE); // FALSE注意!
	}
}

例子程序

  • main.c中输入如下代码:
#include <string.h>

#include "libgraphics/extgraph.h"
#include "libg3D/graphics3D.h"

#include <math.h>

void timerCallback(int timerID)
{
	static unsigned char color = 0;
	static char increase = 1;

	// 划出一个小的范围(视口)用于显示3D内容
	int windowW = libg3DGetWindowPixelWidth();
	int windowH = libg3DGetWindowPixelHeight();
	int viewportW = windowW / 4;
	int viewportH = windowH / 4;
	// 左下 四分之一,显示红色
	libg3DViewport(windowW / 4, windowH / 4, viewportW, viewportH);
	libg3DClearWithColor(color, 0, 0);
	// 右下 四分之一,显示绿色
	libg3DViewport(windowW / 2, windowH / 4, viewportW, viewportH);
	libg3DClearWithColor(0, color, 0);
	// 左上 四分之一,显示蓝色
	libg3DViewport(windowW / 4, windowH / 2, viewportW, viewportH);
	libg3DClearWithColor(0, 0, color);
	// 右上 四分之一,显示白色(灰色)
	libg3DViewport(windowW / 2, windowH / 2, viewportW, viewportH);
	libg3DClearWithColor(color, color, color);

	// 参数时变
	if (increase == 1) ++color;
	else color--;

	if (color == 255) increase = 0;
	else if (color == 0) increase = 1;
}

void Main()
{
	// 设置全屏
	double w = GetFullScreenWidth();
	double h = GetFullScreenHeight();
	SetWindowSize(w, h);

	InitGraphics();
	SetWindowTitle("3D Viewport LibGraphics");
	registerTimerEvent(timerCallback);
	// 15ms为周期,近似于60FPS
	startTimer(0, 15);
}
  • 最终结果如下: