⛰️

パーリンノイズ生成ライブラリ perlin_noise gem の使い方

2023/10/16に公開

https://github.com/junegunn/perlin_noise

いちばん簡単な使い方

require "perlin_noise"
noise = Perlin::Noise.new(1)
noise[0.01]  # => 0.505004826794
noise[0.02]  # => 0.510037257216
noise[0.03]  # => 0.515121258026
  • 第一引数に次元数を指定する
  • 0.0..1.0 の範囲で返ってくる
  • しかし、ほとんどの値は中央付近に分布する (調整方法あり)
  • 位置の値はそれ自体よりも間隔が感覚的に重要になる
    • 0.1 間隔 → 起伏が強め
    • 0.01 間隔 → ちょうどいいくらい
    • 0.001 間隔 → かなりなめらか
  • ノイズというより波形と表現した方が合っているような気がする

2次元の場合

noise = Perlin::Noise.new(2)
noise[0.01, 0.01]      # => 0.49849334098480735
noise[0.02, 0.02]      # => 0.49702264996355916
noise[0.03, 0.03]      # => 0.495620893799757
noise[0.04] rescue $!  # => #<ArgumentError: Invalid coordinates>
  • 1次元のときと同じ要領でいける
  • つまり何次元でもいける
  • 第一引数と #[] への引数の数を合わせること

コントラストを上げる

noise = Perlin::Noise.new(1)
v = noise[0.01]  # => 0.4950049253
c = v
c = Perlin::Curve::CUBIC.call(c)  # => 0.4925076372119325
c = Perlin::Curve::CUBIC.call(c)  # => 0.4887622969929636
c = Perlin::Curve::CUBIC.call(c)  # => 0.4831462838178697
c = Perlin::Curve::CUBIC.call(c)  # => 0.47472900024709724
c = Perlin::Curve::CUBIC.call(c)  # => 0.462125777675654

中央付近に分布してしまう値の差を広げることを、作者のドキュメントでは「コントラストを上げる」と表現している。求めた値を Perlin::Curve::CUBIC に通すたびに差が広がっていくのがわかる。上のコードを簡潔にすると、

5.times.inject(v, &Perlin::Curve::CUBIC)  # => 0.462125777675654

と書けるので、それで別にいいのだけど、作者のほうでもショートカットを用意してくれている。

contrast = Perlin::Curve.contrast(Perlin::Curve::CUBIC, 5)
contrast.call(v)  # => 0.462125777675654

どちらを使ってもいい。

範囲を -0.5..+0.5 に調整する

0.0..1.0 を -0.5..+0.5 に補正する場合、

noise = Perlin::Noise.new(1)
noise[0.01] - 0.5  # => 0.00500482679400005

とする。またコントラスト調整する場合はコントラスト調整を先に行う点に注意する。

Bad
v = noise[0.01] - 0.5             # => 0.00500482679400005
v = Perlin::Curve::CUBIC.call(v)  # => 7.489414899555933e-05
Good
v = noise[0.01]                         # => 0.505004826794
v = Perlin::Curve::CUBIC.call(v) - 0.5  # => 0.00750698946628181

合成とは?

noise = Perlin::Noise.new(1)
v = 0
# 大きな波
v += noise[0.01] - 0.5  # => -0.004995074699999991
# 小さな波
v += noise[0.1] - 0.5   # => -0.050715074699999974

スケールの異なる波形を合成(といっても足しているだけ)すると大きな波のなかに小さな波が表われるようになる。

可視化する

波形の想像が難しいときは可視化する。

コード
require "perlin_noise"
require "gosu"

width    = 800
height   = 400
edge     = width
scale    = 0.02
contrast = 3

noise = Perlin::Noise.new(1)
step = width.ceildiv(edge.pred)
contrast_fn = Perlin::Curve.contrast(Perlin::Curve::CUBIC, contrast)

image = Gosu.render(width, height) do
  from = nil
  edge.times do |x|
    y = noise[x * scale]
    y = contrast_fn[y] - 0.5
    color = Gosu::Color.from_hsv(0, 0, 0.0)
    to = V[x * step, height * 0.5 + y * height * 0.5]
    if from
      Gosu.draw_line(*from, color, *to, color)
    end
    from = to
  end
end

image.save("images/visualize.png")

リアルな山を表現する

波形を合成すると凸凹した稜線を表現できる。

コード
require "perlin_noise"
require "gosu"

width    = 800
height   = 600
edge     = width
scale    = 0.02
contrast = 2

noise = Perlin::Noise.new(1)
step = width.ceildiv(edge.pred)
contrast = Perlin::Curve.contrast(Perlin::Curve::CUBIC, 0)

from = nil
image = Gosu.render(width, height) do
  from1 = nil
  edge.times do |x|
    y = 0.0

    # 大きな間隔の波
    v = noise[x * 0.01]
    v = 2.times.inject(v, &Perlin::Curve::CUBIC) - 0.5
    y += v

    # 小さな間隔の波
    v = noise[x * 0.06]
    v = (0.times.inject(v, &Perlin::Curve::CUBIC) - 0.5) * 0.2
    y += v

    color = Gosu::Color.from_hsv(0, 0, 0.0)
    to = V[x * step, height * 0.5 + y * height * 0.5]
    if from1
      Gosu.draw_line(*from1, color, *to, color)
    end
    from1 = to
  end
end

image.save("images/mountain.png")

テクスチャを生成する

黒と白は必ず中間色を経由しているのがわかる。

コード
require "perlin_noise"
require "gosu"

width    = 800
height   = 600
size     = 8
scale    = 0.08
contrast = 1

noise = Perlin::Noise.new(2)
image = Gosu.render(width, height) do
  height.ceildiv(size).times do |y|
    width.ceildiv(size).times do |x|
      v = noise[x * scale, y * scale]
      v = contrast.times.inject(v, &Perlin::Curve::CUBIC)
      color = Gosu::Color.from_hsv(0, 0, v)
      Gosu.draw_rect(x * size, y * size, size, size, color)
    end
  end
end

image.save("images/texture.png")

RPGのマップを生成する

波形を高さと見なして色に変換する。

コード
require "perlin_noise"
require "gosu"

width    = 800
height   = 600
size     = 16
scale    = 0.08
contrast = 5

noise = Perlin::Noise.new(2)
image = Gosu.render(width, height) do
  height.ceildiv(size).times do |y|
    width.ceildiv(size).times do |x|
      v = noise[scale * x, scale * y]
      v = contrast.times.inject(v, &Perlin::Curve::CUBIC)
      case
      when v <= 0.1
        h = 180
      when v <= 0.9
        h = 60
      else
        h = 120
      end
      color = Gosu::Color.from_hsv(h, 0.4, 1.0)
      Gosu.draw_rect(x * size, y * size, size, size, color)
    end
  end
end

image.save("images/map.png")

地場を確認する

二次元のノイズを使えば座標から向きがわかる。

コード
require "perlin_noise"

class WaterFlea
  def initialize
    @location = V.rand * $app.window_wh
  end

  def draw
    v = @location * 0.01
    angle = $app.noise[*v] * 100.0
    @location += V.from_angle(angle)
    if @location.x >= $app.window_wh.x || @location.x < 0 || @location.y >= $app.window_wh.y || @location.y < 0
      @location = V.rand * $app.window_wh
    end
    $app.point_draw(@location, radius: 2)
  end
end

class App < Base
  attr_accessor :noise

  def initialize
    super

    @noise = Perlin::Noise.new(2, interval: 1024)
    @water_fleas = 500.times.collect { WaterFlea.new }
  end

  def draw
    super

    @water_fleas.each(&:draw)
  end

  show
end

微生物を動かす

ランダムとは違って意思を持っているように見えなくもない。

コード
require "perlin_noise"

class WaterFlea
  def initialize
    @location = $app.window_wh * 0.5
    @velocity = V.zero
    @noise_offset = V.rand * 10000.0
    @noise_scale = 0.005
    @generation = 0
  end

  def draw
    if $app.mode == 0
      values = *(@noise_offset + @generation * @noise_scale).collect do |e|
        $app.noise[e] - 0.5
      end
      acceleration = V[*values] * 0.1
    else
      acceleration = V.rand(-1.0..1.0)
    end
    @velocity += acceleration
    @velocity = @velocity.clamp_length_max(2.0)
    @location += @velocity
    @location = @location.modulo($app.window_wh)
    $app.point_draw(@location, radius: 2)
    @generation += 1
  end
end

class App < Base
  attr_accessor :noise
  attr_accessor :mode

  def initialize
    super

    @noise = Perlin::Noise.new(1, interval: 1024)
    @mode = 0
    reset
  end

  def reset
    @water_fleas = 100.times.collect { WaterFlea.new }
  end

  def button_down(id)
    super

    case id
    when Gosu::KB_R
      reset
    when Gosu::KB_Z
      @mode = @mode.next.modulo(2)
    end
  end

  def draw
    super

    @water_fleas.each(&:draw)
  end

  show
end

Discussion