Voronoi Derivatives
Derivatives of Shortest Distance
- Support derivatives of Voronoi noise.
- Calculate derivatives for Worley and Chebyshev noise.
- Create exponential and polynomial smooth Worley variants.
This is the fourth tutorial in a series about pseudorandom surfaces. In it we will calculate derivatives of Voronoi noise.
This tutorial is made with Unity 2020.3.38f1.
Worley Noise
We start with the Worley version of Voronoi noise. Add F1, F2, and F2−F1 variants of it to ProceduralSurface
.
static SurfaceJobScheduleDelegate[,] surfaceJobs = { …, { SurfaceJob<Voronoi1D<LatticeNormal, Worley, F1>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, Worley, F1>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, Worley, F1>>.ScheduleParallel }, { SurfaceJob<Voronoi1D<LatticeNormal, Worley, F2>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, Worley, F2>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, Worley, F2>>.ScheduleParallel }, { SurfaceJob<Voronoi1D<LatticeNormal, Worley, F2MinusF1>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, Worley, F2MinusF1>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, Worley, F2MinusF1>>.ScheduleParallel } }; public enum NoiseType { Perlin€, PerlinSmoothTurbulence, PerlinValue, Simplex€, SimplexSmoothTurbulence, SimplexValue, VoronoiWorleyF1, VoronoiWorleyF2, VoronoiWorleyF2MinusF1 }
Voronoi Data
Because Voronoi noise is constructed by keeping track of the distance to the nearest cell point we also have to keep track of the derivatives matching the directing to that point. To also support F2 a second distance has to be stored as well. So we need derivates for both.
Create a new struct type to store the needed data in Noise.Voronoi, containing two samples. As we'll use the same data in a different way later, let's give it a non-specific VoronoiData
name with Sample4
fields a
and b
.
public struct VoronoiData { public Sample4 a, b; }
We'll need to select the minimum of two samples, so let's add a convenient static Select
method for this to Sample4
, based on math.select
.
public static Sample4 Select (Sample4 f, Sample4 t, bool4 b) => new Sample4 { v = select(f.v, t.v, b), dx = select(f.dx, t.dx, b), dy = select(f.dy, t.dy, b), dz = select(f.dz, t.dz, b) };
Then replace the UpdateVoronoiMinima
method with UpdateVoronoiData
, which does the same thing but with VoronoiData
and Sample4
values.
//static float4x2 UpdateVoronoiMinima (float4x2 minima, float4 distances) {static VoronoiData UpdateVoronoiData (VoronoiData data, Sample4 sample) { bool4 newMinimum = sample.v < data.a.v; data.b = Sample4.Select( Sample4.Select(data.b, sample, sample.v < data.b.v), data.a, newMinimum ); data.a = Sample4.Select(data.a, sample, newMinimum); return data; }
Now we have to adjust the Voronoi methods so they work correctly. First adjust Voronoi1D.GetNoise4
so it works with the new data and sets its derivative correctly. Adjust the 2D and 3D variants likewise.
//float4x2 minima = 2f;VoronoiData data = default; data.a.v = data.b.v = 2f; for (int u = -1; u <= 1; u++) { SmallXXHash4 h = hash.Eat(l.ValidateSingleStep(x.p0 + u, frequency)); data = UpdateVoronoiData(data, d.GetDistance(h.Floats01A + u - x.g0)); } Sample4 s = default(F).Evaluate(d.Finalize1D(data)); s.dx *= frequency; return s;
To make this compile we have to change the IVoronoiDistance
interface to also work with Sample4
and VoronoiData
.
public interface IVoronoiDistance { Sample4 GetDistance (float4 x); Sample4 GetDistance (float4 x, float4 y); Sample4 GetDistance (float4 x, float4 y, float4 z); VoronoiData Finalize1D (VoronoiData data); VoronoiData Finalize2D (VoronoiData data); VoronoiData Finalize3D (VoronoiData data); }
Adjust the Worley
and Chebyshev
implementation to match. All changes are trivial type adjustments, except for Worley.Finalize2D
and Worley.Finalize3D
, which need a bit more work because they perform the delayed square root operation on the distances. Ignore the derivatives for now.
public VoronoiData Finalize2D (VoronoiData data) { data.a.v = sqrt(min(data.a.v, 1f)); data.b.v = sqrt(min(data.b.v, 1f)); return data; } public VoronoiData Finalize3D (VoronoiData data) => Finalize2D(data);
Finally, adjust the IVoronoiFunction
interface and its F1
, F2
, and F2MinusF1
implementations to also act on the new data.
public interface IVoronoiFunction { Sample4 Evaluate (VoronoiData data); } public struct F1 : IVoronoiFunction { public Sample4 Evaluate (VoronoiData data) => data.a; } public struct F2 : IVoronoiFunction { public Sample4 Evaluate (VoronoiData data) => data.b; } public struct F2MinusF1 : IVoronoiFunction { public Sample4 Evaluate (VoronoiData data) => data.b - data.a; }
At this point Voronoi noise should work as before, but is now ready to also provide analytical derivatives.
1D Derivatives
Once again we start with the derivatives for a single dimension. Adjust Worley.GetDistance
with a single parameter so it explicitly returns a Sample4
value.
public Sample4 GetDistance (float4 x) => new Sample4 { v = abs(x) };
In a single dimension distance is simply `d(x)=|x|`, so the derivative is the same as for turbulence gradients: `d'(x)=x/|x|`. But because we're evaluating increasing distances the sign of the derivative is flipped compared to turbulence.
v = abs(x), dx = select(-1f, 1f, x < 0f)
Note that we get derivative discontinuities in valleys just like with turbulence, and also at peaks. So the results won't be smooth.
2D Derivatives
For the 2D case we have to normalize a 2D vector, so we have the function `d(x,z)=sqrt(x^2+z^2)`. The partial X derivative of that is `d'_x(x,z)=-x/(d(x,z))`.
Because we delay the square root operation we also delay the scaling of the derivative. We'll use `x` and `z` verbatim in GetDistance
for two dimensions.
public Sample4 GetDistance (float4 x, float4 y) => new Sample4 { v = x * x + y * y, dx = x, dz = y };
Next, adjust Finalize2D
so it applies the square root to the value and uses a select
statement to enforce keeping distances only if they are less than 1.
public VoronoiData Finalize2D (VoronoiData data) {//data.a.v = sqrt(min(data.a.v, 1f));//data.b.v = sqrt(min(data.b.v, 1f));bool4 keepA = data.a.v < 1f; data.a.v = select(1f, sqrt(data.a.v), keepA); bool4 keepB = data.b.v < 1f; data.b.v = select(1f, sqrt(data.b.v), keepB); return data; }
Then add the division and sign flip of the derivatives if they are to be kept, and set them to zero otherwise.
data.a.v = select(1f, sqrt(data.a.v), keepA); data.a.dx = select(0f, -data.a.dx / data.a.v, keepA); data.a.dz = select(0f, -data.a.dz / data.a.v, keepA); bool4 keepB = data.b.v < 1f; data.b.v = select(1f, sqrt(data.b.v), keepB); data.b.dx = select(0f, -data.b.dx / data.b.v, keepB); data.b.dz = select(0f, -data.b.dz / data.b.v, keepB);
3D Derivatives
Adjust GetDistance
for three dimensions in the same way that we adjusted the 2D version.
public Sample4 GetDistance (float4 x, float4 y, float4 z) => new Sample4 { v = x * x + y * y + z * z, dx = x, dy = y, dz = z };
Finalize3D
becomes a copy of Finalize2D
that includes all three derivatives.
public VoronoiData Finalize3D (VoronoiData data) { bool4 keepA = data.a.v < 1f; data.a.v = select(1f, sqrt(data.a.v), keepA); data.a.dx = select(0f, -data.a.dx / data.a.v, keepA); data.a.dy = select(0f, -data.a.dy / data.a.v, keepA); data.a.dz = select(0f, -data.a.dz / data.a.v, keepA); bool4 keepB = data.b.v < 1f; data.b.v = select(1f, sqrt(data.b.v), keepB); data.b.dx = select(0f, -data.b.dx / data.b.v, keepB); data.b.dy = select(0f, -data.b.dy / data.b.v, keepB); data.b.dz = select(0f, -data.b.dz / data.b.v, keepB); return data; }
We can make the 2D versions forward to the 3D versions because Burst will detect that the Y derivative will always be zero and optimize it away.
public Sample4 GetDistance (float4 x, float4 y) => GetDistance(x, 0f, y); … public VoronoiData Finalize2D (VoronoiData data) => Finalize3D(data);
Chebyshev Noise
With Worley noise completed we also add Chebyshev noise to ProceduralNoise
.
static SurfaceJobScheduleDelegate[,] surfaceJobs = { …, { SurfaceJob<Voronoi1D<LatticeNormal, Worley, F1>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, Chebyshev, F1>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, Chebyshev, F1>>.ScheduleParallel }, { SurfaceJob<Voronoi1D<LatticeNormal, Worley, F2>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, Chebyshev, F2>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, Chebyshev, F2>>.ScheduleParallel }, { SurfaceJob<Voronoi1D<LatticeNormal, Worley, F2MinusF1>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, Chebyshev, F2MinusF1>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, Chebyshev, F2MinusF1>>.ScheduleParallel } }; public enum NoiseType { … VoronoiWorleyF1, VoronoiWorleyF2, VoronoiWorleyF2MinusF1, VoronoiChebyshevF1, VoronoiChebyshevF2, VoronoiChebyshevF2MinusF1 }
1D Worley and Chebyshev noise are identical, so for that case Chebyshev.GetDistance
can simply rely on the implementation of Worley
.
public Sample4 GetDistance (float4 x) => default(Worley).GetDistance(x);
2D Derivatives
Chebyshev noise effectively works like separate 1D noise in all used dimensions, picking the greatest distance. Adjust the 2D version of GetDistance
so it returns a Sample4
value. Do this with select
logic, meaning that we keep X if it is greater than Y.
public Sample4 GetDistance (float4 x, float4 y) {//=> max(abs(x), abs(y));bool4 keepX = abs(x) > abs(y); return new Sample4 { v = select(abs(y), abs(x), keepX) }; }
Then add the X derivative, if the X result it kept.
v = select(abs(y), abs(x), keepX), dx = select(0f, select(-1f, 1f, x < 0f), keepX)
Likewise for the Z derivative, but as the alternative option based on Y.
dx = select(0f, select(-1f, 1f, x < 0f), keepX), dz = select(select(-1f, 1f, y < 0f), 0f, keepX)
3D Derivatives
Also adjust the 3D version of GetDistance
so it returns an Sample4
value, again using select
logic. In this case we keep X if it is greater than both Y and Z. Otherwise we keep Y if it is greater than Z.
public Sample4 GetDistance (float4 x, float4 y, float4 z) { bool4 keepX = abs(x) > abs(y) & abs(x) > abs(z); bool4 keepY = abs(y) > abs(z); return new Sample4 { v = select(select(abs(z), abs(y), keepY), abs(x), keepX) }; }
The X derivative is exactly the same as for 2D.
v = select(select(abs(z), abs(y), keepY), abs(x), keepX), dx = select(0f, select(-1f, 1f, x < 0f), keepX)
The Y derivative requires an extra select
, because it is only used when Y is kept while X is not.
dx = select(0f, select(-1f, 1f, x < 0f), keepX), dy = select(select(0f, select(-1f, 1f, y < 0f), keepY), 0f, keepX)
And the Z derivative is for the only remaining option.
dy = select(select(0f, select(-1f, 1f, y < 0f), keepY), 0f, keepX), dz = select(select(select(-1f, 1f, z < 0f), 0f, keepY), 0f, keepX)
Smooth Worley Noise
Like the turbulence noise variants, Voronoi noise isn't smooth and can thus produce jagged ridges and ugly shading when used to displace a surface. As Chebyshev noise is defined by its straight sharp ridges we leave it be, but Worley noise is often used for organic patterns so a smooth variant of that would be useful.
We cannot fix the ridges with a simple application of the smoothstep function on top of the minimum distance, because the ridges occur at arbitrary distances. Smoothing them requires applying a filter to the process of selecting the minimum distance.
To visualize the problem I will base it on the following configurable Desmos graph, which contains three 1D Voronoi cells with one point each:
Our goal is to smooth that graph.
Log-Sum-Exp
One way to come up with a smooth minimum is to apply a function known as Log-Sum-Exp: `lse(v)=logsum_(i=1)^(n)e^(v_i)` where `v` is a vector with `n` values, `e` is Euler's number and `log` is the natural logarithm. For three values it would look like `lse(a,b,c)=log(e^a+e^b+e^c)`.
The idea is that `log(e^x)=x` but if we take the logarithm of the sum of multiple parts we merge them somehow, depending on the exact math we use. To create a smoothing effect approximating the minimum we can use `sm(v)=-lse(-v)`.
The result rounds ridges and is strictly smaller than a straight minimum, but isn't useful yet. We can control how closely we approximate the true minimum by adding a smoothing factor `s` so we get `sm(v)=(lse(-sv))/-s`.
With the smoothing factor set to 10 we get a fairly good approximation of the minimum with rounded ridges. However, the result can fall outside the 0–1 range. That we can end up with negative values is already clear, but in the case of 2D and 3D noise it can also exceed 1 because the true minimum can go up to √3 in three dimensions. If we scale up the graph so each 1D cell has a size of √3 then we can see what happens at such distances.
We can clamp the result to the 0–1 range, then apply the smoothstep function to smooth out the edges. This also rounds the valleys so we end up with a smooth curve everywhere.
Voronoi Data
To support this alternative approach of calculating the distance we have to make the way we evaluate the Voronoi data vary per variant. So add the UpdateVoronoiData
method signature to IVoronoiDistance
along with an InitialData
getter property to initialize the data.
VoronoiData UpdateVoronoiData (VoronoiData data, Sample4 sample); VoronoiData InitialData { get; }
Then copy the code from the existing UpdateVoronoiData
static method into a new method that implements it in Worley
. Also add the required property, which initializes the distances to 2.
public VoronoiData UpdateVoronoiData (VoronoiData data, Sample4 sample) { bool4 newMinimum = sample.v < data.a.v; data.b = Sample4.Select( Sample4.Select(data.b, sample, sample.v < data.b.v), data.a, newMinimum ); data.a = Sample4.Select(data.a, sample, newMinimum); return data; } public VoronoiData InitialData => new VoronoiData { a = new Sample4 { v = 2f }, b = new Sample4 { v = 2f } };
Chebyshev
has the exact same implementation, so can foward to Worley
.
public VoronoiData UpdateVoronoiData (VoronoiData data, Sample4 sample) => default(Worley).UpdateVoronoiData(data, sample); public VoronoiData InitialData => default(Worley).InitialData;
Then update Voronoi1D.GetNoise4
so it relies on the implementation of its distance type for initializing and updating the Voronoi data.
VoronoiData data = d.InitialData;//data.a.v = data.b.v = 2f;for (int u = -1; u <= 1; u++) { SmallXXHash4 h = hash.Eat(l.ValidateSingleStep(x.p0 + u, frequency)); data = d.UpdateVoronoiData(data, d.GetDistance(h.Floats01A + u - x.g0)); }
Change the 2D and 3D methods likewise, then remove the static UpdateVoronoiData
method.
//static VoronoiData UpdateVoronoiData (VoronoiData data, Sample4 sample) { … }
Smooth Variant
Now that we know how to create a smooth minimum we introduce a SmoothWorley
variant implementation of IVoronoiDistance
. Begin with a minimal stub that does nothing. Like with Worley
we can forward the 2D invocations to the 3D versions.
public struct SmoothWorley : IVoronoiDistance { public Sample4 GetDistance (float4 x) => default; public Sample4 GetDistance (float4 x, float4 y) => GetDistance(x, 0f, y); public Sample4 GetDistance (float4 x, float4 y, float4 z) => default; public VoronoiData Finalize1D (VoronoiData data) => data; public VoronoiData Finalize2D (VoronoiData data) => Finalize3D(data); public VoronoiData Finalize3D (VoronoiData data) => data; public VoronoiData UpdateVoronoiData (VoronoiData data, Sample4 sample) => data; public VoronoiData InitialData => default; }
Add the F1 version of it to ProceduralSurface
.
static SurfaceJobScheduleDelegate[,] surfaceJobs = { … { SurfaceJob<Voronoi1D<LatticeNormal, Worley, F2MinusF1>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, Worley, F2MinusF1>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, Worley, F2MinusF1>>.ScheduleParallel }, { SurfaceJob<Voronoi1D<LatticeNormal, SmoothWorley, F1>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, SmoothWorley, F1>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, SmoothWorley, F1>>.ScheduleParallel }, … }; public enum NoiseType { … VoronoiWorleyF1, VoronoiWorleyF2, VoronoiWorleyF2MinusF1, VoronoiWorleySmoothLSE, VoronoiChebyshevF1, VoronoiChebyshevF2, VoronoiChebyshevF2MinusF1 }
Smooth 1D
Beginning with one-dimensional smooth Worley noise, GetDistance
is once again the same as for regular Worley noise, because we'll perform the exponentiation later.
public Sample4 GetDistance (float4 x) => default(Worley).GetDistance(x);
The smoothing factor could be made variable, but we'll simply use a constant set to 10.
const float smoothLSE = 10f;
For this variant UpdateVoronoiData
doesn't select the minimum. It instead sums the exponentiations. This means that the data must start at zero, which is the current default.
public VoronoiData UpdateVoronoiData (VoronoiData data, Sample4 sample) { float4 e = exp(-smoothLSE * sample.v); data.a.v += e; return data; }
We must also sum the derivatives of `e^(-sx)`, which are `-sx'e^(-sx)`. We'll omit the final multiplication with `-s` here, as we can cancel it later, so only calculate `x'e^(-sx)`.
data.a.v += e; data.a.dx += e * sample.dx; data.a.dy += e * sample.dy; data.a.dz += e * sample.dz;
In Finalize1D
we take the logarithm of the value and divide by the negative smoothing factor.
public VoronoiData Finalize1D (VoronoiData data) { data.a.v = log(data.a.v) / -smoothLSE; return data; }
We have to calculate the derivative of `log(f(x))/-s` as well, which is `(f'(x))/(-s f(x))`. Here the multiplication with `-s` that we skipped gets canceled by omitting the division.
data.a.dx /= data.a.v; data.a.v = log(data.a.v) / -smoothLSE;
With the LSE portion complete, we move on to apply smoothstep on top of it. As we're now using that function in two places let's add a convenient property for it to Sample4
, for which we can copy the code from the Smoothstep
gradient modifier.
public Sample4 Smoothstep€ { get { Sample4 s = this; float4 d = 6f * v * (1f - v); s.dx *= d; s.dy *= d; s.dz *= d; s.v *= v * (3f - 2f * v); return s; } }
Smoothstep.EvaluateCombined
can then be simplified.
public Sample4 EvaluateCombined (Sample4 value) => default(G).EvaluateCombined(value).Smoothstep€;
And we can use the property in SmoothWorley.Finalize1D
.
data.a.v = log(data.a.v) / -smoothLSE; data.a = data.a.Smoothstep€;
The final step it to clamp the noise, before applying smoothstep. Because 1D distances never exceed 1 we only have to check for negative values.
data.a = Sample4.Select(default, data.a.Smoothstep€, data.a.v > 0f);
Smooth 2D and 3D
The 2D and 3D versions of SmoothWorley.GetDistance
must provide the linear distance, so must calculate the square root immediately.
public Sample4 GetDistance (float4 x, float4 y, float4 z) { float4 v = sqrt(x * x + y * y + z * z); return new Sample4 { v = v, dx = x / -v, dy = y / -v, dz = y / -v }; }
Finalization is the same as for 1D but with all three derivatives.
public VoronoiData Finalize3D (VoronoiData data) { data.a.dx /= data.a.v; data.a.dy /= data.a.v; data.a.dz /= data.a.v; data.a.v = log(data.a.v) / -smoothLSE; data.a = Sample4.Select(default, data.a.Smoothstep€, data.a.v > 0f); return data; }
And it also has to guard against distances exceeding 1.
data.a = Sample4.Select(default, data.a.Smoothstep€, data.a.v > 0f & data.a.v < 1f);
Polynomial Variant
One downside of the LSE function is that it is slow, because exp
and log
are slow, just like sin
is slow. Another downside is that when using a high smoothing factor the floating-point precision limitations can become a problem. So let's also support a polynomial variant that approximates the LSE approach.
The smooth minimum function that we'll use is `psm(a,b)=min(a,b)-h(a,b)` where `h(a,b)=s/4max(1-|a-b|/s,0)^2`, with smoothness applied at the end.
That function takes the standard minimum and then subtracts a smoothing function `h` that decreases as the difference between `a` and `b` increases.
The `psm` function is non-associative, meaning that `psm(psm(a,b),c)` and `psm(a,psm(b,c))` are not equal, which can lead to slight inconsistencies between cells. However, this difference is unnoticeable in practice.
What follows is a graph that compares the LSE approach at smoothness 10 in red with the polynomial approach at smoothness ¼. It shows two lines for the two ways to evaluate three distances, in blue and green, which almost perfectly overlap. The dashed orange line shows the maximum absolute difference between LSE and the two polynomial lines ×10. The dotted purple line shows the absolute difference between both polynomial lines ×100.
Add entries for this approach to ProceduralSurface
. As again we'll only support F1 let's use the F2 output of our existing SmoothWorley
variant instead of creating another new type.
{ SurfaceJob<Voronoi1D<LatticeNormal, SmoothWorley, F1>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, SmoothWorley, F1>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, SmoothWorley, F1>>.ScheduleParallel }, { SurfaceJob<Voronoi1D<LatticeNormal, SmoothWorley, F2>>.ScheduleParallel, SurfaceJob<Voronoi2D<LatticeNormal, SmoothWorley, F2>>.ScheduleParallel, SurfaceJob<Voronoi3D<LatticeNormal, SmoothWorley, F2>>.ScheduleParallel }, … VoronoiWorleyF1, VoronoiWorleyF2, VoronoiWorleyF2MinusF1, VoronoiWorleySmoothLSE, VoronoiWorleySmoothPoly,
We'll use ¼ as the constant smoothness factor for the polynomial smooth minimum in SmoothWorley
.
const float smoothLSE = 10f, smoothPoly = 0.25f;
As we're taking a smoothed minimum for F2 we must once again initialize the Voronoi data with a large value, but only b
, which we'll use to store the polynomial variant.
public VoronoiData InitialData => new VoronoiData { b = new Sample4 { v = 2f } };
Begin by storing the straight minimum in b
when updating the data.
public VoronoiData UpdateVoronoiData (VoronoiData data, Sample4 sample) { float4 e = exp(-smoothLSE * sample.v); data.a.v += e; data.a.dx += e * sample.dx; data.a.dy += e * sample.dy; data.a.dz += e * sample.dz; … data.b = Sample4.Select(data.b, sample, sample.v < data.b.v); return data; }
And apply smoothstep along with clamping when finalizing, as for the LSE approach.
public VoronoiData Finalize1D (VoronoiData data) { … data.a = Sample4.Select(default, data.a.Smoothstep, data.a.v > 0f); data.b = Sample4.Select(default, data.b.Smoothstep, data.b.v > 0f); return data; } … public VoronoiData Finalize3D (VoronoiData data) { … data.a = Sample4.Select(default, data.a.Smoothstep, data.a.v > 0f & data.a.v < 1f); data.b = Sample4.Select(default, data.b.Smoothstep, data.b.v > 0f & data.b.v < 1f); return data; }
That gets us the standard clamped minimum as F2. To apply smoothing calculate `1-|a-b|/s` before the selection and check whether smoothing needs to be applied, based on whether it is greater than zero.
float4 h = 1f - abs(data.b.v - sample.v) / smoothPoly; bool4 smooth = h > 0f; data.b = Sample4.Select(data.b, sample, sample.v < data.b.v);
Then calculate the rest of `h(a,b)` and subtract it from the minimum if smoothing is needed.
bool4 smooth = h > 0f; h = 0.25f * smoothPoly * h * h; data.b = Sample4.Select(data.b, sample, sample.v < data.b.v); data.b.v -= select(0f, h, smooth); return data;
Next, calculate the derivatives `h'(a,b)`.
float4 h = 1f - abs(data.b.v - sample.v) / smoothPoly; float4 hdx = data.b.dx - sample.dx, hdy = data.b.dy - sample.dy, hdz = data.b.dz - sample.dz; bool4 ds = data.b.v - sample.v < 0f; hdx = select(-hdx, hdx, ds) * 0.5f * h; hdy = select(-hdy, hdy, ds) * 0.5f * h; hdz = select(-hdz, hdz, ds) * 0.5f * h; bool4 smooth = h > 0f; h = 0.25f * smoothPoly * h * h;
Then subtract those derivatives from the derivatives of the minimum when appropriate.
data.b.v -= select(0f, h, smooth); data.b.dx -= select(0f, hdx, smooth); data.b.dy -= select(0f, hdy, smooth); data.b.dz -= select(0f, hdz, smooth);
The next tutorial is Spherical Elevation.