/
DaggerfallTerrain.cs
403 lines (338 loc) · 16 KB
/
DaggerfallTerrain.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
// Project: Daggerfall Unity
// Copyright: Copyright (C) 2009-2023 Daggerfall Workshop
// Web Site: http://www.dfworkshop.net
// License: MIT License (http://www.opensource.org/licenses/mit-license.php)
// Source Code: https://github.com/Interkarma/daggerfall-unity
// Original Author: Gavin Clayton (interkarma@dfworkshop.net)
// Contributors: Hazelnut
//
// Notes:
//
using UnityEngine;
using System;
using DaggerfallConnect;
using DaggerfallConnect.Arena2;
using DaggerfallWorkshop.Utility;
using Unity.Collections;
using Unity.Jobs;
using System.Collections.Generic;
namespace DaggerfallWorkshop
{
/// <summary>
/// Partners with a Unity Terrain for use by StreamingWorld.
/// Each terrain is "self-assembling" based on position in world (1000x500 map pixels).
/// Also serializes additional information about neighbour terrains.
/// </summary>
[RequireComponent(typeof(Terrain))]
[RequireComponent(typeof(TerrainCollider))]
public class DaggerfallTerrain : MonoBehaviour
{
// Settings are tuned for Daggerfall and fast procedural layout
const int tilemapDim = MapsFile.WorldMapTileDim;
const int resolutionPerPatch = 16;
// This controls which map pixel the terrain will represent
[Range(TerrainHelper.minMapPixelX, TerrainHelper.maxMapPixelX)]
public int MapPixelX = TerrainHelper.defaultMapPixelX;
[Range(TerrainHelper.minMapPixelY, TerrainHelper.maxMapPixelY)]
public int MapPixelY = TerrainHelper.defaultMapPixelY;
// Increasing scale will amplify terrain height
// Must be set per terrain for correct tiling
[Range(TerrainHelper.minTerrainScale, TerrainHelper.maxTerrainScale)]
public float TerrainScale = TerrainHelper.defaultTerrainScale;
// Data for this terrain
public MapPixelData MapData;
// Neighbours of this terrain
public Terrain LeftNeighbour;
public Terrain TopNeighbour;
public Terrain RightNeighbour;
public Terrain BottomNeighbour;
// The tile map
[NonSerialized]
public Color32[] TileMap;
// Required for material properties
[SerializeField, HideInInspector]
Texture2D tileMapTexture;
[SerializeField, HideInInspector]
Material terrainMaterial;
public Material TerrainMaterial { get { return terrainMaterial; } set { terrainMaterial = value; } }
DaggerfallUnity dfUnity;
int heightmapDim;
int currentWorldClimate = -1;
DaggerfallDateTime.Seasons season = DaggerfallDateTime.Seasons.Summer;
bool ready;
private float heightMapPixelError = 5; // just a default value in case ini value reading fails (so value will be overwritten by ini file value)
public float HeightMapPixelError
{
get { return heightMapPixelError; }
set { heightMapPixelError = value; }
}
// Dispose any native memory if class is destroyed.
~DaggerfallTerrain()
{
DisposeNativeMemory();
}
void Awake()
{
HeightMapPixelError = DaggerfallUnity.Settings.TerrainHeightmapPixelError;
}
void Start()
{
UpdateNeighbours();
ready = false;
}
/// <summary>
/// This must be called when first creating terrain or before updating terrain.
/// Safe to call multiple times. Recreates expired volatile objects on subsequent calls.
/// </summary>
public void InstantiateTerrain()
{
if (!ReadyCheck())
return;
// Create tileMap texture
if (tileMapTexture == null)
{
tileMapTexture = new Texture2D(tilemapDim, tilemapDim, TextureFormat.ARGB32, false, true);
tileMapTexture.filterMode = FilterMode.Point;
tileMapTexture.wrapMode = TextureWrapMode.Clamp;
}
// Create terrain material
if (terrainMaterial == null)
{
terrainMaterial = dfUnity.TerrainMaterialProvider.CreateMaterial();
UpdateClimateMaterial();
}
// Raise event
RaiseOnInstantiateTerrainEvent();
}
/// <summary>
/// Updates climate material based on current map pixel data.
/// Use <see cref="PromoteTerrainData()"/> to apply changes.
/// </summary>
public void UpdateClimateMaterial(bool init = false)
{
// Update atlas texture if world climate changed
if (currentWorldClimate != MapData.worldClimate || dfUnity.WorldTime.Now.SeasonValue != season || init)
{
currentWorldClimate = MapData.worldClimate;
}
}
/// <summary>
/// Update map pixel data based on current coordinates. (first of a two stage process)
///
/// 1) BeginMapPixelDataUpdate - Schedules terrain data update using jobs system.
/// 2) CompleteMapPixelDataUpdate - Completes terrain data update using jobs system.
/// </summary>
/// <param name="terrainTexturing">Instance of ITerrainTexturing implementation class to use.</param>
/// <returns>JobHandle of the scheduled jobs</returns>
public JobHandle BeginMapPixelDataUpdate(ITerrainTexturing terrainTexturing = null)
{
// Get basic terrain data.
MapData = TerrainHelper.GetMapPixelData(dfUnity.ContentReader, MapPixelX, MapPixelY);
// Create data array for heightmap.
MapData.heightmapData = new NativeArray<float>(heightmapDim * heightmapDim, Allocator.TempJob);
// Create data array for tilemap data.
MapData.tilemapData = new NativeArray<byte>(tilemapDim * tilemapDim, Allocator.TempJob);
// Create data array for shader tile map data.
MapData.tileMap = new NativeArray<Color32>(tilemapDim * tilemapDim, Allocator.TempJob);
// Create data array for average & max heights.
MapData.avgMaxHeight = new NativeArray<float>(new float[] { 0, float.MinValue }, Allocator.TempJob);
// Create list for recording native arrays that need disposal after jobs complete.
MapData.nativeArrayList = new List<IDisposable>();
// Generate heightmap samples. (returns when complete)
JobHandle generateHeightmapSamplesJobHandle = dfUnity.TerrainSampler.ScheduleGenerateSamplesJob(ref MapData);
// Handle location if one is present on terrain.
JobHandle blendLocationTerrainJobHandle;
if (MapData.hasLocation)
{
// Schedule job to calc average & max heights.
JobHandle calcAvgMaxHeightJobHandle = TerrainHelper.ScheduleCalcAvgMaxHeightJob(ref MapData, generateHeightmapSamplesJobHandle);
JobHandle.ScheduleBatchedJobs();
// Set location tiles.
TerrainHelper.SetLocationTiles(ref MapData);
if (!dfUnity.TerrainSampler.IsLocationTerrainBlended())
{
// Schedule job to blend and flatten location heights. (depends on SetLocationTiles being done first)
blendLocationTerrainJobHandle = TerrainHelper.ScheduleBlendLocationTerrainJob(ref MapData, calcAvgMaxHeightJobHandle);
}
else
blendLocationTerrainJobHandle = calcAvgMaxHeightJobHandle;
}
else
blendLocationTerrainJobHandle = generateHeightmapSamplesJobHandle;
// Assign tiles for terrain texturing.
JobHandle assignTilesJobHandle = (terrainTexturing == null) ? blendLocationTerrainJobHandle :
terrainTexturing.ScheduleAssignTilesJob(dfUnity.TerrainSampler, ref MapData, blendLocationTerrainJobHandle);
// Update tile map for shader.
JobHandle updateTileMapJobHandle = TerrainHelper.ScheduleUpdateTileMapDataJob(ref MapData, assignTilesJobHandle);
JobHandle.ScheduleBatchedJobs();
return updateTileMapJobHandle;
}
/// <summary>
/// Complete terrain data update using jobs system. (second of a two stage process)
/// </summary>
/// <param name="terrainTexturing">Instance of ITerrainTexturing implementation class to use.</param>
public void CompleteMapPixelDataUpdate(ITerrainTexturing terrainTexturing = null)
{
// Convert heightmap data back to standard managed 2d array.
MapData.heightmapSamples = new float[heightmapDim, heightmapDim];
for (int i = 0; i < MapData.heightmapData.Length; i++)
MapData.heightmapSamples[JobA.Row(i, heightmapDim), JobA.Col(i, heightmapDim)] = MapData.heightmapData[i];
// Convert tilemap data back to standard managed 2d array.
// (Still needed for nature layout so it can be called again without requiring terrain data generation)
MapData.tilemapSamples = new byte[tilemapDim, tilemapDim];
for (int i = 0; i < MapData.tilemapData.Length; i++)
{
byte tile = MapData.tilemapData[i];
if (tile == byte.MaxValue)
tile = 0;
MapData.tilemapSamples[JobA.Row(i, tilemapDim), JobA.Col(i, tilemapDim)] = tile;
}
// Create tileMap array or resize if needed and copy native array.
if (TileMap == null || TileMap.Length != MapData.tileMap.Length)
TileMap = new Color32[MapData.tileMap.Length];
MapData.tileMap.CopyTo(TileMap);
// Copy max and avg heights. (TODO: Are these needed? Seem to not be used anywhere)
MapData.averageHeight = MapData.avgMaxHeight[TerrainHelper.avgHeightIdx];
MapData.maxHeight = MapData.avgMaxHeight[TerrainHelper.maxHeightIdx];
DisposeNativeMemory();
}
/// <summary>
/// Disposes the native arrays used in jobs system terrain data update.
/// </summary>
private void DisposeNativeMemory()
{
if (MapData.nativeArrayList != null)
{
// Dispose any temp working native array memory.
foreach (IDisposable nativeArray in MapData.nativeArrayList)
nativeArray.Dispose();
MapData.nativeArrayList = null;
}
// Dispose native array memory allocations now data has been extracted.
if (MapData.heightmapData.IsCreated)
MapData.heightmapData.Dispose();
if (MapData.tilemapData.IsCreated)
MapData.tilemapData.Dispose();
if (MapData.avgMaxHeight.IsCreated)
MapData.avgMaxHeight.Dispose();
if (MapData.tileMap.IsCreated)
MapData.tileMap.Dispose();
}
/// <summary>
/// Promote data to live terrain.
/// This must be called after other processing complete.
/// </summary>
public void PromoteTerrainData()
{
// Basemap not used and is just pushed far away
const float basemapDistance = 10000f;
int heightmapDimension = dfUnity.TerrainSampler.HeightmapDimension;
int detailResolution = heightmapDimension;
// Ensure TerrainData is created
Terrain terrain = GetComponent<Terrain>();
if (terrain.terrainData == null)
{
// Calculate width and length of terrain in world units
float terrainSize = (MapsFile.WorldMapTerrainDim * MeshReader.GlobalScale);
// Setup terrain data
// Must set terrainData.heightmapResolution before size (thanks Nystul!)
TerrainData terrainData = new TerrainData();
terrainData.name = "TerrainData";
terrainData.heightmapResolution = heightmapDimension;
terrainData.size = new Vector3(terrainSize, dfUnity.TerrainSampler.MaxTerrainHeight, terrainSize);
terrainData.SetDetailResolution(detailResolution, resolutionPerPatch);
terrainData.alphamapResolution = detailResolution;
terrainData.baseMapResolution = detailResolution;
// Apply terrain data
terrain.terrainData = terrainData;
terrain.GetComponent<TerrainCollider>().terrainData = terrainData;
terrain.basemapDistance = basemapDistance;
terrain.heightmapPixelError = heightMapPixelError;
}
// Promote tileMap
tileMapTexture.SetPixels32(TileMap);
tileMapTexture.Apply(false);
// Promote material
var terrainMaterialData = new TerrainMaterialData(terrainMaterial, terrain.terrainData, tileMapTexture, currentWorldClimate);
dfUnity.TerrainMaterialProvider.PromoteMaterial(this, terrainMaterialData);
terrain.materialTemplate = terrainMaterial;
// Promote heights
Vector3 size = terrain.terrainData.size;
terrain.terrainData.size = new Vector3(size.x, dfUnity.TerrainSampler.MaxTerrainHeight * TerrainScale, size.z);
terrain.terrainData.SetHeights(0, 0, MapData.heightmapSamples);
// Raise event
RaiseOnPromoteTerrainDataEvent(terrain.terrainData);
}
/// <summary>
/// Updates neighbour terrains.
/// </summary>
public void UpdateNeighbours()
{
Terrain terrain = GetComponent<Terrain>();
terrain.SetNeighbors(LeftNeighbour, TopNeighbour, RightNeighbour, BottomNeighbour);
}
#region Editor Support
//#if UNITY_EDITOR
// /// <summary>
// /// Allows editor to set terrain independently of StreamingWorld.
// /// Mainly for testing purposes, but could be used for static scenes.
// /// Also shows full terrain setup procedure for reference.
// /// </summary>
// public void __EditorUpdateTerrain()
// {
// // Setup terrain
// InstantiateTerrain();
// // Update data for terrain
// UpdateMapPixelData();
// UpdateTileMapData();
// //UpdateHeightData();
// // Promote data to live terrain
// UpdateClimateMaterial();
// PromoteTerrainData();
// // Set neighbours
// UpdateNeighbours();
// }
//#endif
#endregion
#region Private Methods
private bool ReadyCheck()
{
if (ready)
return true;
// Ensure we have a DaggerfallUnity reference
if (dfUnity == null)
{
dfUnity = DaggerfallUnity.Instance;
heightmapDim = dfUnity.TerrainSampler.HeightmapDimension;
}
// Do nothing if DaggerfallUnity not ready
if (!dfUnity.IsReady)
{
DaggerfallUnity.LogMessage("DaggerfallTerrain: DaggerfallUnity component is not ready. Have you set your Arena2 path?");
return false;
}
// Raise ready flag
ready = true;
return true;
}
#endregion
#region Event Handlers
// OnInstantiateTerrain
public delegate void OnInstantiateTerrainEventHandler(DaggerfallTerrain sender);
public static event OnInstantiateTerrainEventHandler OnInstantiateTerrain;
protected virtual void RaiseOnInstantiateTerrainEvent()
{
if (OnInstantiateTerrain != null)
OnInstantiateTerrain(this);
}
// OnPromoteTerrainData
public delegate void OnPromoteTerrainDataEventHandler(DaggerfallTerrain sender, TerrainData terrainData);
public static event OnPromoteTerrainDataEventHandler OnPromoteTerrainData;
protected virtual void RaiseOnPromoteTerrainDataEvent(TerrainData terrainData)
{
if (OnPromoteTerrainData != null)
OnPromoteTerrainData(this, terrainData);
}
#endregion
}
}