2
\$\begingroup\$

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:

enter image description here

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

enter image description 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.

enter image description here

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 * (new Vector3(m_Radius, height, 0f)));       // top right
        seg.vertexes.Add(rotation * (new Vector3(m_Radius, 0f, 0f)));           // bottom left
        seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 1]);      // bottom right
        seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 3]);      // top left

        // Add triangle indices.
        seg.triangles.Add(index_tr);    // 0
        seg.triangles.Add(index_bl);    // 1
        seg.triangles.Add(index_br);    // 2
        seg.triangles.Add(index_tr);    // 0
        seg.triangles.Add(index_br);    // 2
        seg.triangles.Add(index_tl);    // 3

        // UV.
        seg.uv.Add(rotation * seg.vertexes[seg.vertexes.Count - 4]);
        seg.uv.Add(rotation * seg.vertexes[seg.vertexes.Count - 3]);
        seg.uv.Add(rotation * seg.vertexes[seg.vertexes.Count - 2]);
        seg.uv.Add(rotation * seg.vertexes[seg.vertexes.Count - 1]);

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

            // First triangle.
            seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 2]); 

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

            seg.uv.Add(rotation * seg.vertexes[seg.vertexes.Count - 1]);

            // Second triangle.
            seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 2]);

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

            seg.uv.Add(rotation * seg.vertexes[seg.vertexes.Count - 1]);

            angle += angleStep;
        }
        m_Segments.Add(seg);
    }
}

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

Edit: Current progress, updated code

    // Making the first two UV coords:

rad = angle * Mathf.Deg2Rad;
ux = rad * m_Radius;

uy =  seg.vertexes[0].y;
seg.uv.Add(rotation * new Vector2(ux, uy));

uy = seg.vertexes[1].y;
seg.uv.Add(rotation * new Vector2(ux, uy));

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

    rad = angle * Mathf.Deg2Rad;
    ux = rad * m_Radius;

    // First vertex.
    seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 2]);

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

    // UV
    uy = seg.vertexes[seg.vertexes.Count - 1].y;
    seg.uv.Add(rotation * new Vector2(ux, uy));

    // Second vertex.
    seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 2]);

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

    // UV
    uy = seg.vertexes[seg.vertexes.Count - 1].y;
    seg.uv.Add(rotation * new Vector2(ux, uy));
}

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)
ux = rad * m_Radius;

uy = seg.vertexes[seg.vertexes.Count - 2].y;
seg.uv.Add(rotation * new Vector2(ux, uy));

uy = seg.vertexes[seg.vertexes.Count - 1].y;
seg.uv.Add(rotation * new Vector2(ux, uy));

enter image description here

this is the seam from the last section meeting the first

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.

enter image description here

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;
            float ux, uy, rad;

            // Make first side.
            seg.vertexes.Add(rotation * (new Vector3(m_RadiusTop, height, 0f)));    // tr
            seg.vertexes.Add(rotation * (new Vector3(m_RadiusBottom, 0f, 0f)));     // bl
            seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 1]);      // br
            seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 3]);      // tl

            // Add triangle indices.
            seg.triangles.Add(0);
            seg.triangles.Add(1);
            seg.triangles.Add(2);
            seg.triangles.Add(0);
            seg.triangles.Add(2);
            seg.triangles.Add(3);

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

            // 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.
            rad = angle * Mathf.Deg2Rad;

            ux = 0;
            uy = seg.vertexes[0].y * uvScaleTop;
            seg.uv.Add(new Vector2(ux, uy));

            uy = seg.vertexes[1].y * uvScaleBottom;
            seg.uv.Add(new Vector2(ux, uy));

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

                // First vertex.
                seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 2]);

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

                // UV: x = arc length around the cylinder, y = height, scaled to tile uniformly.
                ux = (rad * m_RadiusTop) * uvScaleTop;
                uy = seg.vertexes[seg.vertexes.Count - 1].y * uvScaleTop;
                seg.uv.Add(new Vector2(ux, uy));

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

                // Second vertex.
                seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 2]);

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

                // UV: x is the same, update y for new vertical position.
                ux = (rad * m_RadiusBottom) * uvScaleBottom;
                uy = seg.vertexes[seg.vertexes.Count - 1].y * uvScaleBottom;
                seg.uv.Add(new Vector2(ux, uy));

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

            // Last UV to connect the seam.
            rad = 360f * Mathf.Deg2Rad;

            ux = (rad * m_RadiusTop) * uvScaleTop;
            uy = seg.vertexes[1].y * uvScaleTop;
            seg.uv.Add(new Vector2(ux, uy));

            ux = (rad * m_RadiusBottom) * uvScaleBottom;
            uy = seg.vertexes[0].y * uvScaleBottom;
            seg.uv.Add(new Vector2(ux, uy));

            m_Segments.Add(seg);
        }
    }

