首页 > 代码库 > 使用Windows GDI 做一个3D”软引擎“-Part1

使用Windows GDI 做一个3D”软引擎“-Part1

前:

  最近几天一个很虎比的教程吸引了我的视线,原作者使用c# / JavaScript逐步实现了一个基本的3D软引擎。

我不懂上面提到的语言,所以,准备用我熟悉的C++和Win32实现重造这个轮子。:)

注意:

  • 这不是一篇关于DirectX / OpenGL (GPU)的文章,本系列文章将实现一个软件(CPU)驱动的“DirectX”,很有趣吧,啊哈。
  • 本文假设读者有一定的计算机图形学的基础,使用OpenGL / DirectX 写过程序。
  • 本文假设读者有一定的Win32基础(不是MFC),最起码能写出一个空白窗口的程序。
  • 有人可能会问,现在的计算机都有显卡,问什么还要写软引擎呢?的确,对于实际的应用来说,这东西确实没啥用,写这个东西只是出于好玩,另外,它也能帮助你真正的理解3D流水线,当你再去学DirectX和OpenGL的时候,也会变得更加简单。

正文:

  本文将实现下面这个小玩意(仅仅使用Win32中的SetPixel()函数):

 源码下载:链接: http://pan.baidu.com/s/1kTidLYn 密码: 5qul

(注:源码中使用了一点C++11的特性,请使用支持C++11的编译器编译)

 

 

1.创建窗口.

 

 要绘制东西,首先要有一个窗口,为此我们设计一个BasicGame类:

BasicGame.h

 1 #ifndef _BASIC_GAME_ 2 #define _BASIC_GAME_ 3  4 #include <windows.h> 5 #include <string> 6  7 class BasicGame 8 { 9 public:10     virtual bool init(){return true;}11     virtual void render(){}12     virtual void quit(){}13     virtual void update(float deltaTime){};14 15     BasicGame();16     virtual ~BasicGame(){}17 18     bool create(HINSTANCE instance, int cmdShow);19 20     std::string getCaption() {return caption_;}21     int    getWidth() {return width_;}22     int getHeight() {return height_;}23     HWND getHwnd(){return hwnd_;}24 25     void setCaption(std::string caption){caption_ = caption;}26     void setWidth(int width) {width_ = width;}27     void setHeight(int height) {height_ = height;}28 29 private:30     WORD registerClass(HINSTANCE instance);31     bool windowInit(HINSTANCE instance, int cmdShow);32 33     static LRESULT CALLBACK WndProc(HWND wnd, UINT msg, WPARAM wParam, LPARAM lParam);34 35 protected:36     std::string caption_;37     int height_;38     int width_;39     HWND hwnd_;40 };41 42 #endif //_BASIC_GAME_

BasicGame.cpp

 1 #include "BasicGame.h" 2  3 BasicGame::BasicGame() 4 { 5     width_ = 800; 6     height_ = 600; 7 } 8  9 bool BasicGame::create(HINSTANCE instance, int cmdShow)10 {11     registerClass(instance);12 13     if(!windowInit(instance, cmdShow))14     {15         return false;16     }17 18     return true;19 }20 21 WORD BasicGame::registerClass(HINSTANCE instance)22 {23     WNDCLASSEX wcex;24 25     wcex.cbSize = sizeof(WNDCLASSEX); 26 27     wcex.style            = CS_HREDRAW | CS_VREDRAW;28     wcex.lpfnWndProc    = (WNDPROC)BasicGame::WndProc;29     wcex.cbClsExtra        = 0;30     wcex.cbWndExtra        = 0;31     wcex.hInstance        = instance;32     wcex.hIcon            = NULL;33     wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);34     wcex.hbrBackground    = (HBRUSH)(COLOR_WINDOW+1);35     wcex.lpszMenuName    = NULL;36     wcex.lpszClassName    = "BasicGame";37     wcex.hIconSm        = NULL;38 39     return RegisterClassEx(&wcex);40 }41 42 bool BasicGame::windowInit(HINSTANCE instance, int cmdShow)43 {44     hwnd_ = CreateWindow(45         "BasicGame", 46         caption_.c_str(), 47         WS_OVERLAPPEDWINDOW,48         CW_USEDEFAULT, 49         0, 50         CW_USEDEFAULT,51         0, 52         NULL, 53         NULL, 54         instance, 55         NULL);56 57     if (!hwnd_)58         return false;59 60     MoveWindow(hwnd_,0,0,width_,height_,true);61     ShowWindow(hwnd_, cmdShow);62     UpdateWindow(hwnd_);63 64     return true;65 }66 67 LRESULT CALLBACK  BasicGame::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)68 {69     switch (msg) 70     {71     case WM_DESTROY:72         PostQuitMessage(0);73         break;74     }75     return DefWindowProc(hWnd, msg, wParam, lParam);76 }

