3

Smoothing Between Chunks

So I've been working on a game in unity and want to expand my world from a 150x150 map into a seemingly infinite procedural world. My plan is to use Perlin Noise as the base and use the different values from 0-1 to determine the terrain type. The issue I'm running into is when I draw out my chunks and offset accordingly my chunks do not line up correctly, which kind of break the illusion of an infinite world.

(seen here)

broken chunks


WorldChunk.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Unity.Mathematics;

[System.Serializable]
public class WorldChunk
{
    public int2 Position;
    public int[,] Data;
    public float[,] Sample;

    public WorldChunk(int chunkSize = 16){
        Data = new int[chunkSize, chunkSize];
        Sample = new float[chunkSize, chunkSize];
    }
}

WorldGenerator.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Unity.Mathematics;

public class WorldGenerator : MonoBehaviour
{

    // Base World Data
    public int ChunkSize = 75;
    public string Seed = "";
    [Range(1f, 40f)]
    public float PerlinScale = 10f;
    // Pseudo Random Number Generator
    private System.Random pseudoRandom;

    // Chunk Data Split into Sections (Each Chunk having Coords (x, y))
    public Dictionary<string, WorldChunk> chunks = new Dictionary<string, WorldChunk>();


    //============================================================
    // Set Warm-Up Data
    //============================================================
    private void Awake() {
        // Get/Create Seed
        if (Seed == ""){
            Seed = GenerateRandomSeed();
        }
        // Get Random Number Generator
        pseudoRandom = new System.Random(Seed.GetHashCode());
        // Using to Clear while Making Test Adjustments
        chunks.Clear();
        // Generate Starting Chunk
        for (int x = -1; x <= 1; x++)
        {
            for (int y = -1; y <= 1; y++)
            {
                // Draw Test Chunks
                GenerateChunk(x, y);
            }
        }
    }

    //============================================================
    // Generation Code
    //============================================================

    // ===
    //  Create New Chunks
    // ===
    public void GenerateChunk(int x, int y){
        // Set Key to use
        string key = $"{x},{y}";
        // Check if key exists if not Generate New Chunk
        if (!chunks.ContainsKey(key)){
            // Add Chunk, Set Position in chunk grid (for calling and block data later), Then Generate data
            chunks.Add(key, new WorldChunk(ChunkSize));
            chunks[key].Position = new int2(x, y);
            GenerateChunkData(chunks[key]);
        }
    }

    // ===
    //  Fill Chunks with Perlin Data
    // ===
    private void GenerateChunkData(WorldChunk chunk){
        // Set Offsets
        float xOffset = (float)chunk.Position.x * ChunkSize;
        float yOffset = (float)chunk.Position.y * ChunkSize;
        // Set Data to Chunk
        for (int x = 0; x < ChunkSize; x++)
        {
            for (int y = 0; y < ChunkSize; y++)
            {
                // Get Perlin Map
                float px = (float)(x) / ChunkSize * PerlinScale + xOffset;
                float py = (float)(y) / ChunkSize * PerlinScale + yOffset;

                // Set Temp Sample For Testing (This will change for Map Data (Hills and Water) later)
                chunk.Sample[x,y] = Mathf.PerlinNoise(px, py);
            }
        }
    }

    // ===
    //  Generate Random Seed of Length
    // ===
    private string GenerateRandomSeed(int maxCharAmount = 10, int minCharAmount = 10){
        //Set Characters To Pick from
        const string glyphs= "abcdefghijklmnopqrstuvwxyz0123456789";
        //Set Length from min to max
        int charAmount = UnityEngine.Random.Range(minCharAmount, maxCharAmount);
        // Set output Variable
        string output = "";
        // Do Random Addition
        for(int i=0; i<charAmount; i++)
        {
            output += glyphs[UnityEngine.Random.Range(0, glyphs.Length)];
        }
        // Output New Random String
        return output;
    }

    //============================================================
    // Draw Example
    //============================================================

    private void OnDrawGizmos() {
        // Do this because I'm lazy and don't want to draw pixels to generated Sprites
        Awake();
        // For Each WorldChunk in the chunk Data
        foreach (WorldChunk c in chunks.Values)
        {
            // Check if it exists (Foreach is stupid sometimes... When live editing)
            if (c != null){
                // Get World Positions for Chunk (Should probably Set to a Variable in the Chunk Data)
                Vector3 ChunkPosition = new Vector3(c.Position.x * ChunkSize, c.Position.y * ChunkSize);

                // For Each X & For Each Y in the chunk
                for (int x = 0; x < ChunkSize; x++)
                {
                    for (int y = 0; y < ChunkSize; y++)
                    {
                        // Get Cell position
                        Vector3 cellPos = new Vector3((ChunkPosition.x - ChunkSize/2f) + x, (ChunkPosition.y - ChunkSize/2f) + y);
                        // Get Temp Sample and set to color
                        float samp = c.Sample[x,y];
                        Gizmos.color = new Color(samp, samp, samp);
                        // Draw Tile as Sample black or white.
                        Gizmos.DrawCube(cellPos, Vector3.one);
                    }
                }

                // Size for Cubes
                Vector3 size = new Vector3(ChunkSize, ChunkSize, 1f);
                // Set Color Opaque Green
                Gizmos.color = new Color(0f, 1f, 0f, 0.25f);
                // Draw Chunk Borders (Disable to show issue)
                // Gizmos.DrawWireCube(ChunkPosition, size);
                
            }
        }
        
    }
}

