简介
我很少写关于 3D 图形的文章,因为觉得所有的事情似乎已经说了很多很多遍了。但是在采访中,特别是在招聘初级开发者时,我注意到一个问题让 9 出 10 的候选人都很难受:“在 Unity 等 GPU 上绘制一个立方体(例如使用 Unity)中需要多少个顶点,才能实现正确的光照效果?”这里的正确光照意味着每个面都有均匀的描边(这是一个非常重要的线索)。特别是对困难的三角形保存者来说,还有一个额外的条件:不能使用透明度和丢弃。我们假设每个面使用 2 个三角形。
那么,我们需要多少个顶点呢?
如果你的答案是 8,让我们看一看第一部分。如果你认为是 24,跳过第一部分,直接看第二部分。第二部分我将与我最近的宠物项目相关的实现想法进行分享:具有自定义属性和 Houdini 风格区分域的程序化网格。
刚开始。3D 引擎 (Unity 示例)
在 Unity 和其他实时引擎中,网格由顶点缓冲区和索引缓冲区定义。这些项目在 CPU 上有 CPU 端的抽象化(Unity 中的 Jobs 友好 MeshData 和旧管理的 Mesh)。
顶点缓冲区是一个顶点的数组,其中包含其数据。顶点是一个固定格式的记录,包含一套属性:位置、法线、切线、UV、颜色等。如果这些属性没有按照预期在着色器中使用,这也一样。理论上,所有顶点共享相同的结构,可以通过索引来访问(尽管实际上这些属性可以存储在多个顶点流中)。
索引缓冲区是一个索引的数组,定义了顶点是如何连接到表面上的。通过三角形拓扑,每三个索引形成一个三角形。
因此,网格是一个顶点结构和索引数组的组合,其中顶点具有属性,而索引定义了连接情况。
需要区分几何点和顶点。几何点是仅仅在空间中的一个位置。顶点是一个网格元素,位置与属性(例如法线)一起存储,每个顶点可能有不同的属性。从 Blender 或 3ds Max 来,可能会习惯将法线视为多边形属性。但是,GPU 上的情况是多边形仍然通过三角形渲染,需要将法线保存在每个顶点中,通过从顶点着色器传递并在三角形表面上进行中间插值。在片段着色器中接收的法线通常是经过插值的。
让我们来看一下立方体的光照。立方体有八个顶点和六个面,每个面都有一个法线平行于其表面。
为了清晰起见,以下是立方体的示意图。https://preview.redd.it/capruw9aljsg1.png?width=668&format=png&auto=webp&s=bce5a79da5528f982e9e17ab6597e72987b80d49
三个面在每个顶点相交。如果使用每个顶点一个顶点,共用多个面时此顶点可以只有一个法线。当值在三角形上进行插值时,光照会在面之间进行平滑,导致立方体外观“圆滑”,并在三角形上出现算术。
需要注意的是,顶点重复复制不是因为法线这样一项。这也同样是因为任何属性之间的不同(例如 UV、切线、颜色或皮肤权重),即使位置相同,都需要一个顶点。
在实践中,每个顶点是所有属性的唯一组合,因此只需一个属性不同,就需要一个新的顶点。
https://preview.redd.it/pwqrddphljsg1.png?width=3188&format=png&auto=webp&s=7c367fe555b68d450647ea7edce375171c4fc5ca
示例 1。我们尝试用 8 个顶点和 12 个三角形 (36 个索引) 尝试合并。我很清楚不是足够的法线来正确计算光照。即使在此不适合物理盒子用于交点测试时。这将足够的三角形也算不上。
为了解决这个问题,顶点在每个三角形中复制。同一个顶点由三个不同的顶点表示:相似位置,但具有每个面的不同法线,允许每个面独立受光照,并与其相邻三角形保持接口。
因此,在这种表示法中,立方体的顶点描述为 24 个顶点:每个面四个顶点。索引缓冲区定义 12 个三角形,每个三角形使用此类顶点。
https://preview.redd.it/2pshtvalljsg1.png?width=3192&format=png&auto=webp&s=05bfb3c4348243b13952f0f08878eea22d84924c
示例 2。Sharp faces 因为顶点不交叉在三角形之间。此前的 36 个索引,顶点数量 - 24,三角形每个需要三角形顶点。
最后,我们得到什么呢?
这种结构直接符合 GPU 上的数据处理方法,所以它对渲染相当有利。方便的索引访问、紧凑的存储、良好的缓存局部性以及批处理处理顶点,对线性变换(例如旋转、缩放、平移、扭曲或压缩等)同样具有很好的效率。整个模型在着色管口中不需要额外转换。
然而,请注意所有这些方便性的结束点是网格编辑。在这种编辑中,索引缓冲区的连接定义为仅取决于索引,只有当属性变异(例如法线或 UV)时,才能要求顶点复制。结果,在实践中,这是三角形“汤”。顶点间的明确拓扑对应结构不直接被表示,只是通过索引而被表示;需要在需要时重建。因此,在显性拓扑的三角形网格中很难理解相邻的面、沿边行进,以及整个表面如何表达。这种结构会导致这些网格很不方便进行几何形体分析和拓扑任务:布尔运算、轮廓化三角形、切边、切割、棱半径轮廓化等。有许多这样的方法可以组合在一起:半边、边界描述、面连通性和许多其变体。
而这使我们来到了部分 2。
第 2 部分。几何形态属性 + 拓扑
我很喜欢程序化三维建模,其中都由一组规则和不同参数和属性之间的依赖关系来描述这些几何。这个方法使得对象和场景的生成和修改变得很方便。在 Discreet 的年代,我正在研究各种 3D 编辑器的源代码,我研究了 3D 库的各种几何表示方法。因此,我再次回到在 Houdini 中的想法:实现我的自定义网格结构和相关算法。
在 Houdini 中,几何是分为 4 个级别:详细、点、顶点、素元。
- 种是空间中位置的引用,但是也可以存储其他属性。它们不知道任何多边形或连接;它们是素元通过顶点访问的独立元素。
- 素元是几何的元素本身:多边形,曲线,体积等。它们定义几何形态,没有存储坐标直接;取而代之的是它们引用点通过顶点。这样就允许使用一个点在不同素元具有不同属性(例如法线或 UVs,这正是本文开始的地方)。
- 顶点是一个连接的中间层;每个顶点引用一个点,而每个素元存储一个列表以指向顶点。
- 详细是整个几何中的高级级别,存储全球属性可以被整个网格所共享(例如颜色或材料)。
关系类似于:
素 元 -> 顶点 -> 种
因此,网格非常方便的被编辑以及被程序化的处理。
好吧,下一个,看看:
一个点可以被多个素元所共享。而每个素元中使用它的位置被记录为独立顶点。
在这个立方体上,八个点代表角落的坐标,六个素元分别定义了面。在每个面中创建了四个顶点,顶点每个都指向对应的点。最终,总共创建了 24 种 - 每个点的每个用途。
这个几何是如何实现的呢?
- 素 元被简化,从而容易对其进行几何操作。例如,在然后膨胀的过程中,对素元进行切片会变得更加容易。
- UVs 可以在顶点级别上存储。这样一来,每个面在它们的 UV 上都可以有不同的值,而不必在点上复制它们 - 这正是用于创造 UV 岛和 UV 条条纹所需的。
- 当要移动几何时,我们会在点级别上进行工作。当点位置发生更改时,它会影响所有使用它的素元。
- 法线可以在各个级别上进行控制:几何级别上它可以被认为是一个几何值,但是在渲染中,顶点级别上的法线被使用。这种方式可以灵活:可以实现平滑或硬边的群组,设置不同群组的不同顶点的法线。
- 全局参数和材料可以一次性被分配到详细级别 - 整个几何上。
这些属性系统在设计上也是非常重要的。Houdini 中的标准属性集合(例如位置 P、法线 N、颜色 Cd 等)是基于。并且,可以在每个级别上进行自定义 - 详细、种、顶点或素 元。
结果
这是一个zero-GCMesh(意味着在热路径上不需要分配管理资源)使用 Point/Vertex/Primitive 模式 - 8 簇。它不需要三角化:素元保持 N - gon。网格有两种状态:
- NativeDetail:可编辑拓扑表示方式,是一种稀疏结构(存活标志,空白列表)。它支持基本的编辑操作(添加或删除点、顶点、素 元),并允许在点或顶点域上存储法线。
- NativeCompiledDetail:仅供读取的状态中的快照。最终的结果是把“活跃”元素(仅索引)打包到连续的数组中,将索引重新定位,编译属性和资源。在过程中执行三角化。三角化可以通过两个途径之一来完成: 1. 使用预先编译好的三角化类 NativeDetialTriangulator。 2. 在 Unity Mesh 转换期间。 3. 在精确的三角化的要求时在此位置。
素元从 ray 导向器上可选择。颜色属性被应用到素 元上。
一个正在运行的动态立体球的例子。Ray 回显的管线: 1. 1、2、3: 生成 UV 立体球,点上有正确的法线 - 无法渲染。 2. 添加颜色属性。 3. 生成素元和顶点的 BVH。 4. 运行 ray 导向器,找到潜在的素元。 5. 在这几个素元上(如果需要的话,可能是 N- gon)准确定位 ray- 素元交点。7: 生成对应的顶点颜色,形成顶点颜色映射。 8. 生成 Unity Mesh(可以通过转换器也可以通过着色器)的 Bake。
该案例显示了动态球体的 ray 回显。这个案例利用 point-level 属性设置,而不像立方体一样采用 vertex-level。这样也可以避免 2 次交叉的不需要。
使用 Burst 和 Job System,已证明在测试中某些计划用于节点管线的操作(具体数值不明)比 Houdini 的相关实现快 5-10 倍。然而,并非所有工具都是实时的,因此另一部分工具仍然需要在线离线性配置。但本地应用是基于动态编程的。
评论 (0)