系列教程(1):学习如何用C#编写一个软渲染引擎

原文地址:http://blogs.msdn.com/b/davrous/archive/2013/06/13/tutorial-series-learning-how-to-write-a-3d-soft-engine-from-scratch-in-c-typescript-or-javascript.aspx

声明:转载请注明出处!!!本人摘录其中C#的部分进行翻译,同时舍弃了其中一些无关紧要的话。另外,英语水平渣,见谅。

由于这位大神是用的win8和XAML,而本人是win7,没法进行实践。所以自己用普通的WinForm写了一个上传到了github。

地址:https://github.com/dx50075/SoftEngine.git

系列教程(1):学习如何用C#编写一个软渲染引擎_第1张图片

系列教程:学习如何用C#编写一个软渲染引擎

 

这个系列教程中,我将与大家分享我是如何学习并构建一个众所周知的 “3D soft engine”。 “Soft engine”意味着我们用一种仅仅使用CPU的古老方式来构建一个3D引擎。

 

那么为什么要构建一个“3D soft engine”?这是因为它真的能很好的帮助我们理解3D模型是如何在GPU上运作的。当我年轻的时候,我也梦想着能编写这样一个引擎,但是我觉得它对我来说太难了。但是最后,你会发现并没有那么难。你只不过需要一个人去帮助你用一种简单方式去理解相关的原则、概念和规则。

 

通过这个系列教程,你将学会如何投影一个3D坐标到与之关联的2D屏幕坐标,如何在不同的点之间画线,如何填充三角形,处理光照,材质等。本篇第一个教程将简单的展示Cube8个顶点以及如何将他们移动到虚拟3D世界。

 

这个系列教程的各部分如下:

1、编写camerameshdevice object的核心逻辑(本篇教程)

2、画线和三角形来得到一个线框渲染

3、加载BlenderJson格式导出的Meshes

4、光栅化三角形并使用Z-Buffer

4bBonus: using tips & parallelism to boost the performance 

5、Flat着色和Gouraud着色处理光照

6、运用纹理,背面消隐和WebGL

 

如果你跟着完成这个系列教程,你将知道如何构建属于自己的3D Soft Engine!你的引擎将拥有线框渲染,光栅化,gouraud着色,运用纹理等功能


阅读前置条件

我考虑如何写这个教程很久了。最后决定不由我来解释所有需要的知识概念等。网上有太多比我解释的更好的资源来解释那些重要的知识概念。但我仍然花了一些时间来收集一些供大家选择

 

World, View and Projection Matrix Unveiled http://web.archive.org/web/20131222170415/http:/robertokoci.com/world-view-projection-matrix-unveiled/

 

 Tutorial 3 : Matrices that will provide you an introduction to matrices, the model, view & projection matrices. 

http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/

 

Cameras on OpenGL ES 2.x – The ModelViewProjection Matrix : this one is really interesting also as it explains the story starting by how cameras and lenses work. 

http://db-in.com/blog/2011/04/cameras-on-opengl-es-2-x/

 

Transforms (Direct3D 9) 

http://msdn.microsoft.com/en-us/library/windows/desktop/bb206269(v=vs.85).aspx

 

 A brief introduction to 3D: an excellent PowerPoint slides deck ! Read at least up to slide 27. After that, it’s too linked to a technology talking to GPU (OpenGL or DirectX). 

http://inear.se/talk/a_brief_introduction_to_3d.pptx

 

OpenGL Transformation

http://www.songho.ca/opengl/gl_transform.html

 

 

阅读这些文章不关注相关一些技术(OpenGL或者DirectX)或三角形的概念,这些我们将会在后面见到。

通过阅读这些文章,你需要明白,有一系列的转换需要完成:

 

1、我们以1个中心位于物体本身中心的3D物体开始(原谅我的渣英语水平)

2、通过矩阵的平移、缩放、旋转操作移动物体到3D虚拟世界

3、通过一个相机在3D世界坐标中去看这个3D物体

4、最后投影到你的屏幕2D空间

 

 