I would like to point out when I use:

// Get Perlin Map
float px = (float)(x + xOffset) / ChunkSize * PerlinScale;
float py = (float)(y + yOffset) / ChunkSize * PerlinScale;

instead of

// Get Perlin Map
float px = (float)(x) / ChunkSize * PerlinScale + xOffset;
float py = (float)(y) / ChunkSize * PerlinScale + yOffset;

Everything aligns up correctly but the perlin noise just repeats.

What would be the best way for me to smooth between the chunks so that everything matches up? Is there a better way to write this maybe?

EDIT:


Thanks for the help Draykoon D! here is the updated info and links to the updated scripts on pastebin if anyone needs them!

example

Here is the update code for anyone who wants it: ** WorldGenerator.cs**


https://pastebin.com/3BjLy5Hk

** WorldGenerator.cs**


https://pastebin.com/v3JJte3N

Hope that helps!

SaveZeQueen
  • 33
  • 1
  • 5

2 Answers2

4

The key word you are looking for is tileable.

But I have a great news for you, noise function such as perlin are periodic in nature. So instead of calling ChunckSize * ChunkSize a noise function you should only call it once and then divide the results.

I will advice you to read this excellent tutorial:

https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/procedural-patterns-noise-part-1/creating-simple-1D-noise

Paltoquet
  • 1,184
  • 1
  • 10
  • 18
  • Thanks for the help! That was an awesome Article! I figured it out, I ended up re-writing the Noise generation off this. As well as adding in some extra functionality I didn't initially have thanks to you! https://i.gyazo.com/8a78c5d0435d2f90f9dba38b409b3b3f.mp4 Here is the update code for anyone who wants it: ** WorldGenerator.cs** *** https://pastebin.com/3BjLy5Hk ** WorldGenerator.cs** *** https://pastebin.com/v3JJte3N Hope that helps! – SaveZeQueen Aug 14 '20 at 18:24
1
  1. Don't use Perlin noise. It has heavy bias towards the 45 and 90 degree directions. Your hills are all aligned to these, and aren't oriented along a more interesting variety of directions. You could use Unity.mathematics.noise.snoise(float2) but its repeat period is rather small, and it might not be very fast if you aren't using Unity Burst jobs. this is what I created/use/recommend, but it's certainly not the only option out there! Note that all these noises are range -1 to 1 rather than 0 to 1, so if that's important than do value=value*0.5+0.5; to rescale it.

  2. Now that that's out of the way, to solve your issue you need to separate the idea of chunks and generation. This is a good idea in general, and I always believe in hiding backend implementation details (e.g chunks) from gameplay as much as possible (e.g. avoid visible boundaries). Each time you generate a chunk, you should find its start coordinate in the world, so that coordinates continue seamlessly with the rest. For example, if the chunks are 128x128, then the chunk starting at (0, 0) should have starting coordinate (0, 0), then the chunk starting at (0, 1) should have starting coordinate (0, 128). Only then, convert a world coordinate into a noise coordinate by multiplying by your desired frequency.

KdotJPG
  • 811
  • 4
  • 6
  • I appreciate the feedback :) Perlin noise is okay for what I'm doing. I did see the Unity Noise functions I will definitely look into that more! Could maybe make some more interesting worlds? who knows. – SaveZeQueen Aug 16 '20 at 14:44
  • 1
    I don't know. I rarely see a project which used Perlin noise over a Simplex or related noise, and seemed better off for it. For me, the first step when using noise in Unity, is always to import Unity.Mathematics.noise.snoise, or use an external noise lib. It's an arguably small but important detail aspect. But if this is just a class project or you're just learning Unity, I can see the reasoning. – KdotJPG Aug 16 '20 at 17:43
  • 1
    That's just my two cents from seeing this scenario frequently. – KdotJPG Aug 16 '20 at 17:46
  • I see, I will look into using simplex noise. Especially if it's going to end up being computationally faster. I'm learning all this from scratch so if I'm unfamiliar with something I'm sorry. I really do appreciate all the feedback and recommendations. – SaveZeQueen Aug 17 '20 at 19:17
  • 1
    It isn't necessarily computationally faster. That's true for higher dimensional noise in particular, but the 2D noise in either case is probably as fast as you'll need anyway. Appearance is always my reason to use a Simplex noise over Perlin. What is probably a performance difference, though, is using the OpenSimplex2 from the link (or similar) instead of the Unity.Mathematics noise, if you're not using Burst jobs which optimize the vector operations in the Unity.Mathematics noise. And no problem, it's all a learning process! – KdotJPG Aug 17 '20 at 20:36