How to make cave shadows cancel out indirect lighting?

I’m pretty sure this is impossible, but here we go. So I have my game, and it has caves. I am using HDRP, so I have indirect lighting which makes the game look great. But the only problem, as you probably have guessed, the indirect lighting makes the caves illuminated. Here’s a screenshot:

But now I turn off indirect lighting:

And hooray! The cave is dark. But look at everything else! Ruined!
Any help would be appreciated. Thanks.

Greedy Meshing Algorithm


###Explanation###

The basic premise of the algorithm is that you scan every block in a chunk. If you have scanned a block already (or the block is air), skip it. Else, keep ‘extending’ the block on the x-axis, until you find a block with differing block type, or you’ve reached the end of the chunk. Then, extend on the y, and z-axis, until you’ve scanned block positions in a cuboid pattern.


Generate a cuboid as vertices and triangles (as well as UVs to match your block type appearance), and append it to your chunk’s mesh data. You can find many articles describing this algorithm in much more detail. For example: [Meshing in a minecraft game by 0fps][1]


###Implementation###

There are many difficulties associated with implementing this algorithm, so I’ll show you the raw C# implementation:


Greedy Mesh
This class defines the necessary data to generate the cuboid mesh.

public struct GreedyMesh
{
    public Vector3Int Start;
    public Vector3Int End;
    public BlockType Type;

    public GreedyMesh(Vector3Int start, BlockType type)
    {
        Start = start;
        End = Start;
        Type = type;
    }
}

It has a start and end position (opposite corners of the cuboid along the diagonals), and a block type, to determine what textures the cuboid should be displayed with.


Mesh Data
This class has utility methods for generating cuboids and packaging them in single meshes:

using System.Collections.Generic;
using UnityEngine;

public class MeshData
{
    public List<Vector3> Vertices;
    public List<int> Triangles;
    public List<Vector2> UVs;
    public List<Vector3> TilingUVs;

    /// <summary>
    /// Width of my texture atlas
    /// (in terms of block faces)
    /// E.g. my texture atlas has
    /// a width of 16x16, since there
    /// are 256 block faces in the
    /// atlas, but the resolution is
    /// 256x256
    /// </summary>
    static int Width;

    public MeshData()
    {
        Vertices = new List<Vector3>();
        Triangles = new List<int>();
        UVs = new List<Vector2>();
        TilingUVs = new List<Vector3>();
    }

    /// <summary>
    /// Generates a cuboid with uvs, using
    /// opposing corners, given by the start
    /// and end positions of the greedy mesh
    /// </summary>
    public void AddGreedyMesh(GreedyMesh mesh)
    {
        var size = mesh.End + Vector3Int.one - mesh.Start;

        var verts = new Vector3Int[]
        {
            new Vector3Int(mesh.Start.x, mesh.Start.y, mesh.Start.z), // left btm back
            new Vector3Int(mesh.End.x + 1, mesh.Start.y, mesh.Start.z), // right btm back
            new Vector3Int(mesh.Start.x, mesh.End.y + 1, mesh.Start.z), // left top back
            new Vector3Int(mesh.End.x + 1, mesh.End.y + 1, mesh.Start.z), // right top back
            new Vector3Int(mesh.Start.x, mesh.Start.y, mesh.End.z + 1), // left btm front
            new Vector3Int(mesh.End.x + 1, mesh.Start.y, mesh.End.z + 1), // right btm front
            new Vector3Int(mesh.Start.x, mesh.End.y + 1, mesh.End.z + 1), // left top front
            new Vector3Int(mesh.End.x + 1, mesh.End.y + 1, mesh.End.z + 1) // right top front
        };

        AddFace(verts[2], verts[3], verts[6], verts[7], false); // top
        AddUVs(BlockFace.Top, mesh.Type, new Vector2Int(size.x, size.z));

        AddFace(verts[0], verts[4], verts[2], verts[6], true); // left
        AddUVs(BlockFace.Left, mesh.Type, new Vector2Int(size.z, size.y));

        AddFace(verts[1], verts[5], verts[3], verts[7], false); // right
        AddUVs(BlockFace.Right, mesh.Type, new Vector2Int(size.z, size.y));

        AddFace(verts[0], verts[1], verts[4], verts[5], true); // bottom
        AddUVs(BlockFace.Bottom, mesh.Type, new Vector2Int(size.x, size.z));

        AddFace(verts[4], verts[5], verts[6], verts[7], true); // front
        AddUVs(BlockFace.Front, mesh.Type, new Vector2Int(size.x, size.y));

        AddFace(verts[0], verts[1], verts[2], verts[3], false); // back
        AddUVs(BlockFace.Back, mesh.Type, new Vector2Int(size.x, size.y));
    }

