riversong code showcase
This commit is contained in:
49
Source/Riversong/Game/World/Terrain/GrassLodGameSystem.cs
Normal file
49
Source/Riversong/Game/World/Terrain/GrassLodGameSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Source/Riversong/Game/World/Terrain/TerrainChunk.cs
Normal file
31
Source/Riversong/Game/World/Terrain/TerrainChunk.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Source/Riversong/Game/World/Terrain/TerrainShaderDebugGUI.cs
Normal file
52
Source/Riversong/Game/World/Terrain/TerrainShaderDebugGUI.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public enum TileHighlightType
|
||||
{
|
||||
ValidTile,
|
||||
|
||||
InvalidTile,
|
||||
|
||||
DeletePreview
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user