[Fix] Update Voxel World

This commit is contained in:
2026-03-31 11:30:35 +07:00
parent 097a86f40b
commit fa36c49583
10 changed files with 586 additions and 141 deletions
+3 -3
View File
@@ -414,10 +414,10 @@ MonoBehaviour:
m_EditorClassIdentifier: VoxelWorld.Runtime::InfiniteWorld.VoxelWorld.VoxelWorldGenerator m_EditorClassIdentifier: VoxelWorld.Runtime::InfiniteWorld.VoxelWorld.VoxelWorldGenerator
streamTarget: {fileID: 1331065949} streamTarget: {fileID: 1331065949}
chunkSize: 16 chunkSize: 16
generationRadius: 4 generationRadius: 7
blockingGenerationRadius: 1 blockingGenerationRadius: 1
seed: 12345 seed: 12345
maxMountainHeight: 5 maxMountainHeight: 12
macroNoiseScale: 0.05 macroNoiseScale: 0.05
detailNoiseScale: 0.12 detailNoiseScale: 0.12
ridgeNoiseScale: 0.18 ridgeNoiseScale: 0.18
@@ -436,7 +436,7 @@ MonoBehaviour:
- {fileID: 11400000, guid: eac8d825dd62e1c439235d273a4ca613, type: 2} - {fileID: 11400000, guid: eac8d825dd62e1c439235d273a4ca613, type: 2}
- {fileID: 11400000, guid: 6d0dbe510ed048440a3925ef40aeeb5b, type: 2} - {fileID: 11400000, guid: 6d0dbe510ed048440a3925ef40aeeb5b, type: 2}
biomeNoiseScale: 0.02 biomeNoiseScale: 0.02
biomeSize: 12 biomeSize: 4
maxAsyncChunkJobs: 8 maxAsyncChunkJobs: 8
maxChunkBuildsPerFrame: 2 maxChunkBuildsPerFrame: 2
--- !u!4 &1842209028 --- !u!4 &1842209028
@@ -41,6 +41,8 @@ namespace InfiniteWorld.VoxelWorld
internal sealed class VoxelWorldAtlas : IDisposable internal sealed class VoxelWorldAtlas : IDisposable
{ {
private const int FallbackTileSize = 32; private const int FallbackTileSize = 32;
private const string ShaderName = "Infinite World/VoxelWorld/TextureArrayUnlit";
private static readonly VoxelSurfaceType[] SurfaceOrder = private static readonly VoxelSurfaceType[] SurfaceOrder =
{ {
VoxelSurfaceType.CliffTop, VoxelSurfaceType.CliffTop,
@@ -49,29 +51,29 @@ namespace InfiniteWorld.VoxelWorld
VoxelSurfaceType.WalkableSurface VoxelSurfaceType.WalkableSurface
}; };
private readonly Dictionary<int, Dictionary<VoxelSurfaceType, Rect>> uvLookup; private readonly Dictionary<int, Dictionary<VoxelSurfaceType, int>> layerLookup;
public VoxelWorldAtlas(Texture2D texture, Material material, Dictionary<int, Dictionary<VoxelSurfaceType, Rect>> uvRects) private VoxelWorldAtlas(Texture2DArray textureArray, Material material, Dictionary<int, Dictionary<VoxelSurfaceType, int>> layers)
{ {
Texture = texture; TextureArray = textureArray;
Material = material; Material = material;
uvLookup = uvRects; layerLookup = layers;
} }
public Texture2D Texture { get; } public Texture2DArray TextureArray { get; }
public Material Material { get; } public Material Material { get; }
public int BiomeCount => uvLookup.Count; public int BiomeCount => layerLookup.Count;
public Rect GetUvRect(int biomeIndex, VoxelSurfaceType surfaceType) public int GetTextureLayer(int biomeIndex, VoxelSurfaceType surfaceType)
{ {
if (uvLookup.TryGetValue(biomeIndex, out Dictionary<VoxelSurfaceType, Rect> biomeRects) && biomeRects.TryGetValue(surfaceType, out Rect rect)) if (layerLookup.TryGetValue(biomeIndex, out Dictionary<VoxelSurfaceType, int> biomeLayers) && biomeLayers.TryGetValue(surfaceType, out int layer))
{ {
return rect; return layer;
} }
return uvLookup[0][surfaceType]; return layerLookup[0][surfaceType];
} }
public static VoxelWorldAtlas CreateRuntimeAtlas(IReadOnlyList<VoxelBiomeProfile> biomeProfiles) public static VoxelWorldAtlas CreateRuntimeAtlas(IReadOnlyList<VoxelBiomeProfile> biomeProfiles)
@@ -94,48 +96,60 @@ namespace InfiniteWorld.VoxelWorld
} }
int tileSize = DetermineTileSize(sourceBiomes); int tileSize = DetermineTileSize(sourceBiomes);
Texture2D texture = new Texture2D(tileSize * SurfaceOrder.Length, tileSize * sourceBiomes.Count, TextureFormat.RGBA32, false) int layerCount = sourceBiomes.Count * SurfaceOrder.Length;
Texture2DArray textureArray = new Texture2DArray(tileSize, tileSize, layerCount, TextureFormat.RGBA32, false, false)
{ {
filterMode = FilterMode.Point, filterMode = FilterMode.Point,
wrapMode = TextureWrapMode.Clamp, wrapMode = TextureWrapMode.Repeat,
name = "VoxelWorld_RuntimeAtlas", anisoLevel = 1,
name = "VoxelWorld_RuntimeTextureArray",
hideFlags = HideFlags.HideAndDontSave hideFlags = HideFlags.HideAndDontSave
}; };
Fill(texture, new Color(1f, 0f, 1f, 1f)); Dictionary<int, Dictionary<VoxelSurfaceType, int>> layers = new Dictionary<int, Dictionary<VoxelSurfaceType, int>>(sourceBiomes.Count);
int layerIndex = 0;
Dictionary<int, Dictionary<VoxelSurfaceType, Rect>> uvRects = new Dictionary<int, Dictionary<VoxelSurfaceType, Rect>>(sourceBiomes.Count);
for (int biomeIndex = 0; biomeIndex < sourceBiomes.Count; biomeIndex++) for (int biomeIndex = 0; biomeIndex < sourceBiomes.Count; biomeIndex++)
{ {
Dictionary<VoxelSurfaceType, Rect> biomeRects = new Dictionary<VoxelSurfaceType, Rect>(SurfaceOrder.Length); Dictionary<VoxelSurfaceType, int> biomeLayers = new Dictionary<VoxelSurfaceType, int>(SurfaceOrder.Length);
uvRects[biomeIndex] = biomeRects; layers[biomeIndex] = biomeLayers;
for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++) for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++)
{ {
VoxelSurfaceType surfaceType = SurfaceOrder[surfaceIndex]; VoxelSurfaceType surfaceType = SurfaceOrder[surfaceIndex];
RectInt rect = new RectInt(surfaceIndex * tileSize, biomeIndex * tileSize, tileSize, tileSize); Texture2D tileTexture = BuildSurfaceTexture(sourceBiomes[biomeIndex], surfaceType, tileSize);
DrawSurfaceTile(texture, rect, sourceBiomes[biomeIndex], surfaceType, tileSize); CopyTextureToArrayLayer(tileTexture, textureArray, layerIndex);
biomeRects[surfaceType] = BuildUvRect(rect, texture.width, texture.height); biomeLayers[surfaceType] = layerIndex;
layerIndex++;
if (Application.isPlaying)
{
UnityEngine.Object.Destroy(tileTexture);
}
else
{
UnityEngine.Object.DestroyImmediate(tileTexture);
}
} }
} }
texture.Apply(false, false); 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.");
}
Shader shader = Shader.Find("Universal Render Pipeline/Unlit") ?? Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
Material material = new Material(shader) Material material = new Material(shader)
{ {
name = "VoxelWorld_RuntimeMaterial", name = "VoxelWorld_RuntimeMaterial",
hideFlags = HideFlags.HideAndDontSave hideFlags = HideFlags.HideAndDontSave
}; };
if (material.HasProperty("_BaseMap")) if (material.HasProperty("_TextureArray"))
{ {
material.SetTexture("_BaseMap", texture); material.SetTexture("_TextureArray", textureArray);
}
if (material.HasProperty("_MainTex"))
{
material.SetTexture("_MainTex", texture);
} }
if (material.HasProperty("_BaseColor")) if (material.HasProperty("_BaseColor"))
@@ -148,19 +162,19 @@ namespace InfiniteWorld.VoxelWorld
material.SetColor("_Color", Color.white); material.SetColor("_Color", Color.white);
} }
return new VoxelWorldAtlas(texture, material, uvRects); return new VoxelWorldAtlas(textureArray, material, layers);
} }
public void Dispose() public void Dispose()
{ {
if (Application.isPlaying) if (Application.isPlaying)
{ {
UnityEngine.Object.Destroy(Texture); UnityEngine.Object.Destroy(TextureArray);
UnityEngine.Object.Destroy(Material); UnityEngine.Object.Destroy(Material);
} }
else else
{ {
UnityEngine.Object.DestroyImmediate(Texture); UnityEngine.Object.DestroyImmediate(TextureArray);
UnityEngine.Object.DestroyImmediate(Material); UnityEngine.Object.DestroyImmediate(Material);
} }
} }
@@ -187,45 +201,66 @@ namespace InfiniteWorld.VoxelWorld
return Mathf.Max(FallbackTileSize, tileSize); return Mathf.Max(FallbackTileSize, tileSize);
} }
private static Rect BuildUvRect(RectInt rect, int textureWidth, int textureHeight) private static Texture2D BuildSurfaceTexture(VoxelBiomeProfile biome, VoxelSurfaceType surfaceType, int tileSize)
{ {
float paddingX = 0.5f / textureWidth; Texture2D texture = new Texture2D(tileSize, tileSize, TextureFormat.RGBA32, false)
float paddingY = 0.5f / textureHeight;
return new Rect(
rect.xMin / (float)textureWidth + paddingX,
rect.yMin / (float)textureHeight + paddingY,
rect.width / (float)textureWidth - paddingX * 2f,
rect.height / (float)textureHeight - paddingY * 2f);
}
private static void DrawSurfaceTile(Texture2D atlas, RectInt targetRect, VoxelBiomeProfile biome, VoxelSurfaceType surfaceType, int tileSize)
{
Sprite sprite = biome != null ? biome.GetSprite(surfaceType) : null;
if (sprite != null && TryBlitSprite(atlas, targetRect, sprite))
{ {
return; 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);
} }
DrawFallbackSurface(atlas, targetRect, surfaceType); texture.Apply(false, false);
return texture;
} }
private static bool TryBlitSprite(Texture2D atlas, RectInt targetRect, Sprite sprite) 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 try
{ {
Texture2D source = sprite.texture; Texture2D source = sprite.texture;
Rect spriteRect = sprite.textureRect; Rect spriteRect = sprite.textureRect;
for (int y = 0; y < targetRect.height; y++) for (int y = 0; y < target.height; y++)
{ {
float sampleY = Mathf.Lerp(spriteRect.yMin, spriteRect.yMax - 1f, y / (float)Mathf.Max(1, targetRect.height - 1)); 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); int sourceY = Mathf.Clamp(Mathf.RoundToInt(sampleY), 0, source.height - 1);
for (int x = 0; x < targetRect.width; x++) for (int x = 0; x < target.width; x++)
{ {
float sampleX = Mathf.Lerp(spriteRect.xMin, spriteRect.xMax - 1f, x / (float)Mathf.Max(1, targetRect.width - 1)); 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); int sourceX = Mathf.Clamp(Mathf.RoundToInt(sampleX), 0, source.width - 1);
atlas.SetPixel(targetRect.x + x, targetRect.y + y, source.GetPixel(sourceX, sourceY)); target.SetPixel(x, y, source.GetPixel(sourceX, sourceY));
} }
} }
@@ -233,7 +268,7 @@ namespace InfiniteWorld.VoxelWorld
} }
catch (UnityException) catch (UnityException)
{ {
Debug.LogWarning($"Sprite '{sprite.name}' texture is not readable. Falling back to generated tile for voxel biome atlas."); Debug.LogWarning($"Sprite '{sprite.name}' texture is not readable. Falling back to generated tile for voxel biome texture array.");
return false; return false;
} }
} }
@@ -400,38 +400,136 @@ namespace InfiniteWorld.VoxelWorld
private void BuildGroundSurface(int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers) private void BuildGroundSurface(int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers)
{ {
bool[,] visited = new bool[chunkSize, chunkSize];
for (int z = 0; z < chunkSize; z++) for (int z = 0; z < chunkSize; z++)
{ {
for (int x = 0; x < chunkSize; x++) for (int x = 0; x < chunkSize; x++)
{ {
if (heights[z * chunkSize + x] > 0) int index = z * chunkSize + x;
if (visited[x, z] || heights[index] > 0)
{ {
continue; continue;
} }
int index = z * chunkSize + x; byte biomeIndex = biomeIndices[index];
AddTopQuad(renderBuffers, x, z, 1f, 1f, 0f, atlas.GetUvRect(biomeIndices[index], VoxelSurfaceType.WalkableSurface)); 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) 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 z = 0; z < chunkSize; z++)
{ {
for (int x = 0; x < chunkSize; x++) for (int x = 0; x < chunkSize; x++)
{ {
int height = heights[z * chunkSize + x]; int height = heights[z * chunkSize + x];
if (height <= 0) if (height <= 0 || visited[x, z])
{ {
continue; continue;
} }
int index = z * chunkSize + x; int index = z * chunkSize + x;
VoxelSurfaceType surfaceType = IsCliffTop(ChunkToWorldCell(coord, x, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface; VoxelSurfaceType surfaceType = IsCliffTop(ChunkToWorldCell(coord, x, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
Rect uvRect = atlas.GetUvRect(biomeIndices[index], surfaceType); byte biomeIndex = biomeIndices[index];
AddTopQuad(renderBuffers, x, z, 1f, 1f, height, uvRect);
AddTopQuad(colliderBuffers, x, z, 1f, 1f, height, Rect.zero, false); 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);
} }
} }
} }
@@ -440,104 +538,210 @@ namespace InfiniteWorld.VoxelWorld
{ {
for (int z = 0; z < chunkSize; z++) for (int z = 0; z < chunkSize; z++)
{ {
for (int x = 0; x < chunkSize; x++) 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++)
{ {
int current = heights[z * chunkSize + x]; if (visited[x, y] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x, z, y, north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer))
if (current <= 0)
{ {
continue; continue;
} }
int worldX = coord.x * chunkSize + x; int width = 1;
int worldZ = coord.y * chunkSize + z; 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)
byte biomeIndex = biomeIndices[z * chunkSize + x]; {
width++;
}
AddNorthSouthFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX, worldZ + 1)), biomeIndex, true); int height = 1;
AddNorthSouthFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX, worldZ - 1)), biomeIndex, false); bool canGrow = true;
AddEastWestFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX + 1, worldZ)), biomeIndex, true); while (y + height < maxMountainHeight && canGrow)
AddEastWestFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX - 1, worldZ)), biomeIndex, false); {
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 AddNorthSouthFace(MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, int z, int current, int neighbor, byte biomeIndex, bool north) private void BuildEastWestFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, bool east)
{ {
if (current <= neighbor) bool[,] visited = new bool[chunkSize, maxMountainHeight];
for (int z = 0; z < chunkSize; z++)
{ {
return; for (int y = 0; y < maxMountainHeight; y++)
}
int bottom = Mathf.Min(current, neighbor);
float faceZ = north ? z + 1f : z;
for (int y = bottom; y < current; y++)
{
VoxelSurfaceType surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
Rect uvRect = atlas.GetUvRect(biomeIndex, surfaceType);
Vector3 bl = new Vector3(x, y, faceZ);
Vector3 br = new Vector3(x + 1f, y, faceZ);
Vector3 tr = new Vector3(x + 1f, y + 1f, faceZ);
Vector3 tl = new Vector3(x, y + 1f, faceZ);
if (north)
{ {
AddVerticalQuad(renderBuffers, bl, br, tr, tl, uvRect, 1f, 1f); if (visited[z, y] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z, y, east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer))
AddVerticalQuad(colliderBuffers, bl, br, tr, tl, Rect.zero, 1f, 1f, false); {
} continue;
else }
{
AddVerticalQuad(renderBuffers, br, bl, tl, tr, uvRect, 1f, 1f); int width = 1;
AddVerticalQuad(colliderBuffers, br, bl, tl, tr, Rect.zero, 1f, 1f, false); 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 void AddEastWestFace(MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, int z, int current, int neighbor, byte biomeIndex, bool east) 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)
{ {
if (current <= neighbor) 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)
{ {
return; biomeIndex = 0;
surfaceType = VoxelSurfaceType.Dirt;
textureLayer = 0;
return false;
} }
int bottom = Mathf.Min(current, neighbor); biomeIndex = biomeIndices[z * chunkSize + x];
float faceX = east ? x + 1f : x; surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
for (int y = bottom; y < current; y++) return true;
{
VoxelSurfaceType surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
Rect uvRect = atlas.GetUvRect(biomeIndex, surfaceType);
Vector3 bl = new Vector3(faceX, y, z);
Vector3 br = new Vector3(faceX, y, z + 1f);
Vector3 tr = new Vector3(faceX, y + 1f, z + 1f);
Vector3 tl = new Vector3(faceX, y + 1f, z);
if (east)
{
AddVerticalQuad(renderBuffers, br, bl, tl, tr, uvRect, 1f, 1f);
AddVerticalQuad(colliderBuffers, br, bl, tl, tr, Rect.zero, 1f, 1f, false);
}
else
{
AddVerticalQuad(renderBuffers, bl, br, tr, tl, uvRect, 1f, 1f);
AddVerticalQuad(colliderBuffers, bl, br, tr, tl, Rect.zero, 1f, 1f, false);
}
}
} }
private void AddTopQuad(MeshBuffers buffers, float x, float z, float width, float depth, float height, Rect uvRect, bool withUv = 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; int baseIndex = buffers.Vertices.Count;
buffers.Vertices.Add(new Vector3(x, height, z)); 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));
buffers.Vertices.Add(new Vector3(x + width, height, z + depth)); buffers.Vertices.Add(new Vector3(x + width, height, z + depth));
buffers.Vertices.Add(new Vector3(x, 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) if (withUv)
{ {
buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMin)); buffers.Uvs.Add(new Vector2(0f, 0f));
buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMin)); buffers.Uvs.Add(new Vector2(width, 0f));
buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMax)); buffers.Uvs.Add(new Vector2(width, depth));
buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMax)); 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);
@@ -548,20 +752,29 @@ namespace InfiniteWorld.VoxelWorld
buffers.Triangles.Add(baseIndex + 2); buffers.Triangles.Add(baseIndex + 2);
} }
private void AddVerticalQuad(MeshBuffers buffers, Vector3 bottomLeft, Vector3 bottomRight, Vector3 topRight, Vector3 topLeft, Rect uvRect, float width, float height, bool withUv = true) 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; int baseIndex = buffers.Vertices.Count;
buffers.Vertices.Add(bottomLeft); buffers.Vertices.Add(bottomLeft);
buffers.Vertices.Add(bottomRight); buffers.Vertices.Add(bottomRight);
buffers.Vertices.Add(topRight); buffers.Vertices.Add(topRight);
buffers.Vertices.Add(topLeft); buffers.Vertices.Add(topLeft);
buffers.Normals.Add(normal);
buffers.Normals.Add(normal);
buffers.Normals.Add(normal);
buffers.Normals.Add(normal);
if (withUv) if (withUv)
{ {
buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMin)); buffers.Uvs.Add(new Vector2(0f, 0f));
buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMin)); buffers.Uvs.Add(new Vector2(width, 0f));
buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMax)); buffers.Uvs.Add(new Vector2(width, height));
buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMax)); 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);
@@ -831,7 +1044,9 @@ namespace InfiniteWorld.VoxelWorld
private sealed class MeshBuffers private sealed class MeshBuffers
{ {
public readonly List<Vector3> Vertices = new List<Vector3>(512); 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> Uvs = new List<Vector2>(512);
public readonly List<Vector2> TextureData = new List<Vector2>(512);
public readonly List<int> Triangles = new List<int>(1024); public readonly List<int> Triangles = new List<int>(1024);
public Mesh ToMesh(string meshName) public Mesh ToMesh(string meshName)
@@ -844,12 +1059,19 @@ namespace InfiniteWorld.VoxelWorld
mesh.indexFormat = Vertices.Count > 65535 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16; mesh.indexFormat = Vertices.Count > 65535 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16;
mesh.SetVertices(Vertices); mesh.SetVertices(Vertices);
if (Normals.Count == Vertices.Count)
{
mesh.SetNormals(Normals);
}
if (Uvs.Count == Vertices.Count) if (Uvs.Count == Vertices.Count)
{ {
mesh.SetUVs(0, Uvs); mesh.SetUVs(0, Uvs);
} }
if (TextureData.Count == Vertices.Count)
{
mesh.SetUVs(1, TextureData);
}
mesh.SetTriangles(Triangles, 0, true); mesh.SetTriangles(Triangles, 0, true);
mesh.RecalculateNormals();
mesh.RecalculateBounds(); mesh.RecalculateBounds();
return mesh; return mesh;
} }
+8
View File
@@ -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:
+1
View File
@@ -12,6 +12,7 @@
"com.unity.2d.tooling": "1.0.2", "com.unity.2d.tooling": "1.0.2",
"com.unity.burst": "1.8.28", "com.unity.burst": "1.8.28",
"com.unity.collab-proxy": "2.11.3", "com.unity.collab-proxy": "2.11.3",
"com.unity.collections": "2.6.2",
"com.unity.ext.nunit": "2.0.5", "com.unity.ext.nunit": "2.0.5",
"com.unity.feature.2d": "2.0.2", "com.unity.feature.2d": "2.0.2",
"com.unity.ide.rider": "3.0.39", "com.unity.ide.rider": "3.0.39",
+3 -3
View File
@@ -127,7 +127,7 @@
}, },
"com.unity.collections": { "com.unity.collections": {
"version": "2.6.2", "version": "2.6.2",
"depth": 1, "depth": 0,
"source": "registry", "source": "registry",
"dependencies": { "dependencies": {
"com.unity.burst": "1.8.23", "com.unity.burst": "1.8.23",
@@ -211,7 +211,7 @@
}, },
"com.unity.nuget.mono-cecil": { "com.unity.nuget.mono-cecil": {
"version": "1.11.6", "version": "1.11.6",
"depth": 2, "depth": 1,
"source": "registry", "source": "registry",
"dependencies": {}, "dependencies": {},
"url": "https://packages.unity.com" "url": "https://packages.unity.com"
@@ -283,7 +283,7 @@
}, },
"com.unity.test-framework.performance": { "com.unity.test-framework.performance": {
"version": "3.2.0", "version": "3.2.0",
"depth": 2, "depth": 1,
"source": "registry", "source": "registry",
"dependencies": { "dependencies": {
"com.unity.test-framework": "1.1.33", "com.unity.test-framework": "1.1.33",
+1
View File
@@ -63,3 +63,4 @@
| TASK-0021 | ToDo | High | architecture | unassigned | 2d | docs/tasks/items/TASK-0021.md | Привести проект в порядок: разнести код по asmdef, навести структуру Editor/Runtime и добавить базовые автотесты. | | TASK-0021 | ToDo | High | architecture | unassigned | 2d | docs/tasks/items/TASK-0021.md | Привести проект в порядок: разнести код по asmdef, навести структуру Editor/Runtime и добавить базовые автотесты. |
| TASK-0022 | ToDo | Highest | worldgen | unassigned | 1d | docs/tasks/items/TASK-0022.md | Интегрировать спавн врагов в VoxelWorldGenerator: спавнить по загрузке чанка и учитывать kill-state. | | TASK-0022 | ToDo | Highest | worldgen | unassigned | 1d | docs/tasks/items/TASK-0022.md | Интегрировать спавн врагов в VoxelWorldGenerator: спавнить по загрузке чанка и учитывать kill-state. |
| TASK-0023 | ToDo | Highest | ai | unassigned | 2d | docs/tasks/items/TASK-0023.md | Реализовать runtime NavMesh bake для voxel-чанка и интегрировать обновление навигации при загрузке/изменении чанков. | | TASK-0023 | ToDo | Highest | ai | unassigned | 2d | docs/tasks/items/TASK-0023.md | Реализовать runtime NavMesh bake для voxel-чанка и интегрировать обновление навигации при загрузке/изменении чанков. |
| TASK-0024 | ToDo | Highest | art | unassigned | 2d | docs/tasks/items/TASK-0024.md | Заменить Minecraft-placeholder арт на легальные ассеты для продакшена и зафиксировать источник/лицензии. |
+99
View File
@@ -0,0 +1,99 @@
---
id: TASK-0024
title: Заменить Minecraft-placeholder арт на легальные ассеты
summary: Убрать заглушки из Minecraft текстурпака (нет прав для продакшена), заменить на легально используемые ассеты и зафиксировать источники/лицензии.
priority: Highest
area: art
owner: unassigned
created: 2026-03-31
updated: 2026-03-31
execution_time: 2d
depends_on: []
canonical_docs:
- docs/tasks/Index.md
related_files:
- Assets/
---
# TASK-0024 - Заменить Minecraft-placeholder арт на легальные ассеты
## Status
Статус задачи ведется в `docs/tasks/Index.md` и является каноническим там.
## Why
Сейчас в проекте используются заглушки из Minecraft текстурпака. Прав на их использование в продакшене нет, это юридический и релизный риск.
## Expected Outcome
- В проекте не осталось ассетов из Minecraft текстурпака.
- Все используемые визуальные ассеты имеют разрешенный источник (собственные/купленные/CC0/лицензия совместима с коммерческим использованием).
- В репозитории есть короткая фиксация: откуда ассеты, какие лицензии, где лежат файлы.
## Current Context
Воксельный рендер использует атлас/материал. Замена арта должна сохранить текущий контракт данных (например, набор surface types), чтобы не сломать генератор.
## Source Of Truth
- фактические ассеты в `Assets/`
- файлы лицензий/README от поставщика ассетов
- документ со списком источников и лицензий (создать в рамках задачи)
## Read First
- `Assets/Scripts/VoxelWorld/Runtime/VoxelWorldAtlas.cs`
- `Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs`
## Scope In
- инвентаризация: какие текстуры/материалы сейчас заглушки
- удаление/замена заглушек на легальные аналоги
- обновление атласа/материалов/спрайтов так, чтобы мир продолжал рендериться
- добавление файла `docs/licenses/art-assets.md` (или эквивалента) со списком источников и лицензий
## Scope Out
- финальный художественный стиль игры
- полноценный арт-пайплайн (если не требуется прямо сейчас)
## Constraints
- не добавлять ассеты с неясной лицензией
- предпочтительно: CC0/покупные с подтверждением/собственные
- не ломать текущие сцены и генерацию мира
## Suggested Approach
1. Найти все текстуры/материалы, пришедшие из Minecraft текстурпака.
2. Выбрать источник замены (CC0 pack или собственные временные ассеты) и добавить их в проект.
3. Обновить атлас/материал и проверить рендер чанков.
4. Зафиксировать источники и лицензии в отдельном документе.
## Acceptance Criteria
- в проекте нет Minecraft-placeholder ассетов
- мир рендерится корректно после замены
- есть документированная таблица "asset -> источник -> лицензия"
## Verification
- ручная проверка в сцене: генерация чанков, отображение поверхностей
- grep/поиск по репозиторию по ключевым словам/именам, связанным с Minecraft pack
## Risks / Open Questions
- часть заглушек может быть уже запечена в атлас; нужно аккуратно заменить без поломки UV
## Human Decisions Needed
- выбрать конкретный источник легальных ассетов (CC0 pack / купленные / собственные)
## Decision Log
- `2026-03-31` - задача добавлена из-за отсутствия прав на текущий placeholder текстурпак.
## Handoff Notes
Если будут добавляться сторонние ассеты, сохраняйте рядом с ними LICENSE/README от автора или ссылку на источник.