namespace Sand.ChunkPrototype; internal static class ChunkVisualTracker { public static long RenderDirtyPages( byte[] destination, int worldWidth, IReadOnlyDictionary cellPages, IReadOnlyDictionary fieldPages, bool enableWindVisuals, bool enablePressureVisuals) { long bytesTouched = 0; foreach (var (coord, page) in cellPages) { if (!page.HasVisualDirty) { continue; } bytesTouched += RenderPage(destination, worldWidth, coord, page, fieldPages.GetValueOrDefault(coord), enableWindVisuals, enablePressureVisuals); page.ClearVisualDirty(); } foreach (var (coord, fieldPage) in fieldPages) { if (cellPages.ContainsKey(coord)) { continue; } bytesTouched += RenderFieldOnlyPage(destination, worldWidth, coord, fieldPage, enableWindVisuals, enablePressureVisuals); } return bytesTouched; } private static long RenderPage( byte[] destination, int worldWidth, ChunkCoord coord, ChunkCellPage page, ChunkFieldPage? fieldPage, bool enableWindVisuals, bool enablePressureVisuals) { long bytesTouched = 0; for (var localY = page.VisualDirtyMinRow; localY <= page.VisualDirtyMaxRow; localY++) { for (var localX = page.VisualDirtyMinCol; localX <= page.VisualDirtyMaxCol; localX++) { var particle = page[localX, localY]; var worldX = (coord.X * page.Width) + localX; var worldY = (coord.Y * page.Height) + localY; var rgbaIndex = ((worldY * worldWidth) + worldX) * 4; if (particle.TypeId != 0) { destination[rgbaIndex] = particle.R; destination[rgbaIndex + 1] = particle.G; destination[rgbaIndex + 2] = particle.B; destination[rgbaIndex + 3] = 255; bytesTouched += 4; continue; } var color = ResolveFieldColor(fieldPage, localX, localY, enableWindVisuals, enablePressureVisuals); destination[rgbaIndex] = color.R; destination[rgbaIndex + 1] = color.G; destination[rgbaIndex + 2] = color.B; destination[rgbaIndex + 3] = 255; bytesTouched += 4; } } return bytesTouched; } private static long RenderFieldOnlyPage( byte[] destination, int worldWidth, ChunkCoord coord, ChunkFieldPage page, bool enableWindVisuals, bool enablePressureVisuals) { if (!enableWindVisuals && !enablePressureVisuals) { return 0; } long bytesTouched = 0; foreach (var (localX, localY, cell) in page.EnumerateActiveCells()) { var color = ResolveFieldColor(cell, enableWindVisuals, enablePressureVisuals); var worldX = (coord.X * page.Width) + localX; var worldY = (coord.Y * page.Height) + localY; var rgbaIndex = ((worldY * worldWidth) + worldX) * 4; destination[rgbaIndex] = color.R; destination[rgbaIndex + 1] = color.G; destination[rgbaIndex + 2] = color.B; destination[rgbaIndex + 3] = 255; bytesTouched += 4; } return bytesTouched; } private static (byte R, byte G, byte B) ResolveFieldColor(ChunkFieldPage? fieldPage, int localX, int localY, bool enableWindVisuals, bool enablePressureVisuals) { if (fieldPage is null || !fieldPage.TryGetCell(localX, localY, out var cell)) { return (0, 0, 0); } return ResolveFieldColor(cell, enableWindVisuals, enablePressureVisuals); } private static (byte R, byte G, byte B) ResolveFieldColor(FieldCellData cell, bool enableWindVisuals, bool enablePressureVisuals) { byte r = 0; byte g = 0; byte b = 0; if (enablePressureVisuals) { var pressure = MathF.Abs(cell.Pressure); if (pressure > 0.08f) { var tint = (byte)Math.Clamp((int)(pressure * 32f), 10, 140); if (cell.Pressure >= 0f) { r = (byte)Math.Max((int)r, 26 + tint); g = (byte)Math.Max((int)g, 18 + (tint / 3)); b = (byte)Math.Max((int)b, 24); } else { r = (byte)Math.Max((int)r, 18); g = (byte)Math.Max((int)g, 24 + (tint / 4)); b = (byte)Math.Max((int)b, 30 + tint); } } } if (enableWindVisuals) { var totalFieldX = cell.WindX + cell.ForceX; var totalFieldY = cell.WindY + cell.ForceY; var windStrength = MathF.Sqrt((totalFieldX * totalFieldX) + (totalFieldY * totalFieldY)); if (windStrength > 0.08f) { var tint = (byte)Math.Clamp((int)(windStrength * 40f), 10, 120); r = (byte)Math.Max((int)r, 16 + (tint / 3)); g = (byte)Math.Max((int)g, 24 + (tint / 2)); b = (byte)Math.Max((int)b, 32 + tint); } } return (r, g, b); } }