Voxel/ChunkMesher.cs
namespace Monolith;

/// <summary>
/// Turns a chunk's bitset into a Model using greedy meshing: coplanar faces are merged into
/// the largest rectangles possible. A pristine solid chunk collapses to 6 quads instead of
/// 6144, which is what keeps a 16.7M cube monolith renderable.
/// </summary>
public static class ChunkMesher
{
	/// <summary>
	/// If the monolith renders inside-out (you can see through the near faces and the far
	/// faces are lit), flip this. It is the only handedness assumption in the mesher.
	/// </summary>
	public const bool FlipWinding = false;

	private static readonly int[] MaskBuffer = new int[VoxelChunk.Size * VoxelChunk.Size];

	/// <summary>Geometry for one chunk, split by material so each can be tinted separately.</summary>
	public readonly struct ChunkMesh
	{
		public readonly Model Rock;
		public readonly Model Volatile;

		public ChunkMesh( Model rock, Model unstable )
		{
			Rock = rock;
			Volatile = unstable;
		}

		public bool IsEmpty => Rock == null && Volatile == null;
	}

	/// <summary>
	/// Builds geometry for one chunk in chunk-local space, as TWO models: ordinary rock and
	/// volatile blocks.
	///
	/// They are split so each gets its own ModelRenderer and therefore its own Tint. Colour was
	/// previously carried entirely in vertex colours, which turned out to depend on the material
	/// sampling them - and the standard lit material does not, so every colour was silently
	/// discarded and volatile blocks were invisible. Tint always works, on any material, so
	/// splitting the mesh makes the red robust rather than hopeful.
	///
	/// Vertex colours carry a greyscale face shade, a strata band and per-corner ambient
	/// occlusion. The material MUST read them now: see MonolithRenderer.MaterialPath. Tint
	/// supplies the hue on top.
	/// </summary>
	public static ChunkMesh Build( VoxelWorld world, int chunkIndex, Material material )
	{
		var chunk = world.GetChunkByIndex( chunkIndex );
		if ( chunk.IsEmpty )
			return default;

		var c = chunk.Coord;
		var baseVoxel = new Vector3Int( c.x * VoxelChunk.Size, c.y * VoxelChunk.Size, c.z * VoxelChunk.Size );

		// One buffer per material. A chunk with no volatile blocks simply produces no second
		// mesh, which is the common case and costs nothing.
		var rockVb = new VertexBuffer();
		rockVb.Init( true );

		var volatileVb = new VertexBuffer();
		volatileVb.Init( true );

		int rockQuads = 0;
		int volatileQuads = 0;

		// We track vertex counts ourselves rather than reading them back off the buffer:
		// VertexBuffer does not expose its vertex list publicly, and AddRawIndex needs
		// absolute indices. Four vertices per quad, so these stay trivially in step.
		int rockVerts = 0;
		int volatileVerts = 0;

		for ( int d = 0; d < 3; d++ )
		{
			int u = (d + 1) % 3;
			int v = (d + 2) % 3;

			int dimD = VoxelChunk.Size;
			int dimU = VoxelChunk.Size;
			int dimV = VoxelChunk.Size;

			// Slice from -1 so we also catch faces exposed by the neighbouring chunk.
			for ( int slice = -1; slice < dimD; slice++ )
			{
				// A FULL chunk has no interior faces: every neighbouring pair inside it is
				// solid-solid. Only the two boundary slices can produce anything, and those
				// still consult the world for the neighbour. This takes a pristine chunk from
				// 99 slices to 6, which is most of the cost of loading the Monolith, where all
				// 512 chunks start full.
				if ( chunk.IsFull && slice >= 0 && slice < dimD - 1 )
					continue;

				BuildMask( world, chunk, baseVoxel, d, u, v, slice, dimU, dimV );

				EmitMask( rockVb, volatileVb, d, u, v, slice, dimU, dimV,
					ref rockVerts, ref volatileVerts, ref rockQuads, ref volatileQuads );
			}
		}

		return new ChunkMesh(
			BuildModel( rockVb, rockQuads, material ),
			BuildModel( volatileVb, volatileQuads, material ) );
	}

