[Add] Ref

This commit is contained in:
2026-03-31 13:15:34 +07:00
parent 5f35ed8a6e
commit d126f216b7
12 changed files with 1233 additions and 1005 deletions
+45
View File
@@ -0,0 +1,45 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cac3da4969c2f94985de9f5fb30a682, type: 3}
m_Name: VoxelWorldConfig
m_EditorClassIdentifier: VoxelWorld.Runtime::InfiniteWorld.VoxelWorld.VoxelWorldConfig
chunkSize: 16
generationRadius: 3
blockingGenerationRadius: 0
seed: 12345
maxMountainHeight: 3
macroNoiseScale: 0.05
detailNoiseScale: 0.12
ridgeNoiseScale: 0.18
wallThreshold: 0.6
rockBias: 0.04
smoothingPasses: 2
passNoiseScale: 0.018
passDetailScale: 0.041
passThreshold: 0.22
passFeather: 0.12
heightNoiseScale: 0.08
terraceNoiseScale: 0.17
heightBias: 0.05
biomeProfiles:
- {fileID: 11400000, guid: ae25f699f7f7d4144bbd148907fda668, type: 2}
- {fileID: 11400000, guid: eac8d825dd62e1c439235d273a4ca613, type: 2}
- {fileID: 11400000, guid: 6d0dbe510ed048440a3925ef40aeeb5b, type: 2}
biomeNoiseScale: 0.02
biomeSize: 48
maxAsyncChunkJobs: 2
maxChunkBuildsPerFrame: 1
maxChunkMeshBuildsPerFrame: 1
maxColliderAppliesPerFrame: 1
maxNeighborRefreshesPerFrame: 2
renderRegionSizeInChunks: 4
maxRegionBuildsPerFrame: 1
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b8cf28a5522134b479c23f017234070c
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+1 -26
View File
@@ -413,32 +413,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: VoxelWorld.Runtime::InfiniteWorld.VoxelWorld.VoxelWorldGenerator
streamTarget: {fileID: 1331065949}
chunkSize: 16
generationRadius: 7
blockingGenerationRadius: 1
seed: 12345
maxMountainHeight: 12
macroNoiseScale: 0.05
detailNoiseScale: 0.12
ridgeNoiseScale: 0.18
wallThreshold: 0.6
rockBias: 0.04
smoothingPasses: 2
passNoiseScale: 0.018
passDetailScale: 0.041
passThreshold: 0.22
passFeather: 0.12
heightNoiseScale: 0.08
terraceNoiseScale: 0.17
heightBias: 0.05
biomeProfiles:
- {fileID: 11400000, guid: ae25f699f7f7d4144bbd148907fda668, type: 2}
- {fileID: 11400000, guid: eac8d825dd62e1c439235d273a4ca613, type: 2}
- {fileID: 11400000, guid: 6d0dbe510ed048440a3925ef40aeeb5b, type: 2}
biomeNoiseScale: 0.02
biomeSize: 4
maxAsyncChunkJobs: 8
maxChunkBuildsPerFrame: 2
config: {fileID: 11400000, guid: b8cf28a5522134b479c23f017234070c, type: 2}
--- !u!4 &1842209028
Transform:
m_ObjectHideFlags: 0
@@ -0,0 +1,182 @@
using System.Collections.Generic;
using UnityEngine;
namespace InfiniteWorld.VoxelWorld
{
[CreateAssetMenu(menuName = "Infinite World/Voxel World Config", fileName = "VoxelWorldConfig")]
public sealed class VoxelWorldConfig : ScriptableObject
{
[Header("Streaming")]
[Min(8)] public int chunkSize = 16;
[Min(1)] public int generationRadius = 3;
[Min(0)] public int blockingGenerationRadius;
public int seed = 12345;
[Min(1)] public int maxMountainHeight = 3;
[Header("Shape Noise")]
public float macroNoiseScale = 0.05f;
public float detailNoiseScale = 0.12f;
public float ridgeNoiseScale = 0.18f;
public float wallThreshold = 0.6f;
public float rockBias = 0.04f;
[Min(0)] public int smoothingPasses = 2;
[Header("Global Passes")]
public float passNoiseScale = 0.018f;
public float passDetailScale = 0.041f;
public float passThreshold = 0.22f;
public float passFeather = 0.12f;
[Header("Height")]
public float heightNoiseScale = 0.08f;
public float terraceNoiseScale = 0.17f;
public float heightBias = 0.05f;
[Header("Biomes")]
public List<VoxelBiomeProfile> biomeProfiles = new List<VoxelBiomeProfile>();
public float biomeNoiseScale = 0.02f;
[Min(1f)] public float biomeSize = 48f;
[Header("Runtime")]
[Min(1)] public int maxAsyncChunkJobs = 2;
[Min(1)] public int maxChunkBuildsPerFrame = 1;
[Min(1)] public int maxChunkMeshBuildsPerFrame = 1;
[Min(1)] public int maxColliderAppliesPerFrame = 1;
[Min(0)] public int maxNeighborRefreshesPerFrame = 2;
[Min(1)] public int renderRegionSizeInChunks = 4;
[Min(1)] public int maxRegionBuildsPerFrame = 1;
}
internal readonly struct VoxelWorldResolvedSettings
{
private static readonly IReadOnlyList<VoxelBiomeProfile> EmptyBiomes = System.Array.Empty<VoxelBiomeProfile>();
public static readonly VoxelWorldResolvedSettings Default = Resolve(null);
public VoxelWorldResolvedSettings(
int chunkSize,
int generationRadius,
int blockingGenerationRadius,
int seed,
int maxMountainHeight,
float macroNoiseScale,
float detailNoiseScale,
float ridgeNoiseScale,
float wallThreshold,
float rockBias,
int smoothingPasses,
float passNoiseScale,
float passDetailScale,
float passThreshold,
float passFeather,
float heightNoiseScale,
float terraceNoiseScale,
float heightBias,
IReadOnlyList<VoxelBiomeProfile> biomeProfiles,
float biomeNoiseScale,
float biomeSize,
int maxAsyncChunkJobs,
int maxChunkBuildsPerFrame,
int maxChunkMeshBuildsPerFrame,
int maxColliderAppliesPerFrame,
int maxNeighborRefreshesPerFrame,
int renderRegionSizeInChunks,
int maxRegionBuildsPerFrame)
{
ChunkSize = chunkSize;
GenerationRadius = generationRadius;
BlockingGenerationRadius = blockingGenerationRadius;
Seed = seed;
MaxMountainHeight = maxMountainHeight;
MacroNoiseScale = macroNoiseScale;
DetailNoiseScale = detailNoiseScale;
RidgeNoiseScale = ridgeNoiseScale;
WallThreshold = wallThreshold;
RockBias = rockBias;
SmoothingPasses = smoothingPasses;
PassNoiseScale = passNoiseScale;
PassDetailScale = passDetailScale;
PassThreshold = passThreshold;
PassFeather = passFeather;
HeightNoiseScale = heightNoiseScale;
TerraceNoiseScale = terraceNoiseScale;
HeightBias = heightBias;
BiomeProfiles = biomeProfiles;
BiomeNoiseScale = biomeNoiseScale;
BiomeSize = biomeSize;
MaxAsyncChunkJobs = maxAsyncChunkJobs;
MaxChunkBuildsPerFrame = maxChunkBuildsPerFrame;
MaxChunkMeshBuildsPerFrame = maxChunkMeshBuildsPerFrame;
MaxColliderAppliesPerFrame = maxColliderAppliesPerFrame;
MaxNeighborRefreshesPerFrame = maxNeighborRefreshesPerFrame;
RenderRegionSizeInChunks = renderRegionSizeInChunks;
MaxRegionBuildsPerFrame = maxRegionBuildsPerFrame;
}
public int ChunkSize { get; }
public int GenerationRadius { get; }
public int BlockingGenerationRadius { get; }
public int Seed { get; }
public int MaxMountainHeight { get; }
public float MacroNoiseScale { get; }
public float DetailNoiseScale { get; }
public float RidgeNoiseScale { get; }
public float WallThreshold { get; }
public float RockBias { get; }
public int SmoothingPasses { get; }
public float PassNoiseScale { get; }
public float PassDetailScale { get; }
public float PassThreshold { get; }
public float PassFeather { get; }
public float HeightNoiseScale { get; }
public float TerraceNoiseScale { get; }
public float HeightBias { get; }
public IReadOnlyList<VoxelBiomeProfile> BiomeProfiles { get; }
public float BiomeNoiseScale { get; }
public float BiomeSize { get; }
public int MaxAsyncChunkJobs { get; }
public int MaxChunkBuildsPerFrame { get; }
public int MaxChunkMeshBuildsPerFrame { get; }
public int MaxColliderAppliesPerFrame { get; }
public int MaxNeighborRefreshesPerFrame { get; }
public int RenderRegionSizeInChunks { get; }
public int MaxRegionBuildsPerFrame { get; }
public static VoxelWorldResolvedSettings Resolve(VoxelWorldConfig config)
{
IReadOnlyList<VoxelBiomeProfile> biomes = config != null && config.biomeProfiles != null
? config.biomeProfiles
: EmptyBiomes;
return new VoxelWorldResolvedSettings(
Mathf.Max(8, config != null ? config.chunkSize : 16),
Mathf.Max(1, config != null ? config.generationRadius : 3),
Mathf.Max(0, config != null ? config.blockingGenerationRadius : 0),
config != null ? config.seed : 12345,
Mathf.Max(1, config != null ? config.maxMountainHeight : 3),
config != null ? config.macroNoiseScale : 0.05f,
config != null ? config.detailNoiseScale : 0.12f,
config != null ? config.ridgeNoiseScale : 0.18f,
config != null ? config.wallThreshold : 0.6f,
config != null ? config.rockBias : 0.04f,
Mathf.Max(0, config != null ? config.smoothingPasses : 2),
config != null ? config.passNoiseScale : 0.018f,
config != null ? config.passDetailScale : 0.041f,
config != null ? config.passThreshold : 0.22f,
config != null ? config.passFeather : 0.12f,
config != null ? config.heightNoiseScale : 0.08f,
config != null ? config.terraceNoiseScale : 0.17f,
config != null ? config.heightBias : 0.05f,
biomes,
config != null ? config.biomeNoiseScale : 0.02f,
Mathf.Max(1f, config != null ? config.biomeSize : 48f),
Mathf.Max(1, config != null ? config.maxAsyncChunkJobs : 2),
Mathf.Max(1, config != null ? config.maxChunkBuildsPerFrame : 1),
Mathf.Max(1, config != null ? config.maxChunkMeshBuildsPerFrame : 1),
Mathf.Max(1, config != null ? config.maxColliderAppliesPerFrame : 1),
Mathf.Max(0, config != null ? config.maxNeighborRefreshesPerFrame : 2),
Mathf.Max(1, config != null ? config.renderRegionSizeInChunks : 4),
Mathf.Max(1, config != null ? config.maxRegionBuildsPerFrame : 1));
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0cac3da4969c2f94985de9f5fb30a682
@@ -0,0 +1,165 @@
using UnityEngine;
namespace InfiniteWorld.VoxelWorld
{
public sealed partial class VoxelWorldGenerator
{
private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version, int session, int runtimeId)
{
int margin = Mathf.Max(2, smoothingPasses + 1);
int sampleSize = chunkSize + margin * 2;
bool[,] sampled = new bool[sampleSize, sampleSize];
for (int z = 0; z < sampleSize; z++)
{
for (int x = 0; x < sampleSize; x++)
{
int localX = x - margin;
int localZ = z - margin;
Vector2Int worldCell = ChunkToWorldCell(coord, localX, localZ);
sampled[x, z] = SampleRock(worldCell);
}
}
for (int pass = 0; pass < smoothingPasses; pass++)
{
sampled = SmoothSampledMask(sampled);
}
int[] heights = new int[chunkSize * chunkSize];
byte[] biomeIndices = new byte[chunkSize * chunkSize];
for (int z = 0; z < chunkSize; z++)
{
for (int x = 0; x < chunkSize; x++)
{
Vector2Int worldCell = ChunkToWorldCell(coord, x, z);
biomeIndices[z * chunkSize + x] = SampleBiomeIndex(worldCell);
bool hasMountain = sampled[x + margin, z + margin];
if (!hasMountain)
{
continue;
}
heights[z * chunkSize + x] = SampleHeight(worldCell);
}
}
return new ChunkBuildResult(coord, heights, biomeIndices, version, session, runtimeId);
}
private bool[,] SmoothSampledMask(bool[,] source)
{
int width = source.GetLength(0);
int height = source.GetLength(1);
bool[,] result = new bool[width, height];
for (int z = 0; z < height; z++)
{
for (int x = 0; x < width; x++)
{
int solidNeighbors = CountSampledNeighbors(source, x, z);
if (solidNeighbors >= 5)
{
result[x, z] = true;
}
else if (solidNeighbors <= 2)
{
result[x, z] = false;
}
else
{
result[x, z] = source[x, z];
}
}
}
return result;
}
private static int CountSampledNeighbors(bool[,] sampled, int x, int z)
{
int width = sampled.GetLength(0);
int height = sampled.GetLength(1);
int count = 0;
for (int oz = -1; oz <= 1; oz++)
{
for (int ox = -1; ox <= 1; ox++)
{
if (ox == 0 && oz == 0)
{
continue;
}
int nx = x + ox;
int nz = z + oz;
if (nx < 0 || nz < 0 || nx >= width || nz >= height)
{
count++;
continue;
}
if (sampled[nx, nz])
{
count++;
}
}
}
return count;
}
private bool SampleRock(Vector2Int worldCell)
{
float macro = Mathf.PerlinNoise((worldCell.x + seed * 0.13f) * macroNoiseScale, (worldCell.y - seed * 0.17f) * macroNoiseScale);
float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.23f) * detailNoiseScale, (worldCell.y + seed * 0.19f) * detailNoiseScale);
float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.31f) * ridgeNoiseScale, (worldCell.y + seed * 0.29f) * ridgeNoiseScale) * 2f - 1f);
float rockValue = macro * 0.62f + detail * 0.18f + ridge * 0.20f + rockBias;
if (IsInsideGlobalPass(worldCell))
{
rockValue -= 0.45f;
}
return rockValue >= wallThreshold;
}
private bool IsInsideGlobalPass(Vector2Int worldCell)
{
float primary = Mathf.PerlinNoise((worldCell.x + seed * 0.41f) * passNoiseScale, (worldCell.y - seed * 0.43f) * passNoiseScale);
float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.17f) * passDetailScale, (worldCell.y + seed * 0.23f) * passDetailScale);
float ridged = Mathf.Abs(primary * 2f - 1f);
float warped = Mathf.Lerp(ridged, Mathf.Abs(detail * 2f - 1f), 0.35f);
return warped <= passThreshold + passFeather * detail;
}
private int SampleHeight(Vector2Int worldCell)
{
float macro = Mathf.PerlinNoise((worldCell.x - seed * 0.47f) * heightNoiseScale, (worldCell.y + seed * 0.37f) * heightNoiseScale);
float terrace = Mathf.PerlinNoise((worldCell.x + seed * 0.67f) * terraceNoiseScale, (worldCell.y - seed * 0.59f) * terraceNoiseScale);
float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.71f) * ridgeNoiseScale, (worldCell.y - seed * 0.73f) * ridgeNoiseScale) * 2f - 1f);
float heightValue = macro * 0.55f + terrace * 0.2f + ridge * 0.25f + heightBias;
int height = 1 + Mathf.FloorToInt(Mathf.Clamp01(heightValue) * maxMountainHeight);
return Mathf.Clamp(height, 1, maxMountainHeight);
}
private byte SampleBiomeIndex(Vector2Int worldCell)
{
int biomeCount = Mathf.Max(1, atlasBiomeCount > 0 ? atlasBiomeCount : CountConfiguredBiomes());
if (biomeCount <= 1)
{
return 0;
}
float effectiveScale = biomeNoiseScale / Mathf.Max(1f, biomeSize);
float noise = Mathf.PerlinNoise((worldCell.x + seed * 0.83f) * effectiveScale, (worldCell.y - seed * 0.79f) * effectiveScale);
int biomeIndex = Mathf.FloorToInt(Mathf.Clamp01(noise) * biomeCount);
return (byte)Mathf.Clamp(biomeIndex, 0, biomeCount - 1);
}
private Vector2Int ChunkToWorldCell(Vector2Int coord, int localX, int localZ)
{
return new Vector2Int(coord.x * chunkSize + localX, coord.y * chunkSize + localZ);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9da608950686fb345b172db0a56bced5
@@ -0,0 +1,451 @@
using UnityEngine;
namespace InfiniteWorld.VoxelWorld
{
public sealed partial class VoxelWorldGenerator
{
private void RenderChunk(Vector2Int coord)
{
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData || atlas == null)
{
return;
}
runtime.EnsureCreated(coord, chunkRoot, chunkSize);
ChunkMeshBuild meshBuild = BuildChunkMesh(coord, runtime.Heights, runtime.BiomeIndices);
runtime.ApplyRenderData(meshBuild.RenderSnapshot);
EnqueueColliderApply(coord, runtime.Version, runtime.RuntimeId, meshBuild.ColliderMesh);
runtime.State = ChunkState.Rendered;
MarkRegionDirty(coord);
}
private ChunkMeshBuild BuildChunkMesh(Vector2Int coord, int[] heights, byte[] biomeIndices)
{
MeshBuffers renderBuffers = new MeshBuffers();
MeshBuffers colliderBuffers = new MeshBuffers();
BuildGroundSurface(heights, biomeIndices, renderBuffers);
BuildMountainTops(coord, heights, biomeIndices, renderBuffers, colliderBuffers);
BuildMountainSides(coord, heights, biomeIndices, renderBuffers, colliderBuffers);
ChunkRenderSnapshot renderSnapshot = renderBuffers.ToSnapshot();
return new ChunkMeshBuild(colliderBuffers.ToMesh($"Collider_{coord.x}_{coord.y}"), renderSnapshot);
}
private void BuildGroundSurface(int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers)
{
bool[,] visited = new bool[chunkSize, chunkSize];
for (int z = 0; z < chunkSize; z++)
{
for (int x = 0; x < chunkSize; x++)
{
int index = z * chunkSize + x;
if (visited[x, z] || heights[index] > 0)
{
continue;
}
byte biomeIndex = biomeIndices[index];
int width = 1;
while (x + width < chunkSize && !visited[x + width, z] && heights[z * chunkSize + x + width] == 0 && biomeIndices[z * chunkSize + x + width] == biomeIndex)
{
width++;
}
int depth = 1;
bool canGrow = true;
while (z + depth < chunkSize && canGrow)
{
for (int ix = 0; ix < width; ix++)
{
int candidateIndex = (z + depth) * chunkSize + x + ix;
if (visited[x + ix, z + depth] || heights[candidateIndex] > 0 || biomeIndices[candidateIndex] != biomeIndex)
{
canGrow = false;
break;
}
}
if (canGrow)
{
depth++;
}
}
for (int dz = 0; dz < depth; dz++)
{
for (int dx = 0; dx < width; dx++)
{
visited[x + dx, z + dz] = true;
}
}
AddTopQuad(renderBuffers, x, z, width, depth, 0f, atlas.GetTextureLayer(biomeIndex, VoxelSurfaceType.WalkableSurface));
}
}
}
private void BuildMountainTops(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers)
{
bool[,] visited = new bool[chunkSize, chunkSize];
for (int z = 0; z < chunkSize; z++)
{
for (int x = 0; x < chunkSize; x++)
{
int height = heights[z * chunkSize + x];
if (height <= 0 || visited[x, z])
{
continue;
}
int index = z * chunkSize + x;
VoxelSurfaceType surfaceType = IsCliffTop(ChunkToWorldCell(coord, x, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
byte biomeIndex = biomeIndices[index];
int width = 1;
while (x + width < chunkSize && !visited[x + width, z])
{
int candidateIndex = z * chunkSize + x + width;
if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex)
{
break;
}
VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + width, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
if (candidateSurface != surfaceType)
{
break;
}
width++;
}
int depth = 1;
bool canGrow = true;
while (z + depth < chunkSize && canGrow)
{
for (int ix = 0; ix < width; ix++)
{
if (visited[x + ix, z + depth])
{
canGrow = false;
break;
}
int candidateIndex = (z + depth) * chunkSize + x + ix;
if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex)
{
canGrow = false;
break;
}
VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + ix, z + depth), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
if (candidateSurface != surfaceType)
{
canGrow = false;
break;
}
}
if (canGrow)
{
depth++;
}
}
for (int dz = 0; dz < depth; dz++)
{
for (int dx = 0; dx < width; dx++)
{
visited[x + dx, z + dz] = true;
}
}
int textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
AddTopQuad(renderBuffers, x, z, width, depth, height, textureLayer);
AddTopQuad(colliderBuffers, x, z, width, depth, height, 0, false);
}
}
}
private void BuildMountainSides(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers)
{
for (int z = 0; z < chunkSize; z++)
{
BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, true);
BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, false);
}
for (int x = 0; x < chunkSize; x++)
{
BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, true);
BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, false);
}
}
private void BuildNorthSouthFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int z, bool north)
{
bool[,] visited = new bool[chunkSize, maxMountainHeight];
for (int x = 0; x < chunkSize; x++)
{
for (int y = 0; y < maxMountainHeight; y++)
{
if (visited[x, y] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x, z, y, north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer))
{
continue;
}
int width = 1;
while (x + width < chunkSize && !visited[x + width, y] && TryGetNorthSouthFace(coord, heights, biomeIndices, x + width, z, y, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer)
{
width++;
}
int height = 1;
bool canGrow = true;
while (y + height < maxMountainHeight && canGrow)
{
for (int dx = 0; dx < width; dx++)
{
if (visited[x + dx, y + height] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x + dx, z, y + height, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer)
{
canGrow = false;
break;
}
}
if (canGrow)
{
height++;
}
}
for (int dy = 0; dy < height; dy++)
{
for (int dx = 0; dx < width; dx++)
{
visited[x + dx, y + dy] = true;
}
}
float faceZ = north ? z + 1f : z;
Vector3 bl = new Vector3(x, y, faceZ);
Vector3 br = new Vector3(x + width, y, faceZ);
Vector3 tr = new Vector3(x + width, y + height, faceZ);
Vector3 tl = new Vector3(x, y + height, faceZ);
if (north)
{
AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.forward);
AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.forward, false);
}
else
{
AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.back);
AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.back, false);
}
}
}
}
private void BuildEastWestFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, bool east)
{
bool[,] visited = new bool[chunkSize, maxMountainHeight];
for (int z = 0; z < chunkSize; z++)
{
for (int y = 0; y < maxMountainHeight; y++)
{
if (visited[z, y] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z, y, east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer))
{
continue;
}
int width = 1;
while (z + width < chunkSize && !visited[z + width, y] && TryGetEastWestFace(coord, heights, biomeIndices, x, z + width, y, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer)
{
width++;
}
int height = 1;
bool canGrow = true;
while (y + height < maxMountainHeight && canGrow)
{
for (int dz = 0; dz < width; dz++)
{
if (visited[z + dz, y + height] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z + dz, y + height, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer)
{
canGrow = false;
break;
}
}
if (canGrow)
{
height++;
}
}
for (int dy = 0; dy < height; dy++)
{
for (int dz = 0; dz < width; dz++)
{
visited[z + dz, y + dy] = true;
}
}
float faceX = east ? x + 1f : x;
Vector3 bl = new Vector3(faceX, y, z);
Vector3 br = new Vector3(faceX, y, z + width);
Vector3 tr = new Vector3(faceX, y + height, z + width);
Vector3 tl = new Vector3(faceX, y + height, z);
if (east)
{
AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.right);
AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.right, false);
}
else
{
AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.left);
AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.left, false);
}
}
}
}
private bool TryGetNorthSouthFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)
{
int current = heights[z * chunkSize + x];
int worldX = coord.x * chunkSize + x;
int worldZ = coord.y * chunkSize + z;
int neighbor = GetHeightAtWorldCell(new Vector2Int(worldX, north ? worldZ + 1 : worldZ - 1));
if (y >= current || y < neighbor)
{
biomeIndex = 0;
surfaceType = VoxelSurfaceType.Dirt;
textureLayer = 0;
return false;
}
biomeIndex = biomeIndices[z * chunkSize + x];
surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
return true;
}
private bool TryGetEastWestFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)
{
int current = heights[z * chunkSize + x];
int worldX = coord.x * chunkSize + x;
int worldZ = coord.y * chunkSize + z;
int neighbor = GetHeightAtWorldCell(new Vector2Int(east ? worldX + 1 : worldX - 1, worldZ));
if (y >= current || y < neighbor)
{
biomeIndex = 0;
surfaceType = VoxelSurfaceType.Dirt;
textureLayer = 0;
return false;
}
biomeIndex = biomeIndices[z * chunkSize + x];
surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
return true;
}
private void AddTopQuad(MeshBuffers buffers, float x, float z, float width, float depth, float height, int textureLayer, bool withUv = true)
{
int baseIndex = buffers.Vertices.Count;
buffers.Vertices.Add(new Vector3(x, height, z));
buffers.Vertices.Add(new Vector3(x + width, height, z));
buffers.Vertices.Add(new Vector3(x + width, height, z + depth));
buffers.Vertices.Add(new Vector3(x, height, z + depth));
buffers.Normals.Add(Vector3.up);
buffers.Normals.Add(Vector3.up);
buffers.Normals.Add(Vector3.up);
buffers.Normals.Add(Vector3.up);
if (withUv)
{
buffers.Uvs.Add(new Vector2(0f, 0f));
buffers.Uvs.Add(new Vector2(width, 0f));
buffers.Uvs.Add(new Vector2(width, depth));
buffers.Uvs.Add(new Vector2(0f, depth));
Vector2 textureData = new Vector2(textureLayer, 0f);
buffers.TextureData.Add(textureData);
buffers.TextureData.Add(textureData);
buffers.TextureData.Add(textureData);
buffers.TextureData.Add(textureData);
}
buffers.Triangles.Add(baseIndex);
buffers.Triangles.Add(baseIndex + 2);
buffers.Triangles.Add(baseIndex + 1);
buffers.Triangles.Add(baseIndex);
buffers.Triangles.Add(baseIndex + 3);
buffers.Triangles.Add(baseIndex + 2);
}
private void AddVerticalQuad(MeshBuffers buffers, Vector3 bottomLeft, Vector3 bottomRight, Vector3 topRight, Vector3 topLeft, float width, float height, int textureLayer, Vector3 normal, bool withUv = true)
{
int baseIndex = buffers.Vertices.Count;
buffers.Vertices.Add(bottomLeft);
buffers.Vertices.Add(bottomRight);
buffers.Vertices.Add(topRight);
buffers.Vertices.Add(topLeft);
buffers.Normals.Add(normal);
buffers.Normals.Add(normal);
buffers.Normals.Add(normal);
buffers.Normals.Add(normal);
if (withUv)
{
buffers.Uvs.Add(new Vector2(0f, 0f));
buffers.Uvs.Add(new Vector2(width, 0f));
buffers.Uvs.Add(new Vector2(width, height));
buffers.Uvs.Add(new Vector2(0f, height));
Vector2 textureData = new Vector2(textureLayer, 0f);
buffers.TextureData.Add(textureData);
buffers.TextureData.Add(textureData);
buffers.TextureData.Add(textureData);
buffers.TextureData.Add(textureData);
}
buffers.Triangles.Add(baseIndex);
buffers.Triangles.Add(baseIndex + 1);
buffers.Triangles.Add(baseIndex + 2);
buffers.Triangles.Add(baseIndex);
buffers.Triangles.Add(baseIndex + 2);
buffers.Triangles.Add(baseIndex + 3);
}
private bool IsCliffTop(Vector2Int worldCell, int currentHeight)
{
return GetHeightAtWorldCell(worldCell + Vector2Int.up) < currentHeight ||
GetHeightAtWorldCell(worldCell + Vector2Int.right) < currentHeight ||
GetHeightAtWorldCell(worldCell + Vector2Int.down) < currentHeight ||
GetHeightAtWorldCell(worldCell + Vector2Int.left) < currentHeight;
}
private int GetHeightAtWorldCell(Vector2Int worldCell)
{
Vector2Int coord = new Vector2Int(
Mathf.FloorToInt(worldCell.x / (float)chunkSize),
Mathf.FloorToInt(worldCell.y / (float)chunkSize));
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData)
{
return SampleRock(worldCell) ? SampleHeight(worldCell) : 0;
}
int localX = worldCell.x - coord.x * chunkSize;
int localZ = worldCell.y - coord.y * chunkSize;
if (localX < 0 || localZ < 0 || localX >= chunkSize || localZ >= chunkSize)
{
return SampleRock(worldCell) ? SampleHeight(worldCell) : 0;
}
return runtime.Heights[localZ * chunkSize + localX];
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 811ff92b4e36193499cad8631c9443a7
@@ -0,0 +1,339 @@
using System.Collections.Generic;
using UnityEngine;
namespace InfiniteWorld.VoxelWorld
{
public sealed partial class VoxelWorldGenerator
{
private struct ChunkBuildResult
{
public ChunkBuildResult(Vector2Int coord, int[] heights, byte[] biomeIndices, int version, int session, int runtimeId)
{
Coord = coord;
Heights = heights;
BiomeIndices = biomeIndices;
Version = version;
Session = session;
RuntimeId = runtimeId;
}
public Vector2Int Coord { get; }
public int[] Heights { get; }
public byte[] BiomeIndices { get; }
public int Version { get; }
public int Session { get; }
public int RuntimeId { get; }
}
private struct ChunkMeshBuild
{
public ChunkMeshBuild(Mesh colliderMesh, ChunkRenderSnapshot renderSnapshot)
{
ColliderMesh = colliderMesh;
RenderSnapshot = renderSnapshot;
}
public Mesh ColliderMesh { get; }
public ChunkRenderSnapshot RenderSnapshot { get; }
}
private struct PendingColliderMeshApply
{
public PendingColliderMeshApply(Vector2Int coord, int version, int runtimeId, Mesh colliderMesh)
{
Coord = coord;
Version = version;
RuntimeId = runtimeId;
ColliderMesh = colliderMesh;
}
public Vector2Int Coord { get; }
public int Version { get; }
public int RuntimeId { get; }
public Mesh ColliderMesh { get; }
}
private sealed class ChunkRenderSnapshot
{
public ChunkRenderSnapshot(Vector3[] vertices, Vector3[] normals, Vector2[] uv0, Vector2[] uv1, int[] triangles)
{
Vertices = vertices;
Normals = normals;
Uv0 = uv0;
Uv1 = uv1;
Triangles = triangles;
}
public Vector3[] Vertices { get; }
public Vector3[] Normals { get; }
public Vector2[] Uv0 { get; }
public Vector2[] Uv1 { get; }
public int[] Triangles { get; }
public bool IsEmpty => Vertices == null || Vertices.Length == 0 || Triangles == null || Triangles.Length == 0;
}
private struct RegionChunkSnapshot
{
public RegionChunkSnapshot(ChunkRenderSnapshot snapshot, Vector3 localOffset)
{
Snapshot = snapshot;
LocalOffset = localOffset;
}
public ChunkRenderSnapshot Snapshot { get; }
public Vector3 LocalOffset { get; }
}
private struct RegionBuildRequest
{
public RegionBuildRequest(Vector2Int regionCoord, int version, int session, RegionChunkSnapshot[] chunks)
{
RegionCoord = regionCoord;
Version = version;
Session = session;
Chunks = chunks;
}
public Vector2Int RegionCoord { get; }
public int Version { get; }
public int Session { get; }
public RegionChunkSnapshot[] Chunks { get; }
}
private struct RegionBuildResult
{
public RegionBuildResult(Vector2Int regionCoord, int version, int session, Vector3[] vertices, Vector3[] normals, Vector2[] uv0, Vector2[] uv1, int[] triangles, Bounds bounds)
{
RegionCoord = regionCoord;
Version = version;
Session = session;
Vertices = vertices;
Normals = normals;
Uv0 = uv0;
Uv1 = uv1;
Triangles = triangles;
Bounds = bounds;
}
public Vector2Int RegionCoord { get; }
public int Version { get; }
public int Session { get; }
public Vector3[] Vertices { get; }
public Vector3[] Normals { get; }
public Vector2[] Uv0 { get; }
public Vector2[] Uv1 { get; }
public int[] Triangles { get; }
public Bounds Bounds { get; }
public bool IsEmpty => Vertices == null || Vertices.Length == 0 || Triangles == null || Triangles.Length == 0;
public static RegionBuildResult CreateEmpty(Vector2Int regionCoord, int version, int session)
{
return new RegionBuildResult(regionCoord, version, session, null, null, null, null, null, new Bounds());
}
}
private sealed class MeshBuffers
{
public readonly List<Vector3> Vertices = new List<Vector3>(512);
public readonly List<Vector3> Normals = new List<Vector3>(512);
public readonly List<Vector2> Uvs = new List<Vector2>(512);
public readonly List<Vector2> TextureData = new List<Vector2>(512);
public readonly List<int> Triangles = new List<int>(1024);
public Mesh ToMesh(string meshName)
{
Mesh mesh = new Mesh { name = meshName };
if (Vertices.Count == 0)
{
return mesh;
}
mesh.indexFormat = Vertices.Count > 65535 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16;
mesh.SetVertices(Vertices);
if (Normals.Count == Vertices.Count)
{
mesh.SetNormals(Normals);
}
if (Uvs.Count == Vertices.Count)
{
mesh.SetUVs(0, Uvs);
}
if (TextureData.Count == Vertices.Count)
{
mesh.SetUVs(1, TextureData);
}
mesh.SetTriangles(Triangles, 0, true);
mesh.RecalculateBounds();
return mesh;
}
public ChunkRenderSnapshot ToSnapshot()
{
return new ChunkRenderSnapshot(
Vertices.ToArray(),
Normals.ToArray(),
Uvs.ToArray(),
TextureData.ToArray(),
Triangles.ToArray());
}
}
private sealed class ChunkRuntime
{
public Transform Root;
public MeshCollider MountainCollider;
public BoxCollider GroundCollider;
public Mesh ColliderMesh;
public ChunkRenderSnapshot RenderSnapshot;
public int[] Heights;
public byte[] BiomeIndices;
public ChunkState State;
public int Version;
public int RuntimeId;
public bool HasData => Heights != null && BiomeIndices != null;
public bool IsRendered => State == ChunkState.Rendered && Root != null;
public void EnsureCreated(Vector2Int coord, Transform parent, int chunkSize)
{
if (Root == null)
{
GameObject chunkObject = new GameObject($"VoxelChunk_{coord.x}_{coord.y}");
chunkObject.transform.SetParent(parent, false);
Root = chunkObject.transform;
MountainCollider = chunkObject.AddComponent<MeshCollider>();
GroundCollider = chunkObject.AddComponent<BoxCollider>();
}
Root.localPosition = new Vector3(coord.x * chunkSize, 0f, coord.y * chunkSize);
GroundCollider.size = new Vector3(chunkSize, 0.2f, chunkSize);
GroundCollider.center = new Vector3(chunkSize * 0.5f, -0.1f, chunkSize * 0.5f);
}
public void ApplyRenderData(ChunkRenderSnapshot renderSnapshot)
{
RenderSnapshot = renderSnapshot;
}
public void ApplyColliderMesh(Mesh colliderMesh)
{
if (ColliderMesh != null)
{
DestroyMesh(ColliderMesh);
}
ColliderMesh = colliderMesh;
MountainCollider.sharedMesh = null;
MountainCollider.sharedMesh = ColliderMesh != null && ColliderMesh.vertexCount > 0 ? ColliderMesh : null;
}
public void Dispose()
{
if (Root != null)
{
if (Application.isPlaying)
{
Object.Destroy(Root.gameObject);
}
else
{
Object.DestroyImmediate(Root.gameObject);
}
}
DestroyMesh(ColliderMesh);
RenderSnapshot = null;
}
private static void DestroyMesh(Mesh mesh)
{
if (mesh == null)
{
return;
}
if (Application.isPlaying)
{
Object.Destroy(mesh);
}
else
{
Object.DestroyImmediate(mesh);
}
}
}
private sealed class RegionRuntime
{
public Transform Root;
public MeshFilter Filter;
public MeshRenderer Renderer;
public Mesh RenderMesh;
public void EnsureCreated(Vector2Int regionCoord, Transform parent, int chunkSize, int regionSizeInChunks, Material material)
{
if (Root == null)
{
GameObject regionObject = new GameObject($"VoxelRegion_{regionCoord.x}_{regionCoord.y}");
regionObject.transform.SetParent(parent, false);
Root = regionObject.transform;
Filter = regionObject.AddComponent<MeshFilter>();
Renderer = regionObject.AddComponent<MeshRenderer>();
}
Root.localPosition = new Vector3(regionCoord.x * regionSizeInChunks * chunkSize, 0f, regionCoord.y * regionSizeInChunks * chunkSize);
Renderer.sharedMaterial = material;
}
public void ApplyMesh(Mesh mesh)
{
DestroyMesh(RenderMesh);
RenderMesh = mesh;
Filter.sharedMesh = RenderMesh;
}
public void Dispose()
{
if (Root != null)
{
if (Application.isPlaying)
{
Object.Destroy(Root.gameObject);
}
else
{
Object.DestroyImmediate(Root.gameObject);
}
}
DestroyMesh(RenderMesh);
}
private static void DestroyMesh(Mesh mesh)
{
if (mesh == null)
{
return;
}
if (Application.isPlaying)
{
Object.Destroy(mesh);
}
else
{
Object.DestroyImmediate(mesh);
}
}
}
private enum ChunkState
{
None,
Generating,
SyncBuilding,
ReadyToRender,
Rendered
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 56e0b6ddd79410b40bdda73c4e20515d
File diff suppressed because it is too large Load Diff