返回介绍

最重要的部分:设备对象

发布于 2025-02-18 12:46:45 字数 11272 浏览 0 评论 0 收藏 0

现在,我们有了基本对象,我们知道如何构建一个三维网格。但是我们还缺少一个最重要的部分:设备对象。这是我们的 3D 引擎核心部分。

在引擎渲染的函数内,我们将建立投影矩阵,并根据我们预先定义过摄像机获得视图矩阵。然后我们遍历每个网格提供基于目前的旋转和平移值来构建世界矩阵。

最后,得到这三个矩阵后,我们就可以这样得到最终的变换矩阵:

var transformMatrix = worldMatrix * viewMatrix * projectionMatrix;

你绝对需要通过阅读前面的“阅读前提条件”来理解这个概念。否则,你可能会简单的 Copy/Paste 代码,而无须了解有关神奇的实现。这对于之后的学习并没有太大的影响,但是理解它你能更好的进行编码。

使用该变换矩阵,我们将项目中每个网格的每个顶点从 X, Y, Z 坐标转换到 2D 世界中的 X, Y 坐标,最终在屏幕上绘制。

我们增加一小段逻辑,只通过 PutPixel(方法/函数) 进行绘制显示。

这里有各种版本的设备对象。我增加了些注释,以帮助您更多的理解它。【译者注:还不是要一个一个翻译!】

:微软 Windows 使用 BGRA 颜色空间(蓝色,绿色,红色,阿尔法),而 Html5 的画布使用的是 RGBA 颜色空间(红,绿,蓝,阿尔法)。

这就是你就发现为什么 C#和 Html5 代码有一些小差别的原因。

【译者注:C#代码】

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; 
      // 后台缓冲区大小值是要绘制的像素
      // 屏幕(width*height) * 4 (R,G,B & Alpha 值)
      backBuffer = new byte[bmp.PixelWidth * bmp.PixelHeight * 4];
    }

    // 清除后台缓冲区为指定颜色
    public void Clear(byte r, byte g, byte b, byte a)
    {
      for (var index = 0; index < backBuffer.Length; index += 4)
      {
        // Windows 使用 BGRA,而不是 Html5 中使用的 RGBA
        backBuffer[index] = b;
        backBuffer[index + 1] = g;
        backBuffer[index + 2] = r;
        backBuffer[index + 3] = a;
      }
    }

    // 当一切准备就绪时,我们就可以
    // 刷新后台缓冲区到前台缓冲区
    public void Present()
    {
      using (var stream = bmp.PixelBuffer.AsStream())
      {
        // 将我们的 byte[]后台缓冲区写入到 WriteableBitmap 流
        stream.Write(backBuffer, 0, backBuffer.Length);
      }
      // 请求将整个位图重绘
      bmp.Invalidate();
    }

    // 调用此方法把一个像素绘制到指定的 X, Y 坐标上
    public void PutPixel(int x, int y, Color4 color)
    {
      // 我们的后台缓冲区是一维数组
      // 这里我们简单计算,将 X 和 Y 对应到此一维数组中
      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);
    }

    // 将三维坐标和变换矩阵转换成二维坐标
    public Vector2 Project(Vector3 coord, Matrix transMat)
    {
      // 进行坐标变换
      var point = Vector3.TransformCoordinate(coord, transMat);
      // 变换后的坐标起始点是坐标系的中心点
      // 但是,在屏幕上,我们以左上角为起始点
      // 我们需要重新计算使他们的起始点变成左上角
      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));
    }

    // 如果二维坐标在可视范围内则绘制
    public void DrawPoint(Vector2 point)
    {
      // 判断是否在屏幕内
      if (point.X >= 0 && point.Y >= 0 && point.X < bmp.PixelWidth && point.Y < bmp.PixelHeight)
      {
        // 绘制一个黄色点
        PutPixel((int)point.X, (int)point.Y, new Color4(1.0f, 1.0f, 0.0f, 1.0f));
      }
    }

    // 主循环体,每一帧,引擎都要计算顶点投射
    public void Render(Camera camera, params Mesh[] meshes)
    {
      // 要理解这个部分,请阅读“阅读前提条件”
      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)
      {
        // 请注意,在平移前要先旋转
        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)
        {
          // 首先,我们将三维空间转换为二维空间
          var point = Project(vertex, transformMatrix);
          // 然后我们就可以在屏幕画出点
          DrawPoint(point);
        }
      }
    }
  }
}

【译者注: TypeScript 代码】

///<reference path="babylon.math.ts"/>

module SoftEngine {
  export class Device {
    // 后台缓冲区大小值是要绘制的像素
    // 屏幕(width*height) * 4 (R,G,B & Alpha 值)
    private backbuffer: ImageData;
    private workingCanvas: HTMLCanvasElement;
    private workingContext: CanvasRenderingContext2D;
    private workingWidth: number;
    private workingHeight: number;
    // 等于 backbuffer.data
    private backbufferdata;

    constructor(canvas: HTMLCanvasElement) {
      this.workingCanvas = canvas;
      this.workingWidth = canvas.width;
      this.workingHeight = canvas.height;
      this.workingContext = this.workingCanvas.getContext("2d");
    }

    // 清除后台缓冲区为指定颜色
    public clear(): void {
      // 默认清除为黑色
      this.workingContext.clearRect(0, 0, this.workingWidth, this.workingHeight);
      // 一旦用黑色像素清除我们要找回相关图像数据,以清楚后台缓冲区
      this.backbuffer = this.workingContext.getImageData(0, 0, this.workingWidth, this.workingHeight);
    }

    // 当一切就绪后将后台缓冲区刷新到前台缓冲区
    public present(): void {
      this.workingContext.putImageData(this.backbuffer, 0, 0);
    }

    // 调用此方法把一个像素绘制到指定的 X, Y 坐标上
    public putPixel(x: number, y: number, color: BABYLON.Color4): void {
      this.backbufferdata = this.backbuffer.data;
      // 我们的后台缓冲区是一维数组
      // 这里我们简单计算,将 X 和 Y 对应到此一维数组中
      var index: number = ((x >> 0) + (y >> 0) * this.workingWidth) * 4;

      // 在 Html5 canvas 中使用 RGBA 颜色空间
      this.backbufferdata[index] = color.r * 255;
      this.backbufferdata[index + 1] = color.g * 255;
      this.backbufferdata[index + 2] = color.b * 255;
      this.backbufferdata[index + 3] = color.a * 255;
    }

    // 将三维坐标和变换矩阵转换成二维坐标
    public project(coord: BABYLON.Vector3, transMat: BABYLON.Matrix): BABYLON.Vector2 {
      // 进行坐标变换
      var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
      // 变换后的坐标起始点是坐标系的中心点
      // 但是,在屏幕上,我们以左上角为起始点
      // 我们需要重新计算使他们的起始点变成左上角
      var x = point.x * this.workingWidth + this.workingWidth / 2.0 >> 0;
      var y = -point.y * this.workingHeight + this.workingHeight / 2.0 >> 0;
      return (new BABYLON.Vector2(x, y));
    }

    // 如果二维坐标在可视范围内则绘制
    public drawPoint(point: BABYLON.Vector2): void {
      // 判断是否在屏幕内
      if (point.x >= 0 && point.y >= 0 && point.x < this.workingWidth
        && point.y < this.workingHeight) {
        // 绘制一个黄色点
        this.putPixel(point.x, point.y, new BABYLON.Color4(1, 1, 0, 1));
      }
    }

    // 主循环体,每一帧,引擎都要计算顶点投射
    public render(camera: Camera, meshes: Mesh[]): void {
      // 要理解这个部分,请阅读“阅读前提条件”
      var viewMatrix = BABYLON.Matrix.LookAtLH(camera.Position, camera.Target, BABYLON.Vector3.Up());
      var projectionMatrix = BABYLON.Matrix.PerspectiveFovLH(0.78,
        this.workingWidth / this.workingHeight, 0.01, 1.0);

      for (var index = 0; index < meshes.length; index++) {
        // 缓存当前网格对象
        var cMesh = meshes[index];
        // 请注意,在平移前要先旋转
        var worldMatrix = BABYLON.Matrix.RotationYawPitchRoll(
          cMesh.Rotation.y, cMesh.Rotation.x, cMesh.Rotation.z)
          .multiply(BABYLON.Matrix.Translation(
            cMesh.Position.x, cMesh.Position.y, cMesh.Position.z));

        var transformMatrix = worldMatrix.multiply(viewMatrix).multiply(projectionMatrix);

        for (var indexVertices = 0; indexVertices < cMesh.Vertices.length; indexVertices++) {
          // 首先,我们将三维空间转换为二维空间
          var projectedPoint = this.project(cMesh.Vertices[indexVertices], transformMatrix);
          // 然后我们就可以在屏幕画出点
          this.drawPoint(projectedPoint);
        }
      }
    }
  }
}

