Green texture glitches in edges of custom mesh

Hi, I’m quite new to Unity and have followed a few tutorials on creating custom meshes for a blocky terrain style. I have found quite a nice result so far.

However, when the terrain is generated, each edge of each cube has some strange green texturing glitch going on which is barely noticeable at close range, but multiplies as distance from the camera increases and leads to the entire mesh looking green!

The way the mesh is created is by essentially making each cube and then adding the vertices and triangles to the chunk mesh. Therefore, I have a hunch that it may be something to do with shared vertices/edges.

Note that only faces of the cube that aren’t against a solid object are created (i.e if a cube is surrounded on all sides except from above, only the top face will be created and rendered.)

I will post some screenshots and code snippets when I get the chance. Thank you. :slight_smile:

The code below is what is used to create each chunk mesh from the vertices and triangles of each block.

//Sends the calculated mesh information to the mesh and collision components
    void RenderMesh(MeshData meshData)
    {
        filter.mesh.Clear();
        filter.mesh.vertices = meshData.vertices.ToArray();
        filter.mesh.triangles = meshData.triangles.ToArray();

        filter.mesh.uv = meshData.uv.ToArray ();
        //rend.material.color = meshData.color;
        filter.mesh.RecalculateNormals();

        coll.sharedMesh = null;
        Mesh mesh = new Mesh();
        mesh.vertices = meshData.colVertices.ToArray ();
        mesh.triangles = meshData.colTriangles.ToArray ();
        mesh.RecalculateNormals();

        coll.sharedMesh = mesh;
    }

What do your UVs look like? What does your texture look like?

It sounds like it’s texture bleeding along UV edges from your lower level mipmaps. You want to leave a little space around the UV borders to allow for the mipmaps to be resampled without picking up some incorrect color.

1 Like

Thank you for replying!
Well the way it’s done is that each block’s mesh is calculated (in terms of verts, tris and UVs) and then those same values are added to the chunk’s mesh values, therefore creating a whole mesh from multiple others.
As for the textures, I’m simply using one material with a tilemap of just solid colours.

If it is the texture bleeding like you said, how do I fix it?

Okay, that is probably what it is then. I assume you are calculating your UVs right along the texture borders (the white box in the attached image), but you just want to move them in a bit to leave a buffer around your texture (the black box).

The more pixels you leave as a border, the more mip map levels (distance from the camera) you will get before you start to see bleeding. You will always get it eventually because mip maps are always calculated down to a single pixel where the entire texture is blended together.

2242189--149657--mipmap.jpg

2 Likes

Fantastic! Thank you very much! It’s been annoying me for ages. I will try out what you said and get back to you. Thanks again! :slight_smile:

Great, I did what you said and added a small border to the edges of the UVs and it works like a charm! Thank you! I’m still getting a slight greening at a distance though, so I assume that I simply need to increase the size of the border and that will increase the distance at which the greening/bleeding occurs?

Yes, the larger the border the more distance you will get before bleeding starts to happen. It will always start happening eventually. That’s a problem inherent with using a texture atlas.

Your option to eliminate the problem entirely is to use submeshes. A submesh is just a list of triangles that use a specific material (a material being a certain shader and its assigned textures and uniform variables). Each submesh will create its own draw call which will allow you to use a separate texture for each type of surface you want to represent. Since each texture is handled by the GPU as a separate object you will not get bleeding around the edges; you can define the edges to either wrap (i.e. repeat) or clamp and the GPU will handle it.

Using submeshes should be fairly similar to what you already have, you just need to separate out your tiles and create a new material for each and then take a look at the SetTriangles() method to assign new geometry to the right submesh.

1 Like

Ah ok. Well what you suggested previously has worked fine.
However I had also been looking into a way to use separate materials rather than a tilemap so from what you’ve said, sub meshes definitely sounds like the way to go! Thank you! I will look into using them and will try it out.
Thanks for your help! :slight_smile:

Ok, so I’ve been trying to find some help on submeshes. It seems feasible but I can’t seem to wrap my head around it. I will post my current code to make my current situation more clear.

The Block class:

public class Block
{
    const float tileSize = 0.25f;
    public bool changed = true;

    public enum Direction
    {
        north,
        east,
        south,
        west,
        up,
        down }
    ;

    //Base block constructor
    public Block ()
    {

    }