	private static Model BuildModel( VertexBuffer vb, int quads, Material material )
	{
		if ( quads == 0 )
			return null;

		var mesh = new Mesh( material );
		mesh.CreateBuffers( vb );

		return new ModelBuilder().AddMesh( mesh ).Create();
	}

	/// <summary>
	/// Fills <see cref="MaskBuffer"/> for one slice. +1 means a face pointing along +d,
	/// -1 means a face pointing along -d, 0 means no face.
	/// </summary>
	/// <summary>
	/// Solidity lookup with a fast path for voxels inside the chunk being meshed.
	///
	/// **This is the whole performance fix.** `BuildMask` runs 99 slices of 1024 cells per chunk
	/// and each cell asked the WORLD twice, so a single chunk performed roughly 200,000
	/// world-space lookups, each doing three integer divisions to find the chunk, a bounds check
	/// and an array index. Measured at 14 to 18ms per chunk, which made every remesh a dropped
	/// frame and is why the Monolith appeared not to draw in.
	///
	/// All but the two boundary slices are inside the chunk we already hold, so almost every one
	/// of those lookups can be a direct bitset read instead. Only genuine neighbour queries fall
	/// through to the world.
	/// </summary>
	private static bool SolidAt( VoxelWorld world, VoxelChunk chunk, Vector3Int baseVoxel,
		int x, int y, int z )
	{
		int lx = x - baseVoxel.x;
		int ly = y - baseVoxel.y;
		int lz = z - baseVoxel.z;

		// Unsigned compare catches negatives and overflow in one test.
		if ( (uint)lx < VoxelChunk.Size && (uint)ly < VoxelChunk.Size && (uint)lz < VoxelChunk.Size )
			return chunk.Get( lx, ly, lz );

		return world.IsSolid( x, y, z );
	}

	private static void BuildMask( VoxelWorld world, VoxelChunk chunk, Vector3Int baseVoxel,
		int d, int u, int v, int slice, int dimU, int dimV )
	{
		Array.Clear( MaskBuffer, 0, dimU * dimV );

		for ( int j = 0; j < dimV; j++ )
		for ( int i = 0; i < dimU; i++ )
		{
			var a = baseVoxel;
			Axis( ref a, d, slice );
			Axis( ref a, u, i );
			Axis( ref a, v, j );

			var b = a;
			Axis( ref b, d, 1 );

			bool solidA = SolidAt( world, chunk, baseVoxel, a.x, a.y, a.z );
			bool solidB = SolidAt( world, chunk, baseVoxel, b.x, b.y, b.z );

			if ( solidA == solidB )
				continue;

			// Only emit the face if the solid voxel of the pair belongs to this chunk,
			// otherwise the neighbouring chunk would emit the same quad.
			//
			// The mask stores 1 or 2 (facing +d, ordinary or volatile) and -1 or -2 (facing -d).
			// Volatile is part of the key so greedy merging never fuses a volatile face into an
			// ordinary run, which would smear the colour across cubes that are not volatile.
			if ( solidA )
			{
				if ( slice >= 0 )
					MaskBuffer[i + j * dimU] = Key( world, chunk, baseVoxel,
						a, b, u, v, Stages.IsVolatile( a.x, a.y, a.z ), 1 );
			}
			else
			{
				if ( slice + 1 < VoxelChunk.Size )
					MaskBuffer[i + j * dimU] = Key( world, chunk, baseVoxel,
						b, a, u, v, Stages.IsVolatile( b.x, b.y, b.z ), -1 );
			}
		}
	}

	/// <summary>
	/// Builds the greedy-merge key for one face: type, ambient occlusion and strata band.
	/// </summary>
	/// <remarks>
	/// **This is what makes the shape read as voxels rather than as a box.** Greedy meshing is a
	/// performance win that actively destroys the voxel look, because a flat 7x7 wall becomes ONE
	/// quad with one normal and four vertices. Geometrically it IS a box at that point, and no
	/// amount of lighting or palette work can put the cubes back.
	///
	/// The fix is not to abandon merging, it is to merge less eagerly. Everything that should
	/// visibly differ between two faces goes into the key, so faces only fuse when they would
	/// have looked identical anyway. Interior flat regions still collapse to single quads and
	/// keep the performance; edges, crater rims and ledges break the run and get their geometry
	/// back, which is exactly where the eye is looking.
	/// </remarks>
	private static int Key( VoxelWorld world, VoxelChunk chunk, Vector3Int baseVoxel,
		Vector3Int solid, Vector3Int open, int u, int v, bool isVolatile, int sign )
	{
		int ao = PackAo( world, chunk, baseVoxel, open, u, v );
		int band = BandAt( solid.x, solid.y, solid.z );

		int payload = (isVolatile ? 1 : 0) | (ao << 1) | (band << 9);
		return sign * (payload + 1);
	}

