# How to UV map a procedural cylinder mesh? (Unity)

I have a script that generates a procedural cylinder. Each side is a quad that consists of two triangles. The two triangles share vertexes, so, a side with two triangles is defined by 4 points.

The mesh generates fine as you see here:

However, I am unable to UV map it correctly. My current progress has led me here:

With a different texture you can see how it stretches at certain points. If the first quad is the "front" of the cylinder, then these points occur on each side.

I thought it would work to use the vertex data directly as UV points, but I can't get it to work.

Here is the (old) script.

private void _CreateSegmentSides()
{
if(m_Sides > 2) {

float angleStep = 360.0f / (float)m_Sides;
BranchSegment seg = new BranchSegment(m_NextID++);
Quaternion rotation = Quaternion.Euler(0.0f, angleStep, 0.0f);
float height = m_SegmentStartLength;
int max = m_Sides - 1;
int index_tr = 0, index_tl = 3, index_br = 2, index_bl = 1;
float angle = 0f;

// Make first triangles.
seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 1]);      // bottom right
seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 3]);      // top left

// UV.

for (int i = 0; i < max; i++)
{
Debug.Log("add side ("+(i+1).ToString()+"/"+ max.ToString()+ ") angle: " + angle.ToString());

// First triangle.

seg.triangles.Add(seg.vertexes.Count - 1);  // new vertex

// Second triangle.

seg.triangles.Add(seg.vertexes.Count - 1);  // new vertex

angle += angleStep;
}
}
}


I've been at this all day, what is going wrong here ??

Edit: Current progress, updated code

    // Making the first two UV coords:

uy =  seg.vertexes[0].y;

uy = seg.vertexes[1].y;

// in the loop
for (int i = 0; i < max; i++)
{
angle += angleStep;
Debug.Log("add side (" + (i + 1).ToString() + "/" + max.ToString() + ") angle: " + angle.ToString());

// First vertex.

seg.triangles.Add(seg.vertexes.Count - 1);  // new vertex

// UV
uy = seg.vertexes[seg.vertexes.Count - 1].y;

// Second vertex.

seg.triangles.Add(seg.vertexes.Count - 1);  // new vertex

// UV
uy = seg.vertexes[seg.vertexes.Count - 1].y;
}

And trying to fix the last to the first section
rad = 360f * Mathf.Deg2Rad;  // (I've tried keeping the angle from the loop, but that didn't solve it either)

uy = seg.vertexes[seg.vertexes.Count - 2].y;

uy = seg.vertexes[seg.vertexes.Count - 1].y;


Edit: Implemented the suggestion from the second comment.

Okay, so I'm going over the calculated UV values in a second loop after the part that I've posted, then I use the rounded value from UVx_max / UVx_max_rounded_to_int to multiply each UV to adjust the UV tiling. This works! (I wonder if it is possible to do it without a second loop?) There is a visible line (normal related?) on the seam even if the texture is seamless. I am calling RecalculateNormals() on the last part of the mesh creation.

Edit: Implementation of the corrected code:

private void _CreateSegmentSides2()
{
if(m_Sides > 2)
{
float           angleStep   = 360.0f / (float)m_Sides;
BranchSegment   seg         = new BranchSegment(m_NextID++);
Quaternion      rotation    = Quaternion.Euler(0.0f, angleStep, 0.0f);
float           height      = m_SegmentStartLength;
float           angle       = 0f;
int             max         = m_Sides - 1;

// Make first side.
seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 1]);      // br
seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 3]);      // tl

// Making the first two normals:
if (m_CustomNormals) {
seg.normals.Add(seg.vertexes[0].normalized);  // Points "out" of the cylinder.
}

// Making the first two UV coords:
float circumferenceTop      = Mathf.PI * 2.0f * m_RadiusTop; // We know this in advance.
float circumferenceBottom   = Mathf.PI * 2.0f * m_RadiusBottom;
float uvScaleTop            = Mathf.Max(1, Mathf.Round(circumferenceTop)) / circumferenceTop;
float uvScaleBottom         = Mathf.Max(1, Mathf.Round(circumferenceBottom)) / circumferenceBottom;
float uvScaleDiff           = Mathf.Abs(uvScaleBottom - uvScaleTop);

// UV starts here.

ux = 0;
uy = seg.vertexes[0].y * uvScaleTop;

uy = seg.vertexes[1].y * uvScaleBottom;

