zeux/meshoptimizer

GitHub: zeux/meshoptimizer

专注于GPU渲染效率的3D网格优化库,提供顶点缓存优化、网格压缩和LOD生成等算法,使模型更小更快渲染。

Stars: 7494 | Forks: 624

# 🐇 meshoptimizer [![Actions Status](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/944a6e6f70080842.svg)](https://github.com/zeux/meshoptimizer/actions) [![codecov.io](https://codecov.io/github/zeux/meshoptimizer/coverage.svg?branch=master)](https://codecov.io/github/zeux/meshoptimizer?branch=master) [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) [![GitHub](https://img.shields.io/badge/repo-github-green.svg)](https://github.com/zeux/meshoptimizer) ## 目的 当 GPU 渲染三角形网格时,GPU 管线的各个阶段必须处理顶点和索引数据。这些阶段的效率取决于您输入的数据;本库提供的算法有助于针对这些阶段优化网格,以及降低网格复杂度和存储开销的算法。 本库为所有算法提供 C 和 C++ 接口;您可以在 C/C++ 中使用它,或通过 FFI(如 P/Invoke)从其他语言使用。如果您想从 Rust 使用本库,应使用 [meshopt crate](https://crates.io/crates/meshopt)。部分算法的 JavaScript 接口可通过 [meshoptimizer.js](https://www.npmjs.com/package/meshoptimizer) 获取。 与本库同时开发和分发有两个伴生项目:[gltfpack](./gltf/README.md),一个自动优化 glTF 文件的命令行工具;以及 [clusterlod.h](./demo/clusterlod.h),一个用于使用聚类简化实现连续细节层次的单头 C/C++ 库。 ## 安装 meshoptimizer 托管在 GitHub 上;您可以使用 git 下载最新版本: ``` git clone -b v1.1 https://github.com/zeux/meshoptimizer.git ``` 或者,您也可以[从 GitHub 下载 .zip 压缩包](https://github.com/zeux/meshoptimizer/archive/v1.1.zip)。 该库还在多个发行版中以 Linux 软件包形式提供([ArchLinux](https://aur.archlinux.org/packages/meshoptimizer/)、[Debian](https://packages.debian.org/libmeshoptimizer)、[FreeBSD](https://www.freshports.org/misc/meshoptimizer/)、[Nix](https://mynixos.com/nixpkgs/package/meshoptimizer)、[Ubuntu](https://packages.ubuntu.com/libmeshoptimizer)),以及作为 [Vcpkg port](https://github.com/microsoft/vcpkg/tree/master/ports/meshoptimizer)(参见[安装说明](https://learn.microsoft.com/en-us/vcpkg/get_started/get-started))和 [Conan 包](https://conan.io/center/recipes/meshoptimizer)提供。 [gltfpack](./gltf/README.md) 在 [Releases 页面](https://github.com/zeux/meshoptimizer/releases)提供预构建二进制文件,或通过 [npm 包](https://www.npmjs.com/package/gltfpack)获取。推荐使用原生二进制文件,因为它们效率更高且支持纹理压缩。 ## 构建 meshoptimizer 作为 C/C++ 头文件(`src/meshoptimizer.h`)和一组 C++ 源文件(`src/*.cpp`)分发。要将其包含在您的项目中,可以使用以下两种方式之一: * 使用 CMake 构建库(作为独立项目或作为项目的一部分) * 将源文件添加到项目的构建系统中 源文件的组织方式使您无需更改构建系统设置,只需添加您使用的算法对应的源文件。它们在所有主流编译器上无需警告或特殊编译选项即可构建。如果您喜欢合并构建,也可以将源文件连接成单个 `.cpp` 文件并构建它。 要使用 meshoptimizer 函数,只需 `#include` 头文件 `meshoptimizer.h`;库源码是 C++,但头文件兼容 C。 ## 核心管线 优化网格时,为了最大化渲染效率,通常应通过一组优化传递它(顺序很重要!): 1. 索引化 2. 顶点缓存优化 3. (可选)过度绘制优化 4. 顶点获取优化 5. 顶点量化 6. (可选)阴影索引 ### 索引化 本库中的大多数算法假设网格具有顶点缓冲区和索引缓冲区。为了使算法良好工作以及 GPU 高效渲染网格,顶点缓冲区不得有冗余顶点;您可以从未索引的顶点缓冲区生成索引缓冲区,或按如下方式重新索引现有的(可能冗余的)索引缓冲区: 首先,从现有的顶点(以及可选的索引)数据生成重映射表: ``` size_t index_count = face_count * 3; size_t unindexed_vertex_count = face_count * 3; std::vector remap(unindexed_vertex_count); // temporary remap table size_t vertex_count = meshopt_generateVertexRemap(&remap[0], NULL, index_count, &unindexed_vertices[0], unindexed_vertex_count, sizeof(Vertex)); ``` 请注意,在这种情况下我们只有一个未索引的顶点缓冲区;当输入网格有索引缓冲区时,需要将其传递给 `meshopt_generateVertexRemap` 而不是 `NULL`,以及正确的源顶点计数。无论哪种情况,重映射表都是基于输入顶点的二进制等价性生成的,因此生成的网格将以相同方式渲染。二进制等价性考虑所有输入字节,包括填充字节,如果顶点结构有间隙,应将其零初始化。 生成重映射表后,您可以为目标顶点缓冲区(`vertex_count` 个元素)和索引缓冲区(`index_count` 个元素)分配空间并生成它们: ``` meshopt_remapIndexBuffer(indices, NULL, index_count, &remap[0]); meshopt_remapVertexBuffer(vertices, &unindexed_vertices[0], unindexed_vertex_count, sizeof(Vertex), &remap[0]); ``` 然后,您可以通过在原地调用其他函数来进一步优化生成的缓冲区。 `meshopt_generateVertexRemap` 使用顶点数据的二进制等价性,这通常是一个合理的默认值;然而,在某些情况下,某些属性可能存在浮点漂移,导致生成额外顶点。对于这种情况,可能需要在生成重映射之前量化某些属性(最重要的是法线和切线),或使用 `meshopt_generateVertexRemapCustom` 算法,该算法允许通过提供自定义比较函数来比较具有容差的各个属性: ``` size_t vertex_count = meshopt_generateVertexRemapCustom(&remap[0], NULL, index_count, &unindexed_vertices[0].px, unindexed_vertex_count, sizeof(Vertex), [&](unsigned int lhs, unsigned int rhs) -> bool { const Vertex& lv = unindexed_vertices[lhs]; const Vertex& rv = unindexed_vertices[rhs]; return fabsf(lv.tx - rv.tx) < 1e-3f && fabsf(lv.ty - rv.ty) < 1e-3f; }); ``` ### 顶点缓存优化 当 GPU 渲染网格时,它会为每个顶点运行顶点着色器。历史上,GPU 使用小的固定大小后变换缓存(16-32 个顶点),采用不同的替换策略来存储着色器输出并避免冗余着色器调用。现代 GPU 仍然执行顶点重用,但机制截然不同:顶点调用基于输入索引被批处理为线程组,有效重用取决于顶点着色器输出和光栅化器吞吐量等因素。为了最大化重用顶点引用的局部性,您必须像这样重新排序三角形: ``` meshopt_optimizeVertexCache(indices, indices, index_count, vertex_count); ``` 顶点重用的细节因不同的 GPU 架构而异,因此顶点缓存优化使用自适应算法,生成具有良好局部性的三角形序列,在不同 GPU 上效果良好。或者,您可以使用专门针对固定大小 FIFO 缓存进行优化的算法:`meshopt_optimizeVertexCacheFifo`(建议缓存大小为 16)。虽然在大多数 GPU 上产生的性能结果通常较差,但它运行速度快约 2 倍,这可能有利于快速内容迭代。 ### 过度绘制优化 变换顶点后,GPU 将三角形发送进行光栅化,这导致生成像素,这些像素通常首先经过深度测试,通过测试的像素将执行像素着色器以生成最终颜色。随着像素着色器变得越来越昂贵,减少过度绘制变得越来越重要。虽然通常改善过度绘制需要依赖视图的操作,但本库提供了一种算法来重新排序三角形以最小化来自所有方向的过度绘制,您可以在顶点缓存优化后像这样运行它: ``` meshopt_optimizeOverdraw(indices, indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex), 1.05f); ``` 过度绘制优化器需要从顶点读取 float3 类型的位置;上面的代码片段假设顶点将位置存储为 `float x, y, z`。 执行过度绘制优化时,您必须指定浮点阈值参数。算法试图在顶点缓存效率和过度绘制之间保持平衡;阈值决定了算法可以损害顶点缓存命中率的程度,1.05 表示结果比率最多比优化前差 5%。 请注意,根据渲染器结构和目标硬件,优化可能有益也可能无益;例如,具有分块延迟渲染的移动 GPU(PowerVR、Apple)不会从此优化中受益。对于顶点繁重的场景,建议测量性能影响,以确保降低的顶点缓存效率被降低的过度绘制所抵消。 ### 顶点获取优化 确定最终三角形顺序后,我们仍然可以针对内存效率优化顶点缓冲区。在运行顶点着色器之前,GPU 必须从顶点缓冲区获取顶点属性;获取通常由内存缓存支持,因此优化数据以实现内存访问的局部性很重要。您可以通过运行以下代码来完成此操作: ``` meshopt_optimizeVertexFetch(vertices, indices, index_count, vertices, vertex_count, sizeof(Vertex)); ``` 这将重新排序顶点缓冲区中的顶点以尝试提高引用的局部性,并原地重写索引以匹配;如果顶点数据使用多个流存储,则应改用 `meshopt_optimizeVertexFetchRemap`。必须在最终索引缓冲区上执行此优化,因为最佳顶点顺序取决于三角形顺序。 请注意,该算法不尝试精确模拟缓存替换,而只是按使用顺序对顶点进行排序,这通常会产生接近最佳的结果。 ### 顶点量化 为了进一步优化获取顶点数据时的内存带宽,并减少存储网格所需的内存量,将顶点属性量化为更小的类型通常是有益的。虽然此优化技术上可以在管线的任何部分运行(有时作为第一步进行量化可以通过合并几乎相同的顶点来改善索引),但通常在所有其他优化之后运行此优化更容易,因为其中一些需要访问 float3 位置。 量化通常与特定领域相关;通常使用 3 个 8 位整数来量化法线,但您可以使用更高精度的量化(例如,在 10_10_10_2 格式中每个分量使用 10 位),或使用仅 2 个分量的不同编码。对于位置和纹理坐标数据,两种最常见的存储格式是半精度浮点数,以及编码相对于网格 AABB 或 UV 边界矩形的位置的 16 位归一化整数。 这里可能的组合非常多,但本库确实提供了构建块,特别是将浮点值量化为归一化整数以及半精度浮点数的函数。例如,以下是如何使用 10-10-10 SNORM 编码量化法线: ``` unsigned int normal = ((meshopt_quantizeSnorm(v.nx, 10) & 1023) << 20) | ((meshopt_quantizeSnorm(v.ny, 10) & 1023) << 10) | (meshopt_quantizeSnorm(v.nz, 10) & 1023); ``` 以下是如何使用半精度浮点数量化位置: ``` unsigned short px = meshopt_quantizeHalf(v.x); unsigned short py = meshopt_quantizeHalf(v.y); unsigned short pz = meshopt_quantizeHalf(v.z); ``` 由于量化顶点属性通常需要保持其紧凑表示以进行有效传输和存储,因此它们通常在顶点处理期间通过正确配置 GPU 顶点输入以期望归一化整数或半精度浮点数来进行反量化,这通常不需要或只需极少的着色器代码更改。当需要 CPU 反量化时,可以使用 `meshopt_dequantizeHalf` 将半精度值转换回单精度;对于归一化整数格式,反量化只需将 unorm 变体除以 2^N-1,将 snorm 变体除以 2^(N-1)-1。例如,手动反转 `meshopt_quantizeUnorm(v, 10)` 可以通过除以 1023 来完成。 ### 阴影索引 许多渲染管线需要将网格渲染到仅深度目标,例如阴影贴图或深度预传递,除了颜色/G-buffer 目标之外。虽然两种情况使用相同的几何数据是可能的,但减少仅深度渲染的唯一顶点数量可能是有益的,特别是当源几何由于分面着色或光照贴图纹理接缝而具有许多属性接缝时。 为了实现这一点,本库提供了 `meshopt_generateShadowIndexBuffer` 算法,它生成一个可以与原始顶点数据一起使用的第二(阴影)索引缓冲区: ``` std::vector shadow_indices(index_count); // note: this assumes Vertex starts with float3 positions and should be adjusted accordingly for quantized positions meshopt_generateShadowIndexBuffer(&shadow_indices[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(float) * 3, sizeof(Vertex)); ``` 因为顶点数据是共享的,阴影索引应该在顶点/索引数据的其他优化之后进行。但是,可以(并且建议)针对顶点缓存优化生成的阴影索引缓冲区: ``` meshopt_optimizeVertexCache(&shadow_indices[0], &shadow_indices[0], index_count, vertex_count); ``` 在某些情况下,将顶点位置拆分为单独的缓冲区可能有利于最大化仅深度渲染的效率。请注意,上面的示例假设只有位置与阴影渲染相关,但更复杂的材质可能需要将纹理坐标(用于 alpha 测试)或蒙皮数据添加到用作键的顶点部分。如果相关数据不连续,`meshopt_generateShadowIndexBufferMulti` 可用于这些情况。 请注意,对于具有最佳索引和少量属性接缝的网格,阴影索引缓冲区将与原始索引缓冲区非常相似,因此即使渲染管线依赖于仅深度传递,也可能不总是值得生成单独的阴影索引缓冲区。 ## 聚类 虽然传统上网格作为渲染单元,但新的渲染和光线追踪方法开始使用更小的工作单元,例如 cluster 或 meshlet。这允许在如何处理几何体方面有更大的自由度,并可以提高性能和更有效地利用 GPU 硬件。本节介绍设计用于将网格作为 cluster 集合处理的算法。 ### 网格着色 现代 GPU 开始偏离传统的光栅化模型。从 Turing 开始的 NVIDIA GPU 和从 RDNA2 开始的 AMD GPU 提供了一种新的可编程几何管线,该管线不是围绕索引缓冲区和顶点着色器构建,而是围绕 mesh shader 构建——一种新的着色器类型,允许向光栅化器提供一批工作。 在传统网格渲染的上下文中使用 mesh shader 提供了使用各种优化技术的机会,从更有效的顶点重用,到各种形式的剔除(例如 cluster 视锥体或遮挡剔除)和内存压缩,以最大化 GPU 硬件的利用率。除了传统渲染,mesh shader 还提供了更丰富的编程模型,可以比常见的替代方案(如 geometry shader)更有效地合成新几何体。可以通过 Vulkan 或 Direct3D 12 API 访问 mesh shading;请参阅 [Introduction to Turing Mesh Shaders](https://developer.nvidia.com/blog/introduction-turing-mesh-shaders/) 和 [Mesh Shaders and Amplification Shaders: Reinventing the Geometry Pipeline](https://devblogs.microsoft.com/directx/coming-to-directx-12-mesh-shaders-and-amplification-shaders-reinventing-the-geometry-pipeline/) 获取更多信息。 为了有效地将 mesh shader 用于传统渲染,需要将几何体转换为一系列 meshlet;每个 meshlet 代表原始网格的一个小子集,并带有一小组顶点和一个引用 meshlet 中顶点的单独微索引缓冲区。此信息可以直接从 mesh shader 输送到光栅化器。本库提供了为网格创建 meshlet 数据的算法,并且——假设几何体是静态的——可以计算边界信息,该信息可用于执行 cluster 剔除,拒绝屏幕上不可见的 meshlet。 为了生成 meshlet 数据,库提供了 `meshopt_buildMeshlets` 算法,它试图平衡拓扑效率(通过最大化 meshlet 内的顶点重用)与剔除效率(通过最小化 meshlet 半径和三角形方向差异)并生成 GPU 友好的数据。作为替代方案(对于加载时处理可能有用),`meshopt_buildMeshletsScan` 可以使用顶点缓存优化的索引缓冲区作为起点创建 meshlet 数据,通过贪婪地聚合连续三角形直到超过 meshlet 限制。即使不使用锥体剔除,也建议使用 `meshopt_buildMeshlets` 进行离线数据处理。 ``` const size_t max_vertices = 64; const size_t max_triangles = 126; // note: in v0.25 or prior, max_triangles needs to be divisible by 4 const float cone_weight = 0.0f; size_t max_meshlets = meshopt_buildMeshletsBound(indices.size(), max_vertices, max_triangles); std::vector meshlets(max_meshlets); std::vector meshlet_vertices(indices.size()); std::vector meshlet_triangles(indices.size()); // note: in v0.25 or prior, use indices.size() + max_meshlets * 3 size_t meshlet_count = meshopt_buildMeshlets(meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(), indices.data(), indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex), max_vertices, max_triangles, cone_weight); ``` 为了生成 meshlet 数据,`max_vertices` 和 `max_triangles` 需要设置在硬件支持的范围内;对于 NVIDIA,建议值为 64 和 126。如果不使用 cluster 锥体剔除,`cone_weight` 应保留为 0,并设置为 0 到 1 之间的值以平衡锥体剔除效率与其他形式的剔除(如视锥体或遮挡剔除)(`0.25` 是合理的默认值)。 每个生成的 meshlet 引用 `meshlet_vertices` 和 `meshlet_triangles` 数组的一部分;数组针对最坏情况进行了过度分配,因此建议在将其保存为资产/上传到 GPU 之前修剪它们: ``` const meshopt_Meshlet& last = meshlets[meshlet_count - 1]; meshlet_vertices.resize(last.vertex_offset + last.vertex_count); meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3); meshlets.resize(meshlet_count); ``` 根据应用程序的不同,存储数据的其他策略可能有用;例如,`meshlet_vertices` 充当原始顶点缓冲区的索引,但可能值得为每个 meshlet 生成一个迷你顶点缓冲区以消除访问顶点数据时的额外间接性,或者可能希望压缩顶点数据,因为每个 meshlet 中的顶点可能在空间上非常连贯。 为了获得最佳性能,建议通过在顶点和索引数据上调用 `meshopt_optimizeMeshlet` 来进一步单独优化每个 meshlet 以获得更好的三角形和顶点局部性: ``` meshopt_optimizeMeshlet(&meshlet_vertices[m.vertex_offset], &meshlet_triangles[m.triangle_offset], m.triangle_count, m.vertex_count); ``` 不同的应用程序将选择不同的 meshlet 渲染策略;在支持 mesh shading 的 GPU 上,meshlet 可以直接渲染;例如,`VK_EXT_mesh_shader` 扩展的基本 GLSL 着色器可能如下所示(为简洁起见省略了部分): ``` layout(binding = 0) readonly buffer Meshlets { Meshlet meshlets[]; }; layout(binding = 1) readonly buffer MeshletVertices { uint meshlet_vertices[]; }; layout(binding = 2) readonly buffer MeshletTriangles { uint8_t meshlet_triangles[]; }; void main() { Meshlet meshlet = meshlets[gl_WorkGroupID.x]; SetMeshOutputsEXT(meshlet.vertex_count, meshlet.triangle_count); for (uint i = gl_LocalInvocationIndex; i < meshlet.vertex_count; i += gl_WorkGroupSize.x) { uint index = meshlet_vertices[meshlet.vertex_offset + i]; gl_MeshVerticesEXT[i].gl_Position = world_view_projection * vec4(vertex_positions[index], 1); } for (uint i = gl_LocalInvocationIndex; i < meshlet.triangle_count; i += gl_WorkGroupSize.x) { uint offset = meshlet.triangle_offset + i * 3; gl_PrimitiveTriangleIndicesEXT[i] = uvec3( meshlet_triangles[offset], meshlet_triangles[offset + 1], meshlet_triangles[offset + 2]); } } ``` 生成 meshlet 数据后,可以为每个 meshlet 生成额外的数据,这些数据可以保存并在运行时用于执行 cluster 剔除,其中如果保证每个 meshlet 不可见,则可以将其丢弃。要生成数据,可以使用 `meshopt_computeMeshletBounds`: ``` meshopt_Bounds bounds = meshopt_computeMeshletBounds(&meshlet_vertices[m.vertex_offset], &meshlet_triangles[m.triangle_offset], m.triangle_count, &vertices[0].x, vertices.size(), sizeof(Vertex)); ``` 生成的 `bounds` 值可用于使用包围球执行视锥体或遮挡剔除,或使用锥体轴/角度执行锥体剔除(如果保证所有三角形从相机角度看都是背向的,这将拒绝整个 meshlet): ``` if (dot(normalize(cone_apex - camera_position), cone_axis) >= cone_cutoff) reject(); ``` Cluster 剔除理想情况下应以低于 mesh shading 的频率运行,要么使用 amplification/task shader,要么使用单独的 compute dispatch。 默认情况下,meshlet 构建器尝试形成完整的 meshlet,即使这需要将网格的断开区域合并到单个 meshlet 中。在某些情况下,例如层次细节级别,或使用高级剔除时,可能优先考虑 meshlet 中三角形的空间局部性,即使这会导致部分填充的 meshlet。为此,可以使用 `meshopt_buildMeshletsFlex` 函数代替 `meshopt_buildMeshlets`;它提供两个三角形限制,`min_triangles` 和 `max_triangles`,并使用额外的配置参数 `split_factor`(建议值为 2.0)来决定增加 meshlet 半径是否值得在 meshlet 中容纳更多三角形。使用此函数时,必须使用带有 `min_triangles` 参数而不是 `max_triangles` 的 `meshopt_buildMeshletsBound` 计算 meshlet 数量的最坏情况界限。 ### 聚类光线追踪 除了光栅化,meshlet 也可用于光线追踪。带有最近驱动程序的 Turing 起的 NVIDIA GPU 支持 cluster 加速结构(通过 `VK_NV_cluster_acceleration_structure` 扩展 / NVAPI);无需构建传统的 BLAS,可以为每个 meshlet 构建 cluster 加速结构并组合成单个聚类 BLAS。虽然这目前会导致静态几何的光线追踪性能降低(传统 BLAS 可能更适合静态几何),但它允许更新单个 cluster 而无需重建或调整整个 BLAS,这对于网格变形或层次细节级别很有用。 当将 meshlet 用于光线追踪时,其性能特征与使用光栅化渲染网格时不同。对于光线追踪,首选具有最佳空间划分且最小化射线三角形相交测试的 cluster,而对于光栅化,理想的 cluster 是在顶点限制内具有最大三角形计数的 cluster。 为了生成针对光线追踪优化的 meshlet,本库提供了 `meshopt_buildMeshletsSpatial` 算法,该算法使用表面积启发式(SAH)构建 cluster 以产生对光线追踪友好的 cluster 分布: ``` const size_t max_vertices = 64; const size_t min_triangles = 16; const size_t max_triangles = 64; const float fill_weight = 0.5f; size_t max_meshlets = meshopt_buildMeshletsBound(indices.size(), max_vertices, min_triangles); // note: use min_triangles to compute worst case bound std::vector meshlets(max_meshlets); std::vector meshlet_vertices(indices.size()); std::vector meshlet_triangles(indices.size()); // note: in v0.25 or prior, use indices.size() + max_meshlets * 3 size_t meshlet_count = meshopt_buildMeshletsSpatial(meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(), indices.data(), indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex), max_vertices, min_triangles, max_triangles, fill_weight); ``` 该算法使用 SAH 递归地将三角形细分为类似 BVH 的层次结构,以实现最佳空间划分,同时平衡 cluster 大小;这导致与 `meshopt_buildMeshlets` 生成的 cluster 相比,光线追踪效率显着提高的 cluster,但仍可用于光栅化(例如,构建 visibility buffer 或 G-buffer)。 `min_triangles` 和 `max_triangles` 参数控制每个 cluster 允许的三角形范围。为了获得最佳光线追踪性能,`min_triangles` 最多应为 `max_triangles/2`(或理想情况下为 `max_triangles/4`),以便为算法提供足够的自由度来生成高质量的空间划分。对于由于法线或 UV 不连续性而几乎没有接缝的网格,当光栅化性能是关注点时,建议使用等于 `max_triangles` 的 `max_vertices`;对于有许多接缝的网格或主要将 meshlet 用于光线追踪的渲染器,应使用更高的 `max_vertices` 值,因为它确保更多 cluster 可以充分利用三角形限制。 `fill_weight` 参数(通常在 0 到 1 之间,尽管可以使用高于 1 的值来更多地优先考虑 cluster 填充)控制纯 SAH 优化与三角形利用率之间的权衡。值为 0 将纯粹针对 SAH 进行优化,从而产生最佳光线追踪性能但可能更小的 cluster。0.25 到 0.75 之间的值通常提供 SAH 质量与三角形计数的良好平衡。 当生成的 meshlet 用于生成特定于硬件的加速结构时,使用快速跟踪(例如 `VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR`)构建会产生最佳性能;如果构建性能很重要,使用 `meshopt_optimizeMeshlet` 可以帮助在使用快速构建(例如 `VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR`)时提高光线追踪性能,尽管跟踪性能仍将低于快速跟踪构建。 ### 点云聚类 两种 meshlet 算法都旨在用于三角形网格。在某些情况下,将点云拆分为固定大小的 cluster 可能很有用;生成的点 cluster 可以通过 mesh 或 compute shader 渲染,或者生成的细分可用于并行化点处理同时保持点的局部性。为此,本库提供了 `meshopt_spatialClusterPoints` 算法: ``` const size_t cluster_size = 256; std::vector index(mesh.vertices.size()); meshopt_spatialClusterPoints(&index[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), cluster_size); ``` 生成的索引缓冲区可用于直接处理点,或将点数据重组为平面连续数组。索引缓冲区中每个连续的 `cluster_size` 点块引用单个 cluster,如果总点数不是 `cluster_size` 的倍数,则只有最后一个 cluster 包含较少的点。请注意,索引缓冲区不是重映射表,因此不能使用 `meshopt_remapVertexBuffer` 来展平点数据。 ### 聚类分区 处理聚类几何时,将 cluster 组织成更大的组(分区)以便更有效地处理或工作负载分布可能是有益的。本库提供了一种算法,可以将 cluster 划分为大小相似且优先考虑局部性的组: ``` const size_t partition_size = 24; std::vector cluster_partitions(cluster_count); size_t partition_count = meshopt_partitionClusters(&cluster_partitions[0], &cluster_indices[0], total_index_count, &cluster_index_counts[0], cluster_count, &vertices[0].x, vertex_count, sizeof(Vertex), partition_size); ``` 该算法将每个 cluster 分配给一个分区,以目标分区大小为目标,同时优先考虑拓扑局部性(共享顶点)和空间局部性。生成的分区可用于更有效地批量处理 cluster,或用于类似于 Nanite 的层次简化方案。 如果两个 cluster 引用相同的索引,则认为它们在拓扑上是相邻的。在某些情况下,使用 `meshopt_generateShadowIndexBuffer` 处理索引(或使用 `meshopt_generatePositionRemap` 生成的重映射表手动重映射它们)可能会有所帮助,这允许 cluster 被视为相邻的,即使边界顶点由于属性不连续性而具有不同的索引。 如果指定了顶点位置(不是 `NULL`),空间局部性将影响合并 cluster 的优先级;否则,算法将仅依赖拓扑连接,并且不会将断开连接的 cluster 合并到同一分区中,这可能会导致某些输入的分区较小。 分区后,目标数组中的每个元素都包含相应 cluster 的分区 ID(范围从 0 到返回的分区计数减 1)。请注意,分区可能小于或大于目标大小;给定目标大小,当前返回的最大分区大小为 `target + target / 3`。 ## 网格压缩 如果存储大小或传输带宽很重要,您可能需要额外压缩顶点和索引数据。虽然有几个网格压缩库可用,如 Google Draco,但它们通常旨在以干扰顶点/索引顺序(这使网格在 GPU 上渲染效率低下)或解压缩性能为代价最大化压缩比。它们也经常不支持自定义游戏就绪的量化顶点格式,因此需要在加载后重新量化数据,从而引入额外的量化误差并使解码变慢。 或者,您可以使用通用压缩库(如 zstd 或 Oodle)来压缩顶点/索引数据——但是这些压缩器并非旨在利用顶点/索引数据中的冗余,因此压缩率可能不尽如人意。 为此,本库提供了“编码”顶点和索引数据的算法。编码结果通常明显小于初始数据,并且仍可使用通用压缩器压缩——因此您可以直接存储编码数据(以适中的压缩比和最大的解码性能),或使用 LZ4/zstd/Oodle 进一步压缩以最大化压缩比。 ### 顶点压缩 本库提供了一种无损算法来编码/解码顶点数据。要编码顶点,您需要分配一个目标缓冲区(使用最坏情况界限)并调用编码函数: ``` std::vector vbuf(meshopt_encodeVertexBufferBound(vertex_count, sizeof(Vertex))); vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), vertices, vertex_count, sizeof(Vertex))); ``` 要在运行时解码数据,调用解码函数: ``` int res = meshopt_decodeVertexBuffer(vertices, vertex_count, sizeof(Vertex), &vbuf[0], vbuf.size()); assert(res == 0); ``` 请注意,顶点编码假设顶点缓冲区已针对顶点获取进行了优化,并且顶点已量化。将未优化的数据输入编码器可能会导致糟糕的压缩比。编解码器本身是无损的——唯一的损失步骤是您可能在编码之前应用的量化/重排序或过滤器。此外,如果顶点数据包含填充字节,则应将其零初始化,以确保编码器不需要存储未初始化的数据。 解码器经过高度优化,可以直接针对写组合内存;您可以预期它在现代桌面 CPU 上以 3-6 GB/s 的速度运行。压缩比取决于数据;顶点数据压缩比通常约为 2-4 倍(与已量化和最佳打包的数据相比)。通用无损压缩器可以以牺牲一些解码性能为代价进一步提高压缩比。 顶点编解码器试图利用连续顶点的固有局部性并识别在连续顶点中重复的位模式。通常,顶点缓存 + 顶点获取提供了相当局部的顶点遍历顺序;如果没有索引缓冲区,建议对顶点进行空间排序(通过 `meshopt_spatialSortRemap`)以提高压缩比。 在编码顶点数据时正确指定步幅至关重要;然而,对于压缩比而言,顶点是交错还是去交错并不重要,因为编解码器在内部执行完全的字节去交错。每个流的步幅必须是 4 字节的倍数。 为了获得最佳压缩结果,应将值量化为小整数。使用非 8 倍数的位数可能很有价值。例如,不要使用 16 位来表示纹理坐标,而是使用 12 位整数并在着色器中除以 4095。或者,使用半精度浮点数通常可以获得良好的结果。 对于单精度浮点数据,建议使用 `meshopt_quantizeFloat` 来消除尾数低位中的熵;为了获得最佳结果,请考虑使用 15 位或 7 位进行极限压缩。 对于法线或切线向量,建议使用八面体编码而不是三个分量,因为它减少了冗余;同样,考虑每个分量使用 10-12 位而不是 16 位。 当数据位打包时,指定压缩级别 3(通过 `meshopt_encodeVertexBufferLevel`)可以通过在分量之间重新分配位来进一步改善压缩。 ### 索引压缩 本库还提供了编码/解码索引数据的算法。要编码三角形索引,您需要分配一个目标缓冲区(使用最坏情况界限)并调用编码函数: ``` std::vector ibuf(meshopt_encodeIndexBufferBound(index_count, vertex_count)); ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), indices, index_count)); ``` 要在运行时解码数据,调用解码函数: ``` int res = meshopt_decodeIndexBuffer(indices, index_count, &ibuf[0], ibuf.size()); assert(res == 0); ``` 请注意,索引编码假设索引缓冲区已针对顶点缓存和顶点获取进行了优化。将未优化的数据输入编码器将导致糟糕的压缩比。编解码器保留三角形的顺序,但是它可以旋转每个三角形以提高压缩比(这意味着引发顶点可能会更改)。 解码器经过高度优化,可以直接针对写组合内存;您可以预期它在现代桌面 CPU 上以 3-6 GB/s 的速度运行。 索引编解码器的目标最佳情况是每个三角形 1 个字节(比原始 16 位索引数据小 6 倍);在真实世界的网格上,通常达到每个三角形 1-1.2 字节。为了达到这一点,索引数据需要针对顶点缓存和顶点获取进行优化。不破坏三角形局部性的优化(如过度绘制)可以在其间安全使用。 为了进一步减少数据大小,在针对顶点缓存进行优化时,可以使用 `meshopt_optimizeVertexCacheStrip` 代替 `meshopt_optimizeVertexCache`。这牺牲了一些点变换效率以换取更小的索引(有时是顶点)数据。 当引用的顶点索引不是连续的时,索引编解码器将使用每个索引约 2 个字节。当引用的顶点是顶点缓冲区的稀疏子集时,例如在编码 LOD 时,可能会发生这种情况。在这种情况下,通用压缩可能特别有帮助。 索引缓冲区编解码器仅支持三角形列表拓扑;当编码三角形带或线列表时,请改用 `meshopt_encodeIndexSequence`/`meshopt_decodeIndexSequence`。此编解码器通常将索引编码为每索引约 1 个字节,但使用通用压缩器进一步压缩结果可以将结果提高到每索引 1-3 位。 ### Meshlet 压缩 当使用 mesh shading 或聚类光线追踪时,meshlet 顶点引用和三角形数据可以类似于索引数据进行压缩。本库提供了一个专用的编解码器,利用 meshlet 数据中固有的局部性。与处理整个缓冲区的顶点和索引缓冲区编解码器不同,meshlet 编解码器独立编码每个 meshlet;这允许应用程序在构建运行时存储方面有更大的灵活性,并在解码期间调整解码数据。这也意味着在某些应用程序中,如果解码期间尚不可用,则需要将描述 meshlet 的额外数据(顶点/三角形计数、编码大小)编码到 meshlet 流中。 要编码 meshlet,您需要分配一个目标缓冲区(使用最坏情况界限)并使用 `meshopt_buildMeshlets` 生成的顶点索引引用和微索引缓冲区调用编码函数: ``` std::vector mbuf(meshopt_encodeMeshletBound(max_vertices, max_triangles)); for (const meshopt_Meshlet& m : meshlets) { size_t msize = meshopt_encodeMeshlet(&mbuf[0], mbuf.size(), &meshlet_vertices[m.vertex_offset], m.vertex_count, &meshlet_triangles[m.triangle_offset], m.triangle_count); // write m.vertex_count, m.triangle_count, msize and mbuf[0..msize-1] to the output stream } ``` 要在运行时解码数据,调用解码函数: ``` uint16_t* vertices = ...; uint8_t* triangles = ...; // automatically deduces `vertex_size=2` and `triangle_size=3` based on pointer types int res = meshopt_decodeMeshlet(vertices, m.vertex_count, triangles, m.triangle_count, stream, encoded_size); assert(res == 0); ``` 顶点索引引用可以解码为 16 位或 32 位整数;三角形数据可以解码为每个三角形 3 个字节(匹配 `meshopt_buildMeshlets` 输出格式)或每个三角形 32 位整数(索引打包为 `a | (b << 8) | (c << 16)` 且顶部字节未使用)。输出缓冲区必须有 4 字节对齐的可用空间;例如,使用每个三角形 3 个字节解码 3 个三角形流需要能够向输出三角形数组写入 12 个字节。 使用 C++ API 时,`meshopt_decodeMeshlet` 将根据顶点和三角形指针的类型自动推断元素大小;使用 C API 时,需要显式指定大小。 解码器经过高度优化,可以直接针对写组合内存;您可以预期它在现代桌面 CPU 上以 7-10 GB/s 的速度运行。 请注意,meshlet 编码假设 meshlet 数据已优化;meshlet 应在编码前使用级别 1 或更高级别(推荐 3 以改善压缩)的 `meshopt_optimizeMeshletLevel` 进行处理。此外,顶点引用应具有高度的引用局部性;这可以通过从针对顶点缓存/获取优化的网格构建 meshlet,或使用 `meshopt_optimizeVertexFetch` 线性化顶点引用数据并重新排序顶点缓冲区来实现。将未优化的数据输入编码器将导致糟糕的压缩比。编解码器保留三角形的顺序,但是它可以旋转每个三角形以提高压缩比(这意味着引发顶点可能会更改)。 支持没有顶点引用的 meshlet;在编码和解码期间传递 `NULL` 顶点和 `0` 顶点计数将生成仅包含三角形数据的编码 meshlet。请注意,解码期间提供的参数必须与编码期间使用的参数匹配;如果 meshlet 是使用顶点引用编码的,则必须使用相同数量的顶点引用进行解码。 meshlet 编解码器针对三角形数据的目标是每个三角形 5-7 位;当编码顶点引用时,编码大小很大程度上取决于引用的线性程度,但通常总体上看到每个三角形 9-12 位。为了进一步减少压缩大小,可以使用通用压缩器压缩生成的编码数据,通常总体上达到每个三角形 5-8 位;请注意,在这种情况下,通用压缩器应一次应用于具有许多编码 meshlet 的流,以摊销其开销。 ### 点云压缩 顶点编码算法可用于压缩任意属性数据流;除了三角形网格之外,另一个用例是点云数据。通常,点云带有位置、颜色和可能的其他属性,但没有隐含的点顺序。 为了有效地压缩点云,建议首先使用空间排序算法通过对点进行排序来预处理点: ``` std::vector remap(point_count); meshopt_spatialSortRemap(&remap[0], positions, point_count, sizeof(vec3)); // for each attribute stream meshopt_remapVertexBuffer(positions, positions, point_count, sizeof(vec3), &remap[0]); ``` 之后,生成的数组应被量化(例如,对位置使用 16 位定点数,对颜色分量使用 8 位),结果可以使用 `meshopt_encodeVertexBuffer` 压缩,如上一节所述。要解压缩,`meshopt_decodeVertexBuffer` 将恢复量化数据,可以直接使用或转换回原始浮点数据。压缩比取决于源数据的性质,对于彩色点,通常每个点 35-40 位。 ### 顶点过滤器 为了进一步利用某些顶点数据的固有结构,可以使用以有损方式编码和解码数据的过滤器。这类似于量化,但可以在不更改着色器代码的情况下使用。解码后,需要反转过滤器变换。对于原生游戏引擎管线,通常更优化的是仔细预量化和预变换顶点数据,但有时(例如,以 glTF 格式序列化数据时),这不是一个实用的选择,过滤器更方便。本库提供四个过滤器: - 八面体过滤器(`meshopt_encodeFilterOct`/`meshopt_decodeFilterOct`)使用八面体编码编码量化向量。可以使用 2 到 16 之间的任意位数,每个向量 4 字节或 8 字节。 - 四元数过滤器(`meshopt_encodeFilterQuat`/`meshopt_decodeFilterQuat`)编码量化的四元数向量;这可用于编码旋转或切线帧。可以使用 4 到 16 之间的任意位数,每个向量 8 字节。 - 指数过滤器(`meshopt_encodeFilterExp`/`meshopt_decodeFilterExp`)编码单精度浮点向量;这可用于更有效地编码任意浮点数据。除了任意位数(<= 24)外,过滤器还接受一个“模式”参数,允许指定如何执行指数共享以权衡压缩比和质量: - `meshopt_EncodeExpSeparate` 不共享指数并产生最大的输出 - `meshopt_EncodeExpSharedVector` 在同一向量的不同分量之间共享指数 - `meshopt_EncodeExpSharedComponent` 在不同向量的同一分量之间共享指数 - `meshopt_EncodeExpClamped` 不共享指数,但限制指数范围以减少指数熵 - 颜色过滤器(`meshopt_encodeFilterColor`/`meshopt_decodeFilterColor`)使用 YCoCg 编码编码量化颜色。可以使用 2 到 16 之间的任意位数,每个向量 4 字节或 8 字节。 请注意,所有过滤器都是有损的,并且要求数据去交错,每个流一个属性;这促进了过滤器解码器的高效 SIMD 实现,在现代桌面 CPU 上以 5-10 GB/s 的速度解码,使整体解压缩速度更接近原始顶点编解码器。 ### 版本控制和兼容性 对于点版本,提供以下数据兼容性保证(对开发分支*不*提供保证): - 使用旧版本库编码的数据始终可以使用新版本解码; - 使用新版本库编码的数据可以使用旧版本解码,前提是正确设置了编码版本;如果编码数据的二进制稳定性很重要,请使用 `meshopt_encodeVertexVersion` 和 `meshopt_encodeIndexVersion` “固定”数据版本(或 `meshopt_encodeVertexBufferLevel` 的 `version` 参数)。 默认情况下,顶点数据编码为格式版本 1(与 meshoptimizer v0.23+ 兼容),索引数据编码为格式版本 1(与 meshoptimizer v0.14+ 兼容)。解码数据时,解码器将自动从数据头检测版本。 ## 简化 目前介绍的所有算法都不会影响视觉外观,除了量化具有最小的受控影响。然而,从根本上说,降低网格渲染或传输成本的最有效方法是减少网格中三角形的数量。 ### 基本简化 本库提供了一个简化算法 `meshopt_simplify`,用于减少网格中三角形的数量。给定顶点和索引缓冲区,它生成使用顶点缓冲区中现有顶点的第二个索引缓冲区。此索引缓冲区可直接用于使用原始顶点缓冲区进行渲染(最好在使用 `meshopt_optimizeVertexCache` 进行顶点缓存优化之后),或者可以使用 `meshopt_optimizeVertexFetch` 生成使用最佳顶点数量和顺序的新紧凑顶点/索引缓冲区。 ``` float threshold = 0.2f; size_t target_index_count = size_t(index_count * threshold); float target_error = 1e-2f; std::vector lod(index_count); float lod_error = 0.f; lod.resize(meshopt_simplify(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex), target_index_count, target_error, /* options= */ 0, &lod_error)); ``` 目标误差是与原始网格偏差的近似度量,使用归一化到 `[0..1]` 范围的距离(例如,`1e-2f` 意味着简化器将尝试将误差保持在网格范围的 1% 以下)。请注意,简化器尝试以最小误差生成请求的索引数量,但由于拓扑限制和误差限制,不能保证达到目标索引计数,并且可能会提前停止。 要禁用误差限制,可以将 `target_error` 设置为 `FLT_MAX`。这使得简化器更有可能达到目标索引计数,但它可能会生成看起来与原始网格显着不同的网格,因此需要使用结果误差来控制查看距离。相反,将 `target_index_count` 设置为 0 将在指定的误差限制内尽可能简化输入网格;这对于生成应在给定查看距离下看起来良好的 LOD 很有用。 该算法遵循原始网格的拓扑,试图保留属性接缝、边界和整体外观。对于具有不一致拓扑或许多接缝(例如分面网格)的网格,可能会导致简化器“卡住”并且无法完全简化网格。因此,相同的顶点必须“焊接”在一起,即输入顶点缓冲区不包含重复项,这一点至关重要。此外,可能值得在不考虑不那么关键且可以稍后重建的顶点属性的情况下焊接顶点,或者使用下面描述的“宽松”模式。 或者,本库提供了另一种简化算法 `meshopt_simplifySloppy`,它不遵循原始网格的拓扑。这意味着它不保留属性接缝或边界,但它可以折叠太小而无关紧要的内部细节,因为它可以合并在拓扑上不相交但在空间上接近的网格特征。通常,与 `meshopt_simplify` 相比,此算法生成几何质量较差和属性质量较差的网格。 该算法还可以返回生成的归一化偏差,可用于根据屏幕大小或立体角选择正确的细节级别;误差可以通过乘以 `meshopt_simplifyScale` 返回的比例因子转换为对象空间。例如,给定具有预计算 LOD 和预缩放误差的网格,可以计算屏幕空间归一化误差并用于 LOD 选择: ``` // lod_factor can be 1 or can be adjusted for more or less aggressive LOD selection float d = max(0, distance(camera_position, mesh_center) - mesh_radius); float e = d * (tan(camera_fovy / 2) * 2 / screen_height); // 1px in mesh space bool lod_ok = e * lod_factor >= lod_error; ``` 当生成全部使用原始顶点缓冲区的一系列 LOD 网格时,必须注意优化顶点顺序,以免惩罚仅能够变换连续顶点缓冲区范围的移动 GPU 架构。在这种情况下,建议首先针对顶点缓存优化每个 LOD,然后从最粗糙的 LOD(三角形最少的 LOD)开始将所有 LOD 组装在一个大型索引缓冲区中,并在最终的大型索引缓冲区上调用 `meshopt_optimizeVertexFetch`。这将确保较粗糙的 LOD 需要较小的顶点范围,并且在顶点获取和变换方面是高效的。 ### 属性感知简化 虽然 `meshopt_simplify` 默认情况下感知属性不连续性(并通过提供的索引缓冲区推断它们)并试图保留它们,但提供有关属性值的信息可能很有用。这允许简化器考虑属性误差,这可以改善着色(通过使用顶点法线)、纹理变形(通过使用纹理坐标),并且可能需要在首先不使用纹理时保留顶点颜色。这可以通过使用带有属性值和权重因子的简化函数变体 `meshopt_simplifyWithAttributes` 来完成: ``` const float nrm_weight = 0.5f; const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight}; std::vector lod(index_count); float lod_error = 0.f; lod.resize(meshopt_simplifyWithAttributes(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex), &vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL, target_index_count, target_error, /* options= */ 0, &lod_error)); ``` 属性作为单独的缓冲区传递(在上面的示例中,它是同一顶点缓冲区的子集),并应存储为连续的浮点数;属性权重用于控制每个属性在简化过程中的重要性。对于法线和顶点颜色等归一化属性,1.0 左右的权重通常是合适的;在内部,距离 `d` 上的属性值变化 `1/weight` 大致相当于位置变化 `d`。使用更高的权重可能适合以位置质量为代价保留属性质量。如果属性具有不同的比例(例如 [0..255] 范围内的未归一化顶点颜色),则权重应除以比例因子(在此示例中为 1/255)。 在属性集中包含纹理坐标是可选的,因为简化通常默认情况下相当好地保留纹理质量;如果包含,通常根据 UV 密度使用 10-100 左右的权重是合适的。也可以通过将权重设置为 UV 的倒数平均密度来自动计算权重,这可以在网格中的所有三角形上计算为 `1/sqrt(average UV area)` = `1/sqrt(sum(abs(uv area)) / triangle count)`,必要时按常数因子缩放。 目标误差和结果误差都结合了位置误差和属性误差,因此假设仔细选择权重,误差可用于在考虑属性质量的同时控制 LOD。 ### 宽松简化 默认情况下,`meshopt_simplify` 保留从提供的索引缓冲区推断的属性不连续性。对于有许多接缝的网格,简化器可能会“卡住”并且无法完全简化网格,因为它无法跨属性接缝折叠顶点。这对于具有分面法线(平面着色)的网格尤其成问题,因为简化器可能根本无法减少三角形计数。`meshopt_SimplifyPermissive` 选项放宽了这些限制,允许简化器在结果误差可接受时跨属性不连续性折叠顶点: ``` std::vector lod(index_count); float lod_error = 0.f; lod.resize(meshopt_simplifyWithAttributes(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex), &vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL, target_index_count, target_error, /* options= */ meshopt_SimplifyPermissive, &lod_error)); ``` 为了保持外观,强烈建议将此选项与属性感知简化一起使用,如上所示,允许简化器保持属性质量。在此模式下,通常需要有选择地保留某些属性接缝,例如 UV 接缝或锐利折痕。这可以通过使用带有为各个顶点设置的 `meshopt_SimplifyVertex_Protect` 标志的 `vertex_lock` 数组来实现,以保护特定的不连续性。要填充此数组,请使用 `meshopt_generatePositionRemap` 为具有相同位置的顶点创建映射表,然后将每个顶点与重映射顶点进行比较以确定哪些属性不同: ``` std::vector remap(vertices.size()); meshopt_generatePositionRemap(&remap[0], &vertices[0].px, vertices.size(), sizeof(Vertex)); std::vector locks(vertices.size()); for (size_t i = 0; i < vertices.size(); ++i) { unsigned int r = remap[i]; if (r != i && (vertices[r].tx != vertices[i].tx || vertices[r].ty != vertices[i].ty)) locks[i] |= meshopt_SimplifyVertex_Protect; // protect UV seams } ``` 这种方法提供了对保留哪些不连续性的精细控制。宽松模式与选择性锁定相结合,在简化质量和属性保留之间提供了平衡,通常对于相同的目标三角形计数产生更高质量的 LOD(并且与 `meshopt_simplifySloppy` 相比质量显着更高)。 ### 带顶点更新的简化 迄今为止描述的所有简化函数都重用原始顶点缓冲区并仅生成新的索引缓冲区。这意味着生成的网格将具有与原始网格相同的顶点位置和属性;这对于最小化内存消耗是最佳的,对于高度详细的网格通常提供良好的质量。然而,对于更激进的简化以保持视觉质量,可能需要调整顶点数据以获得最佳外观。这可以通过使用更新顶点位置和属性的简化函数变体 `meshopt_simplifyWithUpdate` 来完成: ``` indices.resize(meshopt_simplifyWithUpdate(&indices[0], indices.size(), &vertices[0].px, vertices.size(), sizeof(Vertex), &vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL, target_index_count, target_error, /* options= */ 0, &result_error)); ``` 与 `meshopt_simplify`/`meshopt_simplifyWithAttributes` 不同,此函数更新索引缓冲区以及顶点位置和属性。生成的索引仍引用原始顶点缓冲区;任何未传递给简化器的属性都可以保持不变。然而,由于 `vertices` 的原始内容对于渲染原始网格不再有效,应使用 `meshopt_optimizeVertexFetch` 生成新的紧凑顶点/索引缓冲区(在使用 `meshopt_optimizeVertexCache` 优化索引数据之后)。如果原始数据很重要,则应在调用此函数之前将其复制。 由于顶点位置已更新,这可能需要更新一些在使用原始顶点缓冲区时以前可以保持原样的属性。值得注意的是,纹理坐标需要更新以避免纹理扭曲;因此,强烈建议将纹理坐标包含在传递给简化器的属性数据中。对于要更新的属性,相应的属性权重不得为零;对于纹理坐标,在这种情况下通常 1.0 的权重就足够了(尽管可以使用更高或依赖于网格的权重与此函数或其他函数一起使用以减少 UV 拉伸)。 具有特定约束(如法线和颜色)的属性应在函数返回新数据后重新归一化或限制。骨骼索引/权重等属性不必更新即可获得合理的结果(但通过 `meshopt_SimplifyRegularize` 进行正则化可能仍有助于保持变形质量)。如果骨骼权重*作为*属性提供,则需要在更新后将其限制并重新归一化,以确保它们继续加起来为 1。 为链中的每个 LOD 使用唯一的顶点数据可以提高视觉质量,但代价是使用的顶点内存大约翻倍(如果每个 LOD 使用的三角形是前一个 LOD 的一半)。为了减少内存占用,可以将 `meshopt_simplifyWithAttributes` 用于链中的第一个或两个 LOD,并且仅对其余部分切换到 `meshopt_simplifyWithUpdate`。在这种情况下,与前面描述的 `meshopt_simplify` 的使用类似,必须注意优化原始顶点缓冲区中的顶点。 ### 高级简化 `meshopt_simplify*` 函数公开了其他选项和参数,可用于更详细地控制简化过程。 对于基本自定义,可以通过 `options` 位掩码传递许多选项来调整简化器的行为: - `meshopt_SimplifyLockBorder` 限制简化器折叠网格边界上的边。这对于独立简化网格子集很有用,以便 LOD 可以组合而不会引入裂纹。 - `meshopt_SimplifyErrorAbsolute` 将误差度量从相对更改为绝对,既用于输入误差限制,也用于结果误差。这可以代替 `meshopt_simplifyScale` 使用。 - `meshopt_SimplifySparse` 假设输入索引是网格的稀疏子集,改善简化性能。这对于独立简化小型网格子集很有用,旨在用于 meshlet 简化。为了一致性,当需要稀疏简化时,建议使用绝对误差,因为此标志更改了相对误差的含义。 - `meshopt_SimplifyPrune` 允许简化器移除隔离分量,而不管分量内部的拓扑限制。这通常建议用于全网格简化,因为它可以提高质量并减少三角形计数;请注意,使用此选项,连接到锁定顶点的三角形可能会作为其分量的一部分被移除。 - `meshopt_SimplifyRegularize` 在简化过程中产生更规则的三角形大小和形状,但会牺牲一些几何质量。这可以改善变形(如蒙皮)下的几何质量。可以使用 `meshopt_SimplifyRegularizeLight` 代替此标志以使用较小的正则化因子,减少对几何质量的影响。 - `meshopt_SimplifyPermissive` 允许跨属性不连续性进行折叠,除了通过 `vertex_lock` 标记为 `meshopt_SimplifyVertex_Protect` 的顶点。 使用 `meshopt_simplifyWithAttributes` 时,还可以通过提供 `vertex_lock` 数组来锁定某些顶点,该数组为网格中的每个顶点包含一个值,并为不应折叠的顶点设置 `meshopt_SimplifyVertex_Lock`。这对于保留某些顶点(例如网格边界)很有用,比 `meshopt_SimplifyLockBorder` 选项提供更多的控制。使用 `meshopt_simplifyWithUpdate` 时,锁定顶点(无论是通过 `vertex_lock` 还是 `meshopt_SimplifyLockBorder`)也将阻止简化器更新其位置和属性;这对于与 `meshopt_SimplifySparse` 一起用于 meshlet 简化很有用,因为层次结构中一级的 meshlet 可以一起简化而无需过多的数据复制。 锁定顶点会限制简化,并使简化器更有可能在达到索引目标之前卡住;如果网格的某些区域比其他区域更重要但仍应符合简化条件,则可以使用 `vertex_lock` 数组通过 `meshopt_SimplifyVertex_Priority` 位将特定顶点标记为高优先级,这使得顶点更有可能在简化过程中保留。 除了 `meshopt_SimplifyPrune` 标志外,您还可以通过调用 `meshopt_simplifyPrune` 函数显式修剪隔离分量。这可以在常规简化之前完成,也可以作为唯一的步骤完成,这对于等值面清理等场景很有用。与其他简化函数类似,`target_error` 参数控制分量半径的截止值,并以相对单位指定(例如,`1e-2f` 将移除 1% 以下的分量)。如果需要绝对截止值,请将参数除以 `meshopt_simplifyScale` 返回的因子。 简化目前假设输入网格对所有三角形使用相同的材质。如果网格使用多种材质,可以将网格基于材质拆分为子集并独立简化每个子集,使用 `meshopt_SimplifyLockBorder` 或 `vertex_lock` 保留材质边界;然而,这限制了折叠并可能降低结果质量。另一种方法是将有关材质的信息编码到顶点缓冲区中,确保引用同一三角形的所有三个顶点具有相同的材质 ID;这可能需要复制材质之间边界上的顶点。此后,可以照常执行简化,简化后可以根据顶点材质 ID 计算每三角形材质信息。无需通知简化器材质 ID 的值:通过复制具有冲突材质 ID 的顶点创建的隐式边界将自动保留(除非使用宽松简化,在这种情况下应通过 `vertex_lock` 保护材质边界)。如果源网格已经拆分为具有非重叠顶点索引的子集,并且未使用宽松简化,则应足以将子集连接到单个顶点/索引缓冲区中并一次简化整个网格;结果可以在简化后拆分回子集。 生成 LOD 链时,您可以从原始网格重新简化每个 LOD,或者使用前一个 LOD 作为下一级的起点。后一种方法更有效,并产生 LOD 级别之间更平滑的视觉过渡,同时更好地保留网格属性。使用此方法时,应累积来自先前级别的结果误差值以进行 LOD 选择。此外,考虑使用 `meshopt_SimplifySparse` 来提高生成深度 LOD 链时的性能。 ### 点云简化 除了三角形网格简化,本库还提供了简化点云的函数。该算法将点云减少到指定数量的点,同时保留整体外观,并且可以选择考虑每点颜色: ``` const float color_weight = 1; std::vector indices(target_count); indices.resize(meshopt_simplifyPoints(&indices[0], &points[0].x, points.size(), sizeof(Point), &points[0].r, sizeof(Point), color_weight, target_count)); ``` 生成的索引可用于渲染简化的点云;为了减少内存占用,可以重新索引点云以从索引创建点数组。 ## 效率分析器 虽然获得精确性能数据的唯一方法是在目标 GPU 上测量性能,但以独立于 GPU 的方式测量这些优化的影响可能很有价值。为此,本库为所有三个主要优化例程提供了分析器。对于每个优化,都有一个相应的分析函数,如 `meshopt_analyzeOverdraw`,它返回带有统计信息的结构。 `meshopt_analyzeVertexCache` 返回顶点缓存统计信息。常用的度量标准是 ACMR——平均缓存未命中率,即顶点调用总数与三角形计数的比率。最坏情况的 ACMR 是 3(GPU 必须为每个三角形处理 3 个顶点);在规则网格上,最佳 ACMR 接近 0.5。在真实网格上,根据顶点分割的数量,它通常在 [0.5..1.5] 范围内。另一个有用的度量标准是 ATVR——平均变换顶点比率——它表示顶点着色器调用与总顶点的比率,最佳情况为 1.0,无论网格拓扑如何(每个顶点变换一次)。 `meshopt_analyzeVertexFetch` 返回顶点获取统计信息。它使用的主要度量标准是过度获取——从顶点缓冲区读取的字节数与顶点缓冲区中总字节数的比率。假设非冗余顶点缓冲区,最佳情况是 1.0——每个字节获取一次。 `meshopt_analyzeOverdraw` 返回过度绘制统计信息。它使用的主要度量标准是过度绘制——像素着色器调用数与覆盖像素总数的比率,从几个不同的正交相机测量。过度绘制的最佳情况是 1.0——每个像素着色一次。 `meshopt_analyzeCoverage` 返回覆盖统计信息:覆盖像素与每个主轴视口范围的比率。这本身不是效率度量,但它可用于测量简化后的轮廓变化以及更精确的基于距离的剔除,其中可以通过计算视图方向与覆盖向量之间的点积来估计依赖于视图的覆盖量。 请注意,所有分析器都使用相关 GPU 单元的近似模型,因此您将作为结果获得的数字仅是实际性能的粗略近似值。 ## 去交错几何 上面的所有示例都假设几何体表示为单个顶点缓冲区和单个索引缓冲区。这需要将所有顶点属性——位置、法线、纹理坐标、蒙皮权重等——存储在单个连续结构中。然而,在某些情况下,使用多个顶点流可能更可取。特别是,如果某些传递只需要位置数据——例如深度预传递或阴影贴图——那么将其与其余顶点属性分开可能是有益的,以确保这些传递期间的带宽使用是最佳的。在某些移动 GPU 上,仅位置属性流还提高了分块算法的效率。 本库中的大多数函数要么只需要索引缓冲区(如顶点缓存优化),要么只需要位置信息(如过度绘制优化)。然而,有几项任务需要了解所有顶点属性。 对于索引,`meshopt_generateVertexRemap` 假设只有一个顶点流;当使用多个顶点流时,需要按如下方式使用 `meshopt_generateVertexRemapMulti`: ``` meshopt_Stream streams[] = { {&unindexed_pos[0], sizeof(float) * 3, sizeof(float) * 3}, {&unindexed_nrm[0], sizeof(float) * 3, sizeof(float) * 3}, {&unindexed_uv[0], sizeof(float) * 2, sizeof(float) * 2}, }; std::vector remap(index_count); size_t vertex_count = meshopt_generateVertexRemapMulti(&remap[0], NULL, index_count, index_count, streams, sizeof(streams) / sizeof(streams[0])); ``` 此后,需要为每个顶点流调用一次 `meshopt_remapVertexBuffer` 以生成正确重新索引的流。对于阴影索引,类似地,`meshopt_generateShadowIndexBufferMulti` 可作为替代品使用。 与其调用 `meshopt_optimizeVertexFetch` 在单个顶点缓冲区中重新排序顶点以提高效率,建议调用 `meshopt_optimizeVertexFetchRemap`,然后再次为每个流调用 `meshopt_remapVertexBuffer`。 最后,在压缩顶点数据时,应在每个顶点流上单独使用 `meshopt_encodeVertexBuffer`——这允许编码器最好地利用不同顶点的属性值之间的相关性。 ## 特殊处理 除了核心优化技术,本库还为需要顶点和索引数据的特定配置的特定渲染技术和管线优化提供了几种专门的算法。 ### 三角形带转换 在大多数硬件上,索引三角形列表是驱动 GPU 的最有效方式。然而,在某些情况下,三角形带可能是有益的: - 在一些较旧的 GPU 上,渲染三角形带可能稍微更有效率 - 在内存极其受限的系统上,三角形带的索引缓冲区可以节省一些内存 本库提供了一种将顶点缓存优化的三角形列表转换为三角形带的算法: ``` std::vector strip(meshopt_stripifyBound(index_count)); unsigned int restart_index = ~0u; size_t strip_size = meshopt_stripify(&strip[0], indices, index_count, vertex_count, restart_index); ``` 通常,您应该预期三角形带的索引约为三角形列表的 50-60%(每个三角形约 1.5-1.8 个索引),并且 ACMR 约差 5%。 请注意,三角形带可以使用或不使用重启索引支持进行拼接。使用重启索引可以产生约 10% 更小的索引缓冲区,但在某些 GPU 上,重启索引可能会导致性能下降。 为了进一步减小三角形带大小,建议在针对顶点缓存进行优化时使用 `meshopt_optimizeVertexCacheStrip` 代替 `meshopt_optimizeVertexCache`。这牺牲了一些顶点变换效率以换取更小的索引缓冲区。 ### 几何着色器邻接 对于使用几何着色器并需要邻接信息的算法,本库可以生成带有邻接数据的索引缓冲区: ``` std::vector adjacency(indices.size() * 2); meshopt_generateAdjacencyIndexBuffer(&adjacency[0], &indices[0], indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex)); ``` 创建了一个适合使用三角形邻接拓扑渲染的索引缓冲区,为每个三角形提供 3 个额外顶点,表示与每个三角形边相对的顶点。此数据可用于计算轮廓并在几何着色器中执行其他类型的局部几何处理。要使用邻接数据渲染网格,索引缓冲区应与 `D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ`/`VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY`/`GL_TRIANGLES_ADJACENCY` 拓扑一起使用。 请注意,使用几何着色器可能会对某些 GPU 产生性能影响;在某些情况下,替代实现策略可能更有效。 ### 带位移映射的曲面细分 对于具有无裂纹位移映射的硬件曲面细分,本库可以生成支持 PN-AEN 曲面细分的特殊索引缓冲区: ``` std::vector tess(indices.size() * 4); meshopt_generateTessellationIndexBuffer(&tess[0], &indices[0], indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex)); ``` 这为每个输入三角形生成一个 12 顶点补丁,具有以下布局: - 0、1、2:原始三角形顶点 - 3、4:边 0、1 的对边 - 5、6:边 1、2 的对边 - 7、8:边 2、0 的对边 - 9、10、11:角 0、1、2 的主导顶点 这允许使用硬件曲面细分实现 PN-AEN 和/或位移映射,而不会沿 UV 接缝或法线不连续性出现裂纹。要渲染网格,索引缓冲区应与 `D3D_PRIMITIVE_TOPOLOGY_12_CONTROL_POINT_PATCHLIST`/`VK_PRIMITIVE_TOPOLOGY_PATCH_LIST`(`patchControlPoints=12`)拓扑一起使用。有关更多详细信息,请参阅以下论文:[Crack-Free Point-Normal Triangles using Adjacent Edge Normals](https://developer.download.nvidia.com/whitepapers/2010/PN-AEN-Triangles-Whitepaper.pdf)、[Tessellation on Any Budget](https://www.nvidia.com/content/pdf/gdc2011/john_mcdonald.pdf) 和 [My Tessellation Has Cracks!](https://developer.download.nvidia.com/assets/gamedev/files/gdc12/GDC12_DUDASH_MyTessellationHasCracks.pdf)。 ### 可见性缓冲区 要将几何体渲染到可见性缓冲区,需要访问片段着色器中的图元索引。虽然可以在片段着色器中使用 `SV_PrimitiveID`/`gl_PrimitiveID`但这可能会在某些 GPU(特别是 AMD RDNA1 和所有 NVIDIA GPU)上导致次优性能,并且可能不支持移动或主机硬件。使用 mesh shader 生成图元 ID 是有效的,但需要并非普遍可用的硬件支持。为了解决这些限制,本库提供了一种生成使用引发顶点编码图元 ID 的特殊索引缓冲区的方法: ``` std::vector provoke(indices.size()); std::vector reorder(vertices.size() + indices.size() / 3); reorder.resize(meshopt_generateProvokingIndexBuffer(&provoke[0], &reorder[0], &indices[0], indices.size(), vertices.size())); ``` 这生成一个特殊的索引缓冲区以及满足两个约束的重排序表: - `provoke[3 * tri] == tri` - `reorder[provoke[x]]` 引用原始三角形顶点 要使用引发顶点数据渲染网格,应用程序应使用 `provoke` 作为索引缓冲区,并使用一个顶点着色器,该着色器通过 `flat`/`nointerpolation` 属性将顶点索引(`SV_VertexID`/`gl_VertexIndex`)作为图元索引传递给片段着色器,并通过基于 `reorder` 表(`reorder[gl_VertexIndex]`)计算真实顶点索引来手动加载顶点数据。有关更多详细信息,请参阅 [Variable Rate Shading with Visibility Buffer Rendering](https://advances.realtimerendering.com/s2024/content/Hable/Advances_SIGGRAPH_2024_VisibilityVRS-SIGGRAPH_Advances_2024.pptx);当然,此技术不需要 VRS。 因为结果索引缓冲区中的索引顺序必须完全保留才能使该技术起作用,所以必须在生成引发索引缓冲区之前应用所有重新排序索引的优化(如顶点缓存优化)。此外,如果使用索引压缩,则应使用 `meshopt_encodeIndexSequence` 而不是 `meshopt_encodeIndexBuffer`,以确保在编码期间不旋转三角形。 ### 不透明度微图 当结合硬件光线追踪使用 Alpha 测试透明度时,追踪光线需要为每个相交的表面调用 any-hit shader;这可能效率低下,因为它需要光线追踪硬件和 shader 单元之间进行额外的通信和同步。Opacity micromaps (OMM) 通过为每个三角形提供不透明度掩码,可以显著加速这些测试;这需要使用均匀网格(细分级别 N 对应 4^N 个微三角形)细分每个三角形,每个微三角形存储 1 位(对于 2-state micromaps)或 2 位(对于 4-state micromaps)的不透明度数据。为了最小化内存开销,可以使用每个三角形的 OMM 索引缓冲区在三角形之间重用这些映射。该库提供了算法,用于从 Alpha 纹理为给定的网格生成 OMM 数据;生成的数据可以通过 [VK_EXT_opacity_micromap](https://docs.vulkan.org/spec/latest/chapters/raytraversal.html#ray-opacity-micromap) 在 Vulkan 中直接使用,或通过 [DXR1.2](https://github.com/microsoft/DirectX-Specs/blob/master/d3d/Raytracing.md#opacity-micromaps) 在 DirectX 中使用。 生成 opacity micromaps 分为三个阶段:测量(和布局)、光栅化和压缩。压缩是可选的但推荐执行,可以在每个网格上单独执行,也可以在同一模型/场景中的所有网格上执行。 首先,调用 `meshopt_opacityMapMeasure` 根据每个三角形的纹素覆盖范围计算其细分级别;这还会计算初始的每个三角形 OMM 索引,因为源网格中的三角形通常引用相同的 UV: ``` const int states = 4; // 2-state or 4-state OMMs (used after measure) const int max_level = 6; // max subdivision level const float target_edge = 3.0f; // target 3x3px area for each microtriangle std::vector levels(indices.size() / 3); std::vector sources(indices.size() / 3); std::vector omm_indices(indices.size() / 3); size_t omm_count = meshopt_opacityMapMeasure(&levels[0], &sources[0], &omm_indices[0], &indices[0], indices.size(), &vertices[0].u, vertices.size(), sizeof(Vertex), texture_width, texture_height, max_level, target_edge); ``` 每个 OMM 条目需要单独的存储空间,这可以根据细分级别和格式(2-state 或 4-state)确定,并可以通过 `meshopt_opacityMapEntrySize` 计算: ``` std::vector offsets(omm_count); size_t data_size = 0; for (size_t i = 0; i < omm_count; ++i) { offsets[i] = unsigned(data_size); data_size += meshopt_opacityMapEntrySize(levels[i], states); } ``` 其次,为每个三角形调用 `meshopt_opacityMapRasterize` 以计算每个微三角形的不透明度状态。这可以顺序执行或并行执行;它可以使用原始纹理分辨率或较小的 mip 级别,以平衡光栅化成本与质量。生成 4-state micromaps 时,建议使用 mip 0 以产生最大程度的保守输出,从而确保启用 opacity micromaps 不会明显改变光线追踪的输出。对于 2-state micromaps,或者如果原始纹理的分辨率远高于 micromap 细分级别,较小的 mip(例如 1 或 2)也可以很好地工作。 ``` for (size_t i = 0; i < omm_count; ++i) { unsigned int tri = sources[i]; const float* uv0 = &vertices[indices[tri * 3 + 0]].u; const float* uv1 = &vertices[indices[tri * 3 + 1]].u; const float* uv2 = &vertices[indices[tri * 3 + 2]].u; // texture addressing below assumes RGBA texture input without padding; +3 points to A meshopt_opacityMapRasterize(&data[offsets[i]], levels[i], states, uv0, uv1, uv2, texture.data() + 3, 4, texture_width * 4, texture_width, texture_height); } ``` 光栅化后,OMM 数据*可以*直接使用;但是,通常会看到冗余条目,这些条目可以在不同的三角形之间重用,或者对于所有微三角形具有一致的状态,这可以通过每个三角形的“特殊”索引(-4..-1)来表示。因此建议压缩数据 —— 如果数据已经像上面的示例那样按顺序排列,那么只需调用 `meshopt_opacityMapCompact` 并修剪输出数组即可获得最佳输出: ``` omm_count = meshopt_opacityMapCompact(&data[0], data_size, &levels[0], &offsets[0], omm_count, &omm_indices[0], indices.size() / 3, states); data_size = (omm_count == 0) ? 0 : offsets[omm_count - 1] + meshopt_opacityMapEntrySize(levels[omm_count - 1], states); ``` 压缩后,`levels` 和 `offsets`(`omm_count` 条目)以及 `data`(`data_size` 字节)可以被序列化,并随后传递给光线追踪运行时以构建 opacity micromap 结构。通常,跨多个网格压缩 OMM 数据可以产生更小的结果;在这种情况下,所有生成的 OMM 索引都将指向单个 OMM 数组对象。 此外,请注意,虽然上面的代码使用 32 位 OMM 索引,但在压缩后,通常会看到每个网格引用 OMM 数组数据的一小部分,这可以用 16 位(或有时是 8 位索引)表示。在这些情况下,索引数据可以缩小为更短的类型;请注意,当使用 16 位 OMM 索引数据时,由于特殊索引的存在,索引值应在 `[0..65531]` 范围内(对于 8 位索引,假设使用 4-state OMMs,则为 `[0..251]`)。 使用 4-state OMMs 时,光栅化代码根据微三角形覆盖率生成 unknown-transparent 和 unknown-opaque 状态;这允许在遍历期间针对特定效果使用强制 2-state 标志,前提是 micromap 数据足以保证合理的质量;为了性能建议这样做,因为这不会导致任何 any-hit 调用。 ## 内存管理 许多算法分配临时内存以存储中间结果或加速处理。分配的内存量是各种输入参数(如顶点数和索引数)的函数。默认情况下,内存使用 `operator new` 和 `operator delete` 分配;如果应用程序重载了这些运算符,则将使用重载版本。或者,可以使用 `meshopt_setAllocator` 指定自定义分配/释放函数,例如: ``` meshopt_setAllocator(malloc, free); ``` 顶点和索引解码器(`meshopt_decodeVertexBuffer`、`meshopt_decodeIndexBuffer`、`meshopt_decodeIndexSequence`)不分配内存,完全在通过参数提供的缓冲区空间内工作。 所有函数的堆栈使用量都是有界的,任何算法都不超过 32 KB。 ## 实验性 API 该库提供的几个算法被标记为“实验性”;这种状态反映在注释以及每个函数的 `MESHOPTIMIZER_EXPERIMENTAL` 注解中。 非实验性的 API(标有 `MESHOPTIMIZER_API`)被认为是稳定的,这意味着库更新不会破坏兼容性:现有调用应该能够编译(API 兼容性),现有二进制文件应该能够链接(ABI 兼容性),并且现有行为不应发生显著变化(例如,浮点参数将具有相似的行为)。这并不意味着算法的输出将是相同的:未来版本可能会改进算法并产生不同的结果。 *属于*实验性的 API 可能会更改其接口,更改方式可能导致现有调用无法编译,或者可能编译但具有显著不同的行为(例如,参数顺序、含义、有效范围的更改)。在极少数情况下,实验性 API 也可能从未来的库版本中移除。如果正在使用实验性 API,建议在更新库时仔细阅读发布说明。某些实验性 API 在此 README 中也可能缺少文档。 应用程序可以配置库以更改实验性 API 的属性,例如将 `MESHOPTIMIZER_EXPERIMENTAL` 定义为 `__attribute__((deprecated))` 将在使用实验性 API 时发出编译器警告。使用 CMake 构建共享库时,可以设置 `MESHOPT_STABLE_EXPORTS` 选项以仅导出稳定的 API;这会生成一个 ABI 稳定的共享库,可以在不重新编译应用程序代码的情况下进行更新。 目前,以下 API 是实验性的: - 用于 `meshopt_simplify*` 函数的 `meshopt_SimplifyPermissive` 模式 - 用于 `meshopt_simplify*` 函数 `vertex_lock` 参数的 `meshopt_SimplifyVertex_Priority` 标志 - 用于 `meshopt_simplify*` 函数的 `meshopt_SimplifyRegularizeLight` 标志 - `meshopt_encode/decodeMeshlet*` 函数(`meshopt_encodeMeshlet`、`meshopt_encodeMeshletBound`、`meshopt_decodeMeshlet`、`meshopt_decodeMeshletRaw`) - `meshopt_extractMeshletIndices` 和 `meshopt_optimizeMeshletLevel` 函数 - `meshopt_opacityMap*` 函数(`meshopt_opacityMapMeasure`、`meshopt_opacityMapRasterize`、`meshopt_opacityMapCompact`、`meshopt_opacityMapEntrySize`) ## 许可证 该库根据 [MIT License](LICENSE.md) 的条款向任何人免费提供。 为了遵守许可协议,请在面向用户的文档和/或致谢中包含归属信息,例如使用以下或类似的文本:
标签:3D图形, Bash脚本, C++, CMS安全, glTF, gltfpack, GPU渲染, JavaScript, LOD技术, Rust绑定, WebAssembly, 三角形条带化, 几何处理, 增强现实, 实时渲染, 开源库, 性能调优, 搜索引擎爬虫, 数据擦除, 模型压缩, 渲染优化, 游戏开发, 游戏引擎, 网格优化, 虚拟现实, 计算机图形学, 顶点缓存