返回资讯中心
外部精选
软件工程
#开发者实时流

Curled Noise

This is the tenth Godot tutorial in a series that covers the creation of procedural patterns on the GPU with shaders, using the Godot Engine, version 4.7. It follows Tiling Noise and adds UV warping to it.

Hacker News 最新ibobev2026/10/23 · 22 天前30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

This is the tenth Godot tutorial in a series that covers the creation of procedural patterns on the GPU with shaders, using the Godot Engine, version 4.7. It follows Tiling Noise and adds UV warping to it.

Aside from some experimenting we've mostly kept our UV coordinate system constant. It's a square for the plane and a rectangle around the torus. We use the bumpiness configuration option to scale the derivatives so they better match the surface, and that's it. This time we're going to warp the UV coordinates and thus the pattern that gets generated with them. We start with some simple modifications and end up with curl noise.

Our starting point is the plane with a single octave of value noise, at frequency 8, with adaptive fractal scale enabled. Displacement is set to 1, but vertex displacement is disabled so we can get a clear look at the pattern.

We begin with a very simple adjustment of sample_noise() in value_noise.gdshader: negating the U coordinate that we use for sampling.

uv.x = -uv.x; return sample_fractal_pattern( standard_fractal_pattern_settings(), uv, TIME );

This reverses the pattern in the U dimension. It might appear correct at first glance, but closer examination will reveals that the U derivatives are wrong. They still match the surface but have the wrong sign, which causes the surface normals to point in the wrong direction in the U dimension.

The solution is to negate the derivative for the U dimension as well. This makes sense because we're effectively applying the chain rule. Recall that for a given h(x) = f(g(x)) we have h′ (x) = f′(g(x)) g′(x). In this case g(x) = −x and g′ (x) = −1.

uv.x = -uv.x; PatternSample s = sample_fractal_pattern( standard_fractal_pattern_settings(), uv, TIME ); s.dx = -s.dx; return s;

Negative U; correct.

In general we can pass the U coordinate through any warping function U(x) before passing it to the sampling function S(x), as long as we also multiply the derivative of the sampling function with the derivative of the warping function. This is the chain rule specific to our current pattern function P(x), if we ignore the V coordinate for the moment:

To formalize this let's introduce a PatternSample for U.

PatternSample u = zero_pattern_sample(); u.v = -uv.x; u.dx = -1.0; uv.x = u.v; PatternSample s = sample_fractal_pattern( standard_fractal_pattern_settings(), uv, TIME ); s.dx *= u.dx;

Now we can modify that pattern sample and as longs as its derivatives are correct the resulting pattern will also have correct derivatives. For example, we can effectively double the U frequency by using U(x) = 2x and U′(x) = 2.

u.v = 2.0 * uv.x; u.dx = 2.0;

Below the correct result is shown first with the incorrect result below it. The incorrect result has the wrong derivatives due to ignoring the chain rule, simply using U′(x) = 1.

Doubling the frequency makes the derivatives more extreme, but because the change is uniform the error caused by not adjusting derivatives is not that obvious. The difference becomes more noticeable when the adjustment of U is nonlinear, for example by using U(x) = x 2 and U′(x) = 2x.

u.v = uv.x * uv.x; u.dx = 2.0 * uv.x;

Squared U; correct and incorrect.

Both U and V

We also have the V coordinate. Adding it to our pattern function we get an extra parameter and a new function V(x):

Now we have two partial derivatives, one for x and one for y. This requires us to explicitly indicate which parameter a derivative is for. We do this by replacing the single prime ′ sign with a sequence of numbers matching the parameters of the function. Here 1 means the first derivative and 0 means no derivative. We start with the derivative for x:

In our code that's just s.dx * u.dx. Then we get the derivative for y:

This would be s.dy * v.dy. Add a pattern sample for V to include it.

PatternSample u = zero_pattern_sample(); u.v = uv.x * uv.x; u.dx = 2.0 * uv.x; PatternSample v = zero_pattern_sample(); v.v = uv.y; v.dy = 1.0;