【译者注:JavaScript 代码】

var SoftEngine;
(function (SoftEngine) {
  var Device = (function () {
    function Device(canvas) {
      // 后台缓冲区大小值是要绘制的像素
      // 屏幕(width*height) * 4 (R,G,B & Alpha 值)
      this.workingCanvas = canvas;
      this.workingWidth = canvas.width;
      this.workingHeight = canvas.height;
      this.workingContext = this.workingCanvas.getContext("2d");
    }

    // 清除后台缓冲区为指定颜色
    Device.prototype.clear = function () {
      // 默认清除为黑色
      this.workingContext.clearRect(0, 0, this.workingWidth, this.workingHeight);
      // 一旦用黑色像素清除我们要找回相关图像数据,以清楚后台缓冲区
      this.backbuffer = this.workingContext.getImageData(0, 0, this.workingWidth, this.workingHeight);
    };

    // 当一切就绪后将后台缓冲区刷新到前台缓冲区
    Device.prototype.present = function () {
      this.workingContext.putImageData(this.backbuffer, 0, 0);
    };

    // 调用此方法把一个像素绘制到指定的 X, Y 坐标上
    Device.prototype.putPixel = function (x, y, color) {
      this.backbufferdata = this.backbuffer.data;
      // 我们的后台缓冲区是一维数组
      // 这里我们简单计算,将 X 和 Y 对应到此一维数组中
      var index = ((x >> 0) + (y >> 0) * this.workingWidth) * 4;

      // 在 Html5 canvas 中使用 RGBA 颜色空间
      this.backbufferdata[index] = color.r * 255;
      this.backbufferdata[index + 1] = color.g * 255;
      this.backbufferdata[index + 2] = color.b * 255;
      this.backbufferdata[index + 3] = color.a * 255;
    };

    // 将三维坐标和变换矩阵转换成二维坐标
    Device.prototype.project = function (coord, transMat) {
      // 进行坐标变换
      var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
      // 变换后的坐标起始点是坐标系的中心点
      // 但是,在屏幕上,我们以左上角为起始点
      // 我们需要重新计算使他们的起始点变成左上角
      var x = point.x * this.workingWidth + this.workingWidth / 2.0 >> 0;
      var y = -point.y * this.workingHeight + this.workingHeight / 2.0 >> 0;
      return (new BABYLON.Vector2(x, y));
    };

    // 如果二维坐标在可视范围内则绘制
    Device.prototype.drawPoint = function (point) {
      // 判断是否在屏幕内
      if (point.x >= 0 && point.y >= 0 && point.x < this.workingWidth
                       && point.y < this.workingHeight) {
        // 绘制一个黄色点
        this.putPixel(point.x, point.y, new BABYLON.Color4(1, 1, 0, 1));
      }
    };

    // 主循环体,每一帧,引擎都要计算顶点投射
    Device.prototype.render = function (camera, meshes) {
      // 要理解这个部分,请阅读“阅读前提条件”
      var viewMatrix = BABYLON.Matrix.LookAtLH(camera.Position, camera.Target, BABYLON.Vector3.Up());
      var projectionMatrix = BABYLON.Matrix.PerspectiveFovLH(0.78,
                       this.workingWidth / this.workingHeight, 0.01, 1.0);

      for (var index = 0; index < meshes.length; index++) {
        // 缓存当前网格对象
        var cMesh = meshes[index];
        // 请注意,在平移前要先旋转
        var worldMatrix = BABYLON.Matrix.RotationYawPitchRoll(
          cMesh.Rotation.y, cMesh.Rotation.x, cMesh.Rotation.z)
           .multiply(BABYLON.Matrix.Translation(
             cMesh.Position.x, cMesh.Position.y, cMesh.Position.z));

        var transformMatrix = worldMatrix.multiply(viewMatrix).multiply(projectionMatrix);

        for (var indexVertices = 0; indexVertices < cMesh.Vertices.length; indexVertices++) {
          // 首先,我们将三维空间转换为二维空间
          var projectedPoint = this.project(cMesh.Vertices[indexVertices], transformMatrix);
          // 然后我们就可以在屏幕画出点
          this.drawPoint(projectedPoint);
        }
      }
    };
    return Device;
  })();
  SoftEngine.Device = Device;
})(SoftEngine || (SoftEngine = {}));

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文