I'm trying to use simplex Perlin-noise to create topography of a planet (procedurally generated). The basic sphere consists of 6 sided cube with normalized vertices.
Before adding noise the first problem was the Unity did not calculate the normals correctly at the edges of the sides so seams were visible.
According to this solution (Calculate mesh normals with added noise): the gradients are calculated for each vertex and normals are calculated from these gradients. It works with one layer of noise:
I'm trying to implement some features like in this video ([Unity] Procedural Planets (E03: layered noise)):
- multiple layers of noise
- shift the elevation value from (-1,+1) to (0,1)
- add strength
- add minimum value
Multiple layers of noise
Here is the base of my noise and normal generation method:
private float GetElevationAndNormal(Vector3 point,
out Vector3 normal,
float baseFrequency,
float baseAmplitude,
float roughness,
float persistance,
int layers)
{
float frequency = baseFrequency;
float amplitude = baseAmplitude;
float noiseValue = 0;
Vector3 gradients = Vector3.zero;
for (int i = 0; i < layers; i++)
{
// Sample the noise value and gradient at our position.
NoiseEvaluation evaluation = _noise.Evaluate(point * frequency);
noiseValue += evaluation.Value * amplitude;
gradients += evaluation.Gradient;
if (i < layers - 1)
{
amplitude *= persistance;
frequency *= roughness;
}
}
// Zero out the component of the gradient perpendicular to the sphere.
var surfaceGradient = gradients - point * Vector3.Dot(gradients, point);
// Compute an updated normal - leaning away from the direction of increase.
normal = (point - 25f * amplitude * frequency * surfaceGradient).normalized;
// Convert our noise sample to an elevation.
return 1.0f + noiseValue;
}
And the result is not correct:
I know the normal calculation is the following: \$\vec{n} = \vec{x} − s \cdot \vec{h}\$, where \$\vec{h}\$ is the surfaceGradient
variable. (Surface normal to point on displaced sphere)
So maybe the problem is with the value of \$s\$. I tried the following things:
- use the sum of amplitude and frequency of each layers
- use the last value of amplitude and frequency
- use other value than 25f
Update #1: I tried to sum the scaled gradients but it seems there is a little misalignment yet. At 2 layers of noise it is almost OK but the shading on the left is a little bit lighter. At 5 layers of noise there is a lot of differences.
Here is the modified code:
float frequency = baseFrequency;
float amplitude = baseAmplitude;
float noiseValue = 0;
Vector3 gradients = Vector3.zero;
for (int i = 0; i < layers; i++)
{
// Sample the noise value and gradient at our position.
NoiseEvaluation evaluation = _noise.Evaluate(point * frequency);
noiseValue += evaluation.Value * amplitude;
gradients += evaluation.Gradient * 25f * amplitude * frequency;
if (i < layers - 1)
{
amplitude *= persistance;
frequency *= roughness;
}
}
// Zero out the component of the gradient perpendicular to the sphere.
var surfaceGradient = gradients - point * Vector3.Dot(gradients, point);
// Compute an updated normal - leaning away from the direction of increase.
normal = (point - surfaceGradient).normalized;
Question #1: how should I change the normal calculation in case of multiple noise layers? What is the relation between layer numbers and normal calculations? Do I modify amplitude and frequency wrong in the for loop?
Shift elevation
This works perfect I had to modify the noise value and also the gradient value:
NoiseEvaluation evaluation = _noise.Evaluate(point * frequency);
noiseValue += (evaluation.Value + 1) * 0.5f * amplitude;
...
gradients *= 0.5f;
Strength
I wanted to use this parameter to scale the noise, also works perfect, just had to multiply also the sum of noise values and the sum of gradient with the strength value:
noiseValue *= strength;
gradients *= strength;
Minimum value
It has an issue: when a noise value is below the minimum value than I use the minimum value as elevation. In this case the normal vector should be the vector pointing from the center point to the actual vertex point. But the gradient returned from the Evaluate function is calculated based on the original elevation not the minimum. The result:
I modified the algorithm in the following way: when an elevation is a minimum value then use the vector point from center point to the actual vertex point. This solved the issue but not perfectly because normals are not correct at the edge of "plains" (I get better result when use the Unity RecalculateNormals method):
Question #2: Is there any way to fix these few wrong normals at the edge of "plains"?
RecalculateNormals
works by averaging-together adjacent face normals, so you get less resolution that way, smoothing out small bumps or missing them entirely when they land between two vertices. The gradient method samples the exact wiggles of the noise field per vertex, so it can capture more details about how the surface is bending, leading to denser patterns on high-frequency octaves. I'd say the results you got there look plausible, and you might only need to tune that 25f fudge factor by eye. \$\endgroup\$