数字孪生混乱中生成世界11 CurlNoise调节与边界

154 阅读6分钟

欢迎继续一起踏上 The Journey of Chaos, 关于 Shader 生成技术的一些基础性学习。噪声会分为以下几篇内容学习

  1. 随机函数与白噪声

  2. 值噪声 ValueNoise

  3. 柏林噪声 Gradient Noise

  4. 多维柏林噪声

  5. 柏林噪声优化 Simplex Noise

  6. Cell Noise

  7. SmoothVoronoi 与VoroNoise

  8. Simplex Noise变种PsrdNoise

  9. PsrdNoise 应用 FlowNoise

  10. CurlNoise(本篇)

  11. CurlNoise调节

  12. CurlNoise 计算优化 Bitangent Noise

  13. FBM深入

再上一篇中我们理解了 CurlNoise的方法,在本篇我们将继续追着 www.cs.ubc.ca/~rbridson/d… 对其中的控制 curl的方法进行讨论和理解。

一些物理概念

1. 势能(Potential Energy)

高山上的落石滚落下来时能够砸出一个大坑,蓄能水电站水库中的水从高处被释放能够带动发动机发电,紧绷的弓弦能够把箭射出去。你看,深处高处的石头、水流和紧绷的弓弦都具有能量。这种能量是什么?势能(Potential energy)是体系内因物体间的相对位置或状态而储存的能量,它能够释放或转化为其他形式的能量,如动能。势能是一个状态量,也称为位能。在流体力学中,势能通常指流体的重力势能或其他形式的势能(如压力势能)。势能与流体的高度、压力分布等相关,但在描述速度场时,通常不直接使用势能,而是通过势函数或流函数间接体现。

2. 势函数 (Velocity Potential, ϕ)

势函数 \phi 是用于描述**无旋流动(Irrotational Flow)**的标量函数。 在无旋流动中,速度场 \mathbf{v} 可以表示为势函数的梯度:

\mathbf{v} = \nabla \phi
\

这意味着速度场的旋度为零:

\nabla \times \mathbf{v} = 0
\

势函数常用于描述理想流体(无粘性、无旋)的运动。

3. 流函数(Stream Function)

-流函数 \psi 是用于描述**不可压缩流动(Incompressible Flow)**的标量函数。在二维不可压缩流动中,速度场 \mathbf{v} = (u, v) 可以表示为流函数的偏导数:

u = \frac{\partial \psi}{\partial y}, \quad v = -\frac{\partial \psi}{\partial x}
\

这意味着流函数满足连续性方程(质量守恒):

\nabla \cdot \mathbf{v} = 0
\

流函数的等值线\psi = \text{constant}表示流线,即流体微团的运动轨迹。也就是我们特效中的线

4. 速度场(Velocity)

速度场 \mathbf{v}是描述流体运动的核心物理量,表示流体微团在空间中的运动方向和速率。 在无旋流动中,速度可以通过势函数的梯度计算:

\mathbf{v} = \nabla \phi
\

在不可压缩流动中,速度可以通过流函数的旋度计算(二维情况下):

\mathbf{v} = \nabla \times \psi , \hat{\mathbf{z}}
\

其中 \hat{\mathbf{z}} 是垂直于二维平面的单位向量。

5. 势函数与流函数的关系

在二维无旋且不可压缩流动中,势函数 \phi 和流函数 \psi 满足柯西-黎曼条件

\frac{\partial \phi}{\partial x} = \frac{\partial \psi}{\partial y}, \quad \frac{\partial \phi}{\partial y} = -\frac{\partial \psi}{\partial x}
\

这意味着势函数和流函数是共轭调和函数,可以通过复变函数理论联系起来。听起来很复杂对不对。不过我们看其物理意义就会很清楚。

实边界 Solid Boundary

2503214345

2503195601

论文中给出了上做图图,在流线中,有这么一块里面没有流线,同时源的周围有一圈流线保证了整体无散。同时论文中并给出下列公式

\psi(\mathbf{x}, t) = \text{ramp}\left(\frac{d(\mathbf{x})}{d_0}\right) A(\mathbf{x}) N\left(\frac{\mathbf{x}}{d_0}, t\right).
\