所有这些魔法般的变换都是通过矩阵操作来完成。在进行这个系列教程之前,你真的应该至少有一点点熟悉这些概念。如果你不了解这些东西,你第一时间应该去了解。否则在你后面自己编写3D soft engine的时候,你很可能也会回过头来去读那些文章。这很正常,别担心!

学习3D最好的方式就是不断试验和犯错。

 

我们将不再花时间在矩阵如何运作上面了。好消息是其实你不必真的去了解矩阵。把它看作是一个做了正确操作的黑盒子吧。我也不是矩阵方面的大师,但我能自己编写3D soft engine。因此你们也能成功。

 

我们将使用一些库来完成我们的工作:SharpDX,一个供C#开发者使用的DirectX顶层封装库。

 

软件需求

 

1、1 - Windows 8 
2 - Visual Studio 2012 Express for Windows Store Apps. You can download it for free: http://msdn.microsoft.com/en-US/windows/apps/br211386

 

 

请创建一个名为“SoftEngine”的工程,通过NeGet添加“SharpDX core assembly

 

 

 

Back Buffer & Rendering loop

在一个3D引擎中,我们在每帧中渲染完整的场景,并希望保持一个最佳的60fps流畅画面。为了完成我们的渲染工作,我们需要back buffer。它可以视为一个和屏幕/窗口大小一致的二维数组。

数组的每一个单元对应着屏幕上的一个像素。

 

在我们的XAML Windows Store Apps中,我们将使用 byte[]来表示动态的 back bufferFor every frame being rendered in the animation loop (tick), this buffer will be affected to a WriteableBitmap acting as the source of a XAML image control that will be called the front buffer. For the rendering loop, we’re going to ask to the XAML rendering engine to call us for every frame it will generate. The registration is done thanks to this line of code: (怕翻译的不准,见谅)

CompositionTarget.Rendering += CompositionTarget_Rendering;

 

 

Camera & Mesh Objects

我们开始编码。首先,我们定义一些对象来描述相机和网格的细节。网格是一个很酷的用来描述3D物体的名字。

 

Camera将拥有2个属性:它在世界中的位置和它观察的目标。它们都是用3D坐标Vector3来描述。C#将用SharpDX.Vector3表示。

 

Mesh将含有顶点集合来组建3D对象,它在世界中的位置和它的旋转状态。

// Camera.cs & Mesh.cs
using SharpDX;namespace SoftEngine
{
    public class Camera
    {
        public Vector3 Position { get; set; }
        public Vector3 Target { get; set; }
    }
    public class Mesh
    {
        public string Name { get; set; }
        public Vector3[] Vertices { get; private set; }
        public Vector3 Position { get; set; }
        public Vector3 Rotation { get; set; }

        public Mesh(string name, int verticesCount)
        {
            Vertices = new Vector3[verticesCount];
            Name = name;
        }
    }
}



例如,如果你想通过Mesh对象来表示一个Cube,你需要创建与cube相关的8个顶点。下面是在Blendercube的坐标展示:

 

采用了左手坐标系。记住当你创建一个mesh,坐标系统原点在mesh中心。因此x=0,y=0,z=0在cube的中心。

 

代码如下:

var mesh = new Mesh("Cube", 8);
mesh.Vertices[0] = new Vector3(-1, 1, 1);
mesh.Vertices[1] = new Vector3(1, 1, 1);
mesh.Vertices[2] = new Vector3(-1, -1, 1);
mesh.Vertices[3] = new Vector3(-1, -1, -1);
mesh.Vertices[4] = new Vector3(-1, 1, -1);
mesh.Vertices[5] = new Vector3(1, 1, -1);
mesh.Vertices[6] = new Vector3(1, -1, 1);
mesh.Vertices[7] = new Vector3(1, -1, -1);


 

 

最重要的部分:the Device object(学过图形学的应该都知道这个东西吧)

既然我们已经有了基本知识并知道如何构建3d mesh,我们将需要最重要的部分:the device object。它是我们3D引擎的核心。

 

在渲染函数中,我们将构造基于camera的View matrix和projection matrix。