	/// <summary>
	/// Ambient occlusion for the four corners of one face, two bits each.
	/// </summary>
	/// <remarks>
	/// The standard voxel AO test, and the single technique that most makes a blocky mass read as
	/// individual cubes: for each corner, look at the two edge neighbours and the diagonal in the
	/// OPEN space just off the face. The more of them are solid, the darker that corner. Two
	/// facing edges with nothing between them is the fully occluded case and is forced to zero,
	/// which is what produces the hard dark seam in an inside corner.
	///
	/// Screen space AO cannot do this job. It softens crevices at the scale of the whole shape,
	/// while this articulates the boundary between one cube and the next.
	///
	/// Only computed for cells that actually emit a face, which is a small fraction of the mask,
	/// so the twelve extra lookups per face are affordable against the ~200k this pass used to do.
	/// </remarks>
	private static int PackAo( VoxelWorld world, VoxelChunk chunk, Vector3Int baseVoxel,
		Vector3Int open, int u, int v )
	{
		// Corner order MUST match p0..p3 in EmitQuad: (-u,-v), (+u,-v), (+u,+v), (-u,+v).
		int c0 = CornerAo( world, chunk, baseVoxel, open, u, v, -1, -1 );
		int c1 = CornerAo( world, chunk, baseVoxel, open, u, v, 1, -1 );
		int c2 = CornerAo( world, chunk, baseVoxel, open, u, v, 1, 1 );
		int c3 = CornerAo( world, chunk, baseVoxel, open, u, v, -1, 1 );

		return c0 | (c1 << 2) | (c2 << 4) | (c3 << 6);
	}

	private static int CornerAo( VoxelWorld world, VoxelChunk chunk, Vector3Int baseVoxel,
		Vector3Int open, int u, int v, int su, int sv )
	{
		var a = open; Axis( ref a, u, su );
		var b = open; Axis( ref b, v, sv );
		var c = open; Axis( ref c, u, su ); Axis( ref c, v, sv );

		bool sideA = SolidAt( world, chunk, baseVoxel, a.x, a.y, a.z );
		bool sideB = SolidAt( world, chunk, baseVoxel, b.x, b.y, b.z );

		// Both edges solid means the corner is sealed regardless of the diagonal.
		if ( sideA && sideB )
			return 0;

		bool corner = SolidAt( world, chunk, baseVoxel, c.x, c.y, c.z );

		return 3 - ((sideA ? 1 : 0) + (sideB ? 1 : 0) + (corner ? 1 : 0));
	}

	/// <summary>
	/// Which strata band a voxel belongs to.
	/// </summary>
	/// <remarks>
	/// AO alone fixes carved surfaces and does nothing for a pristine one: an untouched box has no
	/// occlusion anywhere, so every face merges into one quad and it stays a box until you shoot
	/// it. Banding is what gives the shape internal structure BEFORE you touch it, and layered
	/// rock is the most legible structure a solid mass can have.
	///
	/// Horizontal, with a coarse wobble so the layers are not perfect planes. Thickness is the
	/// performance dial: bands break the merge run, so thin bands mean many more quads. Set
	/// <see cref="Tuning.StrataBands"/> to 1 to switch the whole thing off.
	/// </remarks>
	private static int BandAt( int x, int y, int z )
	{
		if ( Tuning.StrataBands <= 1 )
			return 0;

		// Coarse cell hash, so the wobble moves in patches rather than per voxel.
		uint h = Hash( (uint)(x >> 2), (uint)(y >> 2) );
		int wobble = (int)(h % 3u);

		int layer = (z + wobble) / Tuning.StrataThickness;

		// Positive modulo: z can be negative near the world origin.
		return ((layer % Tuning.StrataBands) + Tuning.StrataBands) % Tuning.StrataBands;
	}