    /// <summary>
    /// Used to initialize the greedy mesh
    /// shader with a texture2D array
    /// </summary>
    public static void InitializeShader(Material material, Texture2D atlas, int width)
    {
        Width = width;
        var array = GetTexture2DArray(atlas, width);
        
        // The property name is distinct for
        // your shader, check in the shader
        // for the name of this property
        material.SetTexture(Shader.PropertyToID("Texture2DArray_ac26f7b1a0a74967897423fd947e028c"), array);
    }

    /// <summary>
    /// Generate a texture2D array from
    /// a texture atlas
    /// </summary>
    static Texture2DArray GetTexture2DArray(Texture2D atlas, int width)
    {
        var array = new Texture2DArray(atlas.width / width, atlas.height / width, width * width, atlas.format, false)
        {
            wrapMode = TextureWrapMode.Repeat,
            filterMode = FilterMode.Point
        };

        var atlasPixels = atlas.GetPixels32();

        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < width; y++)
            {
                var pixels = new Color32[array.width * array.height];

                for (int pixX = 0; pixX < array.width; pixX++)
                {
                    for (int pixY = 0; pixY < array.height; pixY++)
                    {
                        pixels[pixY * array.width + pixX] = atlasPixels[(pixY + y * array.height) * atlas.width + (pixX + x * array.width)];
                    }
                }

                array.SetPixels32(pixels, y * width + x);
            }
        }

        array.Apply();
        return array;
    }

    void AddFace(Vector3 bl, Vector3 br, Vector3 tl, Vector3 tr, bool faceOut)
    {
        var triangles = faceOut
                        ? new int[6] { 0, 1, 2, 1, 3, 2 }
                        : new int[6] { 2, 1, 0, 2, 3, 1 };

        for (int i = 0; i < 6; i++)
        {
            triangles *+= Vertices.Count;*

}

Vertices.AddRange(new Vector3[] { bl, br, tl, tr });
Triangles.AddRange(triangles);
}

void AddUVs(BlockFace face, BlockType type, Vector2 size)
{
// The uv coords of each face in a block
var uvCoords = BlockData.Blocks[(int)type][(int)face];

var uvs = new Vector2[4]
{
new Vector2(0, 0),
new Vector2(1, 0),
new Vector2(0, 1),
new Vector2(1, 1)
};

var tileUV = new Vector3(size.x, size.y, uvCoords.y * Width + uvCoords.x);

UVs.AddRange(uvs);
TilingUVs.AddRange(new Vector3[] { tileUV, tileUV, tileUV, tileUV });
}

public Mesh ToMesh()
{
var mesh = new Mesh()
{
indexFormat = UnityEngine.Rendering.IndexFormat.UInt32,
vertices = Vertices.ToArray(),
triangles = Triangles.ToArray(),
uv = UVs.ToArray()
};

// Channel 1 is used to store
// the uvs necessary to tile
// the texture
mesh.SetUVs(1, TilingUVs);

mesh.RecalculateNormals();
return mesh;
}
}

public enum BlockFace
{
Left,
Right,
Top,
Bottom,
Front,
Back
}
One big problem with the greedy meshing algorithm is that cubic block faces cannot be applied to a cuboid, because the texture will stretch. The only way to tile a uv is using a custom shader and a texture 2D array, instead of a texture atlas, which will be taxing on memory, but it’s the only way around it, that I see. I have a custom shader which tiles the uvs properly, which I will show later.
----------
Shader
I made this shader in shader graph (which requires URP), in order to tile the uvs properly. The initialize function in the MeshData class needs to be called before this shader runs, in order to generate the texture 2d array from the texture atlas:
[202119-shader.png|202119]*
----------
BlockData
This dictionary stores the uvs of each blocks’ faces:
using System;
using UnityEngine;

///


/// Stores information about the UV’s of each type of block (block display)
///

public class BlockData : MonoBehaviour
{
public BlockProperties[] BlockProperties;
public static Vector2[][] Blocks;

void Awake()
{
Blocks = new Vector2[Enum.GetValues(typeof(BlockType)).Length][];

foreach (var bp in BlockProperties)
{
Blocks[(int)bp.Type] = bp.GetUVDict();
}
}
}