然后,我们遍历可访问的mesh并构造他们平移,旋转,缩放的world matrix。最后的变换矩阵如下:

var transformMatrix = worldMatrix * viewMatrix * projectionMatrix;

 

通过阅读之前的先决条件的资源,这是你绝对需要理解的概念。否则,你将很可能简单的复制/粘贴代码而不了解其原理。这对接下来的教程来讲并不算太大的问题,但再次声明,你最好先了解这些原理再开始编码。

 

通过这个变换矩阵,我们将通过mesh每个顶点的x,y,z坐标来得到2d坐标x,y。为了最后绘制在屏幕上,我们在PutPixel函数中添加一段逻辑来显示可见像素。

 

下面是Device object的代码。我会注释代码来帮助大家尽可能的理解。

using Windows.UI.Xaml.Media.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;
using SharpDX;
namespace SoftEngine
{
    public class Device
    {
        private byte[] backBuffer;
        private WriteableBitmap bmp;

        public Device(WriteableBitmap bmp)
        {
            this.bmp = bmp;
            // the back buffer size is equal to the number of pixels to draw
            // on screen (width*height) * 4 (R,G,B & Alpha values). 
            backBuffer = new byte[bmp.PixelWidth * bmp.PixelHeight * 4];
        }

        // This method is called to clear the back buffer with a specific color
        public void Clear(byte r, byte g, byte b, byte a) {
            for (var index = 0; index < backBuffer.Length; index += 4)
            {
                // BGRA is used by Windows instead by RGBA in HTML5
                backBuffer[index] = b;
                backBuffer[index + 1] = g;
                backBuffer[index + 2] = r;
                backBuffer[index + 3] = a;
            }
        }

        // Once everything is ready, we can flush the back buffer
        // into the front buffer. 
        public void Present()
        {
            using (var stream = bmp.PixelBuffer.AsStream())
            {
                // writing our byte[] back buffer into our WriteableBitmap stream
                stream.Write(backBuffer, 0, backBuffer.Length);
            }
            // request a redraw of the entire bitmap
            bmp.Invalidate();
        }

        // Called to put a pixel on screen at a specific X,Y coordinates
        public void PutPixel(int x, int y, Color4 color)
        {
            // As we have a 1-D Array for our back buffer
            // we need to know the equivalent cell in 1-D based
            // on the 2D coordinates on screen
            var index = (x + y * bmp.PixelWidth) * 4;

            backBuffer[index] = (byte)(color.Blue * 255);
            backBuffer[index + 1] = (byte)(color.Green * 255);
            backBuffer[index + 2] = (byte)(color.Red * 255);
            backBuffer[index + 3] = (byte)(color.Alpha * 255);
        }

        // Project takes some 3D coordinates and transform them
        // in 2D coordinates using the transformation matrix
        public Vector2 Project(Vector3 coord, Matrix transMat)
        {
            // transforming the coordinates
            var point = Vector3.TransformCoordinate(coord, transMat);
            // The transformed coordinates will be based on coordinate system
            // starting on the center of the screen. But drawing on screen normally starts
            // from top left. We then need to transform them again to have x:0, y:0 on top left.
            var x = point.X * bmp.PixelWidth + bmp.PixelWidth / 2.0f;
            var y = -point.Y * bmp.PixelHeight + bmp.PixelHeight / 2.0f;
            return (new Vector2(x, y));
        }

        // DrawPoint calls PutPixel but does the clipping operation before
        public void DrawPoint(Vector2 point)
        {
            // Clipping what's visible on screen
            if (point.X >= 0 && point.Y >= 0 && point.X < bmp.PixelWidth && point.Y < bmp.PixelHeight)
            {
                // Drawing a yellow point
                PutPixel((int)point.X, (int)point.Y, new Color4(1.0f, 1.0f, 0.0f, 1.0f));
            }
        }

        // The main method of the engine that re-compute each vertex projection
        // during each frame
        public void Render(Camera camera, params Mesh[] meshes)
        {
            // To understand this part, please read the prerequisites resources
            var viewMatrix = Matrix.LookAtLH(camera.Position, camera.Target, Vector3.UnitY);
            var projectionMatrix = Matrix.PerspectiveFovRH(0.78f, 
                                                           (float)bmp.PixelWidth / bmp.PixelHeight, 
                                                           0.01f, 1.0f);

            foreach (Mesh mesh in meshes) 
            {
                // Beware to apply rotation before translation 
                var worldMatrix = Matrix.RotationYawPitchRoll(mesh.Rotation.Y, 
                                                              mesh.Rotation.X, mesh.Rotation.Z) * 
                                  Matrix.Translation(mesh.Position);

                var transformMatrix = worldMatrix * viewMatrix * projectionMatrix;

                foreach (var vertex in mesh.Vertices)
                {
                    // First, we project the 3D coordinates into the 2D space
                    var point = Project(vertex, transformMatrix);
                    // Then we can draw on screen
                    DrawPoint(point);
                }
            }
        }
    }
}


 

 

整合所有代码

我们最后需要创建一个mesh。创建一个camera并看向mesh。初始化Device object

 

一旦完成,我们将启动动画/渲染循环。在最佳情况下,循环没16ms调用一次(60fps)。在每次循环中,我们将运行如下逻辑:

 

1、清除屏幕所有相关的像素并用黑色填充(Clear() function)

2、更新meshes 各顶点的位置和旋转

3、通过必要的矩阵操作渲染到back buffer(Render() function)

4、Flush back buffer的数据到front buffer并显示到屏幕 (Present() function)

 

private Device device;
Mesh mesh = new Mesh("Cube", 8);
Camera mera = new Camera();private void Page_Loaded(object sender, RoutedEventArgs e)
{
    // Choose the back buffer resolution here
    WriteableBitmap bmp = new WriteableBitmap(640, 480);

    device = new Device(bmp);

    // Our XAML Image control
    frontBuffer.Source = bmp;

    mesh.Vertices[0] = new Vector3(-1, 1, 1);
    mesh.Vertices[1] = new Vector3(1, 1, 1);
    mesh.Vertices[2] = new Vector3(-1, -1, 1);
    mesh.Vertices[3] = new Vector3(-1, -1, -1);
    mesh.Vertices[4] = new Vector3(-1, 1, -1);
    mesh.Vertices[5] = new Vector3(1, 1, -1);
    mesh.Vertices[6] = new Vector3(1, -1, 1);
    mesh.Vertices[7] = new Vector3(1, -1, -1);

    mera.Position = new Vector3(0, 0, 10.0f);
    mera.Target = Vector3.Zero;

    // Registering to the XAML rendering loop
    CompositionTarget.Rendering += CompositionTarget_Rendering;
}// Rendering loop handlervoid CompositionTarget_Rendering(object sender, object e)
{
    device.Clear(0, 0, 0, 255);

    // rotating slightly the cube during each frame rendered
    mesh.Rotation = new Vector3(mesh.Rotation.X + 0.01f, mesh.Rotation.Y + 0.01f, mesh.Rotation.Z);

    // Doing the various matrix operations
    device.Render(mera, mesh);
    // Flushing the back buffer into the front buffer
    device.Present();
}


 

本教程示意图:

 系列教程(1):学习如何用C#编写一个软渲染引擎_第2张图片

 

在下个教程中,我们将学习如何在不同顶点之间画线,下个教程示意图:

 

 系列教程(1):学习如何用C#编写一个软渲染引擎_第3张图片

 

-----------------------------------------------以上是翻译-------------------------------------------------------------------------------

 

SharpDX在这个教程里面,仅仅是充当数学库的角色,我们完全可以不用它,感兴趣的同学可以自己实现向量,矩阵的相关操作。强烈建议自己实现。

WinForm实现的版本:

系列教程(1):学习如何用C#编写一个软渲染引擎_第4张图片

 

后续如果时间允许,会继续翻译并完善这个引擎,可能也会用C++来实现。

你可能感兴趣的:(系列教程(1):学习如何用C#编写一个软渲染引擎)