BasicGame简单的对窗口创建进行了封装,其中

1 virtual bool init(){return true;}2 virtual void render(){}3 virtual void quit(){}4 virtual void update(float deltaTime){};
View Code

是为了后面实现游戏循环预留的接口,我们要创建窗口,只需要继承BasicGame就可以了。

 

2.游戏循环。

利用上面的类,我们便可以这样写WinMain()函数:

 

 1 #include "BasicGame.h" 2 #include <memory> 3  4 int WINAPI WinMain(HINSTANCE hInstance, 5                    HINSTANCE hPrevInstance, 6                    LPSTR lpCmdLine, 7                    int nCmdShow) 8 { 9     std::unique_ptr<BasicGame> game(new BasicGame);10 11     game->setCaption("Hello,World");12     game->setWidth(800);13     game->setHeight(600);14 15     if(!game->create(hInstance, nCmdShow))16         return -1;17 18     game->init();19 20     float tNow = 0.f;21     float tPre = static_cast<float>(GetTickCount()) / 1000;22 23     float timeSinceLastUpdate = 0.f;24 25     float timePerPrame = 1.f / 60.f;26 27     MSG msg;28     for(;;)29     {30         if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))31         {32             if(msg.message == WM_QUIT) break;33             TranslateMessage(&msg);34             DispatchMessage(&msg);35         }36         else37         {38             tNow = static_cast<float>(GetTickCount()) / 1000;39 40             timeSinceLastUpdate += (tNow - tPre);41             while(timeSinceLastUpdate > timePerPrame)42             {43                 timeSinceLastUpdate -= timePerPrame;44                 game->update(timePerPrame);45             }46             tPre = tNow;47             game->render();48         }49     }50     game->quit();51     52     return static_cast<int>(msg.wParam);53 };

 

  • 如果我们在一个继承自BasicGame的新类NewGame,只需要将

 

1 std::unique_ptr<BasicGame> game(new BasicGame);

换成

1 std::unique_ptr<BasicGame> game(new NewGame);

然后再NewGame类中实现init(),render(),quit(),update(float)四个函数即可。

  • 重点在于20-50行到底做了什么? 如果你做过游戏,那么你一定不会陌生:-)

GetTickCount()函数返回系统启动到现在经过的时间(毫秒),能存储的最大值约为49.71天,超过这个期限,就会归零,我们认为是无穷大即可。

我们用TimeSinceLastUpdate变量表示自从上一次执行update函数所经过的时间。

每次执行update函数,我们便减去一个TimePerFrame(每帧所耗费的时间)。

上面的示例中,TimePerFrame = 1.0f / 60.0f ,则update()函数每秒将被执行60次,无论你的电脑性能如何。这个特性对于视频游戏来说是非常非常重要的。

 

3.数学基础。

 本文不是讨论3D数学的,所以直接使用了开源的数学库(GLM),关于这个库的用法请看这篇博文。

4.Camera & Mesh。

现在可以开始我们的软引擎的编码了。:)

首先,我们需要定义Camera和Mesh两个结构体,其中Mesh用来表示3D空间中的一个物体。

代码如下:

 

 1 #define GLM_FORCE_RADIANS 2 #include <glm/glm.hpp> 3 #include <glm/gtc/matrix_transform.hpp> 4 #include <string> 5  6 namespace SoftEngine 7 { 8  9     struct Camera10     {11         glm::vec3 position_;12         glm::vec3 target_;13     };14 15     struct Mesh16     {17         std::string name_;18         glm::vec3 position_;19         float rotation_;20         glm::vec4 *vertices_;21         int verticesCount_;22 23         Mesh(std::string name, int verticesCount)24         {25             verticesCount_ = verticesCount;26             vertices_ = new glm::vec4 [verticesCount_];27 28             name_ = name;29         }30         ~Mesh()31         {32             delete vertices_;33         }34     };35 }

 

 举例来说,如果你用Mesh来描述一个立方体:

通常只需要这样做:

 1 SoftEngine::Mesh mesh("Cube", 8); 2  3 mesh.vertices_[0] = glm::vec4(-1.f, 1.f, 1.f, 1.f); 4 mesh.vertices_[1] = glm::vec4( 1.f, 1.f, 1.f, 1.f); 5 mesh.vertices_[2] = glm::vec4(-1.f,-1.f, 1.f, 1.f); 6 mesh.vertices_[3] = glm::vec4( 1.f,-1.f, 1.f, 1.f); 7 mesh.vertices_[4] = glm::vec4(-1.f, 1.f,-1.f, 1.f); 8 mesh.vertices_[5] = glm::vec4( 1.f, 1.f,-1.f, 1.f); 9 mesh.vertices_[6] = glm::vec4( 1.f,-1.f,-1.f, 1.f);10 mesh.vertices_[7] = glm::vec4(-1.f,-1.f,-1.f, 1.f);

5.Device.

有了Mesh,如何显示它呢?

我们知道,屏幕是二维的,而Mesh中的点是3维的,所以,如何把3维世界中的点画到二维的世界中是关键所在。

我们创建Device类,完成这项工作。

由第三部分提到的那篇博文,我们知道,最重要的部分在于下面的等式:

1 auto transformMatrix = projectionMatrix * viewMatrix * worldMatrix;

我们的Device类如下所示:

 1 namespace SoftEngine 2 { 3     class Device 4     { 5     public: 6  7         Device(HWND hWnd); 8  9         ~Device();10 11         void present();12 13         void render(Camera camera, Mesh *meshes, int length);14 15     private:16         17         glm::vec2 TransformCoordinates(glm::vec4 vector, glm::mat4 transformation);18 19         void putPixel(int x, int y, COLORREF color);20         21         void drawPoint(glm::vec2 point);22         23         glm::vec2 project(glm::vec4 coord, glm::mat4 transMat);24     private:25 26         byte *backBuffer_;27         HWND hWnd_;28         HDC hDc_;29         HDC mDc_;30         float width_;31         float height_;32         HBITMAP bmp_;33 34     };35 }

类的定义:

 1 #include "SoftEngine.h" 2 #include <cmath> 3  4 namespace SoftEngine 5 { 6  7     Device::Device(HWND hWnd) 8     { 9         hWnd_ = hWnd;10 11         hDc_ = GetDC(hWnd_);12         mDc_ = ::CreateCompatibleDC(hDc_);13 14         RECT rect = {0, 0, 0, 0};15         GetClientRect(hWnd_, &rect);16         width_ = static_cast<float>(rect.right - rect.left);17         height_ = static_cast<float>(rect.bottom - rect.top);18 19         DeleteObject(bmp_);20         bmp_ = ::CreateCompatibleBitmap(hDc_, width_, height_);21         ::SelectObject(mDc_, bmp_);22     }23 24     Device::~Device()25     {26         DeleteObject(hDc_);27         DeleteObject(mDc_);28     }29 30     void Device::present()31     {32         BitBlt(hDc_, 0, 0, width_, height_, mDc_, 0, 0, SRCCOPY);33         DeleteObject(bmp_);34         bmp_ = ::CreateCompatibleBitmap(hDc_, width_, height_);35         ::SelectObject(mDc_, bmp_);36     }37 38     void Device::putPixel(int x, int y, COLORREF color)39     {40         SetPixel(mDc_, x, y, color);41     }42 43     glm::vec2 Device::TransformCoordinates(glm::vec4 vector, glm::mat4 transformation)44     {45         auto x = (vector.x * transformation[0][0]) + (vector.y * transformation[1][0]) + (vector.z * transformation[2][0]) + transformation[3][0];46         auto y = (vector.x * transformation[0][1]) + (vector.y * transformation[1][1]) + (vector.z * transformation[2][1]) + transformation[3][1];47         //    auto z = (vector.x * transformation[0][2]) + (vector.y * transformation[1][2]) + (vector.z * transformation[2][2]) + transformation[3][2];48         auto w = (vector.x * transformation[0][3]) + (vector.y * transformation[1][3]) + (vector.z * transformation[2][3]) + transformation[3][3];49         return glm::vec2(x/ w, y / w);50     }51 52     glm::vec2 Device::project(glm::vec4 coord, glm::mat4 transMat)53     {54         glm::vec2 point = TransformCoordinates(coord, transMat);55 56         auto x =   point.x * width_  + width_  / 2.0f;57         auto y = - point.y * height_ + height_ / 2.0f;58 59         return glm::vec2(x, y);60     }61 62     void Device::drawPoint(glm::vec2 point)63     {64         //Clipping.65         if(point.x >= 0 && point.y >= 0 && point.x < width_ && point.y < height_)66         {67             putPixel(point.x, point.y, RGB(255, 255, 0));68         }69     }70 71     void Device::render(Camera camera, Mesh *meshes, int length)72     {73         auto viewMatrix = glm::lookAt(camera.position_, camera.target_,74             glm::vec3(0.0f, 1.0f, 0.0f));75 76         auto temp = width_ / height_;77         auto projectionMatrix = glm::perspective(45.0f, temp, 0.01f, 10.0f);78 79         for(int i = 0; i < length; i++)80         {81             auto worldMatrix = glm::rotate(glm::mat4(1.0f), meshes[i].rotation_, meshes[i].position_);82 83             auto transformMatrix = projectionMatrix * viewMatrix * worldMatrix;84 85             auto &curMesh = meshes[i];86 87             for(int j = 0; j < curMesh.verticesCount_; j++)88             {89                 auto point = project(curMesh.vertices_[j], transformMatrix);90                 drawPoint(point);91             }92         }93     }94 }