	/// <summary>Cheap deterministic hash. No allocation, no Random, same result every rebuild.</summary>
	private static uint Hash( uint x, uint y )
	{
		uint h = x * 374761393u + y * 668265263u;
		h = (h ^ (h >> 13)) * 1274126177u;
		return h ^ (h >> 16);
	}

	/// <summary>Merges the mask into maximal rectangles and writes them as quads.</summary>
	private static void EmitMask( VertexBuffer rockVb, VertexBuffer volatileVb, int d, int u, int v,
		int slice, int dimU, int dimV,
		ref int rockVerts, ref int volatileVerts, ref int rockQuads, ref int volatileQuads )
	{
		for ( int j = 0; j < dimV; j++ )
		{
			for ( int i = 0; i < dimU; )
			{
				int value = MaskBuffer[i + j * dimU];
				if ( value == 0 ) { i++; continue; }

				// Grow along u while the mask matches.
				int width = 1;
				while ( i + width < dimU && MaskBuffer[i + width + j * dimU] == value )
					width++;

				// Grow along v, but only in complete rows.
				int height = 1;
				bool canGrow = true;
				while ( j + height < dimV && canGrow )
				{
					for ( int k = 0; k < width; k++ )
					{
						if ( MaskBuffer[i + k + (j + height) * dimU] == value )
							continue;

						canGrow = false;
						break;
					}

					if ( canGrow ) height++;
				}

				int payload = Math.Abs( value ) - 1;

				bool isVolatile = (payload & 1) != 0;
				int ao = (payload >> 1) & 0xFF;
				int band = payload >> 9;

				if ( isVolatile )
				{
					EmitQuad( volatileVb, d, u, v, slice, i, j, width, height,
						value > 0, ao, band, ref volatileVerts );
					volatileQuads++;
				}
				else
				{
					EmitQuad( rockVb, d, u, v, slice, i, j, width, height,
						value > 0, ao, band, ref rockVerts );
					rockQuads++;
				}

				// Consume the rectangle.
				for ( int jj = 0; jj < height; jj++ )
				for ( int ii = 0; ii < width; ii++ )
					MaskBuffer[i + ii + (j + jj) * dimU] = 0;

				i += width;
			}
		}
	}

	private static void EmitQuad( VertexBuffer vb, int d, int u, int v,
		int slice, int uStart, int vStart, int width, int height, bool positiveFacing,
		int ao, int band, ref int vertexCount )
	{
		// The face plane always sits at the boundary between slice and slice+1.
		float planeCoord = slice + 1;

		var origin = Vector3.Zero;
		SetAxis( ref origin, d, planeCoord );
		SetAxis( ref origin, u, uStart );
		SetAxis( ref origin, v, vStart );

		var du = Vector3.Zero;
		SetAxis( ref du, u, width );

		var dv = Vector3.Zero;
		SetAxis( ref dv, v, height );

		var normal = Vector3.Zero;
		SetAxis( ref normal, d, positiveFacing ? 1f : -1f );

		var tangent = Vector3.Zero;
		SetAxis( ref tangent, u, 1f );

		float s = Tuning.VoxelSize;

		var p0 = origin * s;
		var p1 = (origin + du) * s;
		var p2 = (origin + du + dv) * s;
		var p3 = (origin + dv) * s;

		// Absolute base index BEFORE adding, so we can use AddRawIndex and avoid the
		// relative-index ambiguity entirely. AddTriangleIndex counts back from the top of
		// the buffer and is 1-based there ("0 is Vertex.Count"), which is very easy to get
		// off by one: doing so stitches each quad to a vertex of the previous one and
		// produces long stretched slivers instead of blocks. AddRawIndex is absolute.
		int b = vertexCount;
		vertexCount += 4;

		// Everything the surface looks like is baked here, per corner.
		//
		// Face shade gives the mass form: coplanar faces are otherwise indistinguishable under a
		// single light, which is what makes a carved shape read as a flat silhouette. On top of
		// that goes the strata band, and then per-corner ambient occlusion, which is the part
		// that actually draws the boundary between one cube and the next.
		float shade = FaceShadeValue( d, positiveFacing ) * BandShade( band );

		var v0 = new Vertex( p0, normal, tangent, new Vector4( 0, 0, 0, 0 ) );
		var v1 = new Vertex( p1, normal, tangent, new Vector4( width, 0, 0, 0 ) );
		var v2 = new Vertex( p2, normal, tangent, new Vector4( width, height, 0, 0 ) );
		var v3 = new Vertex( p3, normal, tangent, new Vector4( 0, height, 0, 0 ) );

		// Corner order matches PackAo: (-u,-v), (+u,-v), (+u,+v), (-u,+v).
		v0.Color = Shaded( shade, ao & 3 );
		v1.Color = Shaded( shade, (ao >> 2) & 3 );
		v2.Color = Shaded( shade, (ao >> 4) & 3 );
		v3.Color = Shaded( shade, (ao >> 6) & 3 );

		vb.Add( v0 );
		vb.Add( v1 );
		vb.Add( v2 );
		vb.Add( v3 );

		bool ccw = positiveFacing ^ FlipWinding;

		if ( ccw )
		{
			vb.AddRawIndex( b + 0 ); vb.AddRawIndex( b + 1 ); vb.AddRawIndex( b + 2 );
			vb.AddRawIndex( b + 0 ); vb.AddRawIndex( b + 2 ); vb.AddRawIndex( b + 3 );
		}
		else
		{
			vb.AddRawIndex( b + 0 ); vb.AddRawIndex( b + 2 ); vb.AddRawIndex( b + 1 );
			vb.AddRawIndex( b + 0 ); vb.AddRawIndex( b + 3 ); vb.AddRawIndex( b + 2 );
		}
	}

