Move Files, remove 2D world gen
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f0ac81b911bd5e4eb8659dbd0edd665
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
%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: 0}
|
||||
m_Name: VoxelBiomeProfile 1
|
||||
m_EditorClassIdentifier: VoxelWorld.Runtime:InfiniteWorld.VoxelWorld:VoxelBiomeProfile
|
||||
cliffTopSprite: {fileID: 3392429205621607710, guid: 48c8cb6b6e758e74e852e27073d0e263, type: 3}
|
||||
cliffSideSprite: {fileID: -5744119150125800539, guid: f4c696ca947bbaf4ba5a27cc0f6a2cb8, type: 3}
|
||||
dirtSprite: {fileID: -4783801984881171517, guid: 841a59b6af0505448b39ad06ddbc067f, type: 3}
|
||||
walkableSurfaceSprite: {fileID: 4360390788417139489, guid: 5f3d8f393d5c64b489163ff940369b2d, type: 3}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eac8d825dd62e1c439235d273a4ca613
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
%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: 0}
|
||||
m_Name: VoxelBiomeProfile 2
|
||||
m_EditorClassIdentifier: VoxelWorld.Runtime:InfiniteWorld.VoxelWorld:VoxelBiomeProfile
|
||||
cliffTopSprite: {fileID: 904681902854485019, guid: 3d70b2fe747c8134c8a38cc0e089d139, type: 3}
|
||||
cliffSideSprite: {fileID: -1499911416505331838, guid: 0891183432730d34c8a41445947b6e85, type: 3}
|
||||
dirtSprite: {fileID: -1865540202983660084, guid: 152cabc665e467d448f550899fcd56e8, type: 3}
|
||||
walkableSurfaceSprite: {fileID: 5634026908348037240, guid: e5bbac025bbdb444ab5b16d34a3f38de, type: 3}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d0dbe510ed048440a3925ef40aeeb5b
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
%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: 0}
|
||||
m_Name: VoxelBiomeProfile
|
||||
m_EditorClassIdentifier: VoxelWorld.Runtime:InfiniteWorld.VoxelWorld:VoxelBiomeProfile
|
||||
cliffTopSprite: {fileID: 5725184139431673129, guid: d8a251553db3e5d43ac029f301167578, type: 3}
|
||||
cliffSideSprite: {fileID: 8591976868863569928, guid: b44be66700acb8d4ba360bdeb7c0bf2f, type: 3}
|
||||
dirtSprite: {fileID: 4285751004304663802, guid: 89b3d97a04bee1e4d85e5d28c13de1f8, type: 3}
|
||||
walkableSurfaceSprite: {fileID: 5613978133511580594, guid: 5702a47030ece9f42b0b513238c05bf4, type: 3}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae25f699f7f7d4144bbd148907fda668
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: 6
|
||||
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: 6
|
||||
maxAsyncChunkJobs: 2
|
||||
maxChunkBuildsPerFrame: 1
|
||||
maxChunkMeshBuildsPerFrame: 1
|
||||
maxColliderAppliesPerFrame: 1
|
||||
maxNeighborRefreshesPerFrame: 2
|
||||
renderRegionSizeInChunks: 4
|
||||
maxRegionBuildsPerFrame: 1
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8cf28a5522134b479c23f017234070c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6692637cf5eb1b41a9d619e7557b2f0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "VoxelWorld.Runtime",
|
||||
"rootNamespace": "InfiniteWorld.VoxelWorld",
|
||||
"references": [
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0a085d6765d01448bb424934e24ed9c
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,375 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
public enum VoxelSurfaceType : byte
|
||||
{
|
||||
CliffTop,
|
||||
CliffSide,
|
||||
Dirt,
|
||||
WalkableSurface
|
||||
}
|
||||
|
||||
[CreateAssetMenu(menuName = "Infinite World/Voxel Biome Profile", fileName = "VoxelBiomeProfile")]
|
||||
public sealed class VoxelBiomeProfile : ScriptableObject
|
||||
{
|
||||
public Sprite cliffTopSprite;
|
||||
public Sprite cliffSideSprite;
|
||||
public Sprite dirtSprite;
|
||||
public Sprite walkableSurfaceSprite;
|
||||
|
||||
public Sprite GetSprite(VoxelSurfaceType surfaceType)
|
||||
{
|
||||
return surfaceType switch
|
||||
{
|
||||
VoxelSurfaceType.CliffTop => cliffTopSprite,
|
||||
VoxelSurfaceType.CliffSide => cliffSideSprite,
|
||||
VoxelSurfaceType.Dirt => dirtSprite,
|
||||
VoxelSurfaceType.WalkableSurface => walkableSurfaceSprite,
|
||||
_ => walkableSurfaceSprite
|
||||
};
|
||||
}
|
||||
|
||||
public bool HasAnyAssignedSprites()
|
||||
{
|
||||
return cliffTopSprite != null || cliffSideSprite != null || dirtSprite != null || walkableSurfaceSprite != null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class VoxelWorldAtlas : IDisposable
|
||||
{
|
||||
private const int FallbackTileSize = 32;
|
||||
private const string ShaderName = "Infinite World/VoxelWorld/TextureArrayUnlit";
|
||||
|
||||
private static readonly VoxelSurfaceType[] SurfaceOrder =
|
||||
{
|
||||
VoxelSurfaceType.CliffTop,
|
||||
VoxelSurfaceType.CliffSide,
|
||||
VoxelSurfaceType.Dirt,
|
||||
VoxelSurfaceType.WalkableSurface
|
||||
};
|
||||
|
||||
private readonly Dictionary<int, Dictionary<VoxelSurfaceType, int>> layerLookup;
|
||||
|
||||
private VoxelWorldAtlas(Texture2DArray textureArray, Material material, Dictionary<int, Dictionary<VoxelSurfaceType, int>> layers)
|
||||
{
|
||||
TextureArray = textureArray;
|
||||
Material = material;
|
||||
layerLookup = layers;
|
||||
}
|
||||
|
||||
public Texture2DArray TextureArray { get; }
|
||||
|
||||
public Material Material { get; }
|
||||
|
||||
public int BiomeCount => layerLookup.Count;
|
||||
|
||||
public int GetTextureLayer(int biomeIndex, VoxelSurfaceType surfaceType)
|
||||
{
|
||||
if (layerLookup.TryGetValue(biomeIndex, out Dictionary<VoxelSurfaceType, int> biomeLayers) && biomeLayers.TryGetValue(surfaceType, out int layer))
|
||||
{
|
||||
return layer;
|
||||
}
|
||||
|
||||
return layerLookup[0][surfaceType];
|
||||
}
|
||||
|
||||
public static VoxelWorldAtlas CreateRuntimeAtlas(IReadOnlyList<VoxelBiomeProfile> biomeProfiles)
|
||||
{
|
||||
List<VoxelBiomeProfile> sourceBiomes = new List<VoxelBiomeProfile>();
|
||||
if (biomeProfiles != null)
|
||||
{
|
||||
for (int i = 0; i < biomeProfiles.Count; i++)
|
||||
{
|
||||
if (biomeProfiles[i] != null)
|
||||
{
|
||||
sourceBiomes.Add(biomeProfiles[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceBiomes.Count == 0)
|
||||
{
|
||||
sourceBiomes.Add(null);
|
||||
}
|
||||
|
||||
int tileSize = DetermineTileSize(sourceBiomes);
|
||||
int layerCount = sourceBiomes.Count * SurfaceOrder.Length;
|
||||
Texture2DArray textureArray = new Texture2DArray(tileSize, tileSize, layerCount, TextureFormat.RGBA32, false, false)
|
||||
{
|
||||
filterMode = FilterMode.Point,
|
||||
wrapMode = TextureWrapMode.Repeat,
|
||||
anisoLevel = 1,
|
||||
name = "VoxelWorld_RuntimeTextureArray",
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
|
||||
Dictionary<int, Dictionary<VoxelSurfaceType, int>> layers = new Dictionary<int, Dictionary<VoxelSurfaceType, int>>(sourceBiomes.Count);
|
||||
int layerIndex = 0;
|
||||
for (int biomeIndex = 0; biomeIndex < sourceBiomes.Count; biomeIndex++)
|
||||
{
|
||||
Dictionary<VoxelSurfaceType, int> biomeLayers = new Dictionary<VoxelSurfaceType, int>(SurfaceOrder.Length);
|
||||
layers[biomeIndex] = biomeLayers;
|
||||
|
||||
for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++)
|
||||
{
|
||||
VoxelSurfaceType surfaceType = SurfaceOrder[surfaceIndex];
|
||||
Texture2D tileTexture = BuildSurfaceTexture(sourceBiomes[biomeIndex], surfaceType, tileSize);
|
||||
CopyTextureToArrayLayer(tileTexture, textureArray, layerIndex);
|
||||
biomeLayers[surfaceType] = layerIndex;
|
||||
layerIndex++;
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEngine.Object.Destroy(tileTexture);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(tileTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textureArray.Apply(false, false);
|
||||
|
||||
Shader shader = Shader.Find(ShaderName);
|
||||
if (shader == null)
|
||||
{
|
||||
shader = Shader.Find("Universal Render Pipeline/Unlit") ?? Shader.Find("Standard");
|
||||
Debug.LogError($"Shader '{ShaderName}' was not found. Falling back to '{shader?.name ?? "<missing>"}', but voxel texture array sampling will not work until the shader asset is imported correctly.");
|
||||
}
|
||||
|
||||
Material material = new Material(shader)
|
||||
{
|
||||
name = "VoxelWorld_RuntimeMaterial",
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
|
||||
if (material.HasProperty("_TextureArray"))
|
||||
{
|
||||
material.SetTexture("_TextureArray", textureArray);
|
||||
}
|
||||
|
||||
if (material.HasProperty("_BaseColor"))
|
||||
{
|
||||
material.SetColor("_BaseColor", Color.white);
|
||||
}
|
||||
|
||||
if (material.HasProperty("_Color"))
|
||||
{
|
||||
material.SetColor("_Color", Color.white);
|
||||
}
|
||||
|
||||
return new VoxelWorldAtlas(textureArray, material, layers);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEngine.Object.Destroy(TextureArray);
|
||||
UnityEngine.Object.Destroy(Material);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(TextureArray);
|
||||
UnityEngine.Object.DestroyImmediate(Material);
|
||||
}
|
||||
}
|
||||
|
||||
private static int DetermineTileSize(IReadOnlyList<VoxelBiomeProfile> biomeProfiles)
|
||||
{
|
||||
int tileSize = FallbackTileSize;
|
||||
for (int biomeIndex = 0; biomeIndex < biomeProfiles.Count; biomeIndex++)
|
||||
{
|
||||
VoxelBiomeProfile biome = biomeProfiles[biomeIndex];
|
||||
for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++)
|
||||
{
|
||||
Sprite sprite = biome != null ? biome.GetSprite(SurfaceOrder[surfaceIndex]) : null;
|
||||
if (sprite == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Rect rect = sprite.textureRect;
|
||||
tileSize = Mathf.Max(tileSize, Mathf.CeilToInt(Mathf.Max(rect.width, rect.height)));
|
||||
}
|
||||
}
|
||||
|
||||
return Mathf.Max(FallbackTileSize, tileSize);
|
||||
}
|
||||
|
||||
private static Texture2D BuildSurfaceTexture(VoxelBiomeProfile biome, VoxelSurfaceType surfaceType, int tileSize)
|
||||
{
|
||||
Texture2D texture = new Texture2D(tileSize, tileSize, TextureFormat.RGBA32, false)
|
||||
{
|
||||
filterMode = FilterMode.Point,
|
||||
wrapMode = TextureWrapMode.Repeat,
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
|
||||
Fill(texture, new Color(1f, 0f, 1f, 1f));
|
||||
Sprite sprite = biome != null ? biome.GetSprite(surfaceType) : null;
|
||||
if (sprite == null || !TryBlitSprite(texture, sprite))
|
||||
{
|
||||
DrawFallbackSurface(texture, new RectInt(0, 0, tileSize, tileSize), surfaceType);
|
||||
}
|
||||
|
||||
texture.Apply(false, false);
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static void CopyTextureToArrayLayer(Texture2D source, Texture2DArray destination, int layerIndex)
|
||||
{
|
||||
bool canUseCopyTexture = SystemInfo.supports2DArrayTextures &&
|
||||
SystemInfo.copyTextureSupport != UnityEngine.Rendering.CopyTextureSupport.None &&
|
||||
source.width == destination.width &&
|
||||
source.height == destination.height;
|
||||
|
||||
if (canUseCopyTexture)
|
||||
{
|
||||
try
|
||||
{
|
||||
Graphics.CopyTexture(source, 0, 0, destination, layerIndex, 0);
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fall back to CPU copy when runtime copy is unavailable for this platform/import setup.
|
||||
}
|
||||
}
|
||||
|
||||
destination.SetPixels(source.GetPixels(), layerIndex, 0);
|
||||
}
|
||||
|
||||
private static bool TryBlitSprite(Texture2D target, Sprite sprite)
|
||||
{
|
||||
try
|
||||
{
|
||||
Texture2D source = sprite.texture;
|
||||
Rect spriteRect = sprite.textureRect;
|
||||
|
||||
for (int y = 0; y < target.height; y++)
|
||||
{
|
||||
float sampleY = Mathf.Lerp(spriteRect.yMin, spriteRect.yMax - 1f, y / (float)Mathf.Max(1, target.height - 1));
|
||||
int sourceY = Mathf.Clamp(Mathf.RoundToInt(sampleY), 0, source.height - 1);
|
||||
|
||||
for (int x = 0; x < target.width; x++)
|
||||
{
|
||||
float sampleX = Mathf.Lerp(spriteRect.xMin, spriteRect.xMax - 1f, x / (float)Mathf.Max(1, target.width - 1));
|
||||
int sourceX = Mathf.Clamp(Mathf.RoundToInt(sampleX), 0, source.width - 1);
|
||||
target.SetPixel(x, y, source.GetPixel(sourceX, sourceY));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (UnityException)
|
||||
{
|
||||
Debug.LogWarning($"Sprite '{sprite.name}' texture is not readable. Falling back to generated tile for voxel biome texture array.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawFallbackSurface(Texture2D texture, RectInt rect, VoxelSurfaceType surfaceType)
|
||||
{
|
||||
Color baseColor;
|
||||
Color accentColor;
|
||||
switch (surfaceType)
|
||||
{
|
||||
case VoxelSurfaceType.CliffTop:
|
||||
baseColor = new Color(0.40f, 0.72f, 0.28f, 1f);
|
||||
accentColor = new Color(0.18f, 0.35f, 0.13f, 1f);
|
||||
break;
|
||||
case VoxelSurfaceType.CliffSide:
|
||||
baseColor = new Color(0.47f, 0.34f, 0.22f, 1f);
|
||||
accentColor = new Color(0.25f, 0.52f, 0.18f, 1f);
|
||||
break;
|
||||
case VoxelSurfaceType.Dirt:
|
||||
baseColor = new Color(0.44f, 0.31f, 0.20f, 1f);
|
||||
accentColor = new Color(0.31f, 0.20f, 0.12f, 1f);
|
||||
break;
|
||||
default:
|
||||
baseColor = new Color(0.28f, 0.58f, 0.25f, 1f);
|
||||
accentColor = new Color(0.18f, 0.40f, 0.16f, 1f);
|
||||
break;
|
||||
}
|
||||
|
||||
FillRect(texture, rect, baseColor);
|
||||
ApplyNoise(texture, rect, 0.06f, 101 + (int)surfaceType * 43);
|
||||
|
||||
if (surfaceType == VoxelSurfaceType.CliffSide)
|
||||
{
|
||||
FillRect(texture, new RectInt(rect.x, rect.yMax - Mathf.Max(4, rect.height / 6), rect.width, Mathf.Max(4, rect.height / 6)), accentColor);
|
||||
}
|
||||
else if (surfaceType == VoxelSurfaceType.CliffTop)
|
||||
{
|
||||
FillRect(texture, new RectInt(rect.x, rect.yMax - Mathf.Max(4, rect.height / 5), rect.width, Mathf.Max(4, rect.height / 5)), accentColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int y = rect.y + 4; y < rect.yMax - 4; y += Mathf.Max(6, rect.height / 4))
|
||||
{
|
||||
for (int x = rect.x + 4; x < rect.xMax - 4; x += Mathf.Max(6, rect.width / 4))
|
||||
{
|
||||
DrawDisc(texture, x, y, 2, accentColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Fill(Texture2D texture, Color color)
|
||||
{
|
||||
Color[] pixels = new Color[texture.width * texture.height];
|
||||
for (int i = 0; i < pixels.Length; i++)
|
||||
{
|
||||
pixels[i] = color;
|
||||
}
|
||||
|
||||
texture.SetPixels(pixels);
|
||||
}
|
||||
|
||||
private static void FillRect(Texture2D texture, RectInt rect, Color color)
|
||||
{
|
||||
for (int y = rect.yMin; y < rect.yMax; y++)
|
||||
{
|
||||
for (int x = rect.xMin; x < rect.xMax; x++)
|
||||
{
|
||||
texture.SetPixel(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawDisc(Texture2D texture, int centerX, int centerY, int radius, Color color)
|
||||
{
|
||||
int sqrRadius = radius * radius;
|
||||
for (int y = -radius; y <= radius; y++)
|
||||
{
|
||||
for (int x = -radius; x <= radius; x++)
|
||||
{
|
||||
if (x * x + y * y > sqrRadius)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
texture.SetPixel(centerX + x, centerY + y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyNoise(Texture2D texture, RectInt rect, float strength, int seed)
|
||||
{
|
||||
for (int y = rect.yMin; y < rect.yMax; y++)
|
||||
{
|
||||
for (int x = rect.xMin; x < rect.xMax; x++)
|
||||
{
|
||||
Color pixel = texture.GetPixel(x, y);
|
||||
float noise = Mathf.PerlinNoise((x + seed) * 0.18f, (y - seed) * 0.18f) - 0.5f;
|
||||
texture.SetPixel(x, y, pixel * (1f + noise * strength * 2f));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 981858f28439459429fa291e1f6cb935
|
||||
@@ -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
|
||||
@@ -0,0 +1,889 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
public sealed partial class VoxelWorldGenerator : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private Transform streamTarget;
|
||||
[SerializeField] private VoxelWorldConfig config;
|
||||
|
||||
private readonly Dictionary<Vector2Int, ChunkRuntime> chunks = new Dictionary<Vector2Int, ChunkRuntime>();
|
||||
private readonly Dictionary<Vector2Int, RegionRuntime> regions = new Dictionary<Vector2Int, RegionRuntime>();
|
||||
private readonly Dictionary<Vector2Int, int> regionVersions = new Dictionary<Vector2Int, int>();
|
||||
private readonly Queue<ChunkBuildResult> completedBuilds = new Queue<ChunkBuildResult>();
|
||||
private readonly Queue<RegionBuildResult> completedRegionBuilds = new Queue<RegionBuildResult>();
|
||||
private readonly Queue<PendingColliderMeshApply> pendingColliderApplies = new Queue<PendingColliderMeshApply>();
|
||||
private readonly Queue<Vector2Int> dirtyChunkMeshes = new Queue<Vector2Int>();
|
||||
private readonly HashSet<Vector2Int> queuedChunkMeshes = new HashSet<Vector2Int>();
|
||||
private readonly Queue<Vector2Int> dirtyRegions = new Queue<Vector2Int>();
|
||||
private readonly HashSet<Vector2Int> queuedRegions = new HashSet<Vector2Int>();
|
||||
private readonly Queue<Vector2Int> pendingNeighborRefreshes = new Queue<Vector2Int>();
|
||||
private readonly HashSet<Vector2Int> queuedNeighborRefreshes = new HashSet<Vector2Int>();
|
||||
private readonly object generationLock = new object();
|
||||
private readonly object regionBuildLock = new object();
|
||||
|
||||
private Transform chunkRoot;
|
||||
private Transform regionRoot;
|
||||
private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue);
|
||||
private int activeGenerationJobs;
|
||||
private int nextChunkRuntimeId;
|
||||
private int generationSession;
|
||||
private int regionRebuildSession;
|
||||
private VoxelWorldAtlas atlas;
|
||||
private int atlasBiomeCount;
|
||||
private bool regionRebuildLoopRunning;
|
||||
private VoxelWorldResolvedSettings settings = VoxelWorldResolvedSettings.Default;
|
||||
|
||||
private int chunkSize => settings.ChunkSize;
|
||||
private int generationRadius => settings.GenerationRadius;
|
||||
private int blockingGenerationRadius => settings.BlockingGenerationRadius;
|
||||
private int seed => settings.Seed;
|
||||
private int maxMountainHeight => settings.MaxMountainHeight;
|
||||
private float macroNoiseScale => settings.MacroNoiseScale;
|
||||
private float detailNoiseScale => settings.DetailNoiseScale;
|
||||
private float ridgeNoiseScale => settings.RidgeNoiseScale;
|
||||
private float wallThreshold => settings.WallThreshold;
|
||||
private float rockBias => settings.RockBias;
|
||||
private int smoothingPasses => settings.SmoothingPasses;
|
||||
private float passNoiseScale => settings.PassNoiseScale;
|
||||
private float passDetailScale => settings.PassDetailScale;
|
||||
private float passThreshold => settings.PassThreshold;
|
||||
private float passFeather => settings.PassFeather;
|
||||
private float heightNoiseScale => settings.HeightNoiseScale;
|
||||
private float terraceNoiseScale => settings.TerraceNoiseScale;
|
||||
private float heightBias => settings.HeightBias;
|
||||
private IReadOnlyList<VoxelBiomeProfile> biomeProfiles => settings.BiomeProfiles;
|
||||
private float biomeNoiseScale => settings.BiomeNoiseScale;
|
||||
private float biomeSize => settings.BiomeSize;
|
||||
private int maxAsyncChunkJobs => settings.MaxAsyncChunkJobs;
|
||||
private int maxChunkBuildsPerFrame => settings.MaxChunkBuildsPerFrame;
|
||||
private int maxChunkMeshBuildsPerFrame => settings.MaxChunkMeshBuildsPerFrame;
|
||||
private int maxColliderAppliesPerFrame => settings.MaxColliderAppliesPerFrame;
|
||||
private int maxNeighborRefreshesPerFrame => settings.MaxNeighborRefreshesPerFrame;
|
||||
private int renderRegionSizeInChunks => settings.RenderRegionSizeInChunks;
|
||||
private int maxRegionBuildsPerFrame => settings.MaxRegionBuildsPerFrame;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
generationSession++;
|
||||
regionRebuildSession++;
|
||||
EnsureRuntimeData();
|
||||
EnsureChunkRoot();
|
||||
EnsureRegionRoot();
|
||||
TryResolveStreamTarget();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
EnsureRuntimeData();
|
||||
EnsureChunkRoot();
|
||||
EnsureRegionRoot();
|
||||
if (!TryResolveStreamTarget())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2Int centerChunk = WorldToChunk(streamTarget.position);
|
||||
if (centerChunk != lastGeneratedCenter)
|
||||
{
|
||||
lastGeneratedCenter = centerChunk;
|
||||
UnloadDistantChunks(centerChunk);
|
||||
}
|
||||
|
||||
DrainCompletedBuilds(maxChunkBuildsPerFrame);
|
||||
DrainDirtyChunkMeshes(maxChunkMeshBuildsPerFrame);
|
||||
DrainPendingColliderApplies(maxColliderAppliesPerFrame);
|
||||
DrainNeighborRefreshes(maxNeighborRefreshesPerFrame);
|
||||
DrainCompletedRegionBuilds(maxRegionBuildsPerFrame);
|
||||
ScheduleChunkGeneration(centerChunk);
|
||||
EnsureBlockingChunksLoaded(centerChunk);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
generationSession++;
|
||||
regionRebuildSession++;
|
||||
lock (generationLock)
|
||||
{
|
||||
completedBuilds.Clear();
|
||||
activeGenerationJobs = 0;
|
||||
}
|
||||
lock (regionBuildLock)
|
||||
{
|
||||
completedRegionBuilds.Clear();
|
||||
}
|
||||
|
||||
CleanupChunks();
|
||||
CleanupRegions();
|
||||
atlas?.Dispose();
|
||||
atlas = null;
|
||||
}
|
||||
|
||||
private void EnsureRuntimeData()
|
||||
{
|
||||
settings = VoxelWorldResolvedSettings.Resolve(config);
|
||||
int configuredBiomeCount = CountConfiguredBiomes();
|
||||
|
||||
if (atlas != null && atlasBiomeCount == configuredBiomeCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
atlas?.Dispose();
|
||||
atlas = VoxelWorldAtlas.CreateRuntimeAtlas(biomeProfiles);
|
||||
atlasBiomeCount = atlas.BiomeCount;
|
||||
RefreshRegionMaterials();
|
||||
}
|
||||
|
||||
private void EnsureChunkRoot()
|
||||
{
|
||||
if (chunkRoot != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Transform existing = transform.Find("VoxelChunks");
|
||||
if (existing != null)
|
||||
{
|
||||
chunkRoot = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject root = new GameObject("VoxelChunks");
|
||||
root.transform.SetParent(transform, false);
|
||||
chunkRoot = root.transform;
|
||||
}
|
||||
|
||||
private void EnsureRegionRoot()
|
||||
{
|
||||
if (regionRoot != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Transform existing = transform.Find("VoxelRenderRegions");
|
||||
if (existing != null)
|
||||
{
|
||||
regionRoot = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject root = new GameObject("VoxelRenderRegions");
|
||||
root.transform.SetParent(transform, false);
|
||||
regionRoot = root.transform;
|
||||
}
|
||||
|
||||
private void RefreshRegionMaterials()
|
||||
{
|
||||
if (atlas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<Vector2Int, RegionRuntime> pair in regions)
|
||||
{
|
||||
if (pair.Value.Renderer != null)
|
||||
{
|
||||
pair.Value.Renderer.sharedMaterial = atlas.Material;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveStreamTarget()
|
||||
{
|
||||
if (streamTarget != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Camera mainCamera = Camera.main;
|
||||
if (mainCamera == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
streamTarget = mainCamera.transform;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ScheduleChunkGeneration(Vector2Int centerChunk)
|
||||
{
|
||||
List<Vector2Int> coords = GetCoordsByPriority(centerChunk, generationRadius);
|
||||
for (int i = 0; i < coords.Count; i++)
|
||||
{
|
||||
Vector2Int coord = coords[i];
|
||||
ChunkRuntime runtime = GetOrCreateChunkRuntime(coord);
|
||||
if (runtime.IsRendered || runtime.HasData || runtime.State == ChunkState.Generating)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsWithinRadius(coord, centerChunk, blockingGenerationRadius))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryReserveGenerationSlot())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
runtime.State = ChunkState.Generating;
|
||||
runtime.Version++;
|
||||
GenerateChunkDataAsync(coord, runtime.Version, generationSession, runtime.RuntimeId).Forget();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureBlockingChunksLoaded(Vector2Int centerChunk)
|
||||
{
|
||||
List<Vector2Int> requiredCoords = GetCoordsByPriority(centerChunk, blockingGenerationRadius);
|
||||
for (int i = 0; i < requiredCoords.Count; i++)
|
||||
{
|
||||
Vector2Int coord = requiredCoords[i];
|
||||
ChunkRuntime runtime = GetOrCreateChunkRuntime(coord);
|
||||
if (runtime.IsRendered || runtime.HasData || runtime.State == ChunkState.Generating || runtime.State == ChunkState.SyncBuilding)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
runtime.Version++;
|
||||
runtime.State = ChunkState.SyncBuilding;
|
||||
if (ApplyBuildResult(GenerateChunkData(coord, runtime.Version, generationSession, runtime.RuntimeId)))
|
||||
{
|
||||
EnqueueChunkMeshBuild(coord);
|
||||
QueueNeighborRefresh(coord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnloadDistantChunks(Vector2Int centerChunk)
|
||||
{
|
||||
List<Vector2Int> coordsToRemove = new List<Vector2Int>();
|
||||
foreach (KeyValuePair<Vector2Int, ChunkRuntime> pair in chunks)
|
||||
{
|
||||
if (IsWithinRadius(pair.Key, centerChunk, generationRadius))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
coordsToRemove.Add(pair.Key);
|
||||
}
|
||||
|
||||
for (int i = 0; i < coordsToRemove.Count; i++)
|
||||
{
|
||||
Vector2Int coord = coordsToRemove[i];
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MarkRegionDirty(coord);
|
||||
chunks.Remove(coord);
|
||||
runtime.Dispose();
|
||||
QueueNeighborRefresh(coord);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2Int WorldToChunk(Vector3 position)
|
||||
{
|
||||
return new Vector2Int(
|
||||
Mathf.FloorToInt(position.x / chunkSize),
|
||||
Mathf.FloorToInt(position.z / chunkSize));
|
||||
}
|
||||
|
||||
private async UniTaskVoid GenerateChunkDataAsync(Vector2Int coord, int version, int session, int runtimeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
ChunkBuildResult result = await UniTask.RunOnThreadPool(() => GenerateChunkData(coord, version, session, runtimeId));
|
||||
lock (generationLock)
|
||||
{
|
||||
if (session == generationSession)
|
||||
{
|
||||
completedBuilds.Enqueue(result);
|
||||
activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
lock (generationLock)
|
||||
{
|
||||
if (session == generationSession)
|
||||
{
|
||||
activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (session == generationSession && chunks.TryGetValue(coord, out ChunkRuntime runtime) && runtime.Version == version && runtime.RuntimeId == runtimeId)
|
||||
{
|
||||
runtime.State = ChunkState.None;
|
||||
}
|
||||
|
||||
Debug.LogException(exception, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainCompletedBuilds(int maxBuilds)
|
||||
{
|
||||
int builds = 0;
|
||||
while (builds < maxBuilds)
|
||||
{
|
||||
ChunkBuildResult result;
|
||||
lock (generationLock)
|
||||
{
|
||||
if (completedBuilds.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
result = completedBuilds.Dequeue();
|
||||
}
|
||||
|
||||
if (!ApplyBuildResult(result))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnqueueChunkMeshBuild(result.Coord);
|
||||
QueueNeighborRefresh(result.Coord);
|
||||
builds++;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainDirtyChunkMeshes(int maxBuilds)
|
||||
{
|
||||
int builds = 0;
|
||||
while (builds < maxBuilds)
|
||||
{
|
||||
if (dirtyChunkMeshes.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Vector2Int coord = dirtyChunkMeshes.Dequeue();
|
||||
queuedChunkMeshes.Remove(coord);
|
||||
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderChunk(coord);
|
||||
builds++;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnqueueChunkMeshBuild(Vector2Int coord)
|
||||
{
|
||||
if (!queuedChunkMeshes.Add(coord))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dirtyChunkMeshes.Enqueue(coord);
|
||||
}
|
||||
|
||||
private void EnqueueColliderApply(Vector2Int coord, int version, int runtimeId, Mesh colliderMesh)
|
||||
{
|
||||
pendingColliderApplies.Enqueue(new PendingColliderMeshApply(coord, version, runtimeId, colliderMesh));
|
||||
}
|
||||
|
||||
private void DrainPendingColliderApplies(int maxApplies)
|
||||
{
|
||||
int applies = 0;
|
||||
while (applies < maxApplies)
|
||||
{
|
||||
if (pendingColliderApplies.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
PendingColliderMeshApply pending = pendingColliderApplies.Dequeue();
|
||||
if (!chunks.TryGetValue(pending.Coord, out ChunkRuntime runtime) || runtime.Version != pending.Version || runtime.RuntimeId != pending.RuntimeId)
|
||||
{
|
||||
DestroyMeshAsset(pending.ColliderMesh);
|
||||
applies++;
|
||||
continue;
|
||||
}
|
||||
|
||||
runtime.ApplyColliderMesh(pending.ColliderMesh);
|
||||
applies++;
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueNeighborRefresh(Vector2Int coord)
|
||||
{
|
||||
if (!queuedNeighborRefreshes.Add(coord))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pendingNeighborRefreshes.Enqueue(coord);
|
||||
}
|
||||
|
||||
private void DrainNeighborRefreshes(int maxRefreshes)
|
||||
{
|
||||
int refreshes = 0;
|
||||
while (refreshes < maxRefreshes)
|
||||
{
|
||||
if (pendingNeighborRefreshes.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Vector2Int coord = pendingNeighborRefreshes.Dequeue();
|
||||
queuedNeighborRefreshes.Remove(coord);
|
||||
RefreshNeighborBorders(coord);
|
||||
refreshes++;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshNeighborBorders(Vector2Int coord)
|
||||
{
|
||||
TryRenderNeighbor(coord + Vector2Int.up);
|
||||
TryRenderNeighbor(coord + Vector2Int.right);
|
||||
TryRenderNeighbor(coord + Vector2Int.down);
|
||||
TryRenderNeighbor(coord + Vector2Int.left);
|
||||
}
|
||||
|
||||
private void TryRenderNeighbor(Vector2Int coord)
|
||||
{
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnqueueChunkMeshBuild(coord);
|
||||
}
|
||||
|
||||
private void MarkRegionDirty(Vector2Int chunkCoord)
|
||||
{
|
||||
Vector2Int regionCoord = ChunkToRegion(chunkCoord);
|
||||
regionVersions.TryGetValue(regionCoord, out int version);
|
||||
regionVersions[regionCoord] = version + 1;
|
||||
|
||||
if (!queuedRegions.Add(regionCoord))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dirtyRegions.Enqueue(regionCoord);
|
||||
EnsureRegionRebuildLoop();
|
||||
}
|
||||
|
||||
private void EnsureRegionRebuildLoop()
|
||||
{
|
||||
if (regionRebuildLoopRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
regionRebuildLoopRunning = true;
|
||||
ProcessDirtyRegionsAsync(regionRebuildSession).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid ProcessDirtyRegionsAsync(int session)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (dirtyRegions.Count > 0)
|
||||
{
|
||||
if (!this || !isActiveAndEnabled || session != regionRebuildSession)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int buildsThisFrame = 0;
|
||||
while (buildsThisFrame < maxRegionBuildsPerFrame && dirtyRegions.Count > 0)
|
||||
{
|
||||
Vector2Int regionCoord = dirtyRegions.Dequeue();
|
||||
queuedRegions.Remove(regionCoord);
|
||||
|
||||
if (!TryCaptureRegionBuildRequest(regionCoord, out RegionBuildRequest request))
|
||||
{
|
||||
buildsThisFrame++;
|
||||
continue;
|
||||
}
|
||||
|
||||
RegionBuildResult result = await UniTask.RunOnThreadPool(() => BuildRegionBuffers(request));
|
||||
lock (regionBuildLock)
|
||||
{
|
||||
completedRegionBuilds.Enqueue(result);
|
||||
}
|
||||
|
||||
buildsThisFrame++;
|
||||
}
|
||||
|
||||
if (dirtyRegions.Count > 0)
|
||||
{
|
||||
await UniTask.Yield(PlayerLoopTiming.Update);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (session == regionRebuildSession)
|
||||
{
|
||||
regionRebuildLoopRunning = false;
|
||||
if (dirtyRegions.Count > 0)
|
||||
{
|
||||
EnsureRegionRebuildLoop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryCaptureRegionBuildRequest(Vector2Int regionCoord, out RegionBuildRequest request)
|
||||
{
|
||||
if (atlas == null || regionRoot == null)
|
||||
{
|
||||
request = new RegionBuildRequest();
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2Int baseChunk = new Vector2Int(regionCoord.x * renderRegionSizeInChunks, regionCoord.y * renderRegionSizeInChunks);
|
||||
List<RegionChunkSnapshot> chunkSnapshots = new List<RegionChunkSnapshot>(renderRegionSizeInChunks * renderRegionSizeInChunks);
|
||||
|
||||
for (int z = 0; z < renderRegionSizeInChunks; z++)
|
||||
{
|
||||
for (int x = 0; x < renderRegionSizeInChunks; x++)
|
||||
{
|
||||
Vector2Int chunkCoord = new Vector2Int(baseChunk.x + x, baseChunk.y + z);
|
||||
if (!chunks.TryGetValue(chunkCoord, out ChunkRuntime runtime) || runtime.RenderSnapshot == null || runtime.RenderSnapshot.IsEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
chunkSnapshots.Add(new RegionChunkSnapshot(runtime.RenderSnapshot, new Vector3(x * chunkSize, 0f, z * chunkSize)));
|
||||
}
|
||||
}
|
||||
|
||||
regionVersions.TryGetValue(regionCoord, out int version);
|
||||
request = new RegionBuildRequest(regionCoord, version, generationSession, chunkSnapshots.ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector2Int ChunkToRegion(Vector2Int chunkCoord)
|
||||
{
|
||||
return new Vector2Int(
|
||||
Mathf.FloorToInt(chunkCoord.x / (float)renderRegionSizeInChunks),
|
||||
Mathf.FloorToInt(chunkCoord.y / (float)renderRegionSizeInChunks));
|
||||
}
|
||||
|
||||
private RegionRuntime GetOrCreateRegionRuntime(Vector2Int regionCoord)
|
||||
{
|
||||
if (!regions.TryGetValue(regionCoord, out RegionRuntime region))
|
||||
{
|
||||
region = new RegionRuntime();
|
||||
regions.Add(regionCoord, region);
|
||||
}
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
private static RegionBuildResult BuildRegionBuffers(RegionBuildRequest request)
|
||||
{
|
||||
if (request.Chunks == null || request.Chunks.Length == 0)
|
||||
{
|
||||
return RegionBuildResult.CreateEmpty(request.RegionCoord, request.Version, request.Session);
|
||||
}
|
||||
|
||||
int totalVertexCount = 0;
|
||||
int totalTriangleCount = 0;
|
||||
for (int i = 0; i < request.Chunks.Length; i++)
|
||||
{
|
||||
ChunkRenderSnapshot snapshot = request.Chunks[i].Snapshot;
|
||||
totalVertexCount += snapshot.Vertices.Length;
|
||||
totalTriangleCount += snapshot.Triangles.Length;
|
||||
}
|
||||
|
||||
if (totalVertexCount == 0 || totalTriangleCount == 0)
|
||||
{
|
||||
return RegionBuildResult.CreateEmpty(request.RegionCoord, request.Version, request.Session);
|
||||
}
|
||||
|
||||
Vector3[] vertices = new Vector3[totalVertexCount];
|
||||
Vector3[] normals = new Vector3[totalVertexCount];
|
||||
Vector2[] uv0 = new Vector2[totalVertexCount];
|
||||
Vector2[] uv1 = new Vector2[totalVertexCount];
|
||||
int[] triangles = new int[totalTriangleCount];
|
||||
|
||||
int vertexOffset = 0;
|
||||
int triangleOffset = 0;
|
||||
Vector3 min = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
|
||||
Vector3 max = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
|
||||
|
||||
for (int chunkIndex = 0; chunkIndex < request.Chunks.Length; chunkIndex++)
|
||||
{
|
||||
RegionChunkSnapshot chunk = request.Chunks[chunkIndex];
|
||||
ChunkRenderSnapshot snapshot = chunk.Snapshot;
|
||||
Vector3 offset = chunk.LocalOffset;
|
||||
|
||||
for (int vertexIndex = 0; vertexIndex < snapshot.Vertices.Length; vertexIndex++)
|
||||
{
|
||||
Vector3 vertex = snapshot.Vertices[vertexIndex] + offset;
|
||||
int writeIndex = vertexOffset + vertexIndex;
|
||||
vertices[writeIndex] = vertex;
|
||||
normals[writeIndex] = snapshot.Normals[vertexIndex];
|
||||
uv0[writeIndex] = snapshot.Uv0[vertexIndex];
|
||||
uv1[writeIndex] = snapshot.Uv1[vertexIndex];
|
||||
|
||||
min = Vector3.Min(min, vertex);
|
||||
max = Vector3.Max(max, vertex);
|
||||
}
|
||||
|
||||
for (int triangleIndex = 0; triangleIndex < snapshot.Triangles.Length; triangleIndex++)
|
||||
{
|
||||
triangles[triangleOffset + triangleIndex] = snapshot.Triangles[triangleIndex] + vertexOffset;
|
||||
}
|
||||
|
||||
vertexOffset += snapshot.Vertices.Length;
|
||||
triangleOffset += snapshot.Triangles.Length;
|
||||
}
|
||||
|
||||
Bounds bounds = new Bounds((min + max) * 0.5f, max - min);
|
||||
return new RegionBuildResult(request.RegionCoord, request.Version, request.Session, vertices, normals, uv0, uv1, triangles, bounds);
|
||||
}
|
||||
|
||||
private void DrainCompletedRegionBuilds(int maxBuilds)
|
||||
{
|
||||
int builds = 0;
|
||||
while (builds < maxBuilds)
|
||||
{
|
||||
RegionBuildResult result;
|
||||
lock (regionBuildLock)
|
||||
{
|
||||
if (completedRegionBuilds.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
result = completedRegionBuilds.Dequeue();
|
||||
}
|
||||
|
||||
ApplyRegionBuildResult(result);
|
||||
builds++;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRegionBuildResult(RegionBuildResult result)
|
||||
{
|
||||
if (result.Session != generationSession || atlas == null || regionRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
regionVersions.TryGetValue(result.RegionCoord, out int currentVersion);
|
||||
if (result.Version != currentVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.IsEmpty)
|
||||
{
|
||||
if (regions.TryGetValue(result.RegionCoord, out RegionRuntime regionToRemove))
|
||||
{
|
||||
regionToRemove.Dispose();
|
||||
regions.Remove(result.RegionCoord);
|
||||
}
|
||||
|
||||
regionVersions.Remove(result.RegionCoord);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
RegionRuntime region = GetOrCreateRegionRuntime(result.RegionCoord);
|
||||
region.EnsureCreated(result.RegionCoord, regionRoot, chunkSize, renderRegionSizeInChunks, atlas.Material);
|
||||
|
||||
Mesh mesh = new Mesh { name = $"Region_{result.RegionCoord.x}_{result.RegionCoord.y}" };
|
||||
mesh.indexFormat = result.Vertices.Length > 65535 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16;
|
||||
mesh.vertices = result.Vertices;
|
||||
mesh.normals = result.Normals;
|
||||
mesh.uv = result.Uv0;
|
||||
mesh.uv2 = result.Uv1;
|
||||
mesh.triangles = result.Triangles;
|
||||
mesh.bounds = result.Bounds;
|
||||
region.ApplyMesh(mesh);
|
||||
}
|
||||
|
||||
private bool ApplyBuildResult(ChunkBuildResult result)
|
||||
{
|
||||
if (result.Session != generationSession)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!chunks.TryGetValue(result.Coord, out ChunkRuntime runtime))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (runtime.Version != result.Version)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (runtime.RuntimeId != result.RuntimeId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
runtime.Heights = result.Heights;
|
||||
runtime.BiomeIndices = result.BiomeIndices;
|
||||
if (!runtime.IsRendered)
|
||||
{
|
||||
runtime.State = ChunkState.ReadyToRender;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryReserveGenerationSlot()
|
||||
{
|
||||
lock (generationLock)
|
||||
{
|
||||
if (activeGenerationJobs >= maxAsyncChunkJobs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
activeGenerationJobs++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Vector2Int> GetCoordsByPriority(Vector2Int centerChunk, int radius)
|
||||
{
|
||||
List<Vector2Int> coords = new List<Vector2Int>((radius * 2 + 1) * (radius * 2 + 1));
|
||||
for (int z = -radius; z <= radius; z++)
|
||||
{
|
||||
for (int x = -radius; x <= radius; x++)
|
||||
{
|
||||
coords.Add(new Vector2Int(centerChunk.x + x, centerChunk.y + z));
|
||||
}
|
||||
}
|
||||
|
||||
coords.Sort((left, right) => CompareChunkPriority(centerChunk, left, right));
|
||||
return coords;
|
||||
}
|
||||
|
||||
private static int CompareChunkPriority(Vector2Int centerChunk, Vector2Int left, Vector2Int right)
|
||||
{
|
||||
int leftDx = Mathf.Abs(left.x - centerChunk.x);
|
||||
int leftDz = Mathf.Abs(left.y - centerChunk.y);
|
||||
int rightDx = Mathf.Abs(right.x - centerChunk.x);
|
||||
int rightDz = Mathf.Abs(right.y - centerChunk.y);
|
||||
|
||||
int leftChebyshev = Mathf.Max(leftDx, leftDz);
|
||||
int rightChebyshev = Mathf.Max(rightDx, rightDz);
|
||||
int chebyshevCompare = leftChebyshev.CompareTo(rightChebyshev);
|
||||
if (chebyshevCompare != 0)
|
||||
{
|
||||
return chebyshevCompare;
|
||||
}
|
||||
|
||||
int leftDistance = leftDx * leftDx + leftDz * leftDz;
|
||||
int rightDistance = rightDx * rightDx + rightDz * rightDz;
|
||||
int distanceCompare = leftDistance.CompareTo(rightDistance);
|
||||
if (distanceCompare != 0)
|
||||
{
|
||||
return distanceCompare;
|
||||
}
|
||||
|
||||
int zCompare = left.y.CompareTo(right.y);
|
||||
return zCompare != 0 ? zCompare : left.x.CompareTo(right.x);
|
||||
}
|
||||
|
||||
private ChunkRuntime GetOrCreateChunkRuntime(Vector2Int coord)
|
||||
{
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime))
|
||||
{
|
||||
runtime = new ChunkRuntime();
|
||||
runtime.RuntimeId = ++nextChunkRuntimeId;
|
||||
chunks.Add(coord, runtime);
|
||||
}
|
||||
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private static bool IsWithinRadius(Vector2Int coord, Vector2Int centerChunk, int radius)
|
||||
{
|
||||
int dx = Mathf.Abs(coord.x - centerChunk.x);
|
||||
int dz = Mathf.Abs(coord.y - centerChunk.y);
|
||||
return dx <= radius && dz <= radius;
|
||||
}
|
||||
|
||||
private void CleanupChunks()
|
||||
{
|
||||
foreach (KeyValuePair<Vector2Int, ChunkRuntime> pair in chunks)
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
|
||||
chunks.Clear();
|
||||
dirtyChunkMeshes.Clear();
|
||||
queuedChunkMeshes.Clear();
|
||||
pendingNeighborRefreshes.Clear();
|
||||
queuedNeighborRefreshes.Clear();
|
||||
while (pendingColliderApplies.Count > 0)
|
||||
{
|
||||
DestroyMeshAsset(pendingColliderApplies.Dequeue().ColliderMesh);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DestroyMeshAsset(Mesh mesh)
|
||||
{
|
||||
if (mesh == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEngine.Object.Destroy(mesh);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupRegions()
|
||||
{
|
||||
foreach (KeyValuePair<Vector2Int, RegionRuntime> pair in regions)
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
|
||||
regions.Clear();
|
||||
regionVersions.Clear();
|
||||
dirtyRegions.Clear();
|
||||
queuedRegions.Clear();
|
||||
lock (regionBuildLock)
|
||||
{
|
||||
completedRegionBuilds.Clear();
|
||||
}
|
||||
regionRebuildLoopRunning = false;
|
||||
}
|
||||
|
||||
private int CountConfiguredBiomes()
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < biomeProfiles.Count; i++)
|
||||
{
|
||||
if (biomeProfiles[i] != null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return Mathf.Max(1, count);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62cd563780165844caddc098f92ff23f
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 109e9bdd0fcc649c5bc59085c35bdc83
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,438 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!29 &1
|
||||
OcclusionCullingSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_OcclusionBakeSettings:
|
||||
smallestOccluder: 5
|
||||
smallestHole: 0.25
|
||||
backfaceThreshold: 100
|
||||
m_SceneGUID: 00000000000000000000000000000000
|
||||
m_OcclusionCullingData: {fileID: 0}
|
||||
--- !u!104 &2
|
||||
RenderSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 10
|
||||
m_Fog: 0
|
||||
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||
m_FogMode: 3
|
||||
m_FogDensity: 0.01
|
||||
m_LinearFogStart: 0
|
||||
m_LinearFogEnd: 300
|
||||
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
|
||||
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
|
||||
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
|
||||
m_AmbientIntensity: 1
|
||||
m_AmbientMode: 3
|
||||
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
|
||||
m_SkyboxMaterial: {fileID: 0}
|
||||
m_HaloStrength: 0.5
|
||||
m_FlareStrength: 1
|
||||
m_FlareFadeSpeed: 3
|
||||
m_HaloTexture: {fileID: 0}
|
||||
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_DefaultReflectionMode: 0
|
||||
m_DefaultReflectionResolution: 128
|
||||
m_ReflectionBounces: 1
|
||||
m_ReflectionIntensity: 1
|
||||
m_CustomReflection: {fileID: 0}
|
||||
m_Sun: {fileID: 0}
|
||||
m_UseRadianceAmbientProbe: 0
|
||||
--- !u!157 &3
|
||||
LightmapSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 13
|
||||
m_BakeOnSceneLoad: 0
|
||||
m_GISettings:
|
||||
serializedVersion: 2
|
||||
m_BounceScale: 1
|
||||
m_IndirectOutputScale: 1
|
||||
m_AlbedoBoost: 1
|
||||
m_EnvironmentLightingMode: 0
|
||||
m_EnableBakedLightmaps: 0
|
||||
m_EnableRealtimeLightmaps: 0
|
||||
m_LightmapEditorSettings:
|
||||
serializedVersion: 12
|
||||
m_Resolution: 2
|
||||
m_BakeResolution: 40
|
||||
m_AtlasSize: 1024
|
||||
m_AO: 0
|
||||
m_AOMaxDistance: 1
|
||||
m_CompAOExponent: 1
|
||||
m_CompAOExponentDirect: 0
|
||||
m_ExtractAmbientOcclusion: 0
|
||||
m_Padding: 2
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_LightmapsBakeMode: 1
|
||||
m_TextureCompression: 1
|
||||
m_ReflectionCompression: 2
|
||||
m_MixedBakeMode: 2
|
||||
m_BakeBackend: 2
|
||||
m_PVRSampling: 1
|
||||
m_PVRDirectSampleCount: 32
|
||||
m_PVRSampleCount: 512
|
||||
m_PVRBounces: 2
|
||||
m_PVREnvironmentSampleCount: 256
|
||||
m_PVREnvironmentReferencePointCount: 2048
|
||||
m_PVRFilteringMode: 1
|
||||
m_PVRDenoiserTypeDirect: 1
|
||||
m_PVRDenoiserTypeIndirect: 1
|
||||
m_PVRDenoiserTypeAO: 1
|
||||
m_PVRFilterTypeDirect: 0
|
||||
m_PVRFilterTypeIndirect: 0
|
||||
m_PVRFilterTypeAO: 0
|
||||
m_PVREnvironmentMIS: 1
|
||||
m_PVRCulling: 1
|
||||
m_PVRFilteringGaussRadiusDirect: 1
|
||||
m_PVRFilteringGaussRadiusIndirect: 1
|
||||
m_PVRFilteringGaussRadiusAO: 1
|
||||
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
|
||||
m_PVRFilteringAtrousPositionSigmaIndirect: 2
|
||||
m_PVRFilteringAtrousPositionSigmaAO: 1
|
||||
m_ExportTrainingData: 0
|
||||
m_TrainingDataDestination: TrainingData
|
||||
m_LightProbeSampleCountMultiplier: 4
|
||||
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_LightingSettings: {fileID: 0}
|
||||
--- !u!196 &4
|
||||
NavMeshSettings:
|
||||
serializedVersion: 2
|
||||
m_ObjectHideFlags: 0
|
||||
m_BuildSettings:
|
||||
serializedVersion: 3
|
||||
agentTypeID: 0
|
||||
agentRadius: 0.5
|
||||
agentHeight: 2
|
||||
agentSlope: 45
|
||||
agentClimb: 0.4
|
||||
ledgeDropHeight: 0
|
||||
maxJumpAcrossDistance: 0
|
||||
minRegionArea: 2
|
||||
manualCellSize: 0
|
||||
cellSize: 0.16666667
|
||||
manualTileSize: 0
|
||||
tileSize: 256
|
||||
buildHeightMesh: 0
|
||||
maxJobWorkers: 0
|
||||
preserveTilesOutsideBounds: 0
|
||||
debug:
|
||||
m_Flags: 0
|
||||
m_NavMeshData: {fileID: 0}
|
||||
--- !u!1 &47249969
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 47249972}
|
||||
- component: {fileID: 47249971}
|
||||
- component: {fileID: 47249970}
|
||||
m_Layer: 0
|
||||
m_Name: Directional Light
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &47249970
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 47249969}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalLightData
|
||||
m_UsePipelineSettings: 1
|
||||
m_AdditionalLightsShadowResolutionTier: 2
|
||||
m_CustomShadowLayers: 0
|
||||
m_LightCookieSize: {x: 1, y: 1}
|
||||
m_LightCookieOffset: {x: 0, y: 0}
|
||||
m_SoftShadowQuality: 0
|
||||
m_RenderingLayersMask:
|
||||
serializedVersion: 0
|
||||
m_Bits: 1
|
||||
m_ShadowRenderingLayersMask:
|
||||
serializedVersion: 0
|
||||
m_Bits: 1
|
||||
m_Version: 4
|
||||
m_LightLayerMask: 1
|
||||
m_ShadowLayerMask: 1
|
||||
m_RenderingLayers: 1
|
||||
m_ShadowRenderingLayers: 1
|
||||
--- !u!108 &47249971
|
||||
Light:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 47249969}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 12
|
||||
m_Type: 1
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Intensity: 1
|
||||
m_Range: 10
|
||||
m_SpotAngle: 30
|
||||
m_InnerSpotAngle: 21.80208
|
||||
m_CookieSize2D: {x: 0.5, y: 0.5}
|
||||
m_Shadows:
|
||||
m_Type: 0
|
||||
m_Resolution: -1
|
||||
m_CustomResolution: -1
|
||||
m_Strength: 1
|
||||
m_Bias: 0.05
|
||||
m_NormalBias: 0.4
|
||||
m_NearPlane: 0.2
|
||||
m_CullingMatrixOverride:
|
||||
e00: 1
|
||||
e01: 0
|
||||
e02: 0
|
||||
e03: 0
|
||||
e10: 0
|
||||
e11: 1
|
||||
e12: 0
|
||||
e13: 0
|
||||
e20: 0
|
||||
e21: 0
|
||||
e22: 1
|
||||
e23: 0
|
||||
e30: 0
|
||||
e31: 0
|
||||
e32: 0
|
||||
e33: 1
|
||||
m_UseCullingMatrixOverride: 0
|
||||
m_Cookie: {fileID: 0}
|
||||
m_DrawHalo: 0
|
||||
m_Flare: {fileID: 0}
|
||||
m_RenderMode: 0
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_RenderingLayerMask: 1
|
||||
m_Lightmapping: 4
|
||||
m_LightShadowCasterMode: 0
|
||||
m_AreaSize: {x: 1, y: 1}
|
||||
m_BounceIntensity: 1
|
||||
m_ColorTemperature: 6570
|
||||
m_UseColorTemperature: 0
|
||||
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_UseBoundingSphereOverride: 0
|
||||
m_UseViewFrustumForShadowCasterCull: 1
|
||||
m_ForceVisible: 0
|
||||
m_ShadowRadius: 0
|
||||
m_ShadowAngle: 0
|
||||
m_LightUnit: 1
|
||||
m_LuxAtDistance: 1
|
||||
m_EnableSpotReflector: 1
|
||||
--- !u!4 &47249972
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 47249969}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.40821794, y: -0.23456973, z: 0.10938166, w: 0.8754261}
|
||||
m_LocalPosition: {x: 0, y: 8, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1331065945
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1331065949}
|
||||
- component: {fileID: 1331065948}
|
||||
- component: {fileID: 1331065947}
|
||||
- component: {fileID: 1331065946}
|
||||
m_Layer: 0
|
||||
m_Name: Main Camera
|
||||
m_TagString: MainCamera
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1331065946
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1331065945}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData
|
||||
m_RenderShadows: 1
|
||||
m_RequiresDepthTextureOption: 2
|
||||
m_RequiresOpaqueTextureOption: 2
|
||||
m_CameraType: 0
|
||||
m_Cameras: []
|
||||
m_RendererIndex: -1
|
||||
m_VolumeLayerMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 1
|
||||
m_VolumeTrigger: {fileID: 0}
|
||||
m_VolumeFrameworkUpdateModeOption: 2
|
||||
m_RenderPostProcessing: 0
|
||||
m_Antialiasing: 0
|
||||
m_AntialiasingQuality: 2
|
||||
m_StopNaN: 0
|
||||
m_Dithering: 0
|
||||
m_ClearDepth: 1
|
||||
m_AllowXRRendering: 1
|
||||
m_AllowHDROutput: 1
|
||||
m_UseScreenCoordOverride: 0
|
||||
m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_RequiresDepthTexture: 0
|
||||
m_RequiresColorTexture: 0
|
||||
m_TaaSettings:
|
||||
m_Quality: 3
|
||||
m_FrameInfluence: 0.1
|
||||
m_JitterScale: 1
|
||||
m_MipBias: 0
|
||||
m_VarianceClampScale: 0.9
|
||||
m_ContrastAdaptiveSharpening: 0
|
||||
m_Version: 2
|
||||
--- !u!81 &1331065947
|
||||
AudioListener:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1331065945}
|
||||
m_Enabled: 1
|
||||
--- !u!20 &1331065948
|
||||
Camera:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1331065945}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 2
|
||||
m_ClearFlags: 1
|
||||
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
|
||||
m_projectionMatrixMode: 1
|
||||
m_GateFitMode: 2
|
||||
m_FOVAxisMode: 0
|
||||
m_Iso: 200
|
||||
m_ShutterSpeed: 0.005
|
||||
m_Aperture: 16
|
||||
m_FocusDistance: 10
|
||||
m_FocalLength: 50
|
||||
m_BladeCount: 5
|
||||
m_Curvature: {x: 2, y: 11}
|
||||
m_BarrelClipping: 0.25
|
||||
m_Anamorphism: 0
|
||||
m_SensorSize: {x: 36, y: 24}
|
||||
m_LensShift: {x: 0, y: 0}
|
||||
m_NormalizedViewPortRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 1
|
||||
height: 1
|
||||
near clip plane: 0.3
|
||||
far clip plane: 300
|
||||
field of view: 60
|
||||
orthographic: 0
|
||||
orthographic size: 5
|
||||
m_Depth: 0
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_RenderingPath: -1
|
||||
m_TargetTexture: {fileID: 0}
|
||||
m_TargetDisplay: 0
|
||||
m_TargetEye: 3
|
||||
m_HDR: 1
|
||||
m_AllowMSAA: 1
|
||||
m_AllowDynamicResolution: 0
|
||||
m_ForceIntoRT: 0
|
||||
m_OcclusionCulling: 1
|
||||
m_StereoConvergence: 10
|
||||
m_StereoSeparation: 0.022
|
||||
--- !u!4 &1331065949
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1331065945}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.2778159, y: 0.3649717, z: -0.115075134, w: 0.88111955}
|
||||
m_LocalPosition: {x: 8, y: 12, z: -10}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1842209026
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1842209028}
|
||||
- component: {fileID: 1842209027}
|
||||
m_Layer: 0
|
||||
m_Name: VoxelWorld
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1842209027
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1842209026}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 62cd563780165844caddc098f92ff23f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: VoxelWorld.Runtime::InfiniteWorld.VoxelWorld.VoxelWorldGenerator
|
||||
streamTarget: {fileID: 1331065949}
|
||||
config: {fileID: 11400000, guid: b8cf28a5522134b479c23f017234070c, type: 2}
|
||||
--- !u!4 &1842209028
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1842209026}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
m_Roots:
|
||||
- {fileID: 1331065949}
|
||||
- {fileID: 47249972}
|
||||
- {fileID: 1842209028}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0ae584ed532105449545cd0c38404e5
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d3a1e4bd82462b4790722c7ebbf2e17
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,70 @@
|
||||
Shader "Infinite World/VoxelWorld/TextureArrayUnlit"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
_TextureArray("Texture Array", 2DArray) = "white" {}
|
||||
_BaseColor("Base Color", Color) = (1, 1, 1, 1)
|
||||
}
|
||||
|
||||
SubShader
|
||||
{
|
||||
Tags { "RenderType"="Opaque" "Queue"="Geometry" "RenderPipeline"="UniversalPipeline" }
|
||||
LOD 100
|
||||
|
||||
Pass
|
||||
{
|
||||
Name "Forward"
|
||||
Tags { "LightMode"="SRPDefaultUnlit" }
|
||||
Cull Back
|
||||
ZWrite On
|
||||
|
||||
HLSLPROGRAM
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag
|
||||
#pragma target 3.5
|
||||
#pragma require 2darray
|
||||
|
||||
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
|
||||
|
||||
TEXTURE2D_ARRAY(_TextureArray);
|
||||
SAMPLER(sampler_TextureArray);
|
||||
|
||||
CBUFFER_START(UnityPerMaterial)
|
||||
half4 _BaseColor;
|
||||
CBUFFER_END
|
||||
|
||||
struct Attributes
|
||||
{
|
||||
float4 positionOS : POSITION;
|
||||
float3 normalOS : NORMAL;
|
||||
float2 uv : TEXCOORD0;
|
||||
float2 textureData : TEXCOORD1;
|
||||
};
|
||||
|
||||
struct Varyings
|
||||
{
|
||||
float4 positionHCS : SV_POSITION;
|
||||
float2 uv : TEXCOORD0;
|
||||
float textureLayer : TEXCOORD1;
|
||||
};
|
||||
|
||||
Varyings vert(Attributes input)
|
||||
{
|
||||
Varyings output;
|
||||
VertexPositionInputs positionInputs = GetVertexPositionInputs(input.positionOS.xyz);
|
||||
output.positionHCS = positionInputs.positionCS;
|
||||
output.uv = input.uv;
|
||||
output.textureLayer = input.textureData.x;
|
||||
return output;
|
||||
}
|
||||
|
||||
half4 frag(Varyings input) : SV_Target
|
||||
{
|
||||
float2 tiledUv = frac(input.uv);
|
||||
half4 albedo = SAMPLE_TEXTURE2D_ARRAY(_TextureArray, sampler_TextureArray, tiledUv, input.textureLayer);
|
||||
return albedo * _BaseColor;
|
||||
}
|
||||
ENDHLSL
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec80aebd8cb61f44cbfa6b7d5f087211
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user