From fa36c495833a5fac36506d818bd87dfea69b79ca Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Tue, 31 Mar 2026 11:30:35 +0700 Subject: [PATCH] [Fix] Update Voxel World --- Assets/Scenes/VoxelWorldTestScene.unity | 6 +- .../VoxelWorld/Runtime/VoxelWorldAtlas.cs | 147 ++++--- .../VoxelWorld/Runtime/VoxelWorldGenerator.cs | 380 ++++++++++++++---- Assets/Shaders.meta | 8 + .../VoxelWorldTextureArrayUnlit.shader | 70 ++++ .../VoxelWorldTextureArrayUnlit.shader.meta | 9 + Packages/manifest.json | 1 + Packages/packages-lock.json | 6 +- docs/tasks/Index.md | 1 + docs/tasks/items/TASK-0024.md | 99 +++++ 10 files changed, 586 insertions(+), 141 deletions(-) create mode 100644 Assets/Shaders.meta create mode 100644 Assets/Shaders/VoxelWorldTextureArrayUnlit.shader create mode 100644 Assets/Shaders/VoxelWorldTextureArrayUnlit.shader.meta create mode 100644 docs/tasks/items/TASK-0024.md diff --git a/Assets/Scenes/VoxelWorldTestScene.unity b/Assets/Scenes/VoxelWorldTestScene.unity index f579601c..5c80f866 100644 --- a/Assets/Scenes/VoxelWorldTestScene.unity +++ b/Assets/Scenes/VoxelWorldTestScene.unity @@ -414,10 +414,10 @@ MonoBehaviour: m_EditorClassIdentifier: VoxelWorld.Runtime::InfiniteWorld.VoxelWorld.VoxelWorldGenerator streamTarget: {fileID: 1331065949} chunkSize: 16 - generationRadius: 4 + generationRadius: 7 blockingGenerationRadius: 1 seed: 12345 - maxMountainHeight: 5 + maxMountainHeight: 12 macroNoiseScale: 0.05 detailNoiseScale: 0.12 ridgeNoiseScale: 0.18 @@ -436,7 +436,7 @@ MonoBehaviour: - {fileID: 11400000, guid: eac8d825dd62e1c439235d273a4ca613, type: 2} - {fileID: 11400000, guid: 6d0dbe510ed048440a3925ef40aeeb5b, type: 2} biomeNoiseScale: 0.02 - biomeSize: 12 + biomeSize: 4 maxAsyncChunkJobs: 8 maxChunkBuildsPerFrame: 2 --- !u!4 &1842209028 diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldAtlas.cs b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldAtlas.cs index 17bdbcae..ed24f97f 100644 --- a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldAtlas.cs +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldAtlas.cs @@ -41,6 +41,8 @@ namespace InfiniteWorld.VoxelWorld 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, @@ -49,29 +51,29 @@ namespace InfiniteWorld.VoxelWorld VoxelSurfaceType.WalkableSurface }; - private readonly Dictionary> uvLookup; + private readonly Dictionary> layerLookup; - public VoxelWorldAtlas(Texture2D texture, Material material, Dictionary> uvRects) + private VoxelWorldAtlas(Texture2DArray textureArray, Material material, Dictionary> layers) { - Texture = texture; + TextureArray = textureArray; Material = material; - uvLookup = uvRects; + layerLookup = layers; } - public Texture2D Texture { get; } + public Texture2DArray TextureArray { 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 biomeRects) && biomeRects.TryGetValue(surfaceType, out Rect rect)) + if (layerLookup.TryGetValue(biomeIndex, out Dictionary biomeLayers) && biomeLayers.TryGetValue(surfaceType, out int layer)) { - return rect; + return layer; } - return uvLookup[0][surfaceType]; + return layerLookup[0][surfaceType]; } public static VoxelWorldAtlas CreateRuntimeAtlas(IReadOnlyList biomeProfiles) @@ -94,48 +96,60 @@ namespace InfiniteWorld.VoxelWorld } 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, - wrapMode = TextureWrapMode.Clamp, - name = "VoxelWorld_RuntimeAtlas", + wrapMode = TextureWrapMode.Repeat, + anisoLevel = 1, + name = "VoxelWorld_RuntimeTextureArray", hideFlags = HideFlags.HideAndDontSave }; - Fill(texture, new Color(1f, 0f, 1f, 1f)); - - Dictionary> uvRects = new Dictionary>(sourceBiomes.Count); + Dictionary> layers = new Dictionary>(sourceBiomes.Count); + int layerIndex = 0; for (int biomeIndex = 0; biomeIndex < sourceBiomes.Count; biomeIndex++) { - Dictionary biomeRects = new Dictionary(SurfaceOrder.Length); - uvRects[biomeIndex] = biomeRects; + Dictionary biomeLayers = new Dictionary(SurfaceOrder.Length); + layers[biomeIndex] = biomeLayers; for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++) { VoxelSurfaceType surfaceType = SurfaceOrder[surfaceIndex]; - RectInt rect = new RectInt(surfaceIndex * tileSize, biomeIndex * tileSize, tileSize, tileSize); - DrawSurfaceTile(texture, rect, sourceBiomes[biomeIndex], surfaceType, tileSize); - biomeRects[surfaceType] = BuildUvRect(rect, texture.width, texture.height); + 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); + } } } - 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 ?? ""}', 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) { name = "VoxelWorld_RuntimeMaterial", hideFlags = HideFlags.HideAndDontSave }; - if (material.HasProperty("_BaseMap")) + if (material.HasProperty("_TextureArray")) { - material.SetTexture("_BaseMap", texture); - } - - if (material.HasProperty("_MainTex")) - { - material.SetTexture("_MainTex", texture); + material.SetTexture("_TextureArray", textureArray); } if (material.HasProperty("_BaseColor")) @@ -148,19 +162,19 @@ namespace InfiniteWorld.VoxelWorld material.SetColor("_Color", Color.white); } - return new VoxelWorldAtlas(texture, material, uvRects); + return new VoxelWorldAtlas(textureArray, material, layers); } public void Dispose() { if (Application.isPlaying) { - UnityEngine.Object.Destroy(Texture); + UnityEngine.Object.Destroy(TextureArray); UnityEngine.Object.Destroy(Material); } else { - UnityEngine.Object.DestroyImmediate(Texture); + UnityEngine.Object.DestroyImmediate(TextureArray); UnityEngine.Object.DestroyImmediate(Material); } } @@ -187,45 +201,66 @@ namespace InfiniteWorld.VoxelWorld 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; - 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)) + Texture2D texture = new Texture2D(tileSize, tileSize, TextureFormat.RGBA32, false) { - 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 { Texture2D source = sprite.texture; 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); - 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); - 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) { - 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; } } diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs index 7acae019..e1c68bc7 100644 --- a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs @@ -400,38 +400,136 @@ namespace InfiniteWorld.VoxelWorld 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++) { - if (heights[z * chunkSize + x] > 0) + int index = z * chunkSize + x; + if (visited[x, z] || heights[index] > 0) { continue; } - int index = z * chunkSize + x; - AddTopQuad(renderBuffers, x, z, 1f, 1f, 0f, atlas.GetUvRect(biomeIndices[index], VoxelSurfaceType.WalkableSurface)); + 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) + if (height <= 0 || visited[x, z]) { continue; } int index = z * chunkSize + x; VoxelSurfaceType surfaceType = IsCliffTop(ChunkToWorldCell(coord, x, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface; - Rect uvRect = atlas.GetUvRect(biomeIndices[index], surfaceType); - AddTopQuad(renderBuffers, x, z, 1f, 1f, height, uvRect); - AddTopQuad(colliderBuffers, x, z, 1f, 1f, height, Rect.zero, false); + 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); } } } @@ -440,104 +538,210 @@ namespace InfiniteWorld.VoxelWorld { 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 (current <= 0) + if (visited[x, y] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x, z, y, north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)) { continue; } - int worldX = coord.x * chunkSize + x; - int worldZ = coord.y * chunkSize + z; - byte biomeIndex = biomeIndices[z * chunkSize + x]; + 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++; + } - AddNorthSouthFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX, worldZ + 1)), biomeIndex, true); - AddNorthSouthFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX, worldZ - 1)), biomeIndex, false); - AddEastWestFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX + 1, worldZ)), biomeIndex, true); - AddEastWestFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX - 1, worldZ)), biomeIndex, false); + 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 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; - } - - 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) + for (int y = 0; y < maxMountainHeight; y++) { - AddVerticalQuad(renderBuffers, bl, br, tr, tl, uvRect, 1f, 1f); - AddVerticalQuad(colliderBuffers, bl, br, tr, tl, Rect.zero, 1f, 1f, false); - } - else - { - AddVerticalQuad(renderBuffers, br, bl, tl, tr, uvRect, 1f, 1f); - AddVerticalQuad(colliderBuffers, br, bl, tl, tr, Rect.zero, 1f, 1f, false); + 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 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); - float faceX = east ? x + 1f : x; - - 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(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); - } - } + 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, 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; 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(uvRect.xMin, uvRect.yMin)); - buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMin)); - buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMax)); - buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMax)); + 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); @@ -548,20 +752,29 @@ namespace InfiniteWorld.VoxelWorld 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; 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(uvRect.xMin, uvRect.yMin)); - buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMin)); - buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMax)); - buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMax)); + 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); @@ -831,7 +1044,9 @@ namespace InfiniteWorld.VoxelWorld private sealed class MeshBuffers { public readonly List Vertices = new List(512); + public readonly List Normals = new List(512); public readonly List Uvs = new List(512); + public readonly List TextureData = new List(512); public readonly List Triangles = new List(1024); 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.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.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } diff --git a/Assets/Shaders.meta b/Assets/Shaders.meta new file mode 100644 index 00000000..e137a80f --- /dev/null +++ b/Assets/Shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1d3a1e4bd82462b4790722c7ebbf2e17 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Shaders/VoxelWorldTextureArrayUnlit.shader b/Assets/Shaders/VoxelWorldTextureArrayUnlit.shader new file mode 100644 index 00000000..de2e3d62 --- /dev/null +++ b/Assets/Shaders/VoxelWorldTextureArrayUnlit.shader @@ -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 + } + } +} diff --git a/Assets/Shaders/VoxelWorldTextureArrayUnlit.shader.meta b/Assets/Shaders/VoxelWorldTextureArrayUnlit.shader.meta new file mode 100644 index 00000000..6dcba563 --- /dev/null +++ b/Assets/Shaders/VoxelWorldTextureArrayUnlit.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: ec80aebd8cb61f44cbfa6b7d5f087211 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index 8df05907..de5e08b3 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -12,6 +12,7 @@ "com.unity.2d.tooling": "1.0.2", "com.unity.burst": "1.8.28", "com.unity.collab-proxy": "2.11.3", + "com.unity.collections": "2.6.2", "com.unity.ext.nunit": "2.0.5", "com.unity.feature.2d": "2.0.2", "com.unity.ide.rider": "3.0.39", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 4b773997..294333de 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -127,7 +127,7 @@ }, "com.unity.collections": { "version": "2.6.2", - "depth": 1, + "depth": 0, "source": "registry", "dependencies": { "com.unity.burst": "1.8.23", @@ -211,7 +211,7 @@ }, "com.unity.nuget.mono-cecil": { "version": "1.11.6", - "depth": 2, + "depth": 1, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" @@ -283,7 +283,7 @@ }, "com.unity.test-framework.performance": { "version": "3.2.0", - "depth": 2, + "depth": 1, "source": "registry", "dependencies": { "com.unity.test-framework": "1.1.33", diff --git a/docs/tasks/Index.md b/docs/tasks/Index.md index 3c683778..e66f9e4c 100644 --- a/docs/tasks/Index.md +++ b/docs/tasks/Index.md @@ -63,3 +63,4 @@ | 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-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 арт на легальные ассеты для продакшена и зафиксировать источник/лицензии. | diff --git a/docs/tasks/items/TASK-0024.md b/docs/tasks/items/TASK-0024.md new file mode 100644 index 00000000..df9766d7 --- /dev/null +++ b/docs/tasks/items/TASK-0024.md @@ -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 от автора или ссылку на источник.