为了显示物体,我们创建自己的Game类:

 1 #ifndef _GAME_ 2 #define _GAME_ 3  4 #include "BasicGame.h" 5 #include "SoftEngine.h" 6 using namespace SoftEngine; 7  8 class Game : public BasicGame 9 {10 public:11     Game();12     virtual bool init();13     virtual void render();14     virtual void quit();15     virtual void update(float deltaTime);16 17 private:18     Device device_;19     Mesh mesh_;20     Camera camera_;21 };22 23 #endif //_GAME_

类的定义:

 1 #include "Game.h" 2  3 Game::Game() 4     :device_() 5     ,mesh_("Cube", 8) 6     ,camera_() 7 {} 8  9 bool Game::init()10 {11     device_.init(getHwnd());12 13     mesh_.vertices_[0] = glm::vec4(-1.f, 1.f, 1.f, 1.f);14     mesh_.vertices_[1] = glm::vec4( 1.f, 1.f, 1.f, 1.f);15     mesh_.vertices_[2] = glm::vec4(-1.f,-1.f, 1.f, 1.f);16     mesh_.vertices_[3] = glm::vec4( 1.f,-1.f, 1.f, 1.f);17     mesh_.vertices_[4] = glm::vec4(-1.f, 1.f,-1.f, 1.f);18     mesh_.vertices_[5] = glm::vec4( 1.f, 1.f,-1.f, 1.f);19     mesh_.vertices_[6] = glm::vec4( 1.f,-1.f,-1.f, 1.f);20     mesh_.vertices_[7] = glm::vec4(-1.f,-1.f,-1.f, 1.f);21     22     mesh_.position_ = glm::vec3(0.5f, 1.0f, 0.0f);23 24     mesh_.rotation_ = 0.f;25 26     camera_.position_ =  glm::vec3(0, 0, 10.f);27     camera_.target_ = glm::vec3(0.f);28 29     return true;30 }31 32 void Game::render()33 {34     device_.render(camera_, &mesh_, 1);35 36     device_.present();37 }38 39 void Game::quit()40 {41 42 }43 44 void Game::update(float deltaTime)45 {46     mesh_.rotation_ += 0.4f * deltaTime;47     if(mesh_.rotation_ > 360.f)48         mesh_.rotation_ = 0.f;49 }

好了,现在运行程序,可以看到立方体的8个顶点在屏幕中央开心的旋转 X)

我们现在要做的是在8个点之间连上线,使其看起来更舒服一些。

那么怎么画线呢?请看这里。

我们编写画线函数:

 1 void Device::drawBresenhamLine(glm::vec2 point0, glm::vec2 point1) 2 { 3     int x0 = (int)point0.x; 4     int y0 = (int)point0.y; 5     int x1 = (int)point1.x; 6     int y1 = (int)point1.y; 7  8     auto dx = abs(x1 - x0); 9     auto dy = abs(y1 - y0);10     auto sx = (x0 < x1) ? 1 : -1;11     auto sy = (y0 < y1) ? 1 : -1;12     auto err = dx - dy;13 14     while (true)15     {16         drawPoint(glm::vec2(x0, y0));17 18         if ((x0 == x1) && (y0 == y1)) break;19         auto e2 = 2 * err;20         if (e2 > -dy) { err -= dy; x0 += sx; }21         if (e2 < dx) { err += dx; y0 += sy; }22     }23 }