uv.x = u.v; uv.y = v.v; PatternSample s = sample_fractal_pattern( standard_fractal_pattern_settings(), uv, TIME ); s.dx *= u.dx; s.dy *= v.dy;

We can go a step further, realizing that the functions that warp the coordinates themselves also have two parameters, until now we just ignored the unused one for each. In its simplest form we have U(x, y) = x and V(x, y) = y. This means that we could swap the dimensions by using U(x, y) = y and V(x, y) = x instead. This would be the same as swapping the order of the arguments of S(x, y).

uv.x = v.v; uv.y = u.v;

That indeed works and the derivatives remain correct, but let's undo that change.

uv.x = u.v; uv.y = v.v;

What we'll do next is make U(x, y) use both its parameters, adding them together: U(x, y) = x + y. Let's for the moment reset its derivative back to its default, ignoring the new dependency.

u.v = uv.x + uv.y; u.dx = 1.0;

Slanted; incorrect.

We get a slanted pattern, but with incorrect derivatives. We already have U(1,0)(x, y) = 1 but now we also have U(0,1)(x, y) = 1.

u.v = uv.x + uv.y; u.dx = 1.0; u.dy = 1.0;

The final partial derivative for x is still correct, but the final partial derivative for y is missing the part that affects the first parameter of our noise function. We have to apply the chain rule to each part and sum that. We already have the second part:

Let's summarize this by omitting x and y, to make it easier to read:

We can also rearrange that to put the remaining arguments at the end:

We complete it by including the missing first part:

Put that into our code, ensuring that we use the unmodified s.dx.

float dx = s.dx; s.dx *= u.dx; s.dy = u.dy * dx + v.dy * s.dy;

Slanted; correct.

The same logic applies to the partial derivative for x:

float dx = s.dx; s.dx = u.dx * dx + v.dx * s.dy; s.dy = u.dy * dx + v.dy * s.dy;

Now we can use both coordinates everywhere. For example, let's make the pattern diagonal by using V(x, y) = y − x.

v.v = uv.y - uv.x; v.dx = -1.0; v.dy = 1.0;

Diagonal; correct and incorrect.

Warping Noise with Noise

Now that we can use arbitrary functions let's feed the noise to itself and see what happens. We'll use U(x, y) = N(x, y) and V(x, y) = y where N(x, y) is our existing value noise pattern function.

PatternSample u = sample_fractal_pattern( standard_fractal_pattern_settings(), uv, TIME ); //u.v = uv.x + uv.y; //u.dx = 1.0; //u.dy = 1.0; PatternSample v = zero_pattern_sample(); v.v = uv.y; v.dx = 0.0; v.dy = 1.0;

U as noise; correct and incorrect.

This works but isn't very useful. Let's instead add the noise to the original coordinate, scaled down to only warp it a little:

PatternSample u = sample_fractal_pattern( standard_fractal_pattern_settings(), uv, TIME ); u = multiply(u, 0.25); u.v += uv.x; u.dx += 1.0;

U with noise offset; correct and incorrect.

We can do the same for V(x, y).

PatternSample v = sample_fractal_pattern( standard_fractal_pattern_settings(), uv, TIME ); v = multiply(v, 0.25); v.v += uv.y; //v.dx = 0.0; u.dy += 1.0;

UV with noise offsets; correct and incorrect.

Separate Pattern Seeds

If we're using noise to offset both coordinates we should use a different pattern for each, and also use a different pattern for the final noise. To make this possible we're going to add the pattern seed to FractalPatternSettings in fractal_pattern_settings.gdshaderinc.

/* Declares the FractalPatternSettings struct used by fractal_pattern.gdshaderinc.

The following settings are supported:

  • pattern_seed: The pattern seed value, potentially used for hashing. … */

…

struct FractalPatternSettings { uint pattern_seed; PATTERN_FREQUENCY_TYPE base_frequency; … };

#ifdef USE_STANDARD_FRACTAL_PATTERN_SETTINGS uniform uint pattern_seed = 0u; uniform PATTERN_FREQUENCY_TYPE base_frequency = PATTERN_FREQUENCY_TYPE(1.0); …

FractalPatternSettings standard_fractal_pattern_settings() { return FractalPatternSettings( pattern_seed, base_frequency, … ); } #else FractalPatternSettings default_fractal_pattern_settings() { FractalPatternSettings settings = FractalPatternSettings( 0u, PATTERN_FREQUENCY_TYPE(1.0), … ); return settings; } #endif

In fractal_pattern.gdshaderinc we'll pass the seed as the first argument to the sample pattern function.

/* … Define SAMPLE_PATTERN_FUNCTION to set the function for sampling an octave. Only define the function name, without parameter list. The matching function declaration must have three parameters:

  • uint for the pattern seed.
  • int for the octave's index. …*/

…

PatternSample dummy_sample_pattern( uint pattern_seed, int octave, … ) { return zero_pattern_sample(); } …

PatternSample sample_fractal_pattern(…) { … for (int i = 0; i < settings.octaves; i++) { PatternSample sample = SAMPLE_PATTERN_FUNCTION( settings.pattern_seed, i, frequency, coordinates, animation_offset ); … } return normalize_fractal_sample(settings, sum); }

Use this seed in sample_value_noise() in value_noise.gdshader, replacing its own configuration option.

//uniform uint hash_seed = 0u; …

PatternSample sample_value_noise( uint seed, int octave, vec2 frequency, vec2 uv, float time ) { …

Hasher h = hasher(seed + uint(octave)); … }

We also have to add the seed parameter to sample_pattern() in sine_waves.gdshader, even through we don't use it there.

PatternSample sample_pattern( uint seed, int octave, vec2 frequency, vec2 uv, vec2 time ) { … }

Back to sample_noise() in value_noise.gdshader, we can now use a different seed per sample. We copy the global settings to specific UV settings, then simply increment the seed for U and again for V.

FractalPatternSettings uv_settings = standard_fractal_pattern_settings(); uv_settings.pattern_seed += 1u; PatternSample u = sample_fractal_pattern(uv_settings, uv, TIME); u = multiply(u, 0.25); u.v += uv.x; u.dx += 1.0; uv_settings.pattern_seed += 1u; PatternSample v = sample_fractal_pattern(uv_settings, uv, TIME);

Separate seeds.

Vector Noise

Let's separate our UV warping code from the sample_noise() function, introducing a sample_vector_noise() function that produces a 2D vector via two pattern samples, made available via output parameters.

void sample_vector_noise(vec2 uv, out PatternSample u, out PatternSample v) { FractalPatternSettings uv_settings = standard_fractal_pattern_settings(); uv_settings.pattern_seed += 1u; u = sample_fractal_pattern(uv_settings, uv, TIME); uv_settings.pattern_seed += 1u; v = sample_fractal_pattern(uv_settings, uv, TIME); }

Let's also center the vector so it can be both positive and negative, by subtracting 0.5 from its output values, which doesn't affect its derivatives.

u = sample_fractal_pattern(uv_settings, uv, TIME); u.v -= 0.5; uv_settings.pattern_seed += 1u; v = sample_fractal_pattern(uv_settings, uv, TIME); v.v -= 0.5;

We'll use this vector noise as an offset when sampling the final pattern. Let's make the offset strength configurable.

uniform bool vertex_displacement = true; uniform float offset_strength : hint_range(-1.0, 1.0) = 0.0;

Use the vector noise and offset strength in sample_noise().

//FractalPatternSettings uv_settings = standard_fractal_pattern_settings(); //uv_settings.pattern_seed += 1u; PatternSample u, v; //u = multiply(u, 0.25); sample_vector_noise(uv, u, v); u = multiply(u, offset_strength); v = multiply(v, offset_strength); u.v += uv.x; u.dx += 1.0; //uv_settings.pattern_seed += 1u; //PatternSample v = sample_fractal_pattern(uv_settings, uv, TIME); //v = multiply(v, 0.25); v.v += uv.y; v.dy += 1.0;

Offset strength from −1 to 1.

Curl Noise

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →