Skip to content

Instantly share code, notes, and snippets.

@Journeyman1337
Last active August 3, 2022 03:34
Show Gist options
  • Save Journeyman1337/10df73fc5050adce625465714b01e97d to your computer and use it in GitHub Desktop.
Save Journeyman1337/10df73fc5050adce625465714b01e97d to your computer and use it in GitHub Desktop.
This is a unity chunking system that supports 32km^2 of untextured terrain with lods. To avoid floating point errors, I also implemented a floating origin system which sets the origin as set intervals no to mess up worldspace coords in case they are used for UVs if this was ever textured. youtube video links in vids.txt
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class Chunk : MonoBehaviour
{
public MeshFilter meshFilter {get; private set;} = null;
public MeshRenderer meshRenderer {get; private set;} = null;
public Mesh mesh {get; private set;} = null;
public int Chunk_X = 0;
public int Chunk_Z = 0;
public int TilesWide = 0;
public int TilesLong = 0;
public int TileCount = 0;
public int VertexCount = 0;
public int TriangleIndexCount = 0;
public int LodLevel;
public bool UnstagedEdits = false;
public Vector3[] vertices;
public int[] triangles;
void Awake() {
meshFilter = GetComponent<MeshFilter>();
meshRenderer = GetComponent<MeshRenderer>();
meshFilter.mesh = mesh = new Mesh();
}
// Start is called before the first frame update
void Start()
{
mesh.MarkDynamic();
LodLevel = GetCurrentLod();
Rebuild();
}
// Update is called once per frame
void Update()
{
int cur_lod = GetCurrentLod();
if (cur_lod != LodLevel)
{
LodLevel = cur_lod;
Rebuild();
}
if (UnstagedEdits)
{
ApplyEdits();
}
}
private void Rebuild()
{
int renderLod = LodLevel;
if (LodLevel >= 9)
{
renderLod = 8;
}
int lod_dim = 1<<(8-renderLod);
int lod_skip = renderLod + 1;
TerrainWorld world = TerrainWorld.Inst;
float tile_scalar = TerrainWorld.ChunkUnitDim / lod_dim;
TilesWide = lod_dim;
TilesLong = lod_dim;
TileCount = TilesWide * TilesLong;
VertexCount = (TilesWide + 1) * (TilesLong + 1);
TriangleIndexCount = TileCount * 6;
vertices = new Vector3[VertexCount];
for (int i = 0, z = 0; z <= TilesLong; z++)
{
for (int x = 0; x <= TilesWide; x++, i++)
{
vertices[i] = new Vector3(x * tile_scalar, world.GetHeight((Chunk_X * TerrainWorld.ChunkTileDim) + (x * lod_skip), (Chunk_Z * TerrainWorld.ChunkTileDim) + (z * lod_skip)), z * tile_scalar);
}
}
triangles = new int[TriangleIndexCount];
for (int ti = 0, vi = 0, z = 0; z < TilesLong; z++, vi++)
{
for (int x = 0; x < TilesWide; x++, ti += 6, vi++)
{
triangles[ti] = vi;
triangles[ti+1] = vi + TilesWide + 1;
triangles[ti+2] = vi + 1;
triangles[ti+3] = vi + 1;
triangles[ti+4] = vi + TilesWide + 1;
triangles[ti+5] = vi + TilesWide + 2;
}
}
UnstagedEdits = true;
}
public void SetHeight(int x, int z, float new_height)
{
Debug.Log("x" + x + " z" + z);
if (LodLevel < 9)
{
int lod_inc = LodLevel + 1;
if (x % lod_inc == 0)
{
vertices[Utility.Array2DTo1D(x / lod_inc, z / lod_inc, TilesWide + 1)].y = new_height;
UnstagedEdits = true;
}
}
}
public void ApplyEdits()
{
if (VertexCount > short.MaxValue)
{
//If this is not set when there are too many triangle indices, triangles past the limit will be bugged/stretched.
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
}
else
{
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt16;
}
mesh.Clear();
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.RecalculateBounds();
mesh.MarkModified();
UnstagedEdits = false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetCurrentLod()
{
TerrainWorld world = TerrainWorld.Inst;
Vector3 view = world.WorldView.position;
view.y = 0.0f;
Vector3 thisCenter = transform.position;
//offset to the center of the chunk...
thisCenter.x += TerrainWorld.ChunkUnitDim / 2.0f;
thisCenter.z += TerrainWorld.ChunkUnitDim / 2.0f;
float distance_from_center = Vector3.Distance(view, thisCenter);
float max_lod_lower_distance = 15.0f; //this is the closest a player can be from a lower lod level
return (int)(distance_from_center / (TerrainWorld.ChunkUnitDim + max_lod_lower_distance));
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FloatingOrigin : MonoBehaviour
{
public static FloatingOrigin Inst {get; private set;} = null;
public Vector3Int OffsetTimes;
public float MaxDistance = 256.0f;
void Awake() {
if (Inst != null)
{
Debug.LogWarning("More than one FloatingOrigin exists! Destroying extras.");
Destroy(this);
}
else
{
Inst = this;
}
}
void OnDestroy() {
if (Inst == this)
{
Inst = null;
}
}
private void Start() {
}
void Update() {
TerrainWorld world = TerrainWorld.Inst;
Vector3 world_view_position = world.WorldView.position;
int x_move_times = 0;
int y_move_times = 0;
int z_move_times = 0;
if (world_view_position.x > MaxDistance || world_view_position.x < -MaxDistance)
{
x_move_times = -Mathf.FloorToInt(world_view_position.x / MaxDistance);
}
if (world_view_position.y > MaxDistance || world_view_position.y < -MaxDistance)
{
y_move_times = -Mathf.FloorToInt(world_view_position.y / MaxDistance);
}
if (world_view_position.z > MaxDistance || world_view_position.z < -MaxDistance)
{
z_move_times = -Mathf.FloorToInt(world_view_position.z / MaxDistance);
}
if (x_move_times != 0 || y_move_times != 0 || z_move_times != 0)
{
OffsetTimes = new Vector3Int(OffsetTimes.x + x_move_times, OffsetTimes.y + y_move_times, OffsetTimes.z + z_move_times);
transform.position = new Vector3(OffsetTimes.x * MaxDistance, OffsetTimes.y * MaxDistance, OffsetTimes.z * MaxDistance);
world.ChunkEnabledCheck(-OffsetTimes.x, -OffsetTimes.z);
}
}
}
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
public static class TerrainUtility
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float[,] CreateRandomHeightData(int tiles_wide, int tiles_long, float min_height, float max_height)
{
float[,] heights = new float[tiles_wide + 1, tiles_long + 1];
for (int x = 0; x <= tiles_wide; x ++)
{
for (int z = 0; z <= tiles_long; z ++)
{
heights[x,z] = Random.Range(min_height, max_height);
}
}
return heights;
}
}
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
public class TerrainWorld : MonoBehaviour
{
public const float TileUnitScale = 1.0f;
public const int ChunkTileDim = 256;
public const float ChunkUnitDim = ChunkTileDim * TileUnitScale;
public const float HeightUnitSize = 0.1f;
public static TerrainWorld Inst {get; private set;} = null;
void Awake() {
if (Inst != null)
{
Debug.LogWarning("More than one TerrainWorld exists! Destroying extras.");
Destroy(this);
}
else
{
Inst = this;
}
}
void OnDestroy()
{
if (Inst == this)
{
Inst = null;
}
}
public GameObject ChunkPrefab = null;
public ushort[,] Heights = null;
public Chunk[,] Chunks = null;
public int TilesWide = 0;
public int TilesLong = 0;
public int TileCount = 0;
public int ChunksWide = 0;
public int ChunksLong = 0;
public int ChunkCount = 0;
public Transform WorldView;
//passing and storing heights by ref. careful for mem leaks and data races!
public void Initialize(ushort[,] heights, int tiles_wide, int tiles_long)
{
if (tiles_wide % ChunkTileDim != 0 || tiles_long % ChunkTileDim != 0)
{
Debug.LogError($"Cannot bulid world with tiles dimensions that are not divisible by " + ChunkTileDim + ".");
}
Heights = heights;
TilesWide = tiles_wide;
TilesLong = tiles_long;
TileCount = TilesWide * TilesLong;
ChunksWide = TilesWide / ChunkTileDim;
ChunksLong = TilesLong / ChunkTileDim;
ChunkCount = ChunksWide * ChunksLong;
Chunks = new Chunk[ChunksWide, ChunksLong];
for (int x = 0; x < ChunksWide; x ++)
{
for (int z = 0; z < ChunksLong; z ++)
{
Chunk new_chunk = Instantiate(ChunkPrefab, new Vector3(x * ChunkUnitDim, 0.0f, z * ChunkUnitDim), Quaternion.identity, transform).GetComponent<Chunk>();
new_chunk.name = "Chunk (" + x + ", " + z + ")";
new_chunk.Chunk_X = x;
new_chunk.Chunk_Z = z;
Chunks[x, z] = new_chunk;
}
}
}
public void SetHeight(int tile_x, int tile_z, ushort height)
{
Heights[tile_x, tile_z] = height;
float set_height = height * HeightUnitSize;
int chunk_x = tile_x / ChunkTileDim;
int chunk_z = tile_z / ChunkTileDim;
int chunk_tile_x = tile_x - (chunk_x * ChunkTileDim);
int chunk_tile_z = tile_z - (chunk_z * ChunkTileDim);
Chunks[chunk_x, chunk_z].SetHeight(chunk_tile_x, chunk_tile_z, set_height);
bool x_border = tile_x % ChunkTileDim == 0 && tile_x != 0;
bool z_border = tile_z % ChunkTileDim == 0 && tile_z != 0;
if (x_border)
{
Chunks[chunk_x - 1, chunk_z].SetHeight(ChunkTileDim, tile_z - (chunk_z * ChunkTileDim), set_height);
}
if (z_border)
{
Chunks[chunk_x, chunk_z - 1].SetHeight(tile_x - (chunk_x * ChunkTileDim), ChunkTileDim, set_height);
}
if (x_border && z_border)
{
Chunks[chunk_x - 1, chunk_z - 1].SetHeight(ChunkTileDim, ChunkTileDim, set_height);
}
}
public void ChunkEnabledCheck(int center_chunk_x, int center_chunk_z)
{
for (int x = 0; x < ChunksWide; x ++)
{
for(int z = 0; z < ChunksLong; z ++)
{
var chunk = Chunks[x, z];
if (System.Math.Abs(chunk.Chunk_X - center_chunk_x) > 11 || System.Math.Abs(chunk.Chunk_Z - center_chunk_z) > 11)
{
chunk.meshRenderer.enabled = false;
chunk.gameObject.SetActive(false);
}
else
{
chunk.meshRenderer.enabled = true;
chunk.gameObject.SetActive(true);
}
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float GetHeight(int x, int y)
{
return Heights[x, y] * HeightUnitSize;
}
}
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
public static class Utility
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Array2DTo1D(int x, int y, int width)
{
return (y * width + x);
}
}
https://www.youtube.com/watch?v=w1ThPSL0kT8&feature=youtu.be
https://www.youtube.com/watch?v=CsI_3Jt5swg&feature=youtu.be
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment