Creating new UV Maps after Edge Chamfering
Diana Algma
DEMO
My master's thesis is 3D Edge Chamfering. After chamfering (also known as beveling) hard edges of a model, new faces are created. The model's UV map has to change and include also the new faces. If the model has a texture, it would have to look nice also on the new UV map and somehow be present on the new faces. This project will be creating new UV maps after the edge chamfering is done on the model.
To do this, new and changed vertices would have to get new UV coordinates which will be done in Unity C# scripts. When new faces are created next to a UV seam, they should be added to the UV map in the right place so the textures could also fit nicely. That might mean, that the faces should all be added next to one other segment, but not randomly next to both. I tried to create a case in Blender where it did it randomly, but I couldn't. I have seen it though but don't know in what circumstances it happens. Below is part of a UV map showing the added bevel that is only on one part of the map, not on the other, which is what I want for my project.
To see the changes and the resulting UV map, I will create a way to view it inside Unity. This will be created as a sort of debug tool.
In the end, the UV map of the model that is chamfered should look nice and the texture it had before should be fitted well onto it.
Technologies
- Unity
- Blender
Foreseen difficulties
- Textures - no idea how to handle it yet.
- Making the map look nice in every case.
- Everything will be done in a Unity C# script.
- Using Blender for reference and model making to test everything.
Milestone 1 (02.03)
- Research how UV maps are created/changed using Unity scripting
- Try to modify an existing UV map in a Unity script
Progress
Created two cubes in Blender with the same texture, one without seams and one with seams.
Texture was the default Color Grid in Blender:
Resulting cubes:
Cubes after tweaking the UV coordinates in Unity (moving the coordinates 0.1 to 0.2 to left or right:
I also looked around and found a cool Unity asset UVee from the Asset Store, that should suit my needs and be better than I could ever create during this course myself. I will try it out instead of trying to create my own.
Milestone 2 (16.03)
- Get UVee (or an alternative) working
- Use it to visualize (and understand) what the current implementation of my chamferer does with the UV map
Progress
I got UVee working, first with the demo scene and then added it to my chamfering project. Currently, the part that I use is the UVee window, that shows the selected object's UV map. The selected object has to have the Mesh Filter component. If only it's parent or child has it, it won't show anything. It shows four squares and my models that have only the first UV set in use will show that in the top right square. I think the other squares are for the other UV sets (second, third and fourth). If there is a texture image it will show that as well.
The edges on the UV map are shown in gray (not very visible on a colorful texture) if not selected and in green after selecting them. There are modes for moving, rotating and scaling the selected vertices on the UV map. The changes are possible to be undone by CTRL-Z.
Current implementation of my chamfering code does not move any UV coordinates. When the chamferer creates new vertices (duplicates the originals), it gives them the same values that the original had, including the UV coordinates. When new faces are created it uses the original (and new duplicate) vertices and thus only creates new edges. That happens also on the UV map. This results in two different ways new faces are textured. Note that hard edges are edges that have an overlap with different vertices on same positions. If a hard edge is not on a seam and both it and its overlap have the same UV coordinates, then the new face will have that same line of texture stretched from one hard edge to the other.
Cube without seams before and after chamfering:
Chamfered hard edge not on a seam:
If a hard edge is on a seam or the overlapping edge has different UV coordinates, the new face will look much different. In this case, the new edges will connect vertices in different places on the UV map and the new face goes over whatever is between those edges on the UV map. This results in large part of the texture mapped to that new edge and it might be rotated and skewed in different ways.
Cube with seams before and after chamfering:
Chamfered hard edge on a seam:
It is clear that this doesn't work as it should. For it to be correct, I have to move the vertices on the UV map as I am moving them on the mesh. The distance should be proportional to the distance moved on the mesh. On seams, new vertices have to be created for the UV coordinates to not stretch over other parts of the texture. Those new vertices have to be placed in the right place next to the old seam in the UV map. New complexities that I don't know about yet will probably reveal themselves in the progress.
Milestone 3 (30.03)
- Create an algorithm/formula that moves the vertices not on seams to appropriate places on the UV map.
- Start creating the same for seams.
Progress
Pulling is done per vertex, not per edge. Information, that is used, consists of
- (int) vertex id
- (Vector3) vertex position
- (List<List<Edge>>) hard edge map
- (List<int>) seam overlaps
- (List<int>) soft overlaps
- (List<int>) neighbours
- (List<Vector3>) vertices (all vertex coordinates)
- (List<Vector3>) normals
- (float) chamfer scale
Vertex position could also be retrieved using only vertex id and vertices list, but it is a bit more comfortable to pass it on directly. Hard edge map holds all hard edges that are connected to a cluster. Clusters are vertices that have the same position, in other words - overlaps.
To change UV coordinates for the vertex, all UV coordinates for all channels also have to be passed on. While calculating the new UV coordinates, old coordinates are used so that changes already done to some previous vertex would not affect the outcome. For this, also temporary UV coordinates are passed on for calculations, new positions are saved in not-temporary UV coordinates. This results in 8 total UV lists to be passed on, 4 temporary and 4 actual.
The same logic, using old coordinates and saving into the new list, is done for vertex position, but the new position is simply returned so there is no need to pass on multiple vertices lists.
By calculating the new vertex position on the model, we get the pulling direction vector and pulling distance. These will also be used to calculate the new positions on the UV map.
Algorithm for moving UV coordinates NOT on seams (edit from the future: most of it is scrapped in the next milestone)
To move a vertex on the UV map, we need a direction vector and pull distance.
First, we need to know, which neighbours are nA and nB. For that, all neighbours are sorted around our vector and its normal using the inverted pulling direction as the reference and signed angle for comparison. The result is a list of neighbours, where the first element is nB and the last element is nA.
Pulling distance should be proportional to the pulling distance on the model. Let's say pulling distance on the model is distA and pulling distance on the UV map is distA'. Let's say that the distance to another edge/vertex in the pulling direction is distB on the model and distB' on the UV map.
Pulling distance on the UV map would then be distA' = (distA * distB')/distB.
We already have distA but distB and distB' aren't that easy to calculate. There are multiple ways, some are that distB is:
- average of the magnitudes of the two hard edges
- average of distances to all the neighbours
- average of two edges from the vector that are closest to the pulling direction
- magnitude of the edge from the vector that is closest to the pulling direction
The last option seemed the most accurate while also not being overly computationally difficult to calculate. I also implemented the algorithm using that option.
Either nA, nB, or equally nA and nB are closest to the pulling direction (by the angle between the edge and the pulling direction vector). The latter happens when the pulling direction is exactly in the middle of the two edges. The length of the closest edge is distB. If nA and nB are equally close, the average of the lengths is distB. distB' is calculated the same way using the distances of the same edges on the UV map.
We have the pulling distance distA' and now we need the pulling direction.
The angles on the UV map are usually different than the same angles on the model. To get the pulling direction, I again use proportions. We already know between what edges the pulling direction lies. We can also calculate the angles between those edges and the pulling direction. The ratio between them should remain the same on the UV map. Angle a is the angle between pulling direction and nA, angle b is the angle between pulling direction and nB. The same angles on the UV map are a' and b'. For the ratios to stay the same (a/b = a'/b'), we can calculate a' = a*b'/b or a' = a/(a+b)*(a'+b'). Although we don't know a' or b', we know (a'+b'), which means we now have everything we need.
To get the pulling direction, we take the vector from our vertex to nA (on the UV map) and subtract (or rotate to the right by) the angle a'.
Algorithm for moving UV coordinates ON seams (thus far)
First, it should be distinguished if the vertex in question is on a seam or not. Well actually, this should be done before the first algorithm is run also, but right now, the first algorithm is used for all vertices regardless of where the seams are. Right now, I think that checking, if the vertex has any seam overlaps, is enough for this. During implementation, I might be proven wrong, but I'll fix it when it happens.
There are different ways a hard edge and a seam can be connected. Even soft overlaps can be a factor in what should be done. One thing is certain, when seam is on a hard edge and the seam overlaps (vertices) are pulled apart and used for a new face, one new vertex has to be created for every two seam overlaps that are now connected with an edge to preserve a seam. All the points have to still be moved similarly to what is done for vertices not on seams.
When a seam is chamfered, both vertices (seam overlaps of each other) are pulled towards the face. One vertex has to now be duplicated and pulled outward away from the face. The new face would have to be created between the duplicates. This way, the texture would be sampled from the right location and not from somewhere across the UV map.
The distances would probably be a little different though than with vertices not on seams. If the distances were the same, some texture at the vertex that isn't duplicated, would be removed or cut from the model while the texture sampled for the new face would go beyond the area where the texture was sampled for the vertex that was duplicated. To get the perfect result, the new face would have to be cut in half and distributed between both seam edges, this would double the vertices that are already being created just for the UV map. Also new faces would have to be created between both. This would be too complicated for now and the difference might not even be visible enough to be worth it. For the next milestone, I will look at what Blender does in the same situation and decide, where the vertices have to be moved to on the UV map.
Milestone 4 (13.04)
- For reference, look at what Blender does with the UV map while beveling seams.
- Create an algorithm/formula that moves the vertices ON seams to appropriate places on the UV map and creates new vertices where needed. Doesn't have to work flawlessly.
Progress
First, I rewrote the algorithm written in the last milestone. The algorithm I created last milestone wasn't as accurate as I needed and it was needlessly complicated. This time, I left in the part where I find points nA and nB, but for calculating the new UV position, I used barycentric coordinates. This also solved some problems that I had previously (which I thought were mainly caused by seams).
When beveling edges in blender, this is what is done with the UV coordinates of the seams:
When pulling two edges apart, a seemingly random one is picked from the two, and the bevel face is mapped next to it. Both original edges move equally far inwards the face on the UV map. The bevel face will be in the empty space the original edge left when it was moved.
UV map before beveling; UV map after beveling one edge; UV map after beveling all edges:
Let's say the hard edges are both moved 1 unit away on the model and the lengths are the same on the UV map. If the angle between normals is 90 degrees, then the width of the bevel is sqrt(2). Then the edges on the UV map are also moved 1 unit but the width of the bevel will also be 1 unit.
The result is that the texture on the bevel will be a stretched version of what was removed from one (random) face. Adjacent bevels might be mapped using the texture near different edges creating a checkerboard effect where there was none.
In the end, there seems to be three options for mapping new seams on the UV map:
- Do it like blender
- Pro: not many new vertices and faces
- Con: Might look weird and not quite right especially with a big chamfer
- Split the chamfer and sample from both sides
- Pro: Looks the best
- Con: A lot more vertices and triangles are added
- Like 1. but make the chamfer texture be the size corresponding to it on the model
- Pro: texture won't be stretched
- Con: Texture might be sampled from a totally wrong place if there is no overflow
I will try to implement both 1 and 2, and see if the visual gain is worth the extra vertices and triangles.
The below image shows how the edges are duplicated and moved on the model for options 1 and 2. Red lines and points are seams.
Algorithm for recreating seams on hard edges (option 1)
The moving of the UV coordinates and pulling the vertices will stay the same as for non-seams. Creating new vertices should take place after all the pulling is done and possibly after or during the face creating phase.
I am still figuring out the best way but right now, let's say that new vertices (or new seams) are created after the faces between hard edges are created. This will be done hard-edge-based not vertex-based. From the two new edges and its points, the new vertices will have the position (on the mesh) of one of the points and UV coordinates of the other point before pulling. Therefore, two types of data is now additionally needed: old UV coordinates, and new info for vertices. We don't want the newly added vertices and modified ones to affect what we do to the rest.
So in short:
- Move the vertices and UV coordinates like every non-seam vertex
- Create faces between hard edges like before
- For hard edges on seams (pair A-B):
- Duplicate B, new edge is C
- UV of B = old UV of A
- Save vertices of C and B somewhere, including new seams and overlaps
Milestone 5 (27.04)
- Implement algorithm for recreating seams using option 1.
- If this algorithm works, create algorithm for recreating seams using option 2.
- If it doesn't work, rethink the algorithm using option 1.
Progress
So I implemented it and it works like a charm... nah, just kidding, it doesn't work at all. When trying to create the algorithm in the last milestone, I isolated it too much and kinda forgot that the edges I'm pulling apart are already connected to some faces. Oh well, here is what actually happens when using this algorithm while also looking at the neighbouring faces:
If I don't detach any faces while duplicating or I don't create the new face (chamfer) using the new edge, I just now pull the old face over the UV map instead of the chamfer. Not what I intended. To fix this, I figured it would be easier to first duplicate the edge, and then create the chamfer using the new edge.
This would be the new (simplified) workflow:
- Move the vertices and UV coordinates like every non-seam vertex
- For hard edges on seams (pair A-B):
- Duplicate B, new edge is C
- UV of C = old UV of A
- For the hard edge A: change its overlap from B to C so the new faces would be created between them
- Save vertices of C and B somewhere, including new seams and overlaps
- Create faces between hard edges like before
At first glance, this might work. For the easiest case (only 2 neighbouring pairs of hard edges, one on each side, both not already handled), this does seem to work without fail. When we look at any harder case though, it needs something more.
This algorithm handles the mesh one pair of hard edges at a time, but it duplicates and assigns UVs to vertices. Vertices are shared between edges so we will probably run into every vertex twice. If one or both of the neighbouring hard edges are already handled, we need to check if we can use its duplicate. If one of the edges is duplicated in one way (and UV pulled in one direction), we would like to pull this edge the same way to avoid the checker-board effect on a hard edge. If one neighbouring edge is pulled one way but the other is pulled the other way, we need to pick one of those and we cannot use one of the already doubled vertices but have to create a new one for this edge. If it turns out, that we should duplicate edge A instead of B, additional steps have to be taken for it to be replaced with the duplicate in all the right places. A lot to consider. But this is only the beginning, places where multiple hard edges (more than 2) meet, are still totally broken.
On the model, the result looks nice. It leaves a nice hole to be filled later. On the UV map however, there is nowhere to sample the triangular hole from and the edges are sampled from wrong places at the holes:
This leads me to think that using the "old UV of A" is a wrong move. Instead, I would have to calculate the UV coordinate of the middle point between the hard edges. This should work also for the part that is working now with the old logic.
One thing that I have to still figure out, is how and where to put the UV coordinates of the faces that fill the holes.
As this algorithm needs a lot more work, I will sadly not get to implement the option 2.
Oh, and so far, a chamfered cube and its new UV map look like this:
Milestone 6 (11.05)
- Solve the problems discovered in milestone 5:
- Wrong UV coordinates at holes (not for the faces filling holes but the faces next to holes)
- Holes of the mesh are not filled anymore
- Start assigning right UV coordinates to the holes.
I replaced the "use old UV of A" with a more complex calculation using barycentric coordinates. First, I tried calculating the UV coordinate of the middle point, but that resulted in the UV coordinate being at half the distance I want. Because I have to stretch the texture 2x wider I have to use the full distance for calculations, not half. So instead of calculating the barycentric coordinates of the middle point, I calculate the coordinates of the duplicated point (points of edge B)
New workflow looks like this:
- Move the vertices and UV coordinates like every non-seam vertex
- For hard edges on seams (pair A-B):
- Duplicate B, new edge is C
- For both points of B (B1 and B2):
- Find the barycentric coordinates of B1 for triangle A1-oldA1-A2
- UV of C1 = point with the same barycentric coordinates in triangle UVofA1-oldUVofA1-UVofA2
- For the hard edge A: change its overlap from B to C so the new faces would be created between them
- Save vertices of C and B somewhere, including new seams and overlaps
- Create faces between hard edges like before
After this I found a new bug that was caused by code written in milestone 3 and left in in milestone 4. The problem was with calculating the points nA and nB. I was using the list of neighbours that also included neighbours of soft and seam overlaps. Simply using a neighbours list without them solved the problem and the coordinates are now correctly calculated from the right triangle.
Next, I started to fill the holes that were now left empty. The cycles I was searching for didn't use the newly duplicated vertices. Adding this in was not easy, but it is done now.
What I had at this point, was a nice hole-less mesh, with the texture of some of the holes nice, but some ugly and wrong.
Now the UV coordinates of the holes...
I started easy, just the triangles. I have 3 vertices, they might be in the same place on the UV map, they might not. To distinguish these new clusters that are based on location on the mesh and UV map, I created a new cluster map for this (let's say seamclusters). Now for all the triangles, I check whether all 3 vertices are in the same seamcluster. If they are not, I will look at the displacements - where I duplicated and moved the vertex on the UV map - and try to find a combination where all the three are in the same seamcluster. This is implemented, It is not perfect but it works at least for the cube.
Now all that is needed is to find what is still missing for the triangles, and something has to be done with holes with more than 3 vertices. I know that one place is missing a check if there is a displacement, but I don't know yet, why it needs it and what the geometry looks like where it happens. All this will have to be done after this milestone before the final presentations.