riversong code showcase

This commit is contained in:
Daniele Marotta
2026-05-21 15:52:18 +02:00
commit 4c9eea1c02
462 changed files with 23406 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class GrassLodGameSystem : GameSystem, IUpdatable
{
[InjectService]
private GameConfig _config;
[InjectService]
private IScene _scene;
[InjectService]
private WorldRenderingState _renderingState;
public GrassLodGameSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var grassConfig = _config.Terrain.GrassLODs;
var cropsConfig = _config.Terrain.CropsLODs;
foreach (var chunk in _renderingState.TerrainChunks)
{
SelectLOD(chunk.GrassLODs, grassConfig);
SelectLOD(chunk.CropsLODs, cropsConfig);
}
}
private void SelectLOD(TerrainChunk.RenderData[] lods, GameConfig.TerrainConfig.GrassLOD[] config)
{
// We can assume all LODs have almost the same center, so we just use the center of the first one
var position = lods[0].Renderer.bounds.center;
var viewSpacePosition = _scene.CinemachineCamera.transform.InverseTransformPoint(position);
var selectedLod = int.MaxValue;
for (var lod = 0; lod < config.Length; lod++)
if (viewSpacePosition.z < config[lod].Threshold)
{
selectedLod = lod;
break;
}
for (var lod = 0; lod < lods.Length; lod++) lods[lod].Renderer.enabled = lod == selectedLod;
}
}
}

View File

@@ -0,0 +1,31 @@
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TerrainChunk
{
public int2 Coords { get; set; }
public GameObject Root { get; set; }
public RenderData Terrain { get; set; }
public RenderData[] GrassLODs { get; set; }
public RenderData[] CropsLODs { get; set; }
public class RenderData
{
public RenderData(Mesh mesh, Renderer renderer)
{
Mesh = mesh;
Renderer = renderer;
}
public Mesh Mesh { get; }
public Renderer Renderer { get; }
}
}
}

View File

@@ -0,0 +1,52 @@
using UnityEngine;
using UnityEngine.InputSystem;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TerrainShaderDebugGUI : MonoBehaviour
{
private const int GUIRectWidth = 500;
private bool _drawGUI;
public Texture2D BlockedTilesMaskTexture { get; set; }
public Texture2D CliffsMaskTexture { get; set; }
public static TerrainShaderDebugGUI Create()
{
return new GameObject(nameof(TerrainShaderDebugGUI)).AddComponent<TerrainShaderDebugGUI>();
}
private void Update()
{
if (Keyboard.current.f1Key.wasPressedThisFrame) _drawGUI = !_drawGUI;
}
private void OnGUI()
{
if (!_drawGUI) return;
GUI.color = new Color(1, 1, 1, 0.8f);
var x = 0;
DrawTexture(BlockedTilesMaskTexture, ref x);
DrawTexture(CliffsMaskTexture, ref x);
}
private void DrawTexture(Texture2D texture, ref int x)
{
const int spacing = 10;
var oldFilterMode = texture.filterMode;
texture.filterMode = FilterMode.Point;
var aspect = (float)texture.height / texture.width;
GUI.DrawTexture(new Rect(spacing + x, spacing, GUIRectWidth, GUIRectWidth * aspect), texture, ScaleMode.ScaleToFit, false);
texture.filterMode = oldFilterMode;
x += GUIRectWidth + spacing;
}
}
}

View File

@@ -0,0 +1,228 @@
using System;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Object = UnityEngine.Object;
namespace DanieleMarotta.RiversongCodeShowcase
{
[RequiresWorldReadyForUpdate]
public class TerrainShaderParametersSystem : GameSystem, IInitializable, IDisposable, IUpdatable, IOnWorldGenerationCompletedCallback
{
private static readonly int BlockedTilesMaskID = Shader.PropertyToID("_Blocked_Tiles_Mask");
private static readonly int CliffsMaskID = Shader.PropertyToID("_Cliffs_Mask");
private static readonly int WindMapID = Shader.PropertyToID("_Wind_Map");
private static readonly int WindMapScaleID = Shader.PropertyToID("_Wind_Map_Scale");
private static readonly int WindMapSpeedID = Shader.PropertyToID("_Wind_Map_Speed");
private static readonly int WindBendID = Shader.PropertyToID("_Wind_Bend");
[InjectService]
private GameConfig _config;
[InjectService]
private EditingState _editingState;
[InjectService]
private World _world;
[InjectService]
private ISignalBus _signalBus;
private Texture2D _blockedTilesMask;
private Texture2D _windMap;
private Texture2D _cliffsMask;
public TerrainShaderParametersSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<WorldGenerationCompletedSignal>(OnWorldGenerationCompleted);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<WorldGenerationCompletedSignal>(OnWorldGenerationCompleted);
if (_blockedTilesMask)
{
Object.Destroy(_blockedTilesMask);
_blockedTilesMask = null;
}
if (_cliffsMask)
{
Object.Destroy(_cliffsMask);
_cliffsMask = null;
}
}
private void OnWorldGenerationCompleted(WorldGenerationCompletedSignal signal)
{
signal.Callbacks.Add(this);
}
public async UniTask OnWorldGenerationCompletedAsync(World world)
{
InitializedBlockedTilesMask();
await InitializeCliffsMaskAsync();
await InitializeWindAsync();
InitializeDebugGUI();
}
private void InitializedBlockedTilesMask()
{
_blockedTilesMask = new Texture2D(_world.Size.x, _world.Size.y, TextureFormat.RGBA32, false);
Shader.SetGlobalTexture(BlockedTilesMaskID, _blockedTilesMask);
var black = new NativeArray<Color32>(_world.Size.x * _world.Size.y, Allocator.Temp);
_blockedTilesMask.SetPixelData(black, 0);
}
private async UniTask InitializeCliffsMaskAsync()
{
var size = _world.Size;
_cliffsMask = new Texture2D(size.x + 1, size.y + 1, TextureFormat.RGBA32, false);
Shader.SetGlobalTexture(CliffsMaskID, _cliffsMask);
var pixelData = new NativeArray<Color32>((size.x + 1) * (size.y + 1), Allocator.Persistent);
var heightmap = _world.Heightmap.GetNativeArray();
Parallel.ForEach(
TileRange.From(0, size),
tile =>
{
var color = default(Color32);
var tileIndex = math.mad(tile.y, size.x, tile.x);
var tr = tile.x < size.x && tile.y < size.y ? heightmap[tileIndex] : 0;
var tl = tile.x > 0 && tile.y < size.y ? heightmap[tileIndex - 1] : 0;
var br = tile.x < size.x && tile.y > 0 ? heightmap[tileIndex - size.x] : 0;
var bl = tile is { x: > 0, y: > 0 } ? heightmap[tileIndex - size.x - 1] : 0;
if (tr != tl || br != bl) color.r = 255;
if (tr != br || tl != bl) color.g = 255;
var pixelIndex = math.mad(tile.y, size.x + 1, tile.x);
// ReSharper disable once AccessToDisposedClosure
pixelData[pixelIndex] = color;
});
await UniTask.NextFrame();
_cliffsMask.SetPixelData(pixelData, 0);
_cliffsMask.Apply();
pixelData.Dispose();
}
private async UniTask InitializeWindAsync()
{
_windMap = await _config.Terrain.Wind.Map.LoadAssetAsync<Texture2D>();
var config = _config.Terrain.Wind;
Shader.SetGlobalTexture(WindMapID, _windMap);
Shader.SetGlobalFloat(WindMapScaleID, config.MapScale);
Shader.SetGlobalFloat(WindMapSpeedID, config.Speed);
Shader.SetGlobalVector(WindBendID, config.BendDirection);
}
private void InitializeDebugGUI()
{
#if DEBUG
var gui = TerrainShaderDebugGUI.Create();
gui.BlockedTilesMaskTexture = _blockedTilesMask;
gui.CliffsMaskTexture = _cliffsMask;
#endif
}
public void Update()
{
UpdateBlockedTiles();
}
private void UpdateBlockedTiles()
{
var pixelData = _blockedTilesMask.GetPixelData<Color32>(0);
new BlockMapJob
{
BlockMap = _world.BlockMap.GetNativeArray(),
Fertility = _world.Fertility.GetNativeArray(),
BlockTexture = pixelData,
GrowthQ8_8 = (int)math.round(_config.Terrain.GrassGrowthRate * Time.deltaTime * byte.MaxValue * 256)
}.Schedule(pixelData.Length, _blockedTilesMask.width)
.Complete();
if (_editingState.ActiveTool != null)
foreach (var (tile, _) in _editingState.ActiveTool.AffectedTiles)
{
if (_world.BlockMap.IsBlocked(tile, BlockReason.InvalidElevation)) continue;
var i = tile.x + tile.y * _blockedTilesMask.width;
var color = pixelData[i];
color.g = byte.MaxValue;
pixelData[i] = color;
}
_blockedTilesMask.Apply();
}
[BurstCompile]
private struct BlockMapJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<BlockReason> BlockMap;
[ReadOnly]
public NativeArray<FertilityMapValue> Fertility;
public NativeArray<Color32> BlockTexture;
public int GrowthQ8_8;
[BurstCompile]
public void Execute(int index)
{
var color = BlockTexture[index];
var (currentFertility, maxFertility) = Fertility[index];
var clear = (currentFertility > 0 && currentFertility < maxFertility) || (BlockMap[index] & BlockReason.ClearGrassMask) != 0;
if (clear)
{
color.r = 255;
color.b = 0;
}
else
{
var q = (color.r << 8) | color.b;
q = math.max(0, q - GrowthQ8_8);
color.r = (byte)(q >> 8);
color.b = (byte)(q & 0xFF);
}
color.g = 0;
BlockTexture[index] = color;
}
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using Object = UnityEngine.Object;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TileHighlight : IDisposable
{
private NativeArray<Color32> _clear;
private NativeGrid<Color32> _grid;
public Texture2D Texture { get; private set; }
public void Create(int2 size)
{
Texture = new Texture2D(size.x, size.y, TextureFormat.RGBA32, false);
_clear = new NativeArray<Color32>(size.x * size.y, Allocator.Persistent);
_grid = new NativeGrid<Color32>(size, Allocator.Persistent);
}
public void Dispose()
{
if (Texture)
{
Object.Destroy(Texture);
Texture = null;
}
if (_clear.IsCreated)
{
_clear.Dispose();
_clear = default;
}
if (_grid != null)
{
_grid.Dispose();
_grid = null;
}
}
public void Apply()
{
var dataArray = _grid.GetNativeArray();
Texture.SetPixelData(dataArray, 0);
Texture.Apply();
NativeArray<Color32>.Copy(_clear, dataArray);
}
public void AddTile(int2 tile, Color32 color)
{
_grid.SetValue(tile, color);
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Object = UnityEngine.Object;
namespace DanieleMarotta.RiversongCodeShowcase
{
[RequiresWorldReadyForUpdate]
[GameSystemGroup(typeof(LateGameSystemGroup))]
public class TileHighlightSystem : GameSystem, IInitializable, IDisposable, IUpdatable, IOnWorldGenerationCompletedCallback
{
private static readonly int TileHighlightTextureID = Shader.PropertyToID("_Tile_Highlight_Texture");
[InjectService]
private GameConfig _config;
[InjectService]
private World _world;
[InjectService]
private WorldRenderingState _renderingState;
[InjectService]
private ISignalBus _signalBus;
private GameObject _tileHighlightGameObject;
public TileHighlightSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<WorldGenerationCompletedSignal>(OnWorldGenerationCompleted);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<WorldGenerationCompletedSignal>(OnWorldGenerationCompleted);
_renderingState.TileHighlight.Dispose();
if (_tileHighlightGameObject)
{
Object.Destroy(_tileHighlightGameObject);
_tileHighlightGameObject = null;
}
}
private void OnWorldGenerationCompleted(WorldGenerationCompletedSignal signal)
{
signal.Callbacks.Add(this);
}
public async UniTask OnWorldGenerationCompletedAsync(World world)
{
_renderingState.TileHighlight.Create(_world.Size);
Shader.SetGlobalTexture(TileHighlightTextureID, _renderingState.TileHighlight.Texture);
var tileHighlightPrefab = await _config.UI.TileHighlight.Prefab.LoadAssetAsync<GameObject>();
var tileHighlightGameObject = Object.Instantiate(tileHighlightPrefab);
tileHighlightGameObject.transform.position = new Vector3(_world.Size.x * 0.5f, _config.GeneralSettings.BaseElevation + 0.002f, _world.Size.x * 0.5f);
tileHighlightGameObject.transform.localScale = new Vector3(_world.Size.x, 1, _world.Size.y);
}
public void Update()
{
_renderingState.TileHighlight.Apply();
}
}
}

View File

@@ -0,0 +1,11 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum TileHighlightType
{
ValidTile,
InvalidTile,
DeletePreview
}
}