有3D基础的人都知道,3D里面最基本的元素就是三角形,如果能画三角形,我们就能画任何物体。

我们称一个三角形为一个“Face”,下面编写我们的Face类:

 1 struct Face 2 { 3     int a_; 4     int b_; 5     int c_; 6  7     void set(int a, int b, int c) 8     { 9         a_ = a;10         b_ = b;11         c_ = c;12     }13 };

改写Mesh类:

 1 struct Mesh 2 { 3     std::string name_; 4     glm::vec3 position_; 5     float rotation_; 6     glm::vec4 *vertices_; 7     int verticesCount_; 8     Face *faces_; 9     int facesCount_;10 11     Mesh(std::string name, int verticesCount, int facesCount)12     {13         verticesCount_ = verticesCount;14         vertices_ = new glm::vec4 [verticesCount_];15         facesCount_ = facesCount;16         faces_ = new Face [facesCount_];17 18         name_ = name;19     }20     ~Mesh()21     {22         delete vertices_;23         delete faces_;24     }25 };

现在,要显示一个四边形,我们只需要如下代码:

1 mesh_.vertices_[0] = glm::vec4(-1.f, 1.f, 1.f, 1.f);2 mesh_.vertices_[1] = glm::vec4( 1.f, 1.f, 1.f, 1.f);3 mesh_.vertices_[2] = glm::vec4(-1.f,-1.f, 1.f, 1.f);4 mesh_.vertices_[3] = glm::vec4( 1.f,-1.f, 1.f, 1.f);5 6 mesh_.faces_[0 ].set(0, 1, 2);7 mesh_.faces_[1 ].set(1, 2, 3);

下面我们为此编写相应的代码:

 1 auto &curMesh = meshes[i]; 2  3 for(int j = 0; j < curMesh.facesCount_; j++) 4 { 5     auto &face = curMesh.faces_[j]; 6  7     auto vertexA = curMesh.vertices_[face.a_]; 8     auto vertexB = curMesh.vertices_[face.b_]; 9     auto vertexC = curMesh.vertices_[face.c_];10 11     auto pixelA = project(vertexA, transformMatrix);12     auto pixelB = project(vertexB, transformMatrix);13     auto pixelC = project(vertexC, transformMatrix);14 15     drawBresenhamLine(pixelA, pixelB);16     drawBresenhamLine(pixelB, pixelC);17     drawBresenhamLine(pixelC, pixelA);18 }

现在我们只需要定义立方体的8个顶点和12个面,立方体就能正确地显示了:

 1 mesh_.vertices_[0] = glm::vec4(-1.f, 1.f, 1.f, 1.f); 2 mesh_.vertices_[1] = glm::vec4( 1.f, 1.f, 1.f, 1.f); 3 mesh_.vertices_[2] = glm::vec4(-1.f,-1.f, 1.f, 1.f); 4 mesh_.vertices_[3] = glm::vec4( 1.f,-1.f, 1.f, 1.f); 5 mesh_.vertices_[4] = glm::vec4(-1.f, 1.f,-1.f, 1.f); 6 mesh_.vertices_[5] = glm::vec4( 1.f, 1.f,-1.f, 1.f); 7 mesh_.vertices_[6] = glm::vec4( 1.f,-1.f,-1.f, 1.f); 8 mesh_.vertices_[7] = glm::vec4(-1.f,-1.f,-1.f, 1.f); 9     10 mesh_.faces_[0 ].set(0, 1, 2);11 mesh_.faces_[1 ].set(1, 2, 3);12 mesh_.faces_[2 ].set(1, 3, 6);13 mesh_.faces_[3 ].set(1, 5, 6);14 mesh_.faces_[4 ].set(0, 1, 4);15 mesh_.faces_[5 ].set(1, 4, 5);16     17 mesh_.faces_[6 ].set(2, 3, 7);18 mesh_.faces_[7 ].set(3, 6, 7);19 mesh_.faces_[8 ].set(0, 2, 7);20 mesh_.faces_[9 ].set(0, 4, 7);21 mesh_.faces_[10].set(4, 5, 6);22 mesh_.faces_[11].set(4, 6, 7);

现在我们的程序看起来象下面这样:

很神奇对吗?我们只用了一个画点的函数,就画出了这么好玩的东西,Awesome!

 

ksco

2014.6.24

转载请注明出处。