    public virtual MeshData Blockdata (Chunk chunk, int x, int y, int z, MeshData meshData)
    {

        meshData.useRenderDataForCol = true;

        if (!chunk.GetBlock (x, y + 1, z).IsSolid (Direction.down)) {
            meshData = FaceDataUp (chunk, x, y, z, meshData);
        }

        if (!chunk.GetBlock (x, y - 1, z).IsSolid (Direction.up)) {
            meshData = FaceDataDown (chunk, x, y, z, meshData);
        }

        if (!chunk.GetBlock (x, y, z + 1).IsSolid (Direction.south)) {
            meshData = FaceDataNorth (chunk, x, y, z, meshData);
        }

        if (!chunk.GetBlock (x, y, z - 1).IsSolid (Direction.north)) {
            meshData = FaceDataSouth (chunk, x, y, z, meshData);
        }

        if (!chunk.GetBlock (x + 1, y, z).IsSolid (Direction.west)) {
            meshData = FaceDataEast (chunk, x, y, z, meshData);
        }

        if (!chunk.GetBlock (x - 1, y, z).IsSolid (Direction.east)) {
            meshData = FaceDataWest (chunk, x, y, z, meshData);
        }

        //meshData.color = SetColor();

        return meshData;

    }

    protected virtual MeshData FaceDataUp
        (Chunk chunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex (new Vector3 (x - 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex (new Vector3 (x - 0.5f, y + 0.5f, z - 0.5f));

        meshData.AddQuadTriangles ();

        meshData.uv.AddRange(FaceUVs (Direction.up));

        return meshData;
    }

    protected virtual MeshData FaceDataDown
        (Chunk chunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex (new Vector3 (x - 0.5f, y - 0.5f, z - 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y - 0.5f, z - 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y - 0.5f, z + 0.5f));
        meshData.AddVertex (new Vector3 (x - 0.5f, y - 0.5f, z + 0.5f));
       
        meshData.AddQuadTriangles ();

        meshData.uv.AddRange(FaceUVs (Direction.down));

        return meshData;
    }

    protected virtual MeshData FaceDataNorth
        (Chunk chunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex (new Vector3 (x + 0.5f, y - 0.5f, z + 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex (new Vector3 (x - 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex (new Vector3 (x - 0.5f, y - 0.5f, z + 0.5f));
       
        meshData.AddQuadTriangles ();

        meshData.uv.AddRange(FaceUVs (Direction.north));

        return meshData;
    }

    protected virtual MeshData FaceDataEast
        (Chunk chunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex (new Vector3 (x + 0.5f, y - 0.5f, z - 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y - 0.5f, z + 0.5f));
       
        meshData.AddQuadTriangles ();

        meshData.uv.AddRange(FaceUVs (Direction.east));

        return meshData;
    }

    protected virtual MeshData FaceDataSouth
        (Chunk chunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex (new Vector3 (x - 0.5f, y - 0.5f, z - 0.5f));
        meshData.AddVertex (new Vector3 (x - 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex (new Vector3 (x + 0.5f, y - 0.5f, z - 0.5f));
       
        meshData.AddQuadTriangles ();

        meshData.uv.AddRange(FaceUVs (Direction.south));

        return meshData;
    }

    protected virtual MeshData FaceDataWest
        (Chunk chunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex (new Vector3 (x - 0.5f, y - 0.5f, z + 0.5f));
        meshData.AddVertex (new Vector3 (x - 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex (new Vector3 (x - 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex (new Vector3 (x - 0.5f, y - 0.5f, z - 0.5f));
       
        meshData.AddQuadTriangles ();

        meshData.uv.AddRange(FaceUVs (Direction.west));

        return meshData;
    }

    public virtual bool IsSolid (Direction direction)
    {
        switch (direction) {
        case Direction.north:
            return true;
        case Direction.east:
            return true;
        case Direction.south:
            return true;
        case Direction.west:
            return true;
        case Direction.up:
            return true;
        case Direction.down:
            return true;
        }

        return false;
    }

    public virtual Tile TexturePosition(Direction direction)
    {
        Tile tile = new Tile();
        tile.x = 0;
        tile.y = 0;

        return tile;
    }

//    public virtual Color SetColor()
//    {
//        Color color = new Color();
//        color.r = 1;
//        color.g = 1;
//        color.b = 1;
//        color.a = 1;
//
//        return color;
//    }

    public virtual Vector2[] FaceUVs(Direction direction)
    {
        Vector2[] UVs = new Vector2[4];
        Tile tilePos = TexturePosition (direction);
        float mipmapAdjustment = 0.07f;

        UVs[0] = new Vector2((tileSize * tilePos.x + tileSize) - mipmapAdjustment, (tileSize * tilePos.y) + mipmapAdjustment);
        UVs[1] = new Vector2((tileSize * tilePos.x + tileSize) - mipmapAdjustment, (tileSize * tilePos.y + tileSize) - mipmapAdjustment);
        UVs[2] = new Vector2((tileSize * tilePos.x) + mipmapAdjustment, (tileSize * tilePos.y + tileSize) - mipmapAdjustment);
        UVs[3] = new Vector2((tileSize * tilePos.x) + mipmapAdjustment, (tileSize * tilePos.y) + mipmapAdjustment);

        return UVs;
    }

    public struct Tile { public int x; public int y;}

}

The MeshData Class:

public class MeshData
{
    public List<Vector3> vertices = new List<Vector3>();
    public List<int> triangles = new List<int>();
    public List<Vector2> uv = new List<Vector2>();
    //public Color color;

    public List<Vector3> colVertices = new List<Vector3>();
    public List<int> colTriangles = new List<int>();

    public bool useRenderDataForCol;

    public MeshData() { }

    public void AddQuadTriangles()
    {
        triangles.Add(vertices.Count - 4);
        triangles.Add(vertices.Count - 3);
        triangles.Add(vertices.Count - 2);

        triangles.Add(vertices.Count - 4);
        triangles.Add(vertices.Count - 2);
        triangles.Add(vertices.Count - 1);

        if(useRenderDataForCol)
        {
            colTriangles.Add(colVertices.Count - 4);
            colTriangles.Add(colVertices.Count - 3);
            colTriangles.Add(colVertices.Count - 2);

            colTriangles.Add(colVertices.Count - 4);
            colTriangles.Add(colVertices.Count - 2);
            colTriangles.Add(colVertices.Count - 1);
        }
    }

    public void AddVertex(Vector3 vertex)
    {
        vertices.Add (vertex);

        if (useRenderDataForCol)
        {
            colVertices.Add(vertex);
        }
    }

    public void AddTriangle(int tri)
    {
        triangles.Add (tri);

        if (useRenderDataForCol)
        {
            colTriangles.Add (tri - (vertices.Count - colVertices.Count));
        }
    }
}

And the Chunk Class (only relevant functions):

// Updates the chunk based on its contents
    void UpdateChunk()
    {
        rendered = true;

        MeshData meshData = new MeshData();
       
        for (int x = 0; x < chunkSize; x++)
        {
            for (int y = 0; y < chunkSize; y++)
            {
                for (int z = 0; z < chunkSize; z++)
                {
                    meshData = blocks[x, y, z].Blockdata(this, x, y, z, meshData);
                }
            }
        }

        RenderMesh(meshData);
    }

    //Sends the calculated mesh information to the mesh and collision components
    void RenderMesh(MeshData meshData)
    {
        filter.mesh.Clear();
        filter.mesh.vertices = meshData.vertices.ToArray();
        filter.mesh.triangles = meshData.triangles.ToArray();

        filter.mesh.uv = meshData.uv.ToArray ();
        //rend.material.color = meshData.color;
        filter.mesh.RecalculateNormals();

        coll.sharedMesh = null;
        Mesh mesh = new Mesh();
        mesh.vertices = meshData.colVertices.ToArray ();
        mesh.triangles = meshData.colTriangles.ToArray ();
        mesh.RecalculateNormals();

        coll.sharedMesh = mesh;
    }

In your meshData class you need to keep a separate array for each material. Instead of calculating the tile UVs for a specific tile, always set the UVs to the entire texture (i.e. your UVs should be (0,0), (0,1), (1,0), (1,1)) and then assign it to the array with the correct material for that type of tile. So, grass could be array index 0, stone could be 1, or however you want to set up the appearance of your blocks. You will need to use an array containing arrays of triangles.

Then when you are assigning the mesh triangles, you replace this:

mesh.vertices= meshData.colTriangles.ToArray();

with something like this:

for (int i = 0; i < meshData.materialCount; i++)
{
     mesh.SetTriangles(meshData.colTriangles[i].ToArray, i);
}

The each submesh will be rendered with the matching material with in the material array of the MeshRenderer object used to render the mesh. So make sure the grass material is assign to Renderer.materials[0], the stone material is assigned to Renderer.materials[1], etc…

Ah right, thank you!

The meshData.colTriangles is the array of triangles for the collider, rather than the materials. Therefore, I assume should I change

mesh.vertices = meshData.triangles.ToArray

instead of

mesh.vertices = meshData.colTriangles.ToArray

?

Also, I don’t quite understand what you mean with the first paragraph, about keeping a separate array for each material. Could you please explain in code? Thank you!

I’m sorry I’m a bit noobish. But you are extremely helpful, thank you again. :slight_smile:

I looked at your code, and you are actually using a List for your triangles. So you’d want an array of Lists.

It would look something like instead of your existing “triangles” variable:

public List<int>[] submeshTriangles = new List<int>[MATERIAL_COUNT];

Where “MATERIAL_COUNT” is the number of materials you want to use. Then you would initialize each list:

for (int i = 0; i < MATERIAL_COUNT; i++)
{
     submeshTriangles[i] = new List<int>();
}

Instead of adding your triangles to a single list “triangles” you add it to a list in your array of lists “submeshTriangles*”.* To get the specific list, you use the index of the material you want those triangles to use (i.e. 0 for grass, 1 for stone, etc.)

Then you’d finally use:

mesh.SetTriangles(meshData.submeshTriangles[i].ToArray, i);

And, yes, if “colTriangles” is for your collider then you’d want to use “triangles” if that’s your geometry. I’m just writing this all off the cuff so no guarantees on the exact syntax or variables names, but hopefully you can get the main idea.

Ah I see! Thank you!
I have put that in place and will let you know how it turns out. :slight_smile: You’ve been a fantastic help! Thank you!! :smile:

Right, it all seems to be going well. Though I’ve now run into this little error when trying to run!

The line it’s referring to is:

for (int i = 0; i < 5; i++)
        {
            filter.mesh.SetTriangles(meshData.submeshTriangles[i].ToArray(), i);
        }

My edited RenderMesh proc is now this:

void RenderMesh(MeshData meshData)
    {
        filter.mesh.Clear();
        filter.mesh.vertices = meshData.vertices.ToArray();
        //filter.mesh.triangles = meshData.triangles.ToArray();

        //New Stuff!
        for (int i = 0; i < 5; i++)
        {
            filter.mesh.SetTriangles(meshData.submeshTriangles[i].ToArray(), i);
        }

        //filter.mesh.uv = meshData.uv.ToArray ();

        filter.mesh.RecalculateNormals();

        coll.sharedMesh = null;
        Mesh mesh = new Mesh();
        mesh.vertices = meshData.colVertices.ToArray ();
        mesh.triangles = meshData.colTriangles.ToArray ();
        mesh.RecalculateNormals();

        coll.sharedMesh = mesh;
    }

Also, I have changed my MeshData script as you suggested:

public class MeshData
{
    public List<Vector3> vertices = new List<Vector3>();

    public List<int>[] submeshTriangles = new List<int>[5];       //New array of Lists with 5 materials available

    //public List<int> triangles = new List<int>();
    public List<Vector2> uv = new List<Vector2>();
    //public Color color;

    public List<Vector3> colVertices = new List<Vector3>();
    public List<int> colTriangles = new List<int>();

    public bool useRenderDataForCol;

    public MeshData() { }

    public void AddQuadTriangles(int blockType)   
//Where blockType refers to the block (0 for stone, 1 for grass, etc; -1 for Air)
if (blockType != -1)
        {
            submeshTriangles[blockType].Add(vertices.Count - 4);
            submeshTriangles[blockType].Add(vertices.Count - 3);
            submeshTriangles[blockType].Add(vertices.Count - 2);
  
            submeshTriangles[blockType].Add(vertices.Count - 4);
            submeshTriangles[blockType].Add(vertices.Count - 2);
            submeshTriangles[blockType].Add(vertices.Count - 1);

            if(useRenderDataForCol)
            {
                colTriangles.Add(colVertices.Count - 4);
                colTriangles.Add(colVertices.Count - 3);
                colTriangles.Add(colVertices.Count - 2);

                colTriangles.Add(colVertices.Count - 4);
                colTriangles.Add(colVertices.Count - 2);
                colTriangles.Add(colVertices.Count - 1);
            }
        }
    }
}

I will keep trying to fix it, but any help would be appreciated. :slight_smile: Thanks!

The index out of bounds error just means that the submesh array isn’t big enough for the triangles you are trying to assign it. You likely have to set “Mesh.subMeshCount” to the number of submeshes you want first so the mesh object can make the underlying array big enough before you start assigning data.

1 Like

Ah of course! Thank you!

I’m probably being a complete dunce again, but I’m now not getting any errors, but when the terrain is generated, none of the blocks have any texture at all. All I get is the green wireframe of the colliders in the scene window. Let me know if you need me to post any more code. :slight_smile:

I’m not really familiar enough with the details to be of much help, and I don’t really have the time to debug someone else’s code. You should just have to set the submesh count, assign the triangles, and then assign the materials.

I would just create a toy example with only a couple triangles, assign each one to a different submesh, and make sure you can assign a different material to each. Once you have the procedure for that down, you just need to apply that to your terrain builder and make sure you aren’t messing anything up. Write a few routines that will spit out the data so you can see what’s happening, start with a very small test terrain, and step through it slowly to find where things go wrong.

Ok, that’s a good idea thank you. And I really do appreciate how helpful you’ve been, thank you! :slight_smile: