当前位置: 首页 > news >正文

如何找外包网站来做黑龙江省道路建设网站

如何找外包网站来做,黑龙江省道路建设网站,帮我搜一下长沙做网络销售,WordPress 全局Ajax假设你想创建一个甜蜜的弹跳立方体#xff0c;如下所示#xff1a; 一个弹跳的立方体 你可以使用 3D 框架#xff0c;例如 OpenGL 或 Metal。 这涉及编写一个或多个顶点着色器来变换 3D 对象#xff0c;以及编写一个或多个片段着色器来在屏幕上绘制这些变换后的对象。 然…假设你想创建一个甜蜜的弹跳立方体如下所示 一个弹跳的立方体 你可以使用 3D 框架例如 OpenGL 或 Metal。 这涉及编写一个或多个顶点着色器来变换 3D 对象以及编写一个或多个片段着色器来在屏幕上绘制这些变换后的对象。 然后该框架采用这些着色器和您的 3D 数据执行一些魔法并以绚丽的 32 位颜色绘制所有内容。 但 OpenGL 和 Metal 在幕后到底有什么魔力呢 在线工具推荐 Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 回到过去—早在我们拥有硬件加速 3D 显卡之前更不用说可编程 GPU 了—如果你想绘制 3D 场景你必须自己完成所有工作 在只有 7 MHz 处理器的计算机上用汇编语言。 我的书架上摆满了有关 3D 图形的书籍但这些书籍已经过时了因为现在你可以简单地使用 OpenGL 或 Metal。 我很高兴我们能做到 然而即使你利用这些现代 GPU 和 3D API了解在屏幕上绘制 3D 对象所涉及的步骤仍然很有用。 在这篇文章中我将解释如何使用简单的动画和光照但不使用着色器来绘制 3D 弹跳立方体。 它说明了当你使用 OpenGL 或 Metal 时会发生什么我将指出现代顶点和片段着色器在这个故事中的应用。 我们根本不会使用任何 3D API—这只是最基本的。 我们可以使用的唯一渲染基元是 setPixel() 函数用于将单个像素写入 800×600 位图 func setPixel(x: Int, y: Int, r: Float, g: Float, b: Float, a: Float) 这会将位图坐标 (x, y) 处的像素更改为颜色 (r, g, b, a)。 为了进行 3D 绘制我们需要做的所有其他事情都建立在这个非常基本的 setPixel() 函数之上下面将详细解释。 注意我们不会使用任何花哨的数学只会使用基本算术和一点三角函数。 数学不涉及矩阵因此你可以准确地看到何时发生什么以及为什么发生。 即使你在学校数学没及格你也应该能够跟上 读完这篇文章后你将能够从头开始编写自己的基本 3D 渲染器并更好地了解 OpenGL 和 Metal 管道的工作原理。 1、演示应用程序 如果你想先查看完成的应用程序可以在 GitHub 上找到完整的源代码。 这是一个 macOS 应用程序用 Swift 3 编写。语言并不重要你可以轻松地将其移植到任何其他语言。 只需在 Xcode 8 中打开项目并运行即可。 该演示展示了一个上下弹跳并绕垂直轴旋转的彩色立方体 顶部的滑块可向左或向右移动相机让你从不同的有利位置观察立方体。 该应用程序并不是特别漂亮但它确实演示了 3D 渲染中涉及的许多概念。 注意取决于你的Mac 的速度演示应用程序可能运行速度相当慢。 毕竟这篇博文并不是如何制作快速 3D 图形的示例—这才是 GPU 的用途。 我在这里的目标只是展示基本想法是如何工作的因此我们为了清晰度和易于理解而牺牲速度。 所有重要的代码都在 Render.swift 文件中。 代码里有很多解释因此你可以只阅读源代码而不是这篇博文。 简而言之演示应用程序中发生的事情是这样的函数 render() 获取一些 3D 模型数据立方体对其进行转换和投影然后通过一遍又一遍地调用 setPixel() 来绘制它 — 这也是它这么慢的原因。 render() 中发生的情况大致相当于你告诉 OpenGL 或 Metal 在屏幕上绘制 3D 内容时 GPU 上发生的情况。 但我们不使用着色器而是手动完成这一切。 让我们看一下实现这一目标所需的所有部分。 2、3D模型 首先我们定义模型。 这就是我们要绘制的 3D 对象。 该模型由三角形组成因为三角形很容易绘制。 这些三角形也称为模型的几何形状。 立方体的几何形状如下所示 正如你所看到的该立方体的大小从 -10 单位到 10 单位。 单位可以是任何你想要的比如说厘米。 我们使用的坐标系如下所示 左手坐标系 x 坐标向右为正y 为向上正z 为进入屏幕正。 这就是所谓的左手坐标系。 注意轴的选择有些随意。 要获得右手坐标系其中 z 从屏幕中出来为正值只需将所有地方的 z 更改为 -z 即可。 左手还是右手的选择决定了某些事情例如旋转是按顺时针还是逆时针顺序发生。 每个三角形由 3 个顶点组成。 顶点描述 3D 空间中的 (x, y, z) 位置还描述该顶点处三角形的颜色以及用于照明计算的法线向量。 你还可以添加额外的信息例如纹理映射坐标以及想要与顶点关联的任何其他数据。 struct Vertex {var x: Float 0 // coordinate in 3D spacevar y: Float 0var z: Float 0var r: Float 0 // colorvar g: Float 0var b: Float 0var a: Float 1var nx: Float 0 // normal vector (using for lighting)var ny: Float 0var nz: Float 0 }struct Triangle {var vertices [Vertex](repeating: Vertex(), count: 3) } 注意在真实的应用程序中你可能会使用向量 3 和向量 4 结构而不是单独的属性但我想向你展示数学而不需要任何向量或矩阵对象。 为了实现立方体的 3D 模型我们只需要提供这些三角形的列表。 你通常会在 Blender 等工具中设计模型并从 .obj 文件加载它但因为这是一个简单的演示我们手动定义几何形状 let model: [Triangle] {var triangles [Triangle]()// The first trianglevar triangle Triangle()triangle.vertices[0] Vertex(x: -10, y: -10, z: 10, // positionr: 0, g: 0, b: 1, a: 1, // color nx: 0, ny: 0, nz: 1) // normaltriangle.vertices[1] Vertex(x: -10, y: 10, z: 10, r: 0, g: 0, b: 1, a: 1,nx: 0, ny: 0, nz: 1)triangle.vertices[2] Vertex(x: 10, y: -10, z: 10, r: 0, g: 0, b: 1, a: 1,nx: 0, ny: 0, nz: 1)triangles.append(triangle)// The second triangletriangle Triangle()triangle.vertices[0] Vertex(x: -10, y: 10, z: 10, r: 0, g: 0, b: 1, a: 1,nx: 0, ny: 0, nz: 1)triangle.vertices[1] // . . . and so on . . . return triangles }() 该模型中有 12 个三角形因此总共有 36 个顶点。 每个顶点都有自己的位置 (x, y, z)、颜色 (r, g, b, a) 和法线向量 (nx, ny, nz)。 法向量的长度应为 1且指向远离三角形的方向。 这决定了三角形面向的方向我们需要计算任何灯光的影响。 注意你可能已经注意到在上面的立方体几何图形中只有 8 个顶点而不是 36 个。 我们只需 8 个顶点就足够了但不同的三角形也会共享这些顶点的颜色和法向量而不仅仅是它们的位置。 当你想要在多个三角形之间共享相同的顶点数据时除了顶点数据之外OpenGL 和 Metal 还允许你创建所谓的“索引缓冲区”但我们在这里不这样做。 请注意某些 x、y、z 坐标位于 -10某些位于 10。 这样做是为了使立方体的中心位于原点 (0, 0, 0)。 这称为模型的本地原点 —我们需要这个本地原点以便围绕其中心旋转对象。 即使我们希望立方体位于 3D 世界的其他位置而不是原点我们仍然设计模型使其中心位于 (0, 0, 0)。 然后我们将把立方体移动到它在世界中的最终位置。 这就是下一节中发生的事情。 3、3D模型在世界中的位置 立方体不仅具有决定其形状的顶点和三角形而且还具有 3D 世界中的位置、比例以及由三个旋转角度也称为俯仰/pitch、滚动/roll和偏航/yaw给出的方向。 我们为这些属性定义变量 var modelX: Float 0 var modelY: Float 0 var modelZ: Float 0var modelScaleX: Float 1 var modelScaleY: Float 1 var modelScaleZ: Float 1var modelRotateX: Float 0 var modelRotateY: Float 0 var modelRotateZ: Float 0 最初我们将模型放置在世界中心的坐标 (0, 0, 0) 处在每个方向上为其指定比例 1或 100%并将所有旋转角度设置为零。 ViewController.swift 中有一些执行动画的代码。 它有一个每隔几毫秒触发一次的计时器。 在timer方法中我们对上述变量进行更改这使得立方体在3D世界中移动然后我们调用 render()来重绘整个3D场景来显示这些变化。 例如为了使立方体弹起我们更改 modelY 变量以便立方体上下移动。 当立方体撞击地板时通过更改 modelScaleX/Y/Z它会稍微“变平”。 立方体看起来在旋转因为在每个动画步骤中我们都会增加 modelRotateY。 还记得该模型有所谓的“本地”原点吗 这会告诉你模型的中心在哪里。 世界中心和模型中心之间存在概念上的差异尽管两者都有坐标 (0, 0, 0)。 模型在世界范围内的任何移动都与本地原点相关。 对于我们当前定义的立方体其本地原点位于立方体的正中心。 但是如果你从 .obj 文件加载模型则本地原点可能并不完全位于你想要的位置。 我们可以通过声明模型的本地原点相对于其顶点位置的位置来解决此问题 var modelOriginX: Float 10 var modelOriginY: Float 0 var modelOriginZ: Float 10 请注意这里我们没有使用 (0, 0, 0)而是使用 (10, 0, 10)它是立方体的角之一。 旋转围绕这个本地原点进行只是为了好玩演示应用程序将围绕一个角而不是中心旋转。 如果你正在运行演示应用程序请随意使用这些变量和动画代码中的任何一个看看会发生什么。 很有趣 4、相机 到目前为止我们已经定义了一个 3D 对象并在 3D 世界中给了它一个位置。 立方体当前位于世界的中心我们可以通过更改模型的变量来移动它 — 这就是我们让它上下弹跳的方式。 但作为观察者你坐在世界的哪个位置呢 目前你还坐在原点即正好位于立方体的中间。 如果我们现在渲染 3D 场景你会从内到外看到立方体。 我们可以引入一个相机对象它在3D世界中也有一个位置。 该对象没有任何几何形状但它只是表示观察者在世界中的位置以及正在看的位置。 var cameraX: Float 0 var cameraY: Float 20 var cameraZ: Float -20 在演示应用程序中我将相机向上移动了 20 个单位因此你可以稍微向下看场景可以看到立方体的顶部但看不到其底部。 相机还沿着 Z 轴向后拉 -20 个单位使得立方体看起来好像从观察者移开并进入屏幕。 换句话说我们将从“安全”距离观察立方体。 当你拖动应用程序中的滑块时会更改cameraX。 注意在真实的应用程序中你还可以为相机指定一个方向使用“查看”向量或使用旋转角度但在此应用程序中你始终沿着正 z 轴查看。 5、灯光 当我们定义立方体的 3D 几何形状时我们为每个顶点指定了一种颜色。 我们可以简单地使用三角形顶点的颜色来绘制三角形但这有点乏味。 为了使 3D 场景看起来更有趣我们希望拥有逼真的灯光效果。 以下选项控制场景的照明。 首先有环境光这是始终存在的“背景”光 var ambientR: Float 1 var ambientG: Float 1 var ambientB: Float 1 var ambientIntensity: Float 0.2 我们在这里定义的环境光是纯白色的但只有 20% 的强度。 这意味着当我们应用此光时我们将顶点颜色乘以 (0.2, 0.2, 0.2)使它们变得更暗 — 没关系因为我们还将应用定向照明以使它们再次变得更亮。 你可以使用这些环境设置来给整个场景带来某种感觉。 例如如果设置 ambientB 0那么所有顶点将仅保留其红色绿色分量并且所有蓝光将被滤除。 如果将 ambientIntensity设置为0.0那么在没有任何其他光源的情况下立方体将是全黑的。 如果设置为 1.0环境光会淹没任何其他光源。 我们还定义了漫射光源。 与环境光一样它有颜色和强度但也有方向 var diffuseR: Float 1 var diffuseG: Float 1 var diffuseB: Float 1 var diffuseIntensity: Float 0.8var diffuseX: Float 0 // direction of the diffuse light var diffuseY: Float 0 // (this vector should have length 1) var diffuseZ: Float 1 对于我们的演示应用程序漫射光源指向 z 轴正方向这意味着有一个光源与相机观察的方向相同。 立方体的一侧与相机以及光源对齐得越多它就会显得越亮。 在左下图中立方体的绿色面面向定向光因此它显示为亮绿色。 但当立方体旋转远离光线时绿色的一面变得更暗蓝色的一面变得更亮 立方体上方没有光源照射因此顶部的红色三角形看起来很暗因为它们仅被环境光照亮。 6、渲染管线 好的到目前为止我们刚刚定义了 3D 场景将使用的数据结构和变量。 现在是时候展示如何在屏幕上实际渲染这个 3D 世界了。 每个动画帧都会调用 render() 函数。 该函数获取模型的顶点数据、其状态modelX、modelRotateY 等和相机的位置并生成 3D 场景的 2D 再现。 在真实游戏中render() 理想情况下每秒调用 60 次或更多。 使用 OpenGL 或 Metal调用 render() 相当于设置着色器然后调用 glDrawElements() 或 MTLCommandBuffer.commit()。 这是由 render() 执行的渲染管道 我将详细解释每个步骤中发生的情况。 render() 本身的代码非常简单因为它将大部分工作交给了辅助函数。 这是代码 func render() {// 1: Erase what we drew last time.clearRenderBuffer(color: 0xff302010)// 2: Also clear out the depth buffer.for i in 0..depthBuffer.count {depthBuffer[i] Float.infinity}// 3: Take the cube, place it in the 3D world, adjust the viewpoint for// the camera, and project everything to two-dimensional triangles.let projected transformAndProject()// 4: Draw these 2D triangles on the screen.for triangle in projected {draw(triangle: triangle)} } 首先它删除了在前一帧中绘制的所有内容以便我们可以从一个干净的平板开始。 步骤 1 和 2 然后它调用 transformAndProject()将3D立方体转换为二维三角形列表。 这大致相当于顶点着色器中发生的情况。 步骤3 最后它调用 draw(triangle) 在屏幕上渲染每个 2D 三角形。 这就是片段着色器中发生的情况。 例如应用照明的计算就是在这里完成的。 步骤4 我还没有提到第 2 步中的深度缓冲区。 这用于确保较远的三角形不会与较近的三角形重叠。 深度缓冲区定义为 var depthBuffer: [Float] {return [Float](repeating: 0, count: Int(context!.width * context!.height)) }() 其中 context!.width 和 .height 是屏幕的大小。 在演示应用程序中它的大小为 800×600 像素与渲染缓冲区的大小相同。 为了清除深度缓冲区我们将其设置为大值 Float.infinity。 稍后我将详细介绍这个深度缓冲区的工作原理。 现在让我们看看步骤 3 中的辅助函数 transformAndProject()。 7、变换三角形 这涉及渲染管线流程图中的前三个操作。 应用程序中的每个 3D 模型我们只有一个立方体都是在其自己的局部坐标空间也称为模型空间中定义的。 为了将这些模型绘制到屏幕上我们必须让它们经历几次“转变” 首先我们必须将模型放置在 3D 世界中。 这是从模型空间到世界空间的转换。 理论上你已经可以在世界空间坐标中定义立方体但更容易在其自己的微小宇宙中创建模型独立于任何其他模型然后通过平移、缩放和旋转将其放置到更大的世界中。然后我们定位相机并通过该相机观察世界从世界空间到相机空间或“眼睛空间”的转换。 此时我们已经可以丢弃一些不可见的三角形因为它们背对相机或因为它们位于相机后面。最后我们将相机的 3D 视图投影到 2D 表面上以便我们可以将其显示在屏幕上 该投影是对屏幕空间也称为“视口空间”的转换。 这些转换是所有数学运算发生的地方。 为了弄清楚发生了什么我将只使用简单的数学—主要是加法、乘法偶尔也使用正弦和余弦。 注意在真正的 3D 应用程序中你会将大部分计算保留在矩阵中因为它们更加高效且易于使用。 但这些矩阵将执行与你在此处看到的完全相同的操作 理解数学总是好的这就是我们手工计算的原因。 TransformAndProject() 中发生的许多事情通常由 GPU 上的顶点着色器完成。 顶点着色器获取模型的顶点并将它们从局部 3D 空间转换为 2D 空间以及其间的所有步骤。 现在我们将详细研究每一个变换。 8、模型空间到世界空间的变换 在 transformAndProject()函数中我们首先执行到世界空间的变换。 我们希望立方体能够旋转并上下弹跳。 因此我们采用定义立方体几何形状的原始顶点以立方体的本地原点为中心并将它们从本地模型空间移动到更大的世界中。 对于此转换我们将使用之前定义的 modelXYZ、modelRotateXYZ、modelScaleXYZ 变量回想一下这些是由动画代码更改的变量。 变换是循环发生的因为我们需要依次将其应用于模型的每个顶点。 我们将结果存储在一个新的临时数组中因为我们不想覆盖原始的多维数据集数据。 循环看起来像这样 var transformed model// Look at each triangle... for (j, triangle) in transformed.enumerated() {var newTriangle Triangle()// Look at each vertex of the triangle...for (i, vertex) in triangle.vertices.enumerated() {var newVertex vertex// TODO: the math happens here// Store the new vertex into the new triangle.newTriangle.vertices[i] newVertex}// Store the new triangle into the model.transformed[j] newTriangle } 8.1 本地原点 首先我们可能需要调整模型的原点这是一个平移数学上的意思是“运动”。 如果 modelOriginX、Y 或 Z 不是 (0, 0, 0)那么我们希望 (modelOriginX, modelOriginY, modelOriginZ) 成为模型的新中心。 我们通过从顶点坐标中减去这些值来做到这一点 newVertex.x - modelOriginX newVertex.y - modelOriginY newVertex.z - modelOriginZ 由于顶点的坐标是相对于模型的局部原点的为了调整这个局部原点我们需要移动顶点—但方向相反。 8.2 缩放 接下来我们将应用缩放如果有。 如果你从 .obj 文件加载模型并且它不使用与 3D 世界相同的单位例如米与厘米则缩放非常有用但它对于特效也非常有用。 在这个演示中我们使用缩放来夸大“弹跳”运动。 缩放是一个简单的乘法 newVertex.x * modelScaleX newVertex.y * modelScaleY newVertex.z * modelScaleZ 默认情况下 modelScaleX、Y 和 Z 均为 1因此乘法不起作用。 但如果缩放因子大于1顶点将远离模型的局部原点使模型显得更大 对于小于 1 的缩放因子顶点将移近原点。 8.3 旋转 接下来是旋转。 这使用了一些三角学知识但你不需要记住这些公式我把它们保存在备忘单上。 首先我们绕 X 轴旋转然后绕 Y 轴旋转最后绕 Z 轴旋转。 尽管你需要注意一个称为万向节锁定gimbal lock的问题但执行这些旋转的顺序并不重要。 这基本上意味着如果你结合某些旋转你就会迷失方向。 解决这个问题的一种方法是使用更奇特的数学。 // Rotate about the X-axis. var tempA cos(modelRotateX)*newVertex.y sin(modelRotateX)*newVertex.z var tempB -sin(modelRotateX)*newVertex.y cos(modelRotateX)*newVertex.z newVertex.y tempA newVertex.z tempB// Rotate about the Y-axis: tempA cos(modelRotateY)*newVertex.x sin(modelRotateY)*newVertex.z tempB -sin(modelRotateY)*newVertex.x cos(modelRotateY)*newVertex.z newVertex.x tempA newVertex.z tempB// Rotate about the Z-axis: tempA cos(modelRotateZ)*newVertex.x sin(modelRotateZ)*newVertex.y tempB -sin(modelRotateZ)*newVertex.x cos(modelRotateZ)*newVertex.y newVertex.x tempA newVertex.y tempB 这些公式围绕模型的调整原点旋转顶点。 这就是为什么区分模型中心和世界中心很重要因为你不希望顶点围绕整个世界的中心旋转。 注意因为我们使用的是左手坐标系所以正旋转是绕旋转轴顺时针旋转。 在右手坐标系中它将是逆时针方向。 8.4 法向量 回想一下顶点不仅有 3D 空间中的坐标还有法向量。 该法线向量描述了顶点或其三角形指向的方向。我们需要旋转法线向量使其与顶点的方向保持对齐。 因为在此演示应用程序中我们仅绕 Y 轴旋转所以我仅包含该旋转公式而不包含其他轴。 tempA cos(modelRotateY)*newVertex.nx sin(modelRotateY)*newVertex.nz tempB -sin(modelRotateY)*newVertex.nx cos(modelRotateY)*newVertex.nz newVertex.nx tempA newVertex.nz tempB 8.5 平移 最后执行到模型在 3D 世界中的目标位置的转换 newVertex.x modelX newVertex.y modelY newVertex.z modelZ 好的这完成了用于在 3D 世界中定位和定向 3D 模型的转换。 现在模型已缩放、旋转并放置在适当的位置。 正如你所看到的我们正在进行相当多的计算以使模型从其局部坐标系进入世界空间并且我们需要对模型中的每个顶点执行这些计算。 如果我们有多个模型就像大多数游戏所做的那样我们需要为每个模型重复所有这些计算。 这就是为什么在实践中你会将这些计算放入一个矩阵中然后你只需将每个顶点与该矩阵相乘即可。 这更简单、更高效因为它可以进行硬件加速—无论是在 CPU 上通过 simd 指令进行加速还是在 GPU 上通过顶点着色器进行加速。 通常在 CPU 上计算矩阵然后将其传递给顶点着色器顶点着色器将使用它来变换所有顶点。 9、世界空间到相机空间的变换 目前我们从 (0, 0, 0) 沿 z 轴垂直观察 3D 世界。 但在真正的 3D 应用程序中观察者可能并不总是处于固定位置。 你可以想象我们正在通过一个“相机”对象观察世界我们可以将这个相机放置在我们想要的任何地方并让它看起来像我们想要的任何地方。 这意味着我们需要将对象从“世界空间”转换为“相机空间”。 这使用与之前相同的数学但方向相反。 for (j, triangle) in transformed.enumerated() {var newTriangle Triangle()for (i, vertex) in triangle.vertices.enumerated() {var newVertex vertex// Move everything in the world opposite to the camera, i.e. if the// camera moves to the left, everything else moves to the right.newVertex.x - cameraXnewVertex.y - cameraYnewVertex.z - cameraZ// Likewise, you can perform rotations as well. If the camera rotates// to the left with angle alpha, everything else rotates away from the// camera to the right with angle -alpha. (I did not implement that in// this demo.)newTriangle.vertices[i] newVertex}transformed[j] newTriangle} 在实践中你还可以使用矩阵进行相机转换。 事实上你可以将模型矩阵和相机矩阵组合成一个矩阵有时称为模型视图modelview矩阵。 10、背面剔除 此时所有三角形都位于“相机空间”中因此你知道通过相机镜头可以看到世界的哪一部分。 为了节省宝贵的处理时间你需要丢弃无论如何都不可见的三角形例如那些位于相机后面或背对相机的三角形。 Metal 或 OpenGL 会自动为你完成此操作。 我没有在演示应用程序中实现它因为它涉及的数学比我想在这里解释的要多一些但一种常见的技术是背面剔除backface culling。 背面剔除的工作原理是计算相机的方向与三角形面向的方向之间的角度。 这告诉你三角形是指向相机即可见还是远离相机不可见。 你只需删除面向错误方向的三角形这样它们就不会被进一步处理。 注意使用背面剔除时三角形中顶点的顺序很重要。 如果这个所谓的缠绕顺序错误你的三角形就根本不会出现 在这个演示应用程序中我们不进行面剔除因此顶点顺序并不重要。 你还可以丢弃相机视野视锥体frustum之外的任何三角形 。但由于这只是一个简单的演示应用程序因此我也没有实现它。 11、相机空间到屏幕空间的变换 现在我们有一组在相机空间中描述的三角形它们以三个维度表示。 但我们的计算机屏幕是二维的。 用数学术语来说我们需要以某种方式将三角形从 3D 投影到 2D。 相机空间的单位是你想要的任何单位我们选择厘米但我们需要将其转换为像素。 此外我们需要决定在屏幕上的哪个位置放置相机空间的原点我们将其放在中心。 我们必须从 3D 坐标投影到 2D这需要摆脱 z 轴。 这一切都发生在最后的变换步骤中。 注意OpenGL 和 Metal 的执行顺序与我们这里的执行顺序略有不同它们的投影变换将顶点放入“剪辑空间”中但我们直接将顶点转换为屏幕空间。 在这篇博文中我主要介绍总体思路因此请原谅我跳过了一些细节。 和以前一样我将说明如何使用基本数学运算进行此转换。 通常你会将所有这些操作组合成一个投影矩阵并将其传递给顶点着色器因此将其应用到顶点将发生在 GPU 上。 再次我们循环遍历所有三角形和所有顶点。 对于每个顶点我们执行以下操作 newVertex.x / (newVertex.z 100) * 0.01 newVertex.y / (newVertex.z 100) * 0.01 进行 3D 到 2D 投影的一种简单方法是将 x 和 y 除以 z。 z越大除法的结果越小。 这是有道理的因为距离较远的物体应该看起来更小。 在真正的 3D 应用程序中你将使用更复杂的投影矩阵但这是总体思路。 注意为了好玩请尝试使用这些神奇的数字 100 和 0.01。 通过调整这些数值你可以获得极端的镜头角度。 我们还需要做两件事。 首先我们将世界单位厘米转换为像素。 在此演示应用程序中我希望相机视口占据大约 -40 到 40 的世界单位厘米。 我们需要放大顶点的x和y值 我们在两个方向上使用相同的数量因此一切都保持正方形 newVertex.x * Float(contextHeight)/80 newVertex.y * Float(contextHeight)/80 终于我们现在开始以像素为单位了 最后我们希望 (0, 0) 位于屏幕的中心。 最初它位于右下角因此将所有内容移动屏幕尺寸的一半以像素为单位 newVertex.x Float(contextWidth/2) newVertex.y Float(contextHeight/2) 请注意这些公式仅更改 newVertex.x 和 .y但不更改 .z。 这是有道理的因为我们将在屏幕上绘制的三角形现在是二维的。 但我们仍然希望保留这个 z 值。 我们可以用它来填充深度缓冲区这让我们可以在尝试绘制三角形时确定是否覆盖任何现有像素。 注意上述内容 - 转换为世界空间、相机空间和屏幕空间 - 是你可以在顶点着色器中执行的操作。 顶点着色器将模型的顶点作为输入并将它们转换为你想要的任何内容。 你可以像我们在这里所做的那样执行基本操作平移、旋转、3D 到 2D 投影等但一切都可以。 这样就完成了转换。 最后我们来画出来吧 12、三角形光栅化 好的到目前为止我们所做的就是将 3D 模型放入现实世界中根据相机的视角进行调整然后转换为 2D 空间。 我们现在拥有的是相同的三角形列表但它们的顶点坐标现在代表屏幕上的特定像素而不是某些想象的三维空间中的点。 我们可以为每个三角形绘制这三个像素但这只给我们顶点它不会填充整个三角形。 为了填充三角形我们必须以某种方式连接这些顶点像素。 这称为光栅化。 Metal 会为你解决大部分问题。 一旦确定了哪些像素属于三角形GPU 就会为每个像素调用片段着色器这样你就可以更改每个像素的绘制方式。 尽管如此了解光栅化在幕后的工作原理还是很有用的所以这就是我们将在本节中讨论的内容。 我们需要弄清楚每个三角形由哪些像素组成以及它们的颜色。 这发生在绘制三角形中。 render() 函数为屏幕空间中的每个三角形调用 draw(triangle)。 这就是 draw(triangle)函数的作用 func draw(triangle: Triangle) {// 1. Only draw the triangle if it is at least partially inside the viewport.guard partiallyInsideViewport(vertex: triangle.vertices[0]) partiallyInsideViewport(vertex: triangle.vertices[1]) partiallyInsideViewport(vertex: triangle.vertices[2]) else {return}// 2. Reset the spans so that were starting with a clean slate.spans .init(repeating: Span(), count: context!.height)firstSpanLine Int.maxlastSpanLine -1// 3. Interpolate all the things!addEdge(from: triangle.vertices[0], to: triangle.vertices[1])addEdge(from: triangle.vertices[1], to: triangle.vertices[2])addEdge(from: triangle.vertices[2], to: triangle.vertices[0])// 4. Draw the horizontal strips.drawSpans() } 第 1 步OpenGL 或 Metal 已经丢弃了所有不可见的三角形。 尽管如此一些三角形可能只是部分可见。 这些将被裁剪到屏幕的边界。 在此演示应用程序中我们采用更简单的方法如果像素落在可见区域之外则不会绘制像素。 第 2、3、4 步绘制三角形的其余部分会发生什么我将在下面解释。 请注意它需要这些附加变量 var spans [Span]() var firstSpanLine 0 var lastSpanLine 0 为了栅格化三角形我们将绘制水平条。 例如如果三角形有这些顶点 那么水平条将如下所示 屏幕上的每个垂直位置都有一个条带因此条带的高度正好是 1 像素。 我将这些水平条带称为跨度span struct Span {var edges [Edge]()var leftEdge: Edge {return edges[0].x edges[1].x ? edges[0] : edges[1]}var rightEdge: Edge {return edges[0].x edges[1].x ? edges[0] : edges[1]} } 为了找出每个跨度的开始和结束位置我们必须从顶点 a 开始垂直向顶点 b 移动以找到每条线上相应的 x 位置。 我们还从顶点 a 到 c以及从 c 到 b始终向上执行此操作。 我们发现的点我称之为边edge 一条边代表一个 x 坐标。 每个跨度都有两个边一个在左侧一个在右侧。 一旦找到这两条边我们只需在它们之间画一条水平线。 对三角形中的所有跨度重复此操作我们将用像素填充三角形 struct Edge {var x 0 // start or end coordinate of horizontal stripvar r: Float 0 // color at this pointvar g: Float 0var b: Float 0var a: Float 0var z: Float 0 // for checking and filling in the depth buffervar nx: Float 0 // interpolated normal vectorvar ny: Float 0var nz: Float 0 } 光栅化中的关键词是插值。 我们插入所有的东西 当我们计算这些跨度及其边时我们不仅会插值顶点的 x 位置还会插值它们的颜色、法向量、深度缓冲区的 z 值、纹理坐标等等。 对于三角形中的每个像素我们将为所有这些属性计算一个插值。 顶点属性之间的插值发生在辅助函数 addEdge(from:to:) 中。 下面是这个函数的缩写版本因为里面有一堆重复的代码。 func addEdge(from vertex1: Vertex, to vertex2: Vertex) {let yDiff ceil(vertex2.y - 0.5) - ceil(vertex1.y - 0.5)guard yDiff ! 0 else { return } // degenerate edgelet (start, end) yDiff 0 ? (vertex1, vertex2) : (vertex2, vertex1)let len abs(yDiff)var yPos Int(ceil(start.y - 0.5)) // y should be integer because itlet yEnd Int(ceil(end.y - 0.5)) // needs to fit on a 1-pixel linelet xStep (end.x - start.x)/len // x can stay floating point for nowvar xPos start.x xStep/2let rStep (end.r - start.r)/lenvar rPos start.r/* . . . more attributes here . . . */while yPos yEnd {let x Int(ceil(xPos - 0.5)) // now we make x an integer too// Dont want to go outside the visible area.if yPos 0 yPos Int(context!.height) {if yPos firstSpanLine { firstSpanLine yPos }if yPos lastSpanLine { lastSpanLine yPos }// Add this edge to the span for this line.spans[yPos].edges.append(Edge(x: x, r: rPos, g: . . .))}// Move the interpolations one step forward.yPos 1xPos xSteprPos rStep} } 对其工作原理的快速描述 我们总是一次只在两个顶点之间进行插值 - 例如上图中从 a 到 b - 因此对于每个三角形我们必须调用 addEdge(from:to:) 三次。 插值从具有最低 y 坐标 yPos 的顶点到具有最高 y 坐标 yEnd 的顶点。 由于每个跨度代表屏幕上的 1 像素水平线因此我们在循环的每次迭代中将 yPos 加 1。 对于其他顶点属性例如 x 位置 (xPos) 和红色分量 (rPos)我们执行简单的线性插值。 在每次迭代中我们都会给它们增加一些小数值xStep 和 rStep以逐渐在它们的起始值和结束值之间移动。 举个例子如果顶点 a 是黄色顶点 b 是红色那么中间的所有点都会慢慢从黄色变成红色。 你可以在图中看到 a 和 b 之间 50% 处的边缘确实是橙色的。 绿色和蓝色、z 位置和法线向量均以相同的方式进行插值。 纹理坐标的行为略有不同因为你还需要考虑视角。 因此对于 yPos 的每个值我们向表示该特定 y 位置的 Span 对象添加一个新的 Edge。 完成后我们就有了一组 Span 对象它们描述了组成这个三角形的水平线。 现在我们终于可以推送一些像素了 OpenGL 和 Metal 将为你完成所有这些插值工作然后将这些插值值传递给三角形中每个像素的片段着色器。 这就是我们最后一节的主题…… 13、最后......绘制三角形 快速提醒一下我们现在所处的位置我们从一个 2D 三角形列表开始这些三角形的顶点代表像素坐标。 在上一节中我们使用 addEdge() 将这些三角形转换为 Span 对象的数组。 每个跨度在屏幕上描述一条水平线。 这条线由两个 Edge 对象定义每条边都有一个 x 坐标、一个颜色、一个法线向量和一个 z 坐标。 这些都是通过在三角形顶点之间插值来计算的。 函数 drawSpans()将循环遍历Span对象数组并通过为每个像素调用 setPixel()来绘制水平线。 然而左边缘的颜色可能与右边缘的颜色不同因此我们也需要在这些颜色之间进行插值 第一次我们插值是为了找到三角形边缘的颜色但这一次我们必须插值来找到穿过三角形的像素的颜色。 水平条带实际上是 1 像素高渐变如下所示 法线向量和 z 位置也是如此它们也是从左边缘到右边缘进行插值的。 drawSpans() 的代码如下所示 func drawSpans() {if lastSpanLine ! -1 {for y in firstSpanLine...lastSpanLine {if spans[y].edges.count 2 {let edge1 spans[y].leftEdgelet edge2 spans[y].rightEdge// How much to interpolate on each step.let step 1 / Float(edge2.x - edge1.x)var pos: Float 0for x in edge1.x .. edge2.x {// Interpolate between the colors again.var r edge1.r (edge2.r - edge1.r) * posvar g edge1.g (edge2.g - edge1.g) * posvar b edge1.b (edge2.b - edge1.b) * poslet a edge1.a (edge2.a - edge1.a) * pos// Also interpolate the normal vector.let nx edge1.nx (edge2.nx - edge1.nx) * poslet ny edge1.ny (edge2.ny - edge1.ny) * poslet nz edge1.nz (edge2.nz - edge1.nz) * pos// TODO: depth buffer// TODO: draw the pixelpos step}}}} } 你可以看到我们如何从左到右一次步进一个像素并插入颜色和法线向量。 注意对于立方体中的许多三角形所有三个顶点都具有相同的法线向量。 因此这样一个三角形中的所有像素也获得相同的法向量。 但这不是必需的我还添加了两个三角形立方体的黄色一侧它们的顶点具有不同的法线向量使它们看起来更“圆润”。 你可以清楚地看到黄色边受定向光影响的方式与其他三角形不同。 还有更多我们将逐步查看。 首先有深度缓冲区。 这是一个与屏幕尺寸相同 (800×600) 的 Floats 数组。 深度缓冲区可确保较远的三角形不会遮挡距相机较近的三角形。 这是通过将每个三角形像素的 z 值存储到深度缓冲区中来完成的。 如果尚未绘制“较近”的像素我们仅绘制该像素。 也就是说只有当 z 值小于当前深度缓冲区中该位置的 z 值时我们才调用 setPixel() — 这也是 Metal 已经为你提供的功能。 var shouldDrawPixel true if useDepthBuffer {let z edge1.z (edge2.z - edge1.z) * poslet offset x y * Int(context!.width)if depthBuffer[offset] z {depthBuffer[offset] z} else {shouldDrawPixel false} } 注意演示应用程序还允许你禁用深度缓冲区。 在这种情况下它将按三角形的平均 z 位置对三角形进行排序以便首先绘制距离较远的三角形。 然而这不是一个理想的解决方案因为它不能保证绘制的三角形不重叠。 但是如果你的三角形是部分透明的那么你可能需要使用 z 排序而不是深度缓冲区。 最后我们可以绘制像素 if shouldDrawPixel {let factor min(max(0, -1*(nx*diffuseX ny*diffuseY nz*diffuseZ)), 1)r * (ambientR*ambientIntensity factor*diffuseR*diffuseIntensity)g * (ambientG*ambientIntensity factor*diffuseG*diffuseIntensity)b * (ambientB*ambientIntensity factor*diffuseB*diffuseIntensity)setPixel(x: x, y: y, r: r, g: g, b: b, a: a) } 这就是片段着色器发挥作用的地方。 对于我们必须绘制的每个像素它都会被调用一次并带有颜色、纹理坐标、法线向量等的插值。 在这里你可以做各种有趣的事情。 在演示应用程序中我们根据非常简单的光照模型计算像素的颜色但你也可以从纹理中采样或者在将像素颜色写入帧缓冲区之前对像素颜色执行许多其他疯狂的操作。 你可以把它变得像你想象的那样疯狂 唷 只是为了在屏幕上得到一个旋转的立方体就需要付出很大的努力。 公平地说诸如 OpenGL 或 Metal 之类的 API 所做的工作比我们在此介绍的要多得多而且效率更高但这从概念上来说就是使用 GPU 绘制 3D 对象时发生的情况。 我希望你觉得它有启发性 原文链接无着色器3D渲染 - BimAnt
http://www.zqtcl.cn/news/330675/

相关文章:

  • 苏州专业网站建设网站模板是什么
  • 科技网站设计案例百度收录情况查询
  • gif放网站有锯齿策划公司宣传语
  • 淘宝客做网站怎样推广空间购买后打不开网站
  • 信阳网站设计银川网站建设nx110
  • 建设安全协会网站58招聘运营网站怎么做
  • 做原创的网站做游戏平面设计好的素材网站有哪些
  • 校园网站wordpress 防攻击插件
  • wordpress 更好的主题丁的老头seo博客
  • 上海市工程信息网站北京专业网站翻译影音字幕翻译速记速记速记速而高效
  • 网站建设心得体会500字网页制作三剑客是指什么
  • 大连做网站优化一级a做爰片 网站就能看
  • 网站优化页面中山seo网络推广
  • 建设网站一定要数据库吗湖北百度seo
  • 下载了wordpress然后怎么用怎样健建设一个有利于优化的网站
  • 网站开发心得500字做代售机票网站程序
  • php电影网站开发凡诺网站建设
  • 兰州道路建设情况网站南宁网站开发
  • 网站开发服务费投资者网站建设
  • 网站开发 如何备案新站点seo联系方式
  • 自动全屏网站模板贵州网站制作公司电话
  • 南昌购物网站制作国外免费网站空间
  • 网站地图模版企业做网站etp和源程序
  • 电子商务企业网站的推广方式外贸长尾关键词挖掘网站
  • 靓号网建站网站商城html模板
  • 广东顺德网站建设wordpress 我爱搜罗网
  • 基金网站建设需求书昆明网站制作工具
  • 京东网上购物商城官方网站国外网站页头设计图片
  • 芯片设计公司排名安卓优化大师app
  • 如何进行网站域名解析网站开发的工作方法