	/// <summary>
	/// Brightness per face direction. Top brightest, bottom darkest, the two horizontal axes
	/// slightly different from each other so corners resolve. Multiplies the chunk tint.
	/// </summary>
	/// <summary>
	/// Full colour of a face, not just a brightness.
	///
	/// This used to return a greyscale shade that the chunk's ModelRenderer.Tint then
	/// multiplied. That made a volatile block **impossible** to see: vertex colour multiplies
	/// tint, the tint is dark rock, and so "bright red" times "dark brown" is dark brown. The
	/// tint is now white and every colour decision lives here, where volatile can simply be
	/// brighter than the rock instead of a fraction of it.
	/// </summary>
	/// <summary>
	/// Greyscale brightness per face direction, written as vertex colour.
	///
	/// A MULTIPLIER, not a colour: hue comes from the renderer's Tint, which every material
	/// honours. If the material also samples vertex colour the faces gain their form back; if
	/// it does not, the tint alone still shows the right colour, just flat. Correct either way,
	/// which is the point after the last attempt silently produced nothing.
	/// </summary>
	private static float FaceShadeValue( int d, bool positiveFacing )
		=> d switch
		{
			2 => positiveFacing ? 1.00f : 0.45f,   // up / down
			1 => positiveFacing ? 0.86f : 0.74f,   // +y / -y
			_ => positiveFacing ? 0.70f : 0.62f,   // +x / -x
		};

	/// <summary>Brightness of one strata band. Subtle: layers, not stripes.</summary>
	private static float BandShade( int band )
	{
		if ( Tuning.StrataBands <= 1 )
			return 1f;

		// Alternating light and dark around 1.0, so no band is the "wrong" brightness overall
		// and the average matches what the palette was tuned against.
		float t = band / (float)Math.Max( 1, Tuning.StrataBands - 1 );
		return 1f - Tuning.StrataContrast * 0.5f + Tuning.StrataContrast * t;
	}

	/// <summary>Combines face shade with a corner's occlusion level, 0 darkest to 3 unoccluded.</summary>
	private static Color32 Shaded( float shade, int ao )
	{
		float occlusion = MathX.Lerp( Tuning.VoxelAoStrength, 1f, ao / 3f );

		byte v = (byte)Math.Clamp( (int)(shade * occlusion * 255f), 0, 255 );
		return new Color32( v, v, v, 255 );
	}

	private static void Axis( ref Vector3Int vec, int axis, int value )
	{
		switch ( axis )
		{
			case 0: vec.x += value; break;
			case 1: vec.y += value; break;
			default: vec.z += value; break;
		}
	}

	private static void SetAxis( ref Vector3 vec, int axis, float value )
	{
		switch ( axis )
		{
			case 0: vec.x = value; break;
			case 1: vec.y = value; break;
			default: vec.z = value; break;
		}
	}
}