45
$\begingroup$

Dear Stack Exchange Community,

i'm trying to achieve the following effect with curves:

enter image description here

Source of this image is here:

https://www.behance.net/gallery/57143191/Visualizing-Concepts-in-Houdini-Personal-Practice

I tried to achieve this effect by following this question:

How to achieve this spline and particle effect

i tried to achieve the desired result using the animation nodes setup from the thread listed above, doing the changes marked "A" and "B": enter image description here

But the curves do not wrap around the suzanne object ("C").

enter image description here

The Blend file with the animation node setup is here

What is the mistake? some advice would be fine. Thank you!

EDIT 1.1 - Final Node Setups for AN 2.0 with Blend files

Here are the final node setups for AN 2.0, created on the discussion listed below.

General Curl

Implicite Form

Mesh Surface Curl

$\endgroup$
  • $\begingroup$ I suggest a) adding an image of the result you're currently getting, b) explain a bit more the effect you're trying to get without solely linking to your previous question and c) Uploading a blend file to here with your current node setup, so people don't have to recreate it themselves. $\endgroup$ – Ray Mairlot Nov 22 '17 at 16:22
  • $\begingroup$ First of all, I suggest you do what @RayMairlot suggested. While I don't know exactly what you want to create, here are some points regarding your setup above: 1) The curl in 3D is computed from a vector field and not a scalar field, so forget about my previous answer, it was only for the 2D case. 2) You can not by any means edit the resulting curl trace because that will render it non divergent, any editing has to be on the field and only the field. Just do as suggested for now and we will discuss this in details later. $\endgroup$ – Omar Ahmad Nov 22 '17 at 19:58
  • $\begingroup$ Hi Ray, Omar, sorry for the bad Question syntax. I edited the question. If there has to be more information, please let me know. Thanks for support! $\endgroup$ – pixelpoems.de Nov 23 '17 at 15:30
  • $\begingroup$ @pixelpoems.de I will research this in couple of days and hopefully write an answer then. $\endgroup$ – Omar Ahmad Nov 23 '17 at 16:36
76
+300
$\begingroup$

The General Curl

The Theory

The image you posted above is trace visualization for what is known as a divergence free vector field. To understand what that means, consider particles that move along the lines you see above (the field represents their instantaneous velocity), if the field is indeed divergence free, those particles will never collide. That's what gives those visualizations their beauty, the lines never intersect.

Mathematically, a vector field $F$ is divergence free if $\nabla \cdot F = 0$. It is hard to compute a random vector field that is divergence free proceduraly, so we use some math to aid us. In vector calculus, the Curl of any vector field is divergence free. The curl is an operator that operates on a vector field and returns another vector field that represents the infinitesimal (microscopic) rotation of the input vector field. The infinitesimal rotation of a vector field can be understood using this intuitive interpretation from Wikipedia1:

Suppose the vector field describes the velocity field of a fluid flow (such as a large tank of liquid or gas) and a small ball is located within the fluid or gas (the center of the ball being fixed at a certain point). If the ball has a rough surface, the fluid flowing past it will make it rotate. The rotation axis (oriented according to the right hand rule) points in the direction of the curl of the field at the center of the ball, and the angular speed of the rotation is half the magnitude of the curl at this point.

We are not interested in what the curl represents, we are only interested in the fact that the curl of any vector field is divergence free. For our artistic use, the input of the curl operator $F$ can be a simplex or perlin noise-based vector field, that is, $F = (F_x, F_y, F_z)$ where $F_x, F_y, F_z$ are different perlin or simplex noise functions.

Let us now look into the curl operator that will take $F$ as an input and returns a divergence free vector field, the curl of $F$ denoted by $\nabla \times F$ is equal to:

$$ \nabla \times F = \left( \frac{\partial F_z}{\partial y} - \frac{\partial F_y}{\partial z} \right)\mathbf{i} + \left( \frac{\partial F_x}{\partial z} - \frac{\partial F_z}{\partial x} \right)\mathbf{j} + \left( \frac{\partial F_y}{\partial x} - \frac{\partial F_x}{\partial y} \right)\mathbf{k} $$

Where $\mathbf{i}$,$\mathbf{j}$ and $\mathbf{k}$ are the fundamental vectors of the rectangular space. Don't worry if you don't understand those equations, we shall understand them soon. The notation $\frac{\partial F_z}{\partial y}$ represents the partial derivative of $F_z$ with respect to the $y$ axis, similarly, $\frac{\partial F_y}{\partial z}$ represents the partial derivative of $F_y$ with respect to the $z$ axis, and so on. A partial derivative measures how much a function changes along an axis. Take a brief look at derivatives before we continue, you don't have to study it, just recognize what it represents. To compute the partial derivative, we use what is known as the central finite difference method or Symmetric Derivative which states that the partial derivative is equal to:

$$ \frac{f(x+h) - f(x-h)}{2h} $$

Where $h$ is some arbitrary small number and $f$ is the function in question. The same equation applies for the $y$ and $z$ axis. For instance to compute $\frac{\partial F_z}{\partial y}$, we add $h$ (a small number) to the $y$ component of the the vector, evaluate the $F_z$ noise at that vector, subtract $h$ from the $y$ component of the vector, evaluate the noise and take the difference between the two evaluated noise values, and finally divide them by $2h$.

To understand how the central finite difference method work, consider this intuitive interpretation:

Suppose one is standing on a mountain blindfolded, one was asked to report the steepness (slope, partial derivative) of the point one is standing at, one might move a step to the right and identify whether one ascended or descended and how much did one ascend or descend by, another might move a step to the left and identify whether one ascended or descended and how much did one ascend or descend by. Now consider the situation where one is at the very top of the mountain, at that point, the slope is zero, one is not ascending or descending, but both persons will report that they are descending, because if one move to the left or the right one will be descending. We conclude that to get an accurate result, we have to move a step to the right, observe, move a step to the left, observe and then determine if you are ascending or descending. In our case, if one move both to the left and to the right, one will understand that one is at an even surface (at the top). So when we say $f(x+h)$ we simply mean that we observe after we move a step $h$ to the right and $f(x-h)$ means to the left.

Once we compute the curl of the vector field, we can do what is known as advection using Euler's integration. When treating the curl vector field as the instantaneous velocity of some particles in space, Euler's integration approximates the location of the particles after one second by adding the curl vector field to the original particles location. So to get the whole trace of a particle, we evaluate the curl, add it to the location of the particle, evaluate the curl at the new location, add it to the new location, and so on.

Implementation

Simple Curl Implementation

The first step is to make a group that generates the vector field by initializing its components with different simplex noises $(F_x, F_y, F_z)$:

Vector Field

The next step is to compute the partial derivatives. This can be done using the formula for the symmetric derivative:

Partial Derivatives

Take your time to understand this node tree. It can be hard to grasp at first. Having all the partial derivatives, we can now compute the curl easily using the equation above:

Curl

And by advecting some initial vectors along the curl vector field using Euler's integration and creating splines from the output points:

Euler's Integeration

We get something like this:

Result

Optimized Curl Implementation

While the previous implementation works, it is very slow because it requires a lot of noise evaluations and noise is expensive to compute. Thankfully, the noise functions in Animation Nodes v2.1 is implemented using SIMD instructions, which means it can somewhat be executed in parallel for CPUs that support Advanced Vector Extensions (AVX) which is pretty much every modern CPU. This result in a speed up of up to 600x the original implementation. SIMD requires the input data to occupy a contiguous memory block. So to utilize SIMD instructions and optimize this setup, we should combine all our data into a single big vector list, evaluate the noise at it, segment the output, and process it. The following implementation is hard to understand, so it is better to do it on your own, my implementation looks like this:

Optimized Curl

Euler's integration loop stays the same:

Euler's Integeration

However, notice that we are appending a list of vectors and not a vector, so the output will be of length $n \cdot m$, where $n$ is the number of iterations and $m$ is the number of input initial vectors. We have to do another segmentation before creating splines from those points, so the spline loop will include segmentation and spline creation:

Spline Loops

And what we get is a fully functional, fast, curl trace generator:

