Blender V5.0
gabor.h
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2024 Blender Foundation
2 *
3 * SPDX-License-Identifier: Apache-2.0 */
4
5/* Implements Gabor noise based on the paper:
6 *
7 * Lagae, Ares, et al. "Procedural noise using sparse Gabor convolution." ACM Transactions on
8 * Graphics (TOG) 28.3 (2009): 1-10.
9 *
10 * But with the improvements from the paper:
11 *
12 * Tavernier, Vincent, et al. "Making gabor noise fast and normalized." Eurographics 2019-40th
13 * Annual Conference of the European Association for Computer Graphics. 2019.
14 *
15 * And compute the Phase and Intensity of the Gabor based on the paper:
16 *
17 * Tricard, Thibault, et al. "Procedural phasor noise." ACM Transactions on Graphics (TOG) 38.4
18 * (2019): 1-13.
19 */
20
21#pragma once
22
23#include "kernel/svm/util.h"
24
25#include "util/hash.h"
26
28
29/* The original Gabor noise paper specifies that the impulses count for each cell should be
30 * computed by sampling a Poisson distribution whose mean is the impulse density. However,
31 * Tavernier's paper showed that stratified Poisson point sampling is better assuming the weights
32 * are sampled using a Bernoulli distribution, as shown in Figure (3). By stratified sampling, they
33 * mean a constant number of impulses per cell, so the stratification is the grid itself in that
34 * sense, as described in the supplementary material of the paper. */
35#define IMPULSES_COUNT 8 // NOLINT
36
37/* Computes a 2D Gabor kernel based on Equation (6) in the original Gabor noise paper. Where the
38 * frequency argument is the F_0 parameter and the orientation argument is the w_0 parameter. We
39 * assume the Gaussian envelope has a unit magnitude, that is, K = 1. That is because we will
40 * eventually normalize the final noise value to the unit range, so the multiplication by the
41 * magnitude will be canceled by the normalization. Further, we also assume a unit Gaussian width,
42 * that is, a = 1. That is because it does not provide much artistic control. It follows that the
43 * Gaussian will be truncated at pi.
44 *
45 * To avoid the discontinuities caused by the aforementioned truncation, the Gaussian is windowed
46 * using a Hann window, that is because contrary to the claim made in the original Gabor paper,
47 * truncating the Gaussian produces significant artifacts especially when differentiated for bump
48 * mapping. The Hann window is C1 continuous and has limited effect on the shape of the Gaussian,
49 * so it felt like an appropriate choice.
50 *
51 * Finally, instead of computing the Gabor value directly, we instead use the complex phasor
52 * formulation described in section 3.1.1 in Tricard's paper. That's done to be able to compute the
53 * phase and intensity of the Gabor noise after summation based on equations (8) and (9). The
54 * return value of the Gabor kernel function is then a complex number whose real value is the
55 * value computed in the original Gabor noise paper, and whose imaginary part is the sine
56 * counterpart of the real part, which is the only extra computation in the new formulation.
57 *
58 * Note that while the original Gabor noise paper uses the cosine part of the phasor, that is, the
59 * real part of the phasor, we use the sine part instead, that is, the imaginary part of the
60 * phasor, as suggested by Tavernier's paper in "Section 3.3. Instance stationarity and
61 * normalization", to ensure a zero mean, which should help with normalization. */
63 const float frequency,
64 const float orientation)
65{
66 const float distance_squared = dot(position, position);
67 const float hann_window = 0.5f + 0.5f * cosf(M_PI_F * distance_squared);
68 const float gaussian_envelop = expf(-M_PI_F * distance_squared);
69 const float windowed_gaussian_envelope = gaussian_envelop * hann_window;
70
71 const float angle = 2.0f * M_PI_F * dot(position, polar_to_cartesian(frequency, orientation));
72 return polar_to_cartesian(windowed_gaussian_envelope, angle);
73}
74
112{
113 const float integral_of_gabor_squared = 0.25f;
114 const float second_moment = 0.5f;
115 return sqrtf(IMPULSES_COUNT * second_moment * integral_of_gabor_squared);
116}
117
118/* Computes the Gabor noise value at the given position for the given cell. This is essentially the
119 * sum in Equation (8) in the original Gabor noise paper, where we sum Gabor kernels sampled at a
120 * random position with a random weight. The orientation of the kernel is constant for anisotropic
121 * noise while it is random for isotropic noise. The original Gabor noise paper mentions that the
122 * weights should be uniformly distributed in the [-1, 1] range, however, Tavernier's paper showed
123 * that using a Bernoulli distribution yields better results, so that is what we do. */
125 const float2 position,
126 const float frequency,
127 const float isotropy,
128 const float base_orientation)
129
130{
131 float2 noise = make_float2(0.0f, 0.0f);
132 for (int i = 0; i < IMPULSES_COUNT; ++i) {
133 /* Compute unique seeds for each of the needed random variables. */
134 const float3 seed_for_orientation = make_float3(cell.x, cell.y, i * 3);
135 const float3 seed_for_kernel_center = make_float3(cell.x, cell.y, i * 3 + 1);
136 const float3 seed_for_weight = make_float3(cell.x, cell.y, i * 3 + 2);
137
138 /* For isotropic noise, add a random orientation amount, while for anisotropic noise, use the
139 * base orientation. Linearly interpolate between the two cases using the isotropy factor. Note
140 * that the random orientation range spans pi as opposed to two pi, that's because the Gabor
141 * kernel is symmetric around pi. */
142 const float random_orientation = (hash_float3_to_float(seed_for_orientation) - 0.5f) * M_PI_F;
143 const float orientation = base_orientation + random_orientation * isotropy;
144
145 const float2 kernel_center = hash_float3_to_float2(seed_for_kernel_center);
146 const float2 position_in_kernel_space = position - kernel_center;
147
148 /* The kernel is windowed beyond the unit distance, so early exit with a zero for points that
149 * are further than a unit radius. */
150 if (dot(position_in_kernel_space, position_in_kernel_space) >= 1.0f) {
151 continue;
152 }
153
154 /* We either add or subtract the Gabor kernel based on a Bernoulli distribution of equal
155 * probability. */
156 const float weight = hash_float3_to_float(seed_for_weight) < 0.5f ? -1.0f : 1.0f;
157
158 noise += weight * compute_2d_gabor_kernel(position_in_kernel_space, frequency, orientation);
159 }
160 return noise;
161}
162
163/* Computes the Gabor noise value by dividing the space into a grid and evaluating the Gabor noise
164 * in the space of each cell of the 3x3 cell neighborhood. */
166 const float frequency,
167 const float isotropy,
168 const float base_orientation)
169{
170 const float2 cell_position = floor(coordinates);
171 const float2 local_position = coordinates - cell_position;
172
173 float2 sum = make_float2(0.0f, 0.0f);
174 for (int j = -1; j <= 1; j++) {
175 for (int i = -1; i <= 1; i++) {
176 const float2 cell_offset = make_float2(i, j);
177 const float2 current_cell_position = cell_position + cell_offset;
178 const float2 position_in_cell_space = local_position - cell_offset;
180 current_cell_position, position_in_cell_space, frequency, isotropy, base_orientation);
181 }
182 }
183
184 return sum;
185}
186
187/* Identical to compute_2d_gabor_kernel, except it is evaluated in 3D space. Notice that Equation
188 * (6) in the original Gabor noise paper computes the frequency vector using (cos(w_0), sin(w_0)),
189 * which we also do in the 2D variant, however, for 3D, the orientation is already a unit frequency
190 * vector, so we just need to scale it by the frequency value. */
192 const float frequency,
193 const float3 orientation)
194{
195 const float distance_squared = dot(position, position);
196 const float hann_window = 0.5f + 0.5f * cosf(M_PI_F * distance_squared);
197 const float gaussian_envelop = expf(-M_PI_F * distance_squared);
198 const float windowed_gaussian_envelope = gaussian_envelop * hann_window;
199
200 const float3 frequency_vector = frequency * orientation;
201 const float angle = 2.0f * M_PI_F * dot(position, frequency_vector);
202 return polar_to_cartesian(windowed_gaussian_envelope, angle);
203}
204
205/* Identical to compute_2d_gabor_standard_deviation except we do triple integration in 3D. The only
206 * difference is the denominator in the integral expression, which is `2^{5 / 2}` for the 3D case
207 * instead of 4 for the 2D case. Similarly, the limit evaluates to `1 / (4 * sqrt(2))`. */
209{
210 const float integral_of_gabor_squared = 1.0f / (4.0f * M_SQRT2_F);
211 const float second_moment = 0.5f;
212 return sqrtf(IMPULSES_COUNT * second_moment * integral_of_gabor_squared);
213}
214
215/* Computes the orientation of the Gabor kernel such that it is constant for anisotropic
216 * noise while it is random for isotropic noise. We randomize in spherical coordinates for a
217 * uniform distribution. */
219 const float isotropy,
220 const float4 seed)
221{
222 /* Return the base orientation in case we are completely anisotropic. */
223 if (isotropy == 0.0f) {
224 return orientation;
225 }
226
227 /* Compute the orientation in spherical coordinates. */
228 float inclination = acosf(orientation.z);
229 float azimuth = (orientation.y < 0.0f ? -1.0f : 1.0f) *
230 acosf(orientation.x / len(make_float2(orientation.x, orientation.y)));
231
232 /* For isotropic noise, add a random orientation amount, while for anisotropic noise, use the
233 * base orientation. Linearly interpolate between the two cases using the isotropy factor. Note
234 * that the random orientation range is to pi as opposed to two pi, that's because the Gabor
235 * kernel is symmetric around pi. */
236 const float2 random_angles = hash_float4_to_float2(seed) * M_PI_F;
237 inclination += random_angles.x * isotropy;
238 azimuth += random_angles.y * isotropy;
239
240 /* Convert back to Cartesian coordinates. */
241 return spherical_to_direction(inclination, azimuth);
242}
243
245 const float3 position,
246 const float frequency,
247 const float isotropy,
248 const float3 base_orientation)
249
250{
251 float2 noise = make_float2(0.0f, 0.0f);
252 for (int i = 0; i < IMPULSES_COUNT; ++i) {
253 /* Compute unique seeds for each of the needed random variables. */
254 const float4 seed_for_orientation = make_float4(cell, i * 3);
255 const float4 seed_for_kernel_center = make_float4(cell, i * 3 + 1);
256 const float4 seed_for_weight = make_float4(cell, i * 3 + 2);
257
258 const float3 orientation = compute_3d_orientation(
259 base_orientation, isotropy, seed_for_orientation);
260
261 const float3 kernel_center = hash_float4_to_float3(seed_for_kernel_center);
262 const float3 position_in_kernel_space = position - kernel_center;
263
264 /* The kernel is windowed beyond the unit distance, so early exit with a zero for points that
265 * are further than a unit radius. */
266 if (dot(position_in_kernel_space, position_in_kernel_space) >= 1.0f) {
267 continue;
268 }
269
270 /* We either add or subtract the Gabor kernel based on a Bernoulli distribution of equal
271 * probability. */
272 const float weight = hash_float4_to_float(seed_for_weight) < 0.5f ? -1.0f : 1.0f;
273
274 noise += weight * compute_3d_gabor_kernel(position_in_kernel_space, frequency, orientation);
275 }
276 return noise;
277}
278
279/* Identical to compute_2d_gabor_noise but works in the 3D neighborhood of the noise. */
281 const float frequency,
282 const float isotropy,
283 const float3 base_orientation)
284{
285 const float3 cell_position = floor(coordinates);
286 const float3 local_position = coordinates - cell_position;
287
288 float2 sum = make_float2(0.0f, 0.0f);
289 for (int k = -1; k <= 1; k++) {
290 for (int j = -1; j <= 1; j++) {
291 for (int i = -1; i <= 1; i++) {
292 const float3 cell_offset = make_float3(i, j, k);
293 const float3 current_cell_position = cell_position + cell_offset;
294 const float3 position_in_cell_space = local_position - cell_offset;
296 current_cell_position, position_in_cell_space, frequency, isotropy, base_orientation);
297 }
298 }
299 }
300
301 return sum;
302}
303
305 ccl_private float *stack,
306 const uint type,
307 const uint stack_offsets_1,
308 const uint stack_offsets_2,
309 int offset)
310{
311 uint coordinates_stack_offset;
312 uint scale_stack_offset;
313 uint frequency_stack_offset;
314 uint anisotropy_stack_offset;
315 uint orientation_2d_stack_offset;
316 uint orientation_3d_stack_offset;
317
318 svm_unpack_node_uchar4(stack_offsets_1,
319 &coordinates_stack_offset,
320 &scale_stack_offset,
321 &frequency_stack_offset,
322 &anisotropy_stack_offset);
324 stack_offsets_2, &orientation_2d_stack_offset, &orientation_3d_stack_offset);
325
326 const float3 coordinates = stack_load_float3(stack, coordinates_stack_offset);
327
328 uint value_stack_offset;
329 uint phase_stack_offset;
330 uint intensity_stack_offset;
331
332 const uint4 node_1 = read_node(kg, &offset);
334 node_1.x, &value_stack_offset, &phase_stack_offset, &intensity_stack_offset);
335 const float scale = stack_load_float_default(stack, scale_stack_offset, node_1.y);
336 float frequency = stack_load_float_default(stack, frequency_stack_offset, node_1.z);
337 const float anisotropy = stack_load_float_default(stack, anisotropy_stack_offset, node_1.w);
338
339 const uint4 node_2 = read_node(kg, &offset);
340 const float orientation_2d = stack_load_float_default(
341 stack, orientation_2d_stack_offset, node_2.x);
342 const float3 orientation_3d = stack_load_float3(stack, orientation_3d_stack_offset);
343
344 const float3 scaled_coordinates = coordinates * scale;
345 const float isotropy = 1.0f - clamp(anisotropy, 0.0f, 1.0f);
346 frequency = max(0.001f, frequency);
347
348 float2 phasor = make_float2(0.0f, 0.0f);
349 float standard_deviation = 1.0f;
350 switch ((NodeGaborType)type) {
351 case NODE_GABOR_TYPE_2D: {
352 phasor = compute_2d_gabor_noise(make_float2(scaled_coordinates.x, scaled_coordinates.y),
353 frequency,
354 isotropy,
355 orientation_2d);
356 standard_deviation = compute_2d_gabor_standard_deviation();
357 break;
358 }
359 case NODE_GABOR_TYPE_3D: {
360 const float3 orientation = normalize(orientation_3d);
361 phasor = compute_3d_gabor_noise(scaled_coordinates, frequency, isotropy, orientation);
362 standard_deviation = compute_3d_gabor_standard_deviation();
363 break;
364 }
365 }
366
367 /* Normalize the noise by dividing by six times the standard deviation, which was determined
368 * empirically. */
369 const float normalization_factor = 6.0f * standard_deviation;
370
371 /* As discussed in compute_2d_gabor_kernel, we use the imaginary part of the phasor as the Gabor
372 * value. But remap to [0, 1] from [-1, 1]. */
373 if (stack_valid(value_stack_offset)) {
374 stack_store_float(stack, value_stack_offset, (phasor.y / normalization_factor) * 0.5f + 0.5f);
375 }
376
377 /* Compute the phase based on equation (9) in Tricard's paper. But remap the phase into the
378 * [0, 1] range. */
379 if (stack_valid(phase_stack_offset)) {
380 const float phase = (atan2f(phasor.y, phasor.x) + M_PI_F) / (2.0f * M_PI_F);
381 stack_store_float(stack, phase_stack_offset, phase);
382 }
383
384 /* Compute the intensity based on equation (8) in Tricard's paper. */
385 if (stack_valid(intensity_stack_offset)) {
386 stack_store_float(stack, intensity_stack_offset, len(phasor) / normalization_factor);
387 }
388
389 return offset;
390}
391
unsigned int uint
NodeGaborType
static double angle(const Eigen::Vector3d &v1, const Eigen::Vector3d &v2)
Definition IK_Math.h:117
static T sum(const btAlignedObjectArray< T > &items)
static unsigned long seed
Definition btSoftBody.h:39
dot(value.rgb, luminance_coefficients)") DEFINE_VALUE("REDUCE(lhs
ccl_device_inline void stack_store_float(ccl_private float *stack, const uint a, const float f)
ccl_device_inline uint4 read_node(KernelGlobals kg, ccl_private int *const offset)
ccl_device_inline float stack_load_float_default(const ccl_private float *stack, const uint a, const uint value)
ccl_device_forceinline void svm_unpack_node_uchar2(const uint i, ccl_private uint *x, ccl_private uint *y)
ccl_device_forceinline void svm_unpack_node_uchar3(const uint i, ccl_private uint *x, ccl_private uint *y, ccl_private uint *z)
ccl_device_forceinline void svm_unpack_node_uchar4(const uint i, ccl_private uint *x, ccl_private uint *y, ccl_private uint *z, ccl_private uint *w)
ccl_device_inline bool stack_valid(const uint a)
CCL_NAMESPACE_BEGIN ccl_device_inline float3 stack_load_float3(const ccl_private float *stack, const uint a)
ccl_device float3 spherical_to_direction(const float theta, const float phi)
ccl_device_inline float2 polar_to_cartesian(const float r, const float phi)
#define M_SQRT2_F
#define ccl_private
const ThreadKernelGlobalsCPU * KernelGlobals
#define ccl_device_noinline
#define expf(x)
#define CCL_NAMESPACE_END
ccl_device_forceinline float3 make_float3(const float x, const float y, const float z)
#define acosf(x)
ccl_device_noinline int svm_node_tex_gabor(KernelGlobals kg, ccl_private float *stack, const uint type, const uint stack_offsets_1, const uint stack_offsets_2, int offset)
Definition gabor.h:304
ccl_device float2 compute_2d_gabor_noise_cell(float2 cell, const float2 position, const float frequency, const float isotropy, const float base_orientation)
Definition gabor.h:124
ccl_device float compute_3d_gabor_standard_deviation()
Definition gabor.h:208
ccl_device float3 compute_3d_orientation(const float3 orientation, const float isotropy, const float4 seed)
Definition gabor.h:218
ccl_device float2 compute_3d_gabor_kernel(const float3 position, const float frequency, const float3 orientation)
Definition gabor.h:191
ccl_device float2 compute_3d_gabor_noise_cell(float3 cell, const float3 position, const float frequency, const float isotropy, const float3 base_orientation)
Definition gabor.h:244
ccl_device float2 compute_2d_gabor_kernel(const float2 position, const float frequency, const float orientation)
Definition gabor.h:62
ccl_device float2 compute_2d_gabor_noise(const float2 coordinates, const float frequency, const float isotropy, const float base_orientation)
Definition gabor.h:165
ccl_device float compute_2d_gabor_standard_deviation()
Definition gabor.h:111
#define IMPULSES_COUNT
Definition gabor.h:35
ccl_device float2 compute_3d_gabor_noise(const float3 coordinates, const float frequency, const float isotropy, const float3 base_orientation)
Definition gabor.h:280
VecBase< float, D > normalize(VecOp< float, D >) RET
#define floor
constexpr T clamp(T, U, U) RET
ccl_device_inline float2 hash_float3_to_float2(const float3 k)
Definition hash.h:328
ccl_device_inline float hash_float3_to_float(const float3 k)
Definition hash.h:227
ccl_device_inline float hash_float4_to_float(const float4 k)
Definition hash.h:232
ccl_device_inline float2 hash_float4_to_float2(const float4 k)
Definition hash.h:334
ccl_device_inline float3 hash_float4_to_float3(const float4 k)
Definition hash.h:314
@ NODE_GABOR_TYPE_2D
@ NODE_GABOR_TYPE_3D
static float noise(int n)
#define sqrtf
#define ccl_device
#define make_float2
#define make_float4
#define M_PI_F
#define cosf
#define atan2f
float x
float y
float z
Definition sky_math.h:136
float y
Definition sky_math.h:136
float x
Definition sky_math.h:136
uint x
Definition types_uint4.h:13
uint y
Definition types_uint4.h:13
uint z
Definition types_uint4.h:13
uint w
Definition types_uint4.h:13
i
Definition text_draw.cc:230
max
Definition text_draw.cc:251
uint len