如果需要明白上述公式,需要结合上面的物理概念基础知识,对公式每一项含义有了解。 在你描述的情境中,N(\mathbf{x}, t)是一个二维噪声函数,其中:

  1. **噪声函数 N(x, t) **: 这是一个随时间 t 和空间位置 \mathbf{x} 变化的噪声函数, N也就是我们 simplex Noise. x也就是 UV坐标

  2. 调制函数 A(\mathbf{x}) : 这是一个平滑的阶跃函数,基于鼠标光标与位置 \mathbf{x} 的距离。它的作用是调整噪声的强度,使得靠近鼠标光标的位置噪声更强,远离鼠标光标的位置噪声更弱。这里调节的其实就是 N生成向量场的大小

  3. 边界距离函数 d(\mathbf{x}) : 这是位置 \mathbf{x}到最近固体边界的距离。用于确保在靠近边界时速度场逐渐减弱,避免在边界处产生不合理的速度。

  4. 斜坡函数\text{ramp}\left(\frac{d(\mathbf{x})}{d_0}\right): 这是一个基于边界距离的斜坡函数,d_0是一个参考距离。当 d(\mathbf{x})小于 d_0时,函数值逐渐减小到零,确保在边界附近速度场平滑过渡到零。其实就是个 smoothstep

  5. 势函数 \psi(\mathbf{x}, t) : 也就是论文中提出的公式,也是上面各个函数的组合

知道原理之后就非常简单,代码就只有一行,修改 noise函数,做一个 smoothstep

float boundary_sdf(vec2 pos) 
{
    return length(pos -vec2(0.2)) - 0.3;
}


float main_noise(in vec2 p) 
{
    float noise = simplex_noise(p);
    float scaleSize = 0.1;
    float boundaryFactor = smoothstep(0., 1.0, boundary_sdf(p) / scaleSize) ;
    return noise * boundaryFactor;
}

下面我在 (0.2, 0.2)位置做了一个 0.3 半径的圆,

控制速度场

目前所有的streamline都是通过随机数生成速度场画出来的,但是由于旋度恒等式,我们在每一帧都已经满足了这个条件

\nabla \cdot (v_x, v_y) = 0
\

那么意味着我们对每一帧的速度场做一些改变,也不会影响到整体流场散度为 0 的特点。这也是上一篇埋的彩蛋,如何通过鼠标让流线移动。 这里也许有非常多的效果。

2503211104 上图为随着鼠标移动产生了旋转的 streamline

vec2 ms = (iMouse.xy * 2.0 - iResolution.xy) / iResolution.y;
vec2 mv = (ms - p0) * 0.5;
mv /= dot(mv, mv) + 0.04;
mv = vec2(-mv.y, mv.x);
v0 += mv * 0.2;

主要是在计算粒子运动方向的时候增加了一个垂直方向的力。 以上代码具体解释
  1. vec2 mv = (m

  2. s - p0) * 0.5; 计算出鼠标与渲染点的向量。

  3. mv /= dot(mv, mv) + 0.04; 计算出越远受到的影响越小,同时加上 0.04避免除 0

  4. mv = vec2(-mv.y, mv.x); 将向量旋转 90 度,实现垂直力,从而产生漩涡

而只要注释掉,mv = vec2(-mv.y, mv.x); 。 就可以产生如下吸引的效果 2503213114

Web中使用Curl

2503203318

2503203321

对比上左右的效果,右侧粒子发散出去后路径是扭曲的曲线,但是扭曲的幅度控制在一定范围。 这里实际上就是用 curlNoise为粒子运动叠加了一个速度。 核心代码非常简单。 以下是 js代码。 来自于 github.com/bobbyroe/cu…

```
function update(t) {
    particles.forEach((p) => {
      p.update(t);
      p.render(ctx);
    });
    let angle = Math.random() * Math.PI * 2;
    let speed = Math.random() * 3 + 1;
    let curl = getCurl(t, 0, 0);
    let pOptions = {
      pos: { x: SCREEN_WIDTH * 0.5, y: SCREEN_HEIGHT * 0.5 },
      vel: { x: Math.cos(angle) * speed, y: Math.sin(angle) * speed },
      col: { hue: 360 * curl.x, lightness: 100, alpha: 1.0 },
      fadeRate: 0.005,
    };
    let particle = getParticle(pOptions);
    particles.push(particle);
    while (particles.length > maxParticles) {
      particles.shift();
    }
  }
  return { update };
}
```