Optimized Result

Blend file for study and practice:

Surface Curl

Now that we have an understanding of what the curl is, we can go ahead and finally answer your question.

Theory

Let us define the problem as follows:

We want the output vector field of the curl operator to always be tangent to the surface of our meshes, that is, the curl should always be perpendicular to the vector field that represents our mesh surface normals.

If our initial points were on the surface of the mesh and the above condition is satisfied, then we will get the result we are looking for. The above condition is satisfied only if $(\nabla \times F) \cdot \vec{N} = 0$, where $(\nabla \times F)$ is the curl and $\vec{N}$ is our normals vector field. The problem is now to compute a vector field $F$ so that the dot product between its curl and the normals vector field is equal to zero.

By some analysis we get, $F$ is equal to $\vec{N} \cdot p(x,y,z)$ where $p$ is a perlin or simplex noise function just like we used in the general case, the curl in that case becomes:

$$ \begin{aligned} F &= p(x,y,z) \begin{bmatrix} N_x \\ N_y \\ N_z \end{bmatrix}\\ (\nabla \times F) &= \left( \frac{\partial F_z}{\partial y} - \frac{\partial F_y}{\partial z}, \frac{\partial F_x}{\partial z} - \frac{\partial F_z}{\partial x}, \frac{\partial F_y}{\partial x} - \frac{\partial F_x}{\partial y} \right)\\ &= \left( N_z \frac{\partial p}{\partial y} - N_y \frac{\partial p}{\partial z}, N_x \frac{\partial p}{\partial z} - N_z \frac{\partial p}{\partial x}, N_y \frac{\partial p}{\partial x} - N_x \frac{\partial p}{\partial y} \right) \end{aligned} $$

And the proof:

$$ \begin{aligned} (\nabla \times F) \cdot \vec{N} = &N_z N_x \frac{\partial p}{\partial y} - N_y N_x \frac{\partial p}{\partial z} +\\ &N_x N_y \frac{\partial p}{\partial z} - N_z N_y \frac{\partial p}{\partial x} +\\ &N_y N_z \frac{\partial p}{\partial x} - N_x N_z \frac{\partial p}{\partial y} = 0 \end{aligned} $$

Notice that if the normal is $\vec{N}=(0,0,1)$, you only end up with the equation that Robert Bridson proposed in his paper "Curl-Noise for Procedural Fluid Flow", which is:

$$ \frac{\partial p}{\partial y}\mathbf{i} -\frac{\partial p}{\partial x}\mathbf{j} $$

So now you know where my other answer came from. Since the curl have zero z component, we shall call this special case the 2D Case.

Implementation

The implementation is much easier because we only have to differentiate a single function. You should by now know how to implement that. Here is the non-trivial implementation of the surface curl; notice how there is an extra input called normals now:

Implementation

Lets consider the simple example in which we use a sphere as our mesh, the normal of the sphere at any point on its surface is equal to that point normalized, so the normal is simply the point locations normalized. After the curl is computed and the the points advected, we can project the output onto the surface to make sure the splines always lie on the surface of the sphere since we are merely performing a finite approximation. We won't need this step for sufficiently small step sizes for Euler's integration which is in our case decided by the magnitude of the normal vector field or the amplitude of the noise function. To project any point onto the surface of a sphere of radius $r$ we normalize the vector and multiply by the radius, so the advection loop looks like this now:

New Advection Loop

The result are those magnificent and beautiful splines:

Sphere Curl

Blend file for study and practice:

Since the 2D case have two zero normal components, let's implement the 2D case using an optimized setup. We simply remove anything that got multiplied by zero to get:

2D Case

This gives:

2D Case result

Blend file:

Ok, I have given you an example where the mesh is defined implicitly, but how do we use an actual mesh? Well, we can use a BVH tree:

BVH Tree

We approximate the normal field by the Nearest Surface Point node Normal output, and we change the generator to be the location of the nearest surface point. Why? Because this is the projection of the point on the surface, which is a step we have to do to make sure we get accurate results as we discussed before. Make sure to reduce the step size, because BVH trees are not as accurate as the implicit definition of our sphere.

It should be noted that constructing BVH trees from objects directly won't apply modifiers, so such node tree can be used:

BVH

And finally, here is the result of both setups:

Result

Inviscid Boundary Condition

An inviscid boundary condition required the dot product between the curl and the surface normals around the boundaries of objects to be zero, in other words, it requires the curl to be orthogonal to the normals of the object around its boundaries. Ensuring this condition in the surface curl case is easy, while it is somewhat challenging for the general 3D case, so I shall only introduce the surface curl.

Looking back at the equation for the 2D case:

$$ \frac{\partial p}{\partial y}\mathbf{i} -\frac{\partial p}{\partial x}\mathbf{j} $$

We notice that the curl is actually orthogonal to the Gradient, the gradient vector being, in simple terms, a vector pointing in the direction of the greatest change. The gradient is computed by:

$$ \frac{\partial p}{\partial x}\mathbf{i} +\frac{\partial p}{\partial y}\mathbf{j} $$

Can you see the similarities? So we can implement the boundary condition by making sure the noise field have a gradient tangent to the normals of the object around its boundaries, in other words, the noise field should increase the most when moving in the direction of the surface normals. We actually know a field that satisfied this condition everywhere, that is, its gradient is tangent to the surface normals. This field is the Signed Distance Field (SDF). An SDF is, for every point, the distance to the closest surface point, having a negative sign if it is inside the surface and positive if outside (However, the sign is redundant in this particular application). So the condition can be implemented by modulating the noise field based on the SDF of the surface using a ramp. As Robert implemented it, the noise field should be multiplied by the ramp:

$$ \begin{cases} 1 & r > 1 \\ \frac{15}{8}r - \frac{10}{8}r^3 + \frac{3}{8}r^5 & -1 \leq r \leq 1 \\ -1 & r < -1 \end{cases} $$

Where $r$ is the SDF divided by a scalar defining the modulation width. Which we can implement as follows:

Boundary Condition

What we did is replace the noise node with the above subprogram and add a BVH tree as an input.

Using Older Animation Nodes Versions

The only node used in this answer and does not exist prior to Animation Nodes v2.1 is the Vector Noise node which provides us with the perlin or simplex noise functions.

To make the node tree work for older versions, you simply have to replace the Vector Noise node with the built-in blender noise functions that exists in the mathutils python module. To do so, we can use the expression node:

Mathutils Noise

With this code:

[mathutils.noise.noise(x) for x in vectors]

Aside from noise.noise, there are other functions that give you more control, which you can find in the API.

File for study:

$\endgroup$
  • 34
    $\begingroup$ This answer deserves an award. $\endgroup$ – cmomoney Nov 30 '17 at 20:06
  • 5
    $\begingroup$ Holy crap Omar, I agree with cmomoney, amazing answer. $\endgroup$ – icYou520 Nov 30 '17 at 20:38
  • 2
    $\begingroup$ @JeffLange I don't think there are compiled addons for that version as it is under development. You can either compile it yourself, ask someone to compile it with you or use an older version as I explained in my other answer. $\endgroup$ – Omar Ahmad Dec 4 '17 at 16:11
  • 2
    $\begingroup$ Hi Omar,thank you very very much for your detailed answer. But i also struggeling with the adaption of the solution to a AN Version < 2.1. I tried to use the expression node using mathutils.noise.noise(x), but it seems that i am doing something wrong with the node itself. Please refer to "EDIT 1.0" Above. Thank you. $\endgroup$ – pixelpoems.de Dec 4 '17 at 19:10
  • 3
    $\begingroup$ @omarAhmad Dear Omar, it is finally done! all Versions are now running on AN 2.0. I really thank you for your patience and support. Without your help, i never would have manage it. As soon as i have some beauty renders done, i will leave you a link with credits here. Again, thank you very much for your great support! I'll edit the answer and post all three Version - general curl, implicit form and the surface curl - so that others don't get confused. $\endgroup$ – pixelpoems.de Dec 9 '17 at 18:34

protected by cegaton Dec 6 '17 at 18:13

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.