IceCreamYou / THREE.Terrain

A procedural terrain generation engine for use with the Three.js 3D graphics library for the web.
https://icecreamyou.github.io/THREE.Terrain/
MIT License
715 stars 112 forks source link

Support disabling rescaling in Clamp to avoid seams with repeating Perlin/Simplex terrains #26

Open campbellgoe opened 5 years ago

campbellgoe commented 5 years ago

Feature request:

It would be great to be able to set an x and y offset for perlin / simplex noise, and potentially other noise functions.

The usage in my case would be to create tiles of THREE.Terrain, each positioned next to each-other, with the same simplex noise seed, but with its x, y axes offset so they match the position, and the edges of the tiles match up. Then you will be able to create many tiles of terrain, loading nearby tiles and unloaded distance tiles.

I did try to implement this by just adding an offsetX and offsetY in the simplex function, but the results were not as expected and I wasn't sure how to do this.

Thanks.

IceCreamYou commented 5 years ago

The Perlin generator is defined here:

https://github.com/IceCreamYou/THREE.Terrain/blob/50b56305705ad50263d28e0e2fd9d73027f6c69f/src/generators.js#L396-L405

The Simplex generator is defined similarly.

You can define your own generator which passes different values to noise.perlin() or noise.simplex().

In the provided generators, the X/Y values passed to those functions are i / divisor, j / divisor. i/j are the X/Y indices, respectively, of the vertex whose height is being calculated. divisor essentially defines the scale of the features in the generated noise. For your use case you would probably just want to pass different values of i/j depending on the location of the affected tile relative to your origin tile.

The other thing to pay attention to is that every call to THREE.Terrain.Perlin() or THREE.Terrain.Simplex() currently re-seeds noise. If you were to write your own generator method, you would want to only seed noise once rather than on each call.

campbellgoe commented 5 years ago

Thanks. I figured that was the correct bit of code to add the offset, and that the random needed to remain the same for each tile.

Although somehow still have a seam, as seen here:

sand seam

But that's probably a problem with my implementation.

campbellgoe commented 5 years ago

For example

Terrain.simplex(
          (i + ox * (options.xSegments + 1)) / divisor,
          (j + oy * (options.ySegments + 1)) / divisor
        ) * range;

