Fix these texture stretch at the corners of the line renderer

Can anyone point me towards a resource on how to fix these texture stretch at the corners of the line renderer? Think it has something to do with UV coordinates

Have you tried playing with this property, Texture Mode?

No actually I take it back, none of the modes seems to be adding extra UV stretch (or I should say, less) to the corners.

I think maybe to do what you want you have two choices:

  • Add more points in that corner
  • Use Splines :slight_smile: and one of the extrusion scripts that come with it. But it’s a completely different system!
1 Like

Adding more points to the corner does not work. Tried adding 100s. I’ll see if splines has the issue.

Ah sorry, I didn’t mean adding geometry to the corner, but literally more “control points”, which should create more segments.
But then it all becomes harder to manage… it depends on how many of these you have, how are they created (by hand?), etc.

Any fix here? This seems like a pretty ridiculous state for corners.

Finally fixed this by generating a custom mesh from scratch. This code assum an animated material on X UV axis. My material is build with shadergraph and animated with a tiling and offset node along the X UVs.

The segmented Quad are needed to realign the U in the UVs after a corner as they get out of sync and start streching the texture. There’s no doupt a better way to do this, but this work around is good enough for now. See it in action:

using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class MeshTest : MonoBehaviour
{
    private Mesh mesh;

    private float width = 0.7f;
    private float height = 0.7f;

    [SerializeField]
    private CornerDirection cornerDirection = CornerDirection.DOWN_TO_LEFT;

    // Angle constants
    private const float ANGLE_0 = 0f;
    private const float ANGLE_90 = Mathf.PI / 2f; // 90 degrees
    private const float ANGLE_180 = Mathf.PI; // 180 degrees
    private const float ANGLE_270 = Mathf.PI * 1.5f; // 270 degrees
    private const float ANGLE_NEG_90 = -Mathf.PI / 2f; // -90 degrees

    public enum CornerDirection
    {

        DOWN_TO_LEFT,
        DOWN_TO_RIGHT,
        RIGHT_TO_DOWN,
        LEFT_TO_DOWN,
        TOP_TO_RIGHT,
        TOP_TO_LEFT,
        LEFT_TO_TOP,
        RIGHT_TO_TOP,

    }

    private enum Direction
    {
        DOWN_TO_UP,
        LEFT_TO_RIGHT,
        RIGHT_TO_LEFT,
        UP_TO_DOWN
    }

    // Start is called before the first frame update
    void Start()
    {
        mesh = new Mesh();
        MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();
        meshFilter.mesh = mesh;

        // Create lists to store mesh data
        List<Vector3> vertices = new List<Vector3>();
        List<int> triangles = new List<int>();
        List<Vector2> uvs = new List<Vector2>();

        vertices.Add(Vector3.zero); // Bottom-left corner
        uvs.Add(new Vector2(1f, 0f)); // Bottom-left corner
        vertices.Add(new Vector3(width, 0f, 0));  // Bottom Right
        uvs.Add(new Vector2(1, 1f));  // Bottom Right

        AddQuad(vertices, triangles, uvs); 
        AddQuad(vertices, triangles, uvs);
        AddQuad(vertices, triangles, uvs);
        AddQuad(vertices, triangles, uvs);
        AddQuad(vertices, triangles, uvs);
        AddQuad(vertices, triangles, uvs);
        var nextIndices = AddQuarterCircle(vertices, triangles, uvs, cornerDirection);
        switch (cornerDirection)
        {
            case CornerDirection.DOWN_TO_LEFT:
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices); 
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.RIGHT_TO_DOWN);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.TOP_TO_RIGHT);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.LEFT_TO_TOP);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.DOWN_TO_LEFT); 
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.RIGHT_TO_TOP); 
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.DOWN_TO_RIGHT);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.LEFT_TO_DOWN);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.TOP_TO_LEFT);
                AddQuad(vertices, triangles, uvs,  startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                break;
            case CornerDirection.DOWN_TO_RIGHT:
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices); 
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.LEFT_TO_DOWN);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.TOP_TO_LEFT);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.RIGHT_TO_TOP);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.DOWN_TO_RIGHT); 
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices); 
                AddQuad(vertices, triangles, uvs); 
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.LEFT_TO_TOP);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices); 
                AddQuad(vertices, triangles, uvs); 
                AddQuad(vertices, triangles, uvs); 
                AddQuad(vertices, triangles, uvs); 
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.DOWN_TO_LEFT);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.RIGHT_TO_DOWN);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                AddQuad(vertices, triangles, uvs);
                nextIndices = AddQuarterCircle(vertices, triangles, uvs, CornerDirection.TOP_TO_RIGHT);
                AddQuad(vertices, triangles, uvs, startIndices: nextIndices);
                AddQuad(vertices, triangles, uvs);
                break;
        }

        // Assign the data to the mesh
        mesh.vertices = vertices.ToArray();
        mesh.triangles = triangles.ToArray();
        //Log.DEBUG("My uvs: " + string.Join(", ", uvs));
        mesh.uv = uvs.ToArray();

        // Recalculate normals for proper lighting
        //mesh.RecalculateNormals();
    }

    (int, int) AddQuarterCircle(List<Vector3> vertices, List<int> triangles, List<Vector2> uvs, CornerDirection direction)
    {
        switch (direction)
        {
            case CornerDirection.DOWN_TO_LEFT:
                return AddQuarterCirclePivotLeft(vertices, triangles, uvs, direction);
            case CornerDirection.DOWN_TO_RIGHT:
                return AddQuarterCirclePivotRight(vertices, triangles, uvs, direction);
            case CornerDirection.RIGHT_TO_DOWN:
                return AddQuarterCirclePivotLeft(vertices, triangles, uvs, direction);
            case CornerDirection.RIGHT_TO_TOP:
                return AddQuarterCirclePivotRight(vertices, triangles, uvs, direction);
            case CornerDirection.LEFT_TO_DOWN:
                return AddQuarterCirclePivotRight(vertices, triangles, uvs, direction);
            case CornerDirection.LEFT_TO_TOP:
                return AddQuarterCirclePivotLeft(vertices, triangles, uvs, direction);
            case CornerDirection.TOP_TO_RIGHT:
                return AddQuarterCirclePivotLeft(vertices, triangles, uvs, direction);
            case CornerDirection.TOP_TO_LEFT:
                return AddQuarterCirclePivotRight(vertices, triangles, uvs, direction);
            default: 
                throw new System.Exception("Invalid direction");
        }
    
    }

 (int, int) AddQuarterCirclePivotRight(List<Vector3> vertices, List<int> triangles, List<Vector2> uvs, CornerDirection direction)
    {
        float radius = 0.7f; // Radius of the quarter-circle
        int segments = 16; // Number of segments to approximate the quarter-circle

        // Store the starting index for the new vertices
        int pivotIndex = vertices.Count - 1;
        var pivotVertex = vertices[vertices.Count - 1]; // Use the 2nd last vertex as the center

        // Use the last two UVs as the base
        var bottomLeftUV = uvs[uvs.Count - 2];
        //var bottomRightUV = uvs[uvs.Count - 1];
     
        // Adjust the angle range based on the orientation
        float startAngle;
        float endAngle;
        switch (direction)
        {
            case CornerDirection.DOWN_TO_RIGHT:
                startAngle = ANGLE_180;
                endAngle = ANGLE_90;
                break;
            case CornerDirection.LEFT_TO_DOWN:
                startAngle = ANGLE_90; 
                endAngle = ANGLE_0;
                break;
            case CornerDirection.TOP_TO_LEFT:
                startAngle = ANGLE_0; 
                endAngle = ANGLE_NEG_90;
                break;
            case CornerDirection.RIGHT_TO_TOP:
                startAngle = ANGLE_NEG_90;
                endAngle = -ANGLE_180; // -180 degrees
                break;
            default:
                throw new System.Exception("Invalid direction");
        }

        // Generate vertices along the quarter-circle
        for (int i = 1; i <= segments; i++)
        {
            float angle = Mathf.Lerp(startAngle, endAngle, i / (float)segments); // Interpolate angle based on orientation
            float x = Mathf.Cos(angle) * radius;
            float y = Mathf.Sin(angle) * radius;
            vertices.Add(pivotVertex + new Vector3(x, y, 0)); // Add vertex on the arc relative to the center

            // Calculate UVs relative to the base UVs
            float u = Mathf.Lerp(bottomLeftUV.x, bottomLeftUV.x - 1, i / (float)segments); // Interpolate U based on the curve
            float v = bottomLeftUV.y;
            var uv = new Vector2(u, v);
            //print(uv);
            uvs.Add(uv);
        }
        //Log.DEBUG("bottom left UV1: " + bottomLeftUV + ", bottomRight: UV2: " + bottomRightUV);
  
        triangles.Add(pivotIndex); // Center vertex
        triangles.Add(pivotIndex - 1);
        triangles.Add(pivotIndex+ 1);
   
        // Generate triangles for the quarter-circle
        for (int i = 1; i < segments; i++)
        {
            triangles.Add(pivotIndex); // Center vertex
            triangles.Add(pivotIndex + i);
            triangles.Add(pivotIndex + i + 1);
        }

        return (vertices.Count-1, pivotIndex);
    }

    (int, int) AddQuarterCirclePivotLeft(List<Vector3> vertices, List<int> triangles, List<Vector2> uvs, CornerDirection direction)
    {
        float radius = 0.7f; // Radius of the quarter-circle
        int segments = 16; // Number of segments to approximate the quarter-circle

        // Store the starting index for the new vertices
        int pivotIndex = vertices.Count - 2;
        var pivotVertex = vertices[vertices.Count - 2]; // Use the 2nd last vertex as the center

        // Use the last two UVs as the base
        //var bottomLeftUV = uvs[uvs.Count - 2];
        var bottomRightUV = uvs[uvs.Count - 1];

        // Adjust the angle range based on the orientation
        float startAngle;
        float endAngle;
        switch (direction)
        {
            case CornerDirection.DOWN_TO_LEFT:
                startAngle = ANGLE_0;
                endAngle = ANGLE_90;
                break;
            case CornerDirection.RIGHT_TO_DOWN:
                startAngle = ANGLE_90; 
                endAngle = ANGLE_180;
                break;
            case CornerDirection.TOP_TO_RIGHT:
                startAngle = ANGLE_180; 
                endAngle = ANGLE_270; 
                break;
            case CornerDirection.LEFT_TO_TOP:
                startAngle = ANGLE_NEG_90; 
                endAngle = ANGLE_0; 
                break;
            default:
                throw new System.Exception("Invalid direction");
        }

        // Generate vertices along the quarter-circle
        for (int i = 1; i <= segments; i++)
        {
            float angle = Mathf.Lerp(startAngle, endAngle, i / (float)segments); // Interpolate angle based on orientation
            float x = Mathf.Cos(angle) * radius;
            float y = Mathf.Sin(angle) * radius;
            vertices.Add(pivotVertex + new Vector3(x, y, 0)); // Add vertex on the arc relative to the center

            // Calculate UVs relative to the base UVs
            float u = Mathf.Lerp(bottomRightUV.x, bottomRightUV.x - 1f, i / (float)segments); // Interpolate U based on the curve
            float v = bottomRightUV.y;
            var uv = new Vector2(u, v);
            //print(uv);
            uvs.Add(uv);
        }
        //Log.DEBUG("bottom left UV1: " + bottomLeftUV + ", bottomRight: UV2: " + bottomRightUV);

        // Generate triangles for the quarter-circle
        for (int i = 1; i <= segments; i++)
        {
            triangles.Add(pivotIndex); // Center vertex
            triangles.Add(pivotIndex + i);
            triangles.Add(pivotIndex + i + 1);
        }

        return (pivotIndex, vertices.Count - 1);
    }

    void AddQuad(
        List<Vector3> vertices,
        List<int> triangles,
        List<Vector2> uvs,
        (int, int)? startIndices = null)
    {
        if (startIndices.HasValue) {
            // After a corner the X UVs will be out of alignment, realign over a series of quads.
            // This should be less noticeable to the player.
            AddQuadSegmented(vertices, triangles, uvs, startIndices: startIndices.Value); 
            return;
        } 

        int startIndex = vertices.Count - 2;
        var bottomLeft = vertices[vertices.Count - 2]; 
        var bottomRight = vertices[vertices.Count - 1]; 
        var bottomLeftUV = uvs[vertices.Count - 2];
        var bottomRightUV = uvs[vertices.Count - 1];

        Direction direction = quadDirection(bottomLeft, bottomRight);

        //print("bottomLeftUV: " + bottomLeftUV + ", bottomRightUV: " + bottomRightUV);

        Vector3 topLeftUV  = bottomLeftUV - new Vector2(1f, 0f);
        Vector3 topRightUV = bottomRightUV - new Vector2(1f, 0f);

        Vector3 topLeft;
        Vector3 topRight;
        switch (direction)
        {
            case Direction.DOWN_TO_UP:
                topLeft = bottomLeft + new Vector3(0, height, 0);
                topRight = bottomRight + new Vector3(0, height, 0f);  
                break;
            case Direction.UP_TO_DOWN:
                topLeft = bottomLeft - new Vector3(0, height, 0);
                topRight = bottomRight - new Vector3(0, height, 0f);  
                break;
            case Direction.LEFT_TO_RIGHT:
                topLeft = bottomLeft + new Vector3(width, 0);
                topRight = bottomRight + new Vector3(width, 0f);
                break;
            case Direction.RIGHT_TO_LEFT:
                topLeft = bottomLeft - new Vector3(width, 0);
                topRight = bottomRight - new Vector3(width, 0f);
                break;
            default:
                throw new System.Exception("Invalid direction");
        }
    
        vertices.Add(topLeft); // Top left
        uvs.Add(topLeftUV); // Top left

        vertices.Add(topRight);  // Top Right
        uvs.Add(topRightUV);  // Top Right  

        triangles.Add(startIndex);
        triangles.Add(startIndex + 2);
        triangles.Add(startIndex + 3);

        triangles.Add(startIndex);
        triangles.Add(startIndex + 3);
        triangles.Add(startIndex + 1);
    }

    private Direction quadDirection(Vector3 bottomLeft, Vector3 bottomRight)
    {
        Vector3 vector = bottomRight - bottomLeft;
        // Determine the direction based on the direction vector
        // Horizontal
        if (Mathf.Approximately(vector.y, 0))
        {
            return vector.x > 0 ? Direction.DOWN_TO_UP : Direction.UP_TO_DOWN;
        }
        else
        {
            // Vertical
            return vector.y > 0 ? Direction.RIGHT_TO_LEFT : Direction.LEFT_TO_RIGHT;
        }
    }

    void AddQuadSegmented(
        List<Vector3> vertices,
        List<int> triangles,
        List<Vector2> uvs,
        (int, int) startIndices)
    {
        int startIndex = vertices.Count - 1;
        var bottomLeft = vertices[startIndices.Item1]; 
        var bottomRight = vertices[startIndices.Item2]; 
        var bottomLeftUV = uvs[startIndices.Item1];
        var bottomRightUV = uvs[startIndices.Item2];

        Direction direction = quadDirection(bottomLeft, bottomRight);

        //print("bottomLeftUV: " + bottomLeftUV + ", bottomRightUV: " + bottomRightUV);
    
        Vector3 topLeftUV;
        Vector3 topRightUV;
        Vector3 topLeft;
        Vector3 topRight;
        switch (direction)
        {
            case Direction.DOWN_TO_UP:
                topLeft = bottomLeft + new Vector3(0, height, 0);
                topRight = bottomRight + new Vector3(0, height, 0f);
                if (bottomLeftUV.x > bottomRightUV.x) {
                    topLeftUV = bottomLeftUV - new Vector2(2f, 0f);
                    topRightUV = bottomRightUV - new Vector2(1f, 0f);  
                } else {
                    topLeftUV = bottomLeftUV - new Vector2(1f, 0f);
                    topRightUV = bottomRightUV - new Vector2(2f, 0f);  
                }
                break;
            case Direction.UP_TO_DOWN:
                topLeft = bottomLeft - new Vector3(0, height, 0);
                topRight = bottomRight - new Vector3(0, height, 0f);
                if (bottomLeftUV.x > bottomRightUV.x) {
                    topLeftUV = bottomLeftUV - new Vector2(2f, 0f);
                    topRightUV = bottomRightUV - new Vector2(1f, 0f);  
                } else {
                    topLeftUV = bottomLeftUV - new Vector2(1f, 0f);
                    topRightUV = bottomRightUV - new Vector2(2f, 0f);  
                }
                break;
            case Direction.LEFT_TO_RIGHT:
                topLeft = bottomLeft + new Vector3(width, 0);
                topRight = bottomRight + new Vector3(width, 0f);
                if (bottomLeftUV.x > bottomRightUV.x) {
                    topLeftUV = bottomLeftUV - new Vector2(2f, 0f);
                    topRightUV = bottomRightUV - new Vector2(1f, 0f);
                } else {
                    topLeftUV = bottomLeftUV - new Vector2(1f, 0f);
                    topRightUV = bottomRightUV - new Vector2(2f, 0f);
                }
                break;
            case Direction.RIGHT_TO_LEFT:
                topLeft = bottomLeft - new Vector3(width, 0);
                topRight = bottomRight - new Vector3(width, 0f);
                if (bottomLeftUV.x > bottomRightUV.x) {
                    topLeftUV = bottomLeftUV - new Vector2(2f, 0f);
                    topRightUV = bottomRightUV - new Vector2(1f, 0f);
                } else {
                    topLeftUV = bottomLeftUV - new Vector2(1f, 0f);
                    topRightUV = bottomRightUV - new Vector2(2f, 0f);
                }
                break;
            default:
                throw new System.Exception("Invalid direction");
        }

        int segments = 6;

        for (int i = 0; i < segments; i++)
        {
            float t = (float)i / (segments - 1);
            var newTopLeft = Vector3.Lerp(bottomLeft, topLeft, t);
            var newTopRight = Vector3.Lerp(bottomRight, topRight, t);
            vertices.Add(newTopLeft); 
            vertices.Add(newTopRight);

            // Calculate UVs relative to the base UVs
            var newTopLeftUV = Vector3.Lerp(bottomLeftUV, topLeftUV, t);
            var newTopRightUV = Vector3.Lerp(bottomRightUV, topRightUV, t);
            uvs.Add(newTopLeftUV);
            uvs.Add(newTopRightUV);
        }
        startIndex += 3;
        triangles.Add(startIndices.Item1);
        triangles.Add(startIndices.Item2);
        triangles.Add(startIndex);

        triangles.Add(startIndices.Item2);
        triangles.Add(startIndex);
        triangles.Add(startIndex + 1);

        for (int i = 0; i < segments + 1; i++)
        {
            triangles.Add(startIndex + i);
            triangles.Add(startIndex + i + 1);
            triangles.Add(startIndex + i + 2);

            triangles.Add(startIndex + i + 1);
            triangles.Add(startIndex + i + 2);
            triangles.Add(startIndex + i + 3);
        }
    }

    // Update is called once per frame
    void Update()
    {
    }
}