普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月29日首页

wolfram详解山峦算法

2026年4月28日 17:04

代码链接:www.wolframcloud.com/obj/1051904…

知识点

  • noise 算法
  • noise 栅格
  • noise 栅格过度
  • 梯度

1-noise 绘制山峦的原理

noise 可以译作杂色,或者噪波,它可以理解为一种肌理,其表现形式有很多。

使用杂色可以绘制山峦、云海等。

noise 绘制山峦的原理如下:

1.杂色。

image-20260426114414985

2.栅格:降低采样频率,将杂色变成栅格。

image-20260426114511276

3.山峦:栅格平滑过度。

image-20260426170319591

4.山峦细节:对山峦进行多次变换叠加。

image-20260428110734479

利用这种算法,可以在shader 中渲染出云山云海的效果。

mount

2-随机数

杂色的实现原理就是随机数。

随机数的写法有很多。接下来,我会使用wolfram 语言演示其算法原理。

1.根据一维数据生成随机数的方法。

random[x_]:=Abs[FractionalPart[1000*Sin[x]]]
DiscretePlot[random[x],{x,0,10,0.1}]

效果如下:

image-20260423183548641

2.根据片元的二维位置生成随机数的方法。

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[{v},
v=FractionalPart[{p[[1]],p[[2]],p[[1]]}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
Plot3D[random[{x,y}],{x,0,10},{y,0,10},ColorFunction -> Function[{x, y, z}, GrayLevel[z]],Mesh->None]

效果如下:

image-20260426114414985

3-栅格

我们可以将点位取整,从而画出大块的杂色。

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[{v},
v=FractionalPart[{p[[1]],p[[2]],p[[1]]}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,a,b,c,d,tx,ty},
i=Floor[p];
random[i]
]
plotSize=10;
Plot3D[noise[{x,y}],{x,0,plotSize},{y,0,plotSize},ColorFunction -> Function[{x, y, z}, GrayLevel[z]]]

效果如下:

image-20260426114511276

noise 栅格颜色的深浅代表了山的高度。但它现在是离散的,缺少过度。所以我们接下来要给栅格一个过度。

4-栅格过度

栅格过度的核心在于使用插值在相邻的栅格间做补间运算。

4-1-一行栅格的过度

一行没有过度的栅格高度图如下:

random[x_]:=Abs[FractionalPart[1000*Sin[x]]];
noise[x_]:=Module[{},
i=Floor[x];
a=random[i];
a
]
Plot[noise[x],{x,0,10}]

效果如下:

image-20260425002625971

我们可以以栅格位置的小数部分为插值,对相邻的两个栅格做补间。

random[x_]:=Abs[FractionalPart[1000*Sin[x]]];
mix[a_,b_,f_]:=Module[{},
(a+(b-a)*f)
];
noise[x_]:=Module[{},
i=Floor[x];
f=FractionalPart[x];
a=random[i];
b=random[i+1];
mix[a,b,f]
]
Plot[noise[x],{x,0,10}]

效果如下:

image-20260425003942467

mix[a_,b_,f_] 方法是基于f差值在a和b间做补间的方法。

在noise 方法中,a,b是相邻的2个栅格的值,f是栅格位置的小数部分,可以用作插值。

4-2-二维栅格的过度

过度算法

image-20241127151550726

已知:

  • 栅格尺寸为1

  • 栅格4个顶点:

    • 点P(px,py,a)
    • 点P右侧的点(px+1,py,b)
    • 点P上方的点(px,py+1,c)
    • 点P右上方的点(px+1,py+1,d)
  • 点F(px+fx,py+fy,e),px和py是整数,fx和fy是小数,e未知

求:e

思路:e 可以理解为b,c,d对a的加权

解:

b 对a 的影响力是:

(b-a)*fx*(1-fy)

(b-a)*fx 是b在x方向对a的影响,同时其影响力还会受到fy 的影响。

c 对a 的影响力是:

(c-a)*fy*(1-fx)

其原理与b同理。

d 对a 的影响力是:

(d-a)*fx*fy

最后把b,c,d 对a 的影响力合到a 上,就是着色点e 的颜色:

e=a+(b-a)*fx*(1-fy)+(c-a)*fy*(1-fx)+(d-a)*fx*fy

e 的颜色是对山的高度的可视化描述,方便大家理解。

算法可视化

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[
{
x=p[[1]],
y=p[[2]],
v
},
v=FractionalPart[{x,y,x}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,a,b,c,d,tx,ty},
i=Floor[p];
f=FractionalPart[p];
a=random[i];
b=random[i+{1,0}];
c=random[i+{0,1}];
d=random[i+{1,1}];
tx=f[[1]];
ty=f[[2]];
a+(b-a)*tx*(1-ty)+(c-a)*ty*(1-tx)+(d-a)*tx*ty
]
plotSize=10;
Plot3D[noise[{x,y}],{x,0,plotSize},{y,0,plotSize},Mesh->None]

效果如下:

image-20260426170319591

5-山体圆滑

当前的山体看起来比较凌厉,我可以使其圆滑一些。

5-1-线性补间与曲线补间

我们之前使用的补间算法是线性补间。

noise[x_]:=Module[{},
FractionalPart[x]
]
Plot[noise[x],{x,0,3}]

image-20260425004053941

我们可以曲线补间。

noise[x_]:=Module[{},
tx=FractionalPart[x];
FractionalPart[3*tx^2-2*tx^3]
]
Plot[noise[x],{x,0,3}]

image-20260425004145361

5-2-曲线补间的应用

曲线补间可以圆滑折线图。

random[x_]:=Abs[FractionalPart[1000*Sin[x]]];
mix[a_,b_,f_]:=Module[{},
(a+(b-a)*f)
];
noise[x_]:=Module[{},
i=Floor[x];
f=FractionalPart[x];
u=3*f^2-2*f^3;
a=random[i];
b=random[i+1];
mix[a,b,u]
]
Plot[noise[x],{x,0,10}]

效果如下:

image-20260425005014832

曲线补间也可以三维山峦。

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[
{
x=p[[1]],
y=p[[2]],
v
},
v=FractionalPart[{x,y,x}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,a,b,c,d,tx,ty},
i=Floor[p];
f=FractionalPart[p];
u=3*f^2-2*f^3;
a=random[i];
b=random[i+{1,0}];
c=random[i+{0,1}];
d=random[i+{1,1}];
tx=u[[1]];
ty=u[[2]];
a+(b-a)*tx*(1-ty)+(c-a)*ty*(1-tx)+(d-a)*tx*ty
]
plotSize=10;
Plot3D[noise[{x,y}],{x,0,plotSize},{y,0,plotSize},Mesh->None]

效果如下:

image-20260426170413707

我们可以让z轴和x,y 轴等比,使之更接近现实。

Plot3D[noise[{x,y}],{x,0,plotSize},{y,0,plotSize},PlotRange -> {0, plotSize},BoxRatios -> {1, 1, 1},Mesh -> None]

效果如下:

image-20260426170732918

当前的山体有些简约,我们可以增加更多的细节。

6-增加山体细节

我可以对山体进行多次变换叠加,使其具有更能多细节。

原理如下图所示:

山势叠加

第一张c 图是由下面的b 图和a 图叠加而成。

按照此原理,对三维山峦进行变换叠加。

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[
{
x=p[[1]],
y=p[[2]],
v
},
v=FractionalPart[{x,y,x}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,a,b,c,d,tx,ty},
i=Floor[p];
f=FractionalPart[p];
u=3*f^2-2*f^3;
a=random[i];
b=random[i+{1,0}];
c=random[i+{0,1}];
d=random[i+{1,1}];
tx=u[[1]];
ty=u[[2]];
a+(b-a)*tx*(1-ty)+(c-a)*ty*(1-tx)+(d-a)*tx*ty
];
mountainTF=2*{{0.6,-0.8},{0.8,0.6}};
mountain[p:vec2]:=Module[{p2=p,a,b,n},
a=0;
b=1;
For[i = 0, i < 4, i++,
n=1.65*noise[p2*0.5];
a+=b*n/(1+i);
p2=mountainTF.p2;
b*=0.5;
];
a
];
plotSize=10;
Plot3D[mountain[{x,y}],{x,0,plotSize},{y,0,plotSize},PlotRange -> {0, plotSize},BoxRatios -> {1, 1, 1},PlotPoints -> 30,Mesh -> None]

效果如下:

image-20260426185404625

这样山体就有了细节。我们还可以根据山峰的形成规律,使其看起来更加接近现实。

7-山的规律

观察现实中的山峰,我们不难发现一个基本规律:山峰陡峭的地方会更加平滑,山峰平缓的地方会有更多的山岩。

山峰

按照此规律,我们在叠加山体的时候,根据采样点的梯度判断陡峭度,在山峰陡峭的地方,让叠加山体更能矮。

使用wolfram的Grad 方法求noise函数的梯度。

Grad[a+(b-a)*u[x]*(1-u[y])+(c-a)*u[y]*(1-u[x])+(d-a)*u[x]*u[y],{x,y}] //Simplify

输出:

{(-a+b+(a-b-c+d)u[y])u'[x],(-a+c+(a-b-c+d)u[x])u'[y]}
  • u是曲线函数.
u=3*f^2-2*f^3;
  • u'[x]和u'[y] 是u 的导数,即
D[3*f^2 - 2*f^3, f]
du=6f-6

noise函数的梯度可以简化一下:

({b-a,c-a}+a-b-c+d)*{ty,yx})*du

我们可以根据梯度的点积确定山峰的陡峭程度,从而让陡峭的地方山体更矮。

整体程序如下:

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[
{
x=p[[1]],
y=p[[2]],
v
},
v=FractionalPart[{x,y,x}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,du,a,b,c,d,tx,ty,x,yz},
i=Floor[p];
f=FractionalPart[p];
u=3*f^2-2*f^3;
du=6*f-6f*f*f;
a=random[i];
b=random[i+{1,0}];
c=random[i+{0,1}];
d=random[i+{1,1}];
tx=u[[1]];
ty=u[[2]];
x=a+(b-a)*tx*(1-ty)+(c-a)*ty*(1-tx)+(d-a)*tx*ty;
yz=({b-a,c-a}+(a-b-c+d)*{ty,tx})*du;
{x,yz[[1]],yz[[2]]}
];
mountainTF=2*{{0.6,-0.8},{0.8,0.6}};
mountain[p:vec2]:=Module[{p2=p,a,b,d,n,nx,nyz},
a=0;
b=1;
d={0,0};
For[i = 0, i < 4, i++,
n=noise[p2*0.5];
nx=n[[1]];
nyz={n[[2]],n[[3]]};
a+=b*nx/(1+nyz.nyz);
d+=nyx;
p2=mountainTF.p2;
b*=0.5;
];
a
];
plotSize=10;
Plot3D[mountain[{x,y}],{x,0,plotSize},{y,0,plotSize},PlotRange -> {0, plotSize},BoxRatios -> {1, 1, 1},PlotPoints -> 30,Mesh -> None]

效果如下:

image-20260428110734479

总结

这一章我们说了wolfram 绘制山体的基本过程,其基本原理是noise 的变换叠加,并使用梯度判断山峰陡峭程度,让陡峭的山峦更加平滑,更加复合自然界中的山峦规律。

后面我会利用此原理,在shader中写一篇在绘制云山云海的文章。

参考链接:www.bilibili.com/video/BV18P…

❌
❌