5. パーリンノイズ
Ray Tracing: The Next Week (v3.2.3): 5 Perlin Noise / 2.5 パーリンノイズ
パーリンノイズ (Perlin noise) は,1985 年に Ken Perlin が発表した格子ベースの擬似乱数関数です。入力点の近傍では滑らかに変化し,遠方では一見ランダムに振る舞うので,木目・大理石・雲・地形などの「自然らしい揺らぎ」を持つテクスチャを,画像参照に頼らず手続き的に生成できます。
本章では原典の流れに沿って,ハッシュだけのブロックノイズ → スムージング → 格子点に乱数ベクトルを置く本物のパーリン → 乱流(turbulence)→ 大理石風と,4 段階で改良していきます。各段階を別々のデモクレートとして実装し,WASM 経由でブラウザ上で描画します。
| デモクレート | モード | 対応する原典の Image |
|---|---|---|
r205-perlin-hash | Hash | Image 7(ブロックノイズ) |
r205-perlin-smooth | Solid | Image 11(スムージング後) |
r205-perlin-turb | Turbulence | Image 12(乱流) |
r205-perlin-marble | Marble | Image 13(大理石) |
共通の道具立てとして,新たに common/src/perlin.rs の Perlin 型と,common/src/texture.rs に NoiseTexture / NoiseMode を追加します。シーン側では 2.4 で導入した Lambertian::with_texture をそのまま使えます。
ハッシュによるブロックノイズ
最初の段階は「3 次元格子のセルごとに事前に決めた乱数を返す」だけの,最も素朴な実装です。整数格子インデックス
と定義します。ここで
common/src/perlin.rs を新規作成し,まず格子テーブルと noise_hash を書きます。
use crate::utils::{random_double_range, random_int};
use crate::vec3::{Point3, Vec3, dot, unit_vector};
const POINT_COUNT: usize = 256;
pub struct Perlin {
ranvec: [Vec3; POINT_COUNT],
ranfloat: [f64; POINT_COUNT],
perm_x: [usize; POINT_COUNT],
perm_y: [usize; POINT_COUNT],
perm_z: [usize; POINT_COUNT],
}
impl Perlin {
pub fn new() -> Self {
let mut ranvec = [Vec3::new(0.0, 0.0, 0.0); POINT_COUNT];
let mut ranfloat = [0.0_f64; POINT_COUNT];
for i in 0..POINT_COUNT {
ranvec[i] = unit_vector(Vec3::new(
random_double_range(-1.0, 1.0),
random_double_range(-1.0, 1.0),
random_double_range(-1.0, 1.0),
));
ranfloat[i] = random_double_range(0.0, 1.0);
}
Perlin {
ranvec,
ranfloat,
perm_x: perlin_generate_perm(),
perm_y: perlin_generate_perm(),
perm_z: perlin_generate_perm(),
}
}
pub fn noise_hash(&self, p: Point3) -> f64 {
let i = ((4.0 * p.x()) as i32) & 255;
let j = ((4.0 * p.y()) as i32) & 255;
let k = ((4.0 * p.z()) as i32) & 255;
let idx = self.perm_x[i as usize]
^ self.perm_y[j as usize]
^ self.perm_z[k as usize];
self.ranfloat[idx]
}
}ranvec は後の本物のパーリンで使う乱数ベクトル(単位球面上に分布させる),ranfloat はこのハッシュ版でだけ使うスカラ乱数です。最終形ではどちらも保持しておく方が,1 つの Perlin 型で全段階を扱えて便利です。
置換テーブル生成は Fisher–Yates で書きます。これは自由関数で十分で,Perlin の関連関数にする必要はありません。
fn perlin_generate_perm() -> [usize; POINT_COUNT] {
let mut p = [0_usize; POINT_COUNT];
for i in 0..POINT_COUNT {
p[i] = i;
}
// Fisher-Yates shuffle from end to start.
for i in (1..POINT_COUNT).rev() {
let target = random_int(0, i as i32) as usize;
p.swap(i, target);
}
p
}random_int(0, i) は [0, i] の閉区間で一様な整数を返す共通ユーティリティです。utils.rs に小さく追加してあります。
C++ と Rust の違い
C++ 原典は static int* perlin_generate_perm() で生のヒープ配列をリターンし,デストラクタで delete[] する形をとります。Rust では固定長 [usize; 256] をスタックに確保して値返しするのが自然です。配列リテラルが Copy できるので所有権の移動も簡潔で,new/delete 対応を書く必要がありません。static メンバ関数の代わりにモジュール内の自由関数にしているのも,型に紐付ける必要のない単純なヘルパだからです。
NoiseTexture の Hash モードはこの noise_hash をそのまま呼ぶだけです。
pub enum NoiseMode {
Hash,
Solid,
Turbulence,
Marble,
}
pub struct NoiseTexture {
pub noise: Perlin,
pub scale: f64,
pub mode: NoiseMode,
}
impl NoiseTexture {
pub fn new(mode: NoiseMode, scale: f64) -> Self {
NoiseTexture {
noise: Perlin::new(),
scale,
mode,
}
}
}
impl Texture for NoiseTexture {
fn value(&self, _u: f64, _v: f64, p: Point3) -> Color {
let sp = p * self.scale;
let gray = match self.mode {
NoiseMode::Hash => self.noise.noise_hash(sp),
NoiseMode::Solid => 0.5 * (1.0 + self.noise.noise(sp)),
NoiseMode::Turbulence => self.noise.turb(sp, 7),
NoiseMode::Marble => {
0.5 * (1.0 + (sp.z() + 10.0 * self.noise.turb(p, 7)).sin())
}
};
Color::new(1.0, 1.0, 1.0) * gray
}
}最終的な値は灰色 Lambertian のアルベドとして使います。この章では値の範囲が Solid だけは生の noise が
デモ 1:r205-perlin-hash
シーンは原典の two_perlin_spheres() をそのまま移植します。半径 1000 の地面球と,その上に置いた半径 2 の小球が,同じパーリンテクスチャを共有します。
fn two_perlin_spheres() -> HittableList {
let mut objects = HittableList::new();
// Hash-only blocky lookup; no input scaling (the hash internally
// multiplies the sample point by 4).
let pertext: Arc<dyn Texture> = Arc::new(NoiseTexture::new(NoiseMode::Hash, 1.0));
objects.add(Box::new(Sphere::with_material(
Point3::new(0.0, -1000.0, 0.0),
1000.0,
Arc::new(Lambertian::with_texture(pertext.clone())),
)));
objects.add(Box::new(Sphere::with_material(
Point3::new(0.0, 2.0, 0.0),
2.0,
Arc::new(Lambertian::with_texture(pertext)),
)));
objects
}カメラは 2.4 と同じ lookfrom = (13, 2, 3),lookat = (0, 0, 0),vfov = 20°,絞り 0 です。以後のデモも同じカメラを共有します。
セルごとに値が一定なので,球面上には立方体格子の断面がそのまま見えます。これを滑らかにするのがこの章の主題です。
エルミート補間によるスムージング
格子セル内の点
これだけだと値が連続でも,原典が指摘するように「格子点の値を補間しているせいで,セルの中心と頂点が常に極値になる」ため,依然として格子模様が見えます。本物のパーリンノイズの賢いところは,格子点に乱数スカラではなく乱数ベクトル
Perlin::noise がこの本物の実装です。
impl Perlin {
pub fn noise(&self, p: Point3) -> f64 {
let u = p.x() - p.x().floor();
let v = p.y() - p.y().floor();
let w = p.z() - p.z().floor();
let i = p.x().floor() as i32;
let j = p.y().floor() as i32;
let k = p.z().floor() as i32;
let mut c = [[[Vec3::new(0.0, 0.0, 0.0); 2]; 2]; 2];
for di in 0..2 {
for dj in 0..2 {
for dk in 0..2 {
let idx = self.perm_x[((i + di as i32) & 255) as usize]
^ self.perm_y[((j + dj as i32) & 255) as usize]
^ self.perm_z[((k + dk as i32) & 255) as usize];
c[di][dj][dk] = self.ranvec[idx];
}
}
}
perlin_interp(&c, u, v, w)
}
}
fn perlin_interp(c: &[[[Vec3; 2]; 2]; 2], u: f64, v: f64, w: f64) -> f64 {
let uu = u * u * (3.0 - 2.0 * u);
let vv = v * v * (3.0 - 2.0 * v);
let ww = w * w * (3.0 - 2.0 * w);
let mut accum = 0.0_f64;
for i in 0..2 {
for j in 0..2 {
for k in 0..2 {
let weight_v = Vec3::new(u - i as f64, v - j as f64, w - k as f64);
let fi = i as f64;
let fj = j as f64;
let fk = k as f64;
accum += (fi * uu + (1.0 - fi) * (1.0 - uu))
* (fj * vv + (1.0 - fj) * (1.0 - vv))
* (fk * ww + (1.0 - fk) * (1.0 - ww))
* dot(c[i][j][k], weight_v);
}
}
}
accum
}uu, vv, ww がフェード関数 dot(c[i][j][k], weight_v) が「格子点に置いた乱数ベクトルと変位ベクトルの内積」です。値域はだいたい NoiseTexture::value の Solid モードでは
C++ と Rust の違い
C++ 原典は c を vec3 c[2][2][2] のローカル配列で確保し,補間関数 perlin_interp を static メンバとして書きます。Rust では [[[Vec3; 2]; 2]; 2] と多重配列リテラルで同じものを宣言でき,Vec3 が Copy + Default 風に振る舞うので初期化も [Vec3::new(0,0,0); 2]; 2]; 2] で一発です。perlin_interp は Perlin の状態を必要としないので自由関数にしました。所有権・借用は &[[[Vec3; 2]; 2]; 2] の参照を渡すだけで済みます。
入力スケールを大きくすると周波数が上がり,模様が細かくなります。NoiseTexture::new(NoiseMode::Solid, 4.0) は内部で sp = p * 4.0 してから noise(sp) を呼ぶので,原典が「scale パラメータを導入して 4 倍する」と書いている節と等価です。
デモ 2:r205-perlin-smooth
シーンは r205-perlin-hash と同じ two_perlin_spheres で,NoiseTexture::new(NoiseMode::Solid, 4.0) に差し替えるだけです。
let pertext: Arc<dyn Texture> = Arc::new(NoiseTexture::new(NoiseMode::Solid, 4.0));ブロックは消え,等値線がうねうねと走る,いかにも「ノイズ」らしい模様になります。
乱流(turbulence)
パーリンノイズを複数の周波数で重ね合わせると,自己相似的な乱流っぽい模様が得られます。原典では octave 数 depth = 7 とし,各層で周波数を倍に・振幅を半分にしながら足し,最後に絶対値をとります。
絶対値をとることで,谷の部分にカスプ(負の値が 0 で折り返される尖り)が生まれ,雲や火炎の「ぎざぎざ」が出ます。
impl Perlin {
pub fn turb(&self, p: Point3, depth: i32) -> f64 {
let mut accum = 0.0_f64;
let mut temp_p = p;
let mut weight = 1.0_f64;
for _ in 0..depth {
accum += weight * self.noise(temp_p);
weight *= 0.5;
temp_p = temp_p * 2.0;
}
accum.abs()
}
}デモ 3:r205-perlin-turb
NoiseMode::Turbulence は turb(sp, 7) を直接灰色値とします(範囲は概ね
谷に黒い筋が走るのが乱流の特徴です。これを直接テクスチャに使うと「燃えさし」のようになりますが,多くの自然素材では原典が次節で述べるように「乱流を直接表示する」のではなく「別の関数の引数を撹乱する」用途で使われます。
大理石風:位相を撹乱した正弦波
最後の段階は,パーリン本人がチュートリアルで挙げている「大理石」風テクスチャです。基本となる縞模様を
縞の周波数は scale * z(つまり sp.z())で決まり,10 * turb(p) の項が縞をうねうねと押しのけます。係数 10 が大きいほど縞が乱れ,0 に近づけると平行な縞に戻ります。
NoiseMode::Marble => 0.5 * (1.0 + (sp.z() + 10.0 * self.noise.turb(p, 7)).sin()),ここで sp = p * scale を縞の引数に,p(撹乱用)を turb に渡している点に注意してください。turb の側は周波数を上げても格子が小さくなって縞のうねりが細かくなるだけで効果が薄いので,撹乱は世界スケールのままにしてあります。
デモ 4:r205-perlin-marble
ページ冒頭で既に表示しているのがこのデモです。NoiseMode::Marble, 4.0 で生成しています。sin は
let pertext: Arc<dyn Texture> = Arc::new(NoiseTexture::new(NoiseMode::Marble, 4.0));縞は
まとめ
common/src/perlin.rsにPerlin型を新設し,3 段階のメソッドを公開した。noise_hash(p):3 軸置換 XOR + スカラ乱数のブロックノイズnoise(p):格子点の乱数ベクトルとの内積をエルミート補間する本物のパーリンノイズturb(p, depth):周波数倍・振幅半でオクターブを足す乱流(絶対値)
common/src/texture.rsにNoiseMode列挙体とNoiseTextureを追加した。scaleで周波数を制御し,4 つのモード(Hash/Solid/Turbulence/Marble)を 1 型で提供する。- 4 つのデモクレート
r205-perlin-{hash,smooth,turb,marble}を追加し,各モードの効果をtwo_perlin_spheresシーン上で比較できるようにした。
これで「世界座標の関数として色を計算する」道具立てが大きく広がりました。次章では,外部の画像をテクスチャとして読み込む ImageTexture を導入し,地球儀のような UV マップ表現に進みます。