You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: blog/2025-05-07-nanite-at-home/index.md
+19-6Lines changed: 19 additions & 6 deletions
Original file line number
Diff line number
Diff line change
@@ -117,22 +117,35 @@ The very first step is to load our mesh from disk using the [gltf crate](https:/
117
117
3. Simplify the clusters with fixed borders: This gets us to the middle state, where we have a white mesh that has been simplified. But most importantly, the outer borders to the surrounding groups are fixed and have not changed at all. This allows us to decide to draw the higher or lower LOD *independently* of our neighbours, which is critical to ensure we don't get any holes in our model though LOD transitions. The meshoptimizer library also provides a simplification implementation, specifically I'm using [`simplify_with_attributes_and_locks`](https://github.com/gwihlidal/meshopt-rs/blob/c2165927e09c557e717f6fcb6b7690bee65f6c90/src/simplify.rs#L193) cause it's very unlikely I'd be able to build a better simplifier than what the rest of the industry uses. In our cluster graph, we deonte it as a new node, who's children are the 4 clusters, similarly to a quadtree in terrain generation.
118
118
4. Split the mesh back into clusters: This may seem weird at first, but it's a critical step as we see in a bit. In the right image you can see the newly created border going through our group. Note how this border consists just out of a single long edge and it being a lot longer than all the other edges from the locked border.
119
119
120
-
import nanite_mesh_5 from './tikz/nanite_mesh_5.jpg';
120
+
import nanite_mesh_3 from './tikz/nanite_mesh_3.jpg';
121
121
122
122
<figurestyle={{float:"right"}}>
123
-
<imgsrc={nanite_mesh_5}width="350"/>
123
+
<imgsrc={nanite_mesh_3}width="350"/>
124
124
</figure>
125
125
126
-
TODO better image, cut out graph
127
-
128
126
To understand why we split it up into multiple clusters at the end, we need to look at what happens in the next iteration. To start the next iteration, we have to process all groups and collect all the newly generated clusters into a new mesh, like you can see on the right. Then we proceed with the first step again and select groups of 4 clusters. However, there's one detail I've left out earlier: We don't just select *any* group of 4 clusters, we want to select these groups so that there are as few outer edges as possible. Doing so will encourage the grouping algorithm to place the outer edges though longer and more simplified edges, like the edge we created earlier by splitting. Imagine the red lines as the locked borders of the new iteration, and notice how we shifted the locked borders of the previous iteration into the center of the new groups. And my the formerly locked edges being in the center of a group, they can be simplified.
129
127
130
128
I like to think about it like swapping the areas around constantly: One iteration, one area is a locked border where the other is being simplified. And the next iteration they swap, and the other is the locked border whereas the one is being simplified. But instead of two discrete locations swapping, there are lots of borders throughout the mesh constantly swapping between being locked and being simplified.
131
129
132
130
You may notice that group selection is an optimization problem of graph partitioning. Luckily there's the [METIS](https://github.com/KarypisLab/METIS) library (and their [rust bindings](https://github.com/lihpc-computational-geometry/metis-rs)), which has implemented decades of research in graph partitioning to allow solving these with almost linear scaling, which is amazing considering any native implementation would likely take exponential or even factorial time to run. It's also surprisingly trivial to use, if you're interested I recommend reading the [docs of `Graph::new`](https://github.com/LIHPC-Computational-Geometry/metis-rs/blob/410f512740476bac38199a3f3d0ab605cd81fe67/src/lib.rs#L230-L302).
133
131
134
-
## lod selection
135
-
And how in the graph we now have the two clusters as separate nodes, with both having all original clusters as their children.
132
+
## LOD selection
133
+
134
+
import nanite_mesh_3_dag from './tikz/nanite_mesh_3_dag.png';
135
+
136
+
<figurestyle={{float:"right"}}>
137
+
<imgsrc={nanite_mesh_3_dag}width="350"/>
138
+
</figure>
139
+
140
+
We've gone into great detail on why we want to split up our mesh again into multiple clusters, but we haven't talked about the kind of graph splitting creates. We've copied the graph from above to the image on the right, and you may immediately notice how the four children clusters now have two parent clusters instead of just one. But each node having exactly one parent is an important property of a tree, making this graph not a tree but an Acyclic Directed Graph (DAG). A Node potentially having multiple parents complicates iteration a lot, as we can't just trivially traverse a DAG like we do a tree, then we could visit nodes multiple times. And we don't want to render a cluster multiple times.
141
+
142
+

143
+
144
+
In the original nanite talks, they present an interesting way to select a cut though a DAG. If you have a DAG, like the one above, you can assign each node a monotonically decreasing number, meaning that a node must have a number that is less than that of their parents. Imagine this number somehow representing the detail level, and we are searching for the first node where the detail is smaller than 10, which would select the green nodes. We can find all the green nodes by evaluating the following statement on all nodes: You must only draw when your number is below 10 and your parent's number is above 10. The red nodes fail the first condition, as they are not detailed enough. The yellow nodes fail the second condition, as the green parents above them are already detailed enough. Only the green nodes pass both tests and will be drawn. We can extend this to nodes with multiple parents as well, if we assign each parent the same number. As all parents of some node come from the same group, we can simply calculate this value per group and assign it to all parents.
145
+
146
+
But these values are not precomputed constants, after all, moving the camera should change the LOD of objects. Instead, the values come from a function, and we must ensure that for all possible camera positions, the function yields monotonic values throughout the DAG. This depends on the exact function you are using, but generally, this means that your bounding spheres around your clusters must contain all the bounding spheres of your children. Which is a quite simple condition to uphold in practice, though computing the optimal bounding sphere around spheres is a bit complicated, but simpler approximations work just fine.
0 commit comments