Custom normals:

enter image description here

Automatic normals by unity:

enter image description here

UV skew on different top and bottom radius:

enter image description here

(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)

\$\endgroup\$
  • \$\begingroup\$ 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! \$\endgroup\$ – nkint Mar 17 at 21:21
4
\$\begingroup\$

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:

Diagram of planar projection from above link

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:

Diagram of 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.
seg.normals.Add(seg.normals[0]);

// 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;
seg.uv.Add(new Vector2(0, uy));

uy = seg.vertexes[1].y * uvScale;
seg.uv.Add(new Vector2(0, uy));


// 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.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 2]);

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

    // UV: x = arc length around the cylinder, y = height, scaled to tile uniformly.
    ux = angle * Mathf.Deg2Rad * m_Radius * uvScale;
    uy = seg.vertexes[seg.vertexes.Count - 1].y * uvScale;
    seg.uv.Add(new Vector2(ux, uy) * uvScale);

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

    // Second vertex.
    seg.vertexes.Add(rotation * seg.vertexes[seg.vertexes.Count - 2]);

    seg.triangles.Add(seg.vertexes.Count - 3);  // shared
    seg.triangles.Add(seg.vertexes.Count - 2);  // shared
    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;
    seg.uv.Add(new Vector2(ux, uy));

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

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));
\$\endgroup\$
  • \$\begingroup\$ What a great explanation, and yes, it was exactly this that was the problem! I appreciate this so, so much. I managed to get the UV coords correct, although I am not sure how I would scale it to fit 1:1. I'm also not sure how to make it seamless. I tried adding 2(x,y 0 deg) UV points at the start, then loop around, and then add 2(x,y 360 deg) to the end, however I am still seeing an edge. \$\endgroup\$ – Mads M Mar 17 at 15:59
  • \$\begingroup\$ 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. \$\endgroup\$ – DMGregory Mar 17 at 16:08
  • \$\begingroup\$ Thank you, again. This was exactly the problem and I solved it by implementing it as you said. There is just one issue that I noticed, which is visible from the beginning - there is a hard shift in the shading where the seam is. Why do you think this occurs? \$\endgroup\$ – Mads M Mar 17 at 17:24
  • \$\begingroup\$ Most likely your normals at the doubled-up vertices disagree with one another. If you're auto-generating the normals, then because these vertices are not shared between triangles on the two sides of the seam, Unity can't tell that you want a smooth blend there. The solution is to manually provide a normal for each vertex, and ensure the normals provided to vertices on either side of a seam match one another. \$\endgroup\$ – DMGregory Mar 17 at 17:35
  • \$\begingroup\$ Right again. I set the last side to share triangle indices with the first. Then calling unity's RecalculateNormals() makes it look smooth again. Although it did do something strange to the UV map on the last side only. But I will get back to that tomorrow. Thank you so much. You have been a tremendous help and I have learned a lot! I would upvote you 10 times more if I could. \$\endgroup\$ – Mads M Mar 17 at 18:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.