[System.Serializable]
public class BlockProperties
{
public BlockType Type;

[Header(“Faces”)]
public Vector2 LeftUVs;
public Vector2 RightUVs;
public Vector2 TopUVs;
public Vector2 BottomUVs;
public Vector2 FrontUVs;
public Vector2 BackUVs;

public Vector2[] GetUVDict()
{
return new Vector2[]
{
LeftUVs,
RightUVs,
TopUVs,
BottomUVs,
FrontUVs,
BackUVs
};
}
}
----------
Greedy Mesh Algorithm
I’ve waffled enough. Here’s the algorithm:
public MeshData GenerateMeshData()
{
var meshData = new MeshData();
var greedyMeshes = new List();
var scannedPositions = new HashSet();

// Loop over all blocks in the chunk
// (Blocks are stored in a 3D array
// indexed by x,y,z coords)
// (WIDTH and HEIGHT refer to chunk
// width and chunk height)
for (int x = 0; x < WIDTH; x++)
{
for (int y = 0; y < HEIGHT; y++)
{
for (int z = 0; z < WIDTH; z++)
{
var pos = new Vector3Int(x, y, z);

// Don’t perform meshing algorithm
// on block already scanned
if (scannedPositions.Contains(pos))
continue;

var type = Blocks[x, y, z].Type;

// Air blocks can’t be meshed
if (type == BlockType.Air)
continue;

var greedyMesh = new GreedyMesh(pos, type);

// Extend mesh on x-axis
for (int extendX = pos.x + 1; extendX < WIDTH; extendX++)
{
var endPos = new Vector3Int(extendX, pos.y, pos.z);

if (scannedPositions.Contains(endPos) || Blocks[endPos.x, endPos.y, endPos.z].Type != greedyMesh.Type)
break;

greedyMesh.End.x = extendX;
scannedPositions.Add(endPos);
}

// Extend mesh on y-axis
for (int extendY = pos.y + 1; extendY < HEIGHT; extendY++)
{
bool extendedXFully = true;
var blocksScanned = new List();

for (int extendX = greedyMesh.Start.x; extendX <= greedyMesh.End.x; extendX++)
{
var endPos = new Vector3Int(extendX, extendY, pos.z);

if (scannedPositions.Contains(endPos) || Blocks[endPos.x, endPos.y, endPos.z].Type != greedyMesh.Type)
{
extendedXFully = false;
break;
}

blocksScanned.Add(endPos);
}

if (!extendedXFully)
break;

greedyMesh.End.y = extendY;

foreach (var scannedPos in blocksScanned)
{
scannedPositions.Add(scannedPos);
}
}

// Extend mesh on z-axis
for (int extendZ = pos.z + 1; extendZ < WIDTH; extendZ++)
{
bool extendedYFully = true;
var blocksScanned = new List();

for (int extendY = greedyMesh.Start.y; extendY <= greedyMesh.End.y; extendY++)
{
bool extendedXFully = true;

for (int extendX = greedyMesh.Start.x; extendX <= greedyMesh.End.x; extendX++)
{
var endPos = new Vector3Int(extendX, extendY, extendZ);

if (scannedPositions.Contains(endPos) || Blocks[endPos.x, endPos.y, endPos.z].Type != greedyMesh.Type)
{
extendedXFully = false;
break;
}

blocksScanned.Add(endPos);
}

if (!extendedXFully)
{
extendedYFully = false;
break;
}
}

if (!extendedYFully)
break;

greedyMesh.End.z = extendZ;

foreach (var scannedPos in blocksScanned)
{
scannedPositions.Add(scannedPos);
}
}

greedyMeshes.Add(greedyMesh);
}
}
}

// Generate greedy meshes (greedy meshes
// are essentially cuboids, so a cuboid
// is generated using the start and end
// pos given to the greedy mesh)
foreach (var greedyMesh in greedyMeshes)
{
meshData.AddGreedyMesh(greedyMesh);
}

return meshData;
}
----------
Sorry if this is a lot to take in, but this algorithm is terribly difficult to implement perfectly. Even if you somehow implement this correctly, you still need the standard meshing algorithm to mesh water correctly, since it’s transparent, and needs to remove block faces between chunks…
@dcmrobin
[1]: Meshing in a Minecraft Game – 0 FPS
*