Skip to content
Copied!
published on 2026-06-08

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-hashHashImage 7(ブロックノイズ)
r205-perlin-smoothSolidImage 11(スムージング後)
r205-perlin-turbTurbulenceImage 12(乱流)
r205-perlin-marbleMarbleImage 13(大理石)

共通の道具立てとして,新たに common/src/perlin.rsPerlin 型と,common/src/texture.rsNoiseTexture / NoiseMode を追加します。シーン側では 2.4 で導入した Lambertian::with_texture をそのまま使えます。

ハッシュによるブロックノイズ

最初の段階は「3 次元格子のセルごとに事前に決めた乱数を返す」だけの,最も素朴な実装です。整数格子インデックス (i,j,k) に対して

noise(i,j,k)=R[Px[i]Py[j]Pz[k]]

と定義します。ここで R は 256 個の乱数 [0,1) を並べた配列,Px,Py,Pz{0,1,,255} の独立な置換です。 は XOR で,3 軸の置換を「混ぜる」役割を担います。連続値の入力 p=(x,y,z) に対しては,(4x,4y,4z)mod256 を格子インデックスとして使います。

common/src/perlin.rs を新規作成し,まず格子テーブルと noise_hash を書きます。

common/src/perlin.rs
rust
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 の関連関数にする必要はありません。

common/src/perlin.rs
rust
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 メンバ関数の代わりにモジュール内の自由関数にしているのも,型に紐付ける必要のない単純なヘルパだからです。

NoiseTextureHash モードはこの noise_hash をそのまま呼ぶだけです。

common/src/texture.rs
rust
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
    }
}

最終的な値は灰色 (g,g,g) で,Lambertian のアルベドとして使います。この章では値の範囲が [0,1] に収まるようにモード毎に変換しているのがポイントです(Solid だけは生の noise[1,1] なので 0.5(1+) で正規化)。

デモ 1:r205-perlin-hash

シーンは原典の two_perlin_spheres() をそのまま移植します。半径 1000 の地面球と,その上に置いた半径 2 の小球が,同じパーリンテクスチャを共有します。

r205-perlin-hash/src/lib.rs
rust
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 です。以後のデモも同じカメラを共有します。

セルごとに値が一定なので,球面上には立方体格子の断面がそのまま見えます。これを滑らかにするのがこの章の主題です。

エルミート補間によるスムージング

格子セル内の点 p について,周囲 8 つの格子点 (i+di,j+dj,k+dk)di,dj,dk{0,1})の値を線形補間すれば,ブロックは消えます。ただし純粋な線形補間(trilinear)だと一階導関数が格子線で不連続になり,エッジが「マッハバンド」として見えてしまうので,補間係数 u,v,w にエルミート的フェード関数 h(t)=t2(32t) をかけて滑らかにします。

これだけだと値が連続でも,原典が指摘するように「格子点の値を補間しているせいで,セルの中心と頂点が常に極値になる」ため,依然として格子模様が見えます。本物のパーリンノイズの賢いところは,格子点に乱数スカラではなく乱数ベクトル rijk を置き,補間する値を「格子点からクエリ点への変位ベクトル wijk=p(i,j,k)rijk の内積」にすることです。これで格子点では内積がゼロになり,極値が格子に固定されなくなります。

Perlin::noise がこの本物の実装です。

common/src/perlin.rs
rust
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 がフェード関数 h を適用した補間係数で,トリリニア重み wijk=(iu+(1i)(1u)) … の形で展開しています。dot(c[i][j][k], weight_v) が「格子点に置いた乱数ベクトルと変位ベクトルの内積」です。値域はだいたい [1,1] で,NoiseTexture::valueSolid モードでは 0.5(1+)[0,1] にマップします。

C++ と Rust の違い

C++ 原典は cvec3 c[2][2][2] のローカル配列で確保し,補間関数 perlin_interpstatic メンバとして書きます。Rust では [[[Vec3; 2]; 2]; 2] と多重配列リテラルで同じものを宣言でき,Vec3Copy + Default 風に振る舞うので初期化も [Vec3::new(0,0,0); 2]; 2]; 2] で一発です。perlin_interpPerlin の状態を必要としないので自由関数にしました。所有権・借用は &[[[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) に差し替えるだけです。

r205-perlin-smooth/src/lib.rs
rust
let pertext: Arc<dyn Texture> = Arc::new(NoiseTexture::new(NoiseMode::Solid, 4.0));

ブロックは消え,等値線がうねうねと走る,いかにも「ノイズ」らしい模様になります。

乱流(turbulence)

パーリンノイズを複数の周波数で重ね合わせると,自己相似的な乱流っぽい模様が得られます。原典では octave 数 depth = 7 とし,各層で周波数を倍に・振幅を半分にしながら足し,最後に絶対値をとります。

turb(p)=|n=0depth112nnoise(2np)|

絶対値をとることで,谷の部分にカスプ(負の値が 0 で折り返される尖り)が生まれ,雲や火炎の「ぎざぎざ」が出ます。

common/src/perlin.rs
rust
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::Turbulenceturb(sp, 7) を直接灰色値とします(範囲は概ね [0,1] に収まる)。

谷に黒い筋が走るのが乱流の特徴です。これを直接テクスチャに使うと「燃えさし」のようになりますが,多くの自然素材では原典が次節で述べるように「乱流を直接表示する」のではなく「別の関数の引数を撹乱する」用途で使われます。

大理石風:位相を撹乱した正弦波

最後の段階は,パーリン本人がチュートリアルで挙げている「大理石」風テクスチャです。基本となる縞模様を sin(z) で作り,その位相を乱流で撹乱します。

marble(p)=12(1+sin(scalez+10turb(p)))

縞の周波数は scale * z(つまり sp.z())で決まり,10 * turb(p) の項が縞をうねうねと押しのけます。係数 10 が大きいほど縞が乱れ,0 に近づけると平行な縞に戻ります。

common/src/texture.rs
rust
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[1,1] なので 0.5(1+)[0,1] に正規化します。

r205-perlin-marble/src/lib.rs
rust
let pertext: Arc<dyn Texture> = Arc::new(NoiseTexture::new(NoiseMode::Marble, 4.0));

縞は z 軸に沿って走り,球面上では緯線のように見えますが,乱流が位相を局所的にずらすので,本物の大理石らしい不規則な脈ができます。

まとめ

  • common/src/perlin.rsPerlin 型を新設し,3 段階のメソッドを公開した。
    • noise_hash(p):3 軸置換 XOR + スカラ乱数のブロックノイズ
    • noise(p):格子点の乱数ベクトルとの内積をエルミート補間する本物のパーリンノイズ
    • turb(p, depth):周波数倍・振幅半でオクターブを足す乱流(絶対値)
  • common/src/texture.rsNoiseMode 列挙体と NoiseTexture を追加した。scale で周波数を制御し,4 つのモード(Hash / Solid / Turbulence / Marble)を 1 型で提供する。
  • 4 つのデモクレート r205-perlin-{hash,smooth,turb,marble} を追加し,各モードの効果を two_perlin_spheres シーン上で比較できるようにした。

これで「世界座標の関数として色を計算する」道具立てが大きく広がりました。次章では,外部の画像をテクスチャとして読み込む ImageTexture を導入し,地球儀のような UV マップ表現に進みます。