This patch improves the isotropic Gabor noise UI controls such that variations happen in both directions of the base orientation, as opposed to being biased in the positive direction only. Thanks to Charlie Jolly for suggesting this improvement.
341 lines
15 KiB
Plaintext
341 lines
15 KiB
Plaintext
/* SPDX-FileCopyrightText: 2024 Blender Foundation
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 */
|
|
|
|
/* Implements Gabor noise based on the paper:
|
|
*
|
|
* Lagae, Ares, et al. "Procedural noise using sparse Gabor convolution." ACM Transactions on
|
|
* Graphics (TOG) 28.3 (2009): 1-10.
|
|
*
|
|
* But with the improvements from the paper:
|
|
*
|
|
* Tavernier, Vincent, et al. "Making gabor noise fast and normalized." Eurographics 2019-40th
|
|
* Annual Conference of the European Association for Computer Graphics. 2019.
|
|
*
|
|
* And compute the Phase and Intensity of the Gabor based on the paper:
|
|
*
|
|
* Tricard, Thibault, et al. "Procedural phasor noise." ACM Transactions on Graphics (TOG) 38.4
|
|
* (2019): 1-13.
|
|
*/
|
|
|
|
#include "node_hash.h"
|
|
#include "stdcycles.h"
|
|
#include "vector2.h"
|
|
#include "vector4.h"
|
|
|
|
#define vector3 point
|
|
|
|
/* The original Gabor noise paper specifies that the impulses count for each cell should be
|
|
* computed by sampling a Poisson distribution whose mean is the impulse density. However,
|
|
* Tavernier's paper showed that stratified Poisson point sampling is better assuming the weights
|
|
* are sampled using a Bernoulli distribution, as shown in Figure (3). By stratified sampling, they
|
|
* mean a constant number of impulses per cell, so the stratification is the grid itself in that
|
|
* sense, as described in the supplementary material of the paper. */
|
|
#define IMPULSES_COUNT 8
|
|
|
|
/* Computes a 2D Gabor kernel based on Equation (6) in the original Gabor noise paper. Where the
|
|
* frequency argument is the F_0 parameter and the orientation argument is the w_0 parameter. We
|
|
* assume the Gaussian envelope has a unit magnitude, that is, K = 1. That is because we will
|
|
* eventually normalize the final noise value to the unit range, so the multiplication by the
|
|
* magnitude will be canceled by the normalization. Further, we also assume a unit Gaussian width,
|
|
* that is, a = 1. That is because it does not provide much artistic control. It follows that the
|
|
* Gaussian will be truncated at pi.
|
|
*
|
|
* To avoid the discontinuities caused by the aforementioned truncation, the Gaussian is windowed
|
|
* using a Hann window, that is because contrary to the claim made in the original Gabor paper,
|
|
* truncating the Gaussian produces significant artifacts especially when differentiated for bump
|
|
* mapping. The Hann window is C1 continuous and has limited effect on the shape of the Gaussian,
|
|
* so it felt like an appropriate choice.
|
|
*
|
|
* Finally, instead of computing the Gabor value directly, we instead use the complex phasor
|
|
* formulation described in section 3.1.1 in Tricard's paper. That's done to be able to compute the
|
|
* phase and intensity of the Gabor noise after summation based on equations (8) and (9). The
|
|
* return value of the Gabor kernel function is then a complex number whose real value is the
|
|
* value computed in the original Gabor noise paper, and whose imaginary part is the sine
|
|
* counterpart of the real part, which is the only extra computation in the new formulation.
|
|
*
|
|
* Note that while the original Gabor noise paper uses the cosine part of the phasor, that is, the
|
|
* real part of the phasor, we use the sine part instead, that is, the imaginary part of the
|
|
* phasor, as suggested by Tavernier's paper in "Section 3.3. Instance stationarity and
|
|
* normalization", to ensure a zero mean, which should help with normalization. */
|
|
vector2 compute_2d_gabor_kernel(vector2 position, float frequency, float orientation)
|
|
{
|
|
float distance_squared = dot(position, position);
|
|
float hann_window = 0.5 + 0.5 * cos(M_PI * distance_squared);
|
|
float gaussian_envelop = exp(-M_PI * distance_squared);
|
|
float windowed_gaussian_envelope = gaussian_envelop * hann_window;
|
|
|
|
vector2 frequency_vector = frequency * vector2(cos(orientation), sin(orientation));
|
|
float angle = 2.0 * M_PI * dot(position, frequency_vector);
|
|
vector2 phasor = vector2(cos(angle), sin(angle));
|
|
|
|
return windowed_gaussian_envelope * phasor;
|
|
}
|
|
|
|
/* Computes the approximate standard deviation of the zero mean normal distribution representing
|
|
* the amplitude distribution of the noise based on Equation (9) in the original Gabor noise paper.
|
|
* For simplicity, the Hann window is ignored and the orientation is fixed since the variance is
|
|
* orientation invariant. We start integrating the squared Gabor kernel with respect to x:
|
|
*
|
|
* \int_{-\infty}^{-\infty} (e^{- \pi (x^2 + y^2)} cos(2 \pi f_0 x))^2 dx
|
|
*
|
|
* Which gives:
|
|
*
|
|
* \frac{(e^{2 \pi f_0^2}-1) e^{-2 \pi y^2 - 2 pi f_0^2}}{2^\frac{3}{2}}
|
|
*
|
|
* Then we similarly integrate with respect to y to get:
|
|
*
|
|
* \frac{1 - e^{-2 \pi f_0^2}}{4}
|
|
*
|
|
* Secondly, we note that the second moment of the weights distribution is 0.5 since it is a
|
|
* fair Bernoulli distribution. So the final standard deviation expression is square root the
|
|
* integral multiplied by the impulse density multiplied by the second moment.
|
|
*
|
|
* Note however that the integral is almost constant for all frequencies larger than one, and
|
|
* converges to an upper limit as the frequency approaches infinity, so we replace the expression
|
|
* with the following limit:
|
|
*
|
|
* \lim_{x \to \infty} \frac{1 - e^{-2 \pi f_0^2}}{4}
|
|
*
|
|
* To get an approximation of 0.25. */
|
|
float compute_2d_gabor_standard_deviation()
|
|
{
|
|
float integral_of_gabor_squared = 0.25;
|
|
float second_moment = 0.5;
|
|
return sqrt(IMPULSES_COUNT * second_moment * integral_of_gabor_squared);
|
|
}
|
|
|
|
/* Computes the Gabor noise value at the given position for the given cell. This is essentially the
|
|
* sum in Equation (8) in the original Gabor noise paper, where we sum Gabor kernels sampled at a
|
|
* random position with a random weight. The orientation of the kernel is constant for anisotropic
|
|
* noise while it is random for isotropic noise. The original Gabor noise paper mentions that the
|
|
* weights should be uniformly distributed in the [-1, 1] range, however, Tavernier's paper showed
|
|
* that using a Bernoulli distribution yields better results, so that is what we do. */
|
|
vector2 compute_2d_gabor_noise_cell(
|
|
vector2 cell, vector2 position, float frequency, float isotropy, float base_orientation)
|
|
|
|
{
|
|
vector2 noise = vector2(0.0, 0.0);
|
|
for (int i = 0; i < IMPULSES_COUNT; ++i) {
|
|
/* Compute unique seeds for each of the needed random variables. */
|
|
vector3 seed_for_orientation = vector3(cell.x, cell.y, i * 3);
|
|
vector3 seed_for_kernel_center = vector3(cell.x, cell.y, i * 3 + 1);
|
|
vector3 seed_for_weight = vector3(cell.x, cell.y, i * 3 + 2);
|
|
|
|
/* For isotropic noise, add a random orientation amount, while for anisotropic noise, use the
|
|
* base orientation. Linearly interpolate between the two cases using the isotropy factor. Note
|
|
* that the random orientation range spans pi as opposed to two pi, that's because the Gabor
|
|
* kernel is symmetric around pi. */
|
|
float random_orientation = (hash_vector3_to_float(seed_for_orientation) - 0.5) * M_PI;
|
|
float orientation = base_orientation + random_orientation * isotropy;
|
|
|
|
vector2 kernel_center = hash_vector3_to_vector2(seed_for_kernel_center);
|
|
vector2 position_in_kernel_space = position - kernel_center;
|
|
|
|
/* The kernel is windowed beyond the unit distance, so early exit with a zero for points that
|
|
* are further than a unit radius. */
|
|
if (dot(position_in_kernel_space, position_in_kernel_space) >= 1.0) {
|
|
continue;
|
|
}
|
|
|
|
/* We either add or subtract the Gabor kernel based on a Bernoulli distribution of equal
|
|
* probability. */
|
|
float weight = hash_vector3_to_float(seed_for_weight) < 0.5 ? -1.0 : 1.0;
|
|
|
|
noise += weight * compute_2d_gabor_kernel(position_in_kernel_space, frequency, orientation);
|
|
}
|
|
return noise;
|
|
}
|
|
|
|
/* Computes the Gabor noise value by dividing the space into a grid and evaluating the Gabor noise
|
|
* in the space of each cell of the 3x3 cell neighborhood. */
|
|
vector2 compute_2d_gabor_noise(vector2 coordinates,
|
|
float frequency,
|
|
float isotropy,
|
|
float base_orientation)
|
|
{
|
|
vector2 cell_position = floor(coordinates);
|
|
vector2 local_position = coordinates - cell_position;
|
|
|
|
vector2 sum = vector2(0.0, 0.0);
|
|
for (int j = -1; j <= 1; j++) {
|
|
for (int i = -1; i <= 1; i++) {
|
|
vector2 cell_offset = vector2(i, j);
|
|
vector2 current_cell_position = cell_position + cell_offset;
|
|
vector2 position_in_cell_space = local_position - cell_offset;
|
|
sum += compute_2d_gabor_noise_cell(
|
|
current_cell_position, position_in_cell_space, frequency, isotropy, base_orientation);
|
|
}
|
|
}
|
|
|
|
return sum;
|
|
}
|
|
|
|
/* Identical to compute_2d_gabor_kernel, except it is evaluated in 3D space. Notice that Equation
|
|
* (6) in the original Gabor noise paper computes the frequency vector using (cos(w_0), sin(w_0)),
|
|
* which we also do in the 2D variant, however, for 3D, the orientation is already a unit frequency
|
|
* vector, so we just need to scale it by the frequency value. */
|
|
vector2 compute_3d_gabor_kernel(vector3 position, float frequency, vector3 orientation)
|
|
{
|
|
float distance_squared = dot(position, position);
|
|
float hann_window = 0.5 + 0.5 * cos(M_PI * distance_squared);
|
|
float gaussian_envelop = exp(-M_PI * distance_squared);
|
|
float windowed_gaussian_envelope = gaussian_envelop * hann_window;
|
|
|
|
vector3 frequency_vector = frequency * orientation;
|
|
float angle = 2.0 * M_PI * dot(position, frequency_vector);
|
|
vector2 phasor = vector2(cos(angle), sin(angle));
|
|
|
|
return windowed_gaussian_envelope * phasor;
|
|
}
|
|
|
|
/* Identical to compute_2d_gabor_standard_deviation except we do triple integration in 3D. The only
|
|
* difference is the denominator in the integral expression, which is 2^{5 / 2} for the 3D case
|
|
* instead of 4 for the 2D case. Similarly, the limit evaluates to 1 / (4 * sqrt(2)). */
|
|
float compute_3d_gabor_standard_deviation()
|
|
{
|
|
float integral_of_gabor_squared = 1.0 / (4.0 * M_SQRT2);
|
|
float second_moment = 0.5;
|
|
return sqrt(IMPULSES_COUNT * second_moment * integral_of_gabor_squared);
|
|
}
|
|
|
|
/* Computes the orientation of the Gabor kernel such that it is constant for anisotropic
|
|
* noise while it is random for isotropic noise. We randomize in spherical coordinates for a
|
|
* uniform distribution. */
|
|
vector3 compute_3d_orientation(vector3 orientation, float isotropy, vector4 seed)
|
|
{
|
|
/* Return the base orientation in case we are completely anisotropic. */
|
|
if (isotropy == 0.0) {
|
|
return orientation;
|
|
}
|
|
|
|
/* Compute the orientation in spherical coordinates. */
|
|
float inclination = acos(orientation.z);
|
|
float azimuth = sign(orientation.y) *
|
|
acos(orientation.x / length(vector2(orientation.x, orientation.y)));
|
|
|
|
/* For isotropic noise, add a random orientation amount, while for anisotropic noise, use the
|
|
* base orientation. Linearly interpolate between the two cases using the isotropy factor. Note
|
|
* that the random orientation range is to pi as opposed to two pi, that's because the Gabor
|
|
* kernel is symmetric around pi. */
|
|
vector2 random_angles = hash_vector4_to_vector2(seed) * M_PI;
|
|
inclination += random_angles.x * isotropy;
|
|
azimuth += random_angles.y * isotropy;
|
|
|
|
/* Convert back to Cartesian coordinates, */
|
|
return vector3(
|
|
sin(inclination) * cos(azimuth), sin(inclination) * sin(azimuth), cos(inclination));
|
|
}
|
|
|
|
vector2 compute_3d_gabor_noise_cell(
|
|
vector3 cell, vector3 position, float frequency, float isotropy, vector3 base_orientation)
|
|
|
|
{
|
|
vector2 noise = vector2(0.0, 0.0);
|
|
for (int i = 0; i < IMPULSES_COUNT; ++i) {
|
|
/* Compute unique seeds for each of the needed random variables. */
|
|
vector4 seed_for_orientation = vector4(cell.x, cell.y, cell.z, i * 3);
|
|
vector4 seed_for_kernel_center = vector4(cell.x, cell.y, cell.z, i * 3 + 1);
|
|
vector4 seed_for_weight = vector4(cell.x, cell.y, cell.z, i * 3 + 2);
|
|
|
|
vector3 orientation = compute_3d_orientation(base_orientation, isotropy, seed_for_orientation);
|
|
|
|
vector3 kernel_center = hash_vector4_to_vector3(seed_for_kernel_center);
|
|
vector3 position_in_kernel_space = position - kernel_center;
|
|
|
|
/* The kernel is windowed beyond the unit distance, so early exit with a zero for points that
|
|
* are further than a unit radius. */
|
|
if (dot(position_in_kernel_space, position_in_kernel_space) >= 1.0) {
|
|
continue;
|
|
}
|
|
|
|
/* We either add or subtract the Gabor kernel based on a Bernoulli distribution of equal
|
|
* probability. */
|
|
float weight = hash_vector4_to_float(seed_for_weight) < 0.5 ? -1.0 : 1.0;
|
|
|
|
noise += weight * compute_3d_gabor_kernel(position_in_kernel_space, frequency, orientation);
|
|
}
|
|
return noise;
|
|
}
|
|
|
|
/* Identical to compute_2d_gabor_noise but works in the 3D neighborhood of the noise. */
|
|
vector2 compute_3d_gabor_noise(vector3 coordinates,
|
|
float frequency,
|
|
float isotropy,
|
|
vector3 base_orientation)
|
|
{
|
|
vector3 cell_position = floor(coordinates);
|
|
vector3 local_position = coordinates - cell_position;
|
|
|
|
vector2 sum = vector2(0.0, 0.0);
|
|
for (int k = -1; k <= 1; k++) {
|
|
for (int j = -1; j <= 1; j++) {
|
|
for (int i = -1; i <= 1; i++) {
|
|
vector3 cell_offset = vector3(i, j, k);
|
|
vector3 current_cell_position = cell_position + cell_offset;
|
|
vector3 position_in_cell_space = local_position - cell_offset;
|
|
sum += compute_3d_gabor_noise_cell(
|
|
current_cell_position, position_in_cell_space, frequency, isotropy, base_orientation);
|
|
}
|
|
}
|
|
}
|
|
|
|
return sum;
|
|
}
|
|
|
|
shader node_gabor_texture(int use_mapping = 0,
|
|
matrix mapping = matrix(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
|
string type = "2D",
|
|
vector3 Vector = P,
|
|
float Scale = 5.0,
|
|
float Frequency = 2.0,
|
|
float Anisotropy = 1.0,
|
|
float Orientation2D = M_PI / 4.0,
|
|
vector3 Orientation3D = vector3(M_SQRT2, M_SQRT2, 0.0),
|
|
output float Value = 0.0,
|
|
output float Phase = 0.0,
|
|
output float Intensity = 0.0)
|
|
{
|
|
vector3 coordinates = Vector;
|
|
if (use_mapping) {
|
|
coordinates = transform(mapping, coordinates);
|
|
}
|
|
|
|
vector3 scaled_coordinates = coordinates * Scale;
|
|
float isotropy = 1.0 - clamp(Anisotropy, 0.0, 1.0);
|
|
float frequency = max(0.001, Frequency);
|
|
|
|
vector2 phasor = vector2(0.0, 0.0);
|
|
float standard_deviation = 1.0;
|
|
if (type == "2D") {
|
|
phasor = compute_2d_gabor_noise(
|
|
vector2(scaled_coordinates.x, scaled_coordinates.y), frequency, isotropy, Orientation2D);
|
|
standard_deviation = compute_2d_gabor_standard_deviation();
|
|
}
|
|
else if (type == "3D") {
|
|
vector3 orientation = normalize(vector(Orientation3D));
|
|
phasor = compute_3d_gabor_noise(scaled_coordinates, frequency, isotropy, orientation);
|
|
standard_deviation = compute_3d_gabor_standard_deviation();
|
|
}
|
|
else {
|
|
error("Unknown type!");
|
|
}
|
|
|
|
/* Normalize the noise by dividing by six times the standard deviation, which was determined
|
|
* empirically. */
|
|
float normalization_factor = 6.0 * standard_deviation;
|
|
|
|
/* As discussed in compute_2d_gabor_kernel, we use the imaginary part of the phasor as the Gabor
|
|
* value. But remap to [0, 1] from [-1, 1]. */
|
|
Value = (phasor.y / normalization_factor) * 0.5 + 0.5;
|
|
|
|
/* Compute the phase based on equation (9) in Tricard's paper. But remap the phase into the
|
|
* [0, 1] range. */
|
|
Phase = (atan2(phasor.y, phasor.x) + M_PI) / (2.0 * M_PI);
|
|
|
|
/* Compute the intensity based on equation (8) in Tricard's paper. */
|
|
Intensity = length(phasor) / normalization_factor;
|
|
}
|
|
|
|
#undef vector3
|