// The remaining sides.
for (int i = 0; i < max; i++)
{
angle += angleStep;

// First vertex.

seg.triangles.Add(seg.vertexes.Count - 1);  // new vertex

// UV: x = arc length around the cylinder, y = height, scaled to tile uniformly.
uy = seg.vertexes[seg.vertexes.Count - 1].y * uvScaleTop;

// Normal: rotate normal from the previous column.
if(m_CustomNormals) { seg.normals.Add(rotation * seg.normals[seg.normals.Count - 1]);  }

// Second vertex.

seg.triangles.Add(seg.vertexes.Count - 1);  // new vertex

// UV: x is the same, update y for new vertical position.
uy = seg.vertexes[seg.vertexes.Count - 1].y * uvScaleBottom;

// Normal: copy previous normal.
if(m_CustomNormals) { seg.normals.Add(seg.normals[seg.normals.Count - 1]); }
}

// Last UV to connect the seam.

uy = seg.vertexes[1].y * uvScaleTop;

uy = seg.vertexes[0].y * uvScaleBottom;

}
}


Custom normals:

Automatic normals by unity:

UV skew on different top and bottom radius:

(Seems like the UV is rotated ever so slightly sideways with each side, making it really messed up when it comes back around. height is also oddly scaled)

• I've used with success a Cylinder from this Javascript module: github.com/ataber/primitive-cylinder It has correct UV mapping, it is for WebGL in javascript, and it represents the mesh in form of simplicial complex (a point XYZ is an array of 3 numbers) and the code is quite easy to read/follow - also if you're not familiar with javascript I think. Hope it helps! – nkint Mar 17 at 21:21

It looks like you're trying to use (rotated) vertex positions as UV coordinates. When you're using this to look up into a 2D texture, only the x & y values are used and the z is ignored, giving you a planar projection:

The effect is like shining your texture onto the object using an orthographic projector, along one directional axis. Portions of your model perpendicular to that axis catch the image flat-on, and you get a nice undistorted appearance there.

But as the surface turns diagonally to the axis, the image gets stretched. In the worst case, imagine a long edge of your model running parallel to the projection axis. Every point on that edge maps to the same pixel in the source image, so that one point of colour gets smeared out along the whole edge.

For a cylinder, you'll more often want to use a cylindrical projection:

Here, the U (x) coordinate of our UV is our arc-length around the cylinder, and our V (y) coordinate is the vertical position of the vertex.

You can get the arc-length value by multiplying the total angle stepped so far (in radians) by the radius of your cylinder.

Note though that this has a discontinuity, where 360 degrees wraps around to 0 degrees. Here we'll want to duplicate the column of vertices we started with, so we have two sets of co-incident vertices with identical positions & normals: one to be the let edge of the texture sheet we've wrapped around our cylinder, and one to be the right edge.

You may also want to apply some scaling to your UVs so that one complete trip around the cylinder takes you an integer distance in texture space. That way you can use a tiling texture and completely erase the seam at the 360-to-0 wraparound line.

Here's a corrected version of your code:

// Making the first two normals:
seg.normals.Add(seg.vertices[0].normalized);  // Points "out" of the cylinder.

// Making the first two UV coords:
float circumference = Mathf.PI * 2.0f * m_Radius; // We know this in advance.
float uvScale = Mathf.Max(1, Mathf.Round(circumference)) / circumference;
// No second loop required for fix-up - we can scale as we go.

uy =  seg.vertexes[0].y * uvScale;

uy = seg.vertexes[1].y * uvScale;

// in the loop
for (int i = 0; i < max; i++)
{
angle += angleStep;
Debug.Log("add side (" + (i + 1).ToString() + "/" + max.ToString() + ") angle: " + angle.ToString());

// First vertex.

seg.triangles.Add(seg.vertexes.Count - 1);  // new vertex

// UV: x = arc length around the cylinder, y = height, scaled to tile uniformly.
uy = seg.vertexes[seg.vertexes.Count - 1].y * uvScale;

// Normal: rotate normal from the previous column.

// Second vertex.

seg.triangles.Add(seg.vertexes.Count - 1);  // new vertex

// UV: x is the same, update y for new vertical position.
uy = seg.vertexes[seg.vertexes.Count - 1].y * uvScale;

// Normal: copy previous normal.

Now you don't have to ask Unity to calculate normals automatically: you've provided exactly the normals you want. Just watch out for floating point rounding errors, where applying max incremental rotations / angle additions might not bring you to exactly the total you expect. If that becomes a problem, you can do something like...
angle = Mathf.Lerp(0, 360.0f, i/(max - 1.0f));

• This could be because your UVs' x starts at 0, and runs to something non-integer, like say 2*pi = 6.28318.... This would mean the left edge of your UV is on the left edge of the texture, and the right edge falls 28.318...% of the way between the left & right edges. To keep the mapping seamless, round your maximum UV x coordinate to the nearest integer, then multiply all your UVs by roundedMaximumX / maximumX. This will ensure your UV sheet begins & ends at the same part of the tiling texture. – DMGregory Mar 17 at 16:08