Where ox, oy are offsets such as (-1,-1), (0,-1), (1, -1), (-1, 0)... i and j go from 0 through options.xSegments + 1, so I am assuming perfect offsets will be multiples of options.xSegments + 1, however as seen in the image above, there are very small seams. If the logic above is correct (which I'm not sure about) then it likely means a bit of variation may be arising due to a random seed being improperly set/not set.

It could be due to do with the height range, if one tile/chunk has a different natural height range, could it be being distorted on the y axis relative to other tiles/chunks?

Sorry for asking questions here, if there's a better place, I'd be happy to ask there instead.

IceCreamYou commented 5 years ago

Make sure you are passing in exactly the same arguments to noise.simplex for vertices that should have identical locations. My guess is there’s a floating point precision issue somewhere in your code. If you had a seeding issue, the edges of your tiles would not come close to lining up. It seems more likely that you are just passing in X/Y offsets that are very slightly different than expected.

On Jul 28, 2019, at 11:59 PM, George Campbell notifications@github.com wrote:

For example

Terrain.simplex( (i + ox (options.xSegments + 1)) / divisor, (j + oy (options.ySegments + 1)) / divisor ) * range; Where ox, oy are offsets such as (-1,-1), (0,-1), (1, -1), (-1, 0)... i and j go from 0 through options.xSegments + 1, so I am assuming perfect offsets will be multiples of options.xSegments + 1, however as seen in the image above, there are very small seams. If the logic above it correct (which I'm not sure about) then it likely means a bit of variation may be arising due to a random seed being improperly set/not set. Sorry for asking questions here, if there's a better place, I'd be happy to ask there instead.

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub, or mute the thread.

campbellgoe commented 5 years ago

I've done as much as I could to ensure the x,y inputs for the noise function were correct and each chunk edge had the same correct corresponding x,y coords. However, I was still unable to get the chunks perfectly connecting.

Here is a 2x2 grid of 1024x1024 terrains with 15x15 segments, and this gives me these results:

4x4 with seam seam prominent

I find this very odd, as surely the noise function should output the same values for the same x,y coordinates.

I think a solution so I can move on would be to just snap these edges together, and that shouldn't be noticeable.

IceCreamYou commented 5 years ago

I think a solution so I can move on would be to just snap these edges together, and that shouldn't be noticeable.

That's what I would do. That said, I'd still be willing to bet that the arguments you're passing to noise.simplex are very slightly different for vertices that should be at the same X/Y coordinates due to floating point precision issues. The only way to know would be to log the those values.

(Or is it possible there's some other generator or something else other than the simplex generator that's affecting the points on your terrain plane?)

campbellgoe commented 5 years ago

After setting each seams / edge verticies Z all to 0, I get this:

0 z seam

Whereas setting the edge seams Z to 100 removes the seam.

100 z seam

Seam begins to appear somewhere between 92 and 93 z. Setting the edge verticies to less than this amount makes the seam appear.

campbellgoe commented 5 years ago

Could it be due to Clamp ?


 * Rescale the heightmap of a terrain to keep it within the maximum range.
 ...
function Clamp(g, options) {
  ...
  for (i = 0; i < l; i++) {
    g[i].z = options.easing((g[i].z - min) / actualRange) * range + optMin;
  }
}```
campbellgoe commented 5 years ago

Yes, disabling Clamp in the Normalize function fixed it. The seams are no more!

IceCreamYou commented 5 years ago

Good catch! Clamp does indeed rescale / stretch the terrain vertically, so if your max or min Z values are different on the different terrains, it would make the edges misaligned. Without Clamp, you don't get easing, and depending on the generation function used you could end up with values outside of the desired max/min Z-values. However there should probably be a way to disable it for use cases like this!

campbellgoe commented 5 years ago

A related issue I am getting is with the shading/shadows as seen below in the sand version:

SAND SHADOW SEAM same image with UV_Grid

IceCreamYou commented 5 years ago

In addition to what I mentioned in #27, there's probably an additional factor here around how the shadow interpolation works. Honestly I don't really understand the details and haven't spent much time looking at it since 2014, but I think that the shadows are vertex based and so the calculations won't come out the same for edge vertices with significantly different slopes on either side. Fixing this probably requires using different shadow fragments which might require using a different base texture.

campbellgoe commented 5 years ago

I found a solution to shadow seams thanks to thrax.

The idea is to generate terrain with the outer segments overlapping the other tiles, then computeVertexNormals, and finally to trim the excess segments so the tiles no longer overlap.

In other words:

  1. Generate each tile with +2 x/y segments, making sure the size of the tile grows from its center, so that the extreme segments overlap perfectly with the other tiles.
  2. computeVertexNormals
  3. Remove the extra segments made in step 1, so that the terrain is back to the normal tile size, without overlapping.

This ensures the shadows take into account neighbouring vertexes at tile extremes.

IceCreamYou commented 5 years ago

Good idea!

c4b4d4 commented 1 year ago
Terrain.simplex(
          (i + ox * (options.xSegments + 1)) / divisor,
          (j + oy * (options.ySegments + 1)) / divisor
        ) * range;

Where ox, oy are offsets such as (-1,-1), (0,-1), (1, -1), (-1, 0)... i and j go from 0 through options.xSegments + 1, so I am assuming perfect offsets will be multiples of options.xSegments + 1, however as seen in the image above, there are very small seams. If the logic above is correct (which I'm not sure about) then it likely means a bit of variation may be arising due to a random seed being improperly set/not set.

Did you ever finished your project? I'm struggling at the moment with making it seamless:

I tried with your noise modification:

g[j * xl + i] += noise.simplex((i + options.offsetX * (options.xSegments + 1)) / divisor, (j + options.offsetY * (options.ySegments + 1)) / divisor) * range;

But no luck at all...

Screenshot 2022-11-25 at 19 52 09
THREE.Terrain.Simplex = function(g, options) {
    noise.seed(options.seed);
    var range = (options.maxHeight - options.minHeight) * 0.5,
        divisor = (Math.min(options.xSegments, options.ySegments) + 1) * 2 / options.frequency;
    for (var i = 0, xl = options.xSegments + 1; i < xl; i++) {
        for (var j = 0, yl = options.ySegments + 1; j < yl; j++) {
            g[j * xl + i] += noise.simplex((i + options.offsetX * (options.xSegments + 1)) / divisor, (j + options.offsetY * (options.ySegments + 1)) / divisor) * range;
        }
    }
};

I also removed the Clamp function, like you commented on here and I'm using the following options:

const options = {
easing: THREE.Terrain.Linear,
       heightmap:THREE.Terrain.Simplex,
      steps: 2,
      xSegments: 127,
      ySegments: 127,
      maxHeight: 200,
      stretch:false,
      offsetX:tileX,
      offsetY:tileY,
      minHeight: -100,
      seed:0.7281564856921896,
        xSize: 1024,
        ySize: 1024,
    }

Edit: I think it has to do with the steps.

c4b4d4 commented 1 year ago

Yes, the problem is at stepping.

These two have exactly the same settings, except step, I'm variating it from 1 to 2.

Any ideas on how to make it seamless? And using something like step, which creates more desired results.

I think the problem lays here: https://github.com/IceCreamYou/THREE.Terrain/blob/5de9c5ca8a6f5604010a0cab913a83b362145cbe/src/filters.js#L337

smooth step
c4b4d4 commented 1 year ago

So, I replaced the Step function with one of my own that does not require neighborhood checking, cause that's what messes it up when you divide it in chunks; you don't know who your neighborhood is unless you load it, but that doesn't work cause it's an infinite cycle.

Made it really rough, will post it when I have it cleaned up.