「リアルタイムグラフィックスの数学」勉強ログ - 第6章 胞体ノイズ

「リアルタイムグラフィックスの数学」勉強ログ - 第6章 胞体ノイズ

目次

はじめに#

「リアルタイムグラフィックスの数学」の第 6 章の胞体ノイズについての勉強ログです。

胞体ノイズとは?#

値ノイズと勾配ノイズは格子点でのランダムな値を使ったノイズ関数でした。一方この章で学ぶ胞体ノイズは距離を使ってつくられます。胞体(セル)とは生物の細胞などのことで、胞体ノイズは「近さ」によって空間をバラバラに分割します。

第 1 近傍距離とボロノイ分割#

胞体ノイズは空間内に点をバラまいて、各点への距離を測ることで得られますが、バラまかれたこれらの点は特徴点と呼ばれます。ここで特徴点をA1,...,AnA_1,...,A_nとします。空間内の点 P を定めたとき、P から特徴点AiA_iへの距離をd(P,Ai)d(P, A_i)で表すと、すべての特徴点までの距離の最小値は次のように求まります。

min(d(P,A1),...,d(P,An))\mathrm{min}(d(P, A_1), ... , d(P, A_n))

この距離の最小値のことを第 1 近傍距離と呼び、第 1 近傍距離を与える点、つまり最も近くにある特徴点を第 1 近傍点と呼びます。

特徴点の分布#

胞体ノイズは特徴点をランダムにバラまくことによって得られます。ここで特徴点のバラまき方に規則性をもたせることにします。座標が整数値であるようにすると第 1 近傍点は容易に分かります。例えば点 P(4.6,2.3)(4.6, 2.3)の場合は、座標(5,2)(5, 2)が第 1 近傍点となり、第 1 近傍距離は(54.6)2+(22.3)2=0.5\sqrt{(5-4.6)^2 + (2-2.3)^2} = 0.5となります。ここで第 1 近傍距離をとる関数をF1F_1とすると、F1(P)=0.5F_1(P) = 0.5になります。

次に乱数を使って特徴点をマスの内部でずらし、それを新たに特徴点としてみます。特徴点の位置ベクトルは次のように

(格子点ベクトル)+([0.5,0.5]区間の乱数ベクトル)(格子点ベクトル) + ([-0.5, 0.5]区間の乱数ベクトル)

となります。この場合、容易に第 1 近傍点を特定できないので、いくつかの候補となる特徴点への距離を計算して比べる必要があります。縦横 1 の正方形の中には必ず 1 つの特徴点が含まれるので、第 1 近傍距離は12+12=2\sqrt{1^2 + 1^2} = \sqrt{2}以下になります。

したがって、点 P が含まれるマスに対し、そのマスから2\sqrt{2}以内の距離にある上下左右 2 つ隣までのマスをすべて探索すれば、必ず第 1 近傍点は見つけることができます。

コードで書くと次のようになります。

第1近傍距離の計算
float fdist(vec2 p) {
  vec2 n = floor(p + 0.5); // 最も近い格子点
  float dist = sqrt(2.0); // 第1近傍距離の上限
  for (float j = - 2.0; j <= 2.0; j++) {
    for (float i = - 2.0; i < 2.0; i++) {
      vec2 grid = n + vec2(i, j); // 近くの格子点
      vec2 jitter = sin(u_time) * (hash22(grid) - 0.5); // 特徴点と格子点のずれ
      dist = min(dist, distance(grid + jitter, p)); // 第1近傍距離を更新
    }
  }
  return dist;
}

この関数は、2 つ隣まどのマスに含まれる特徴点への距離を順に計算し、第 1 近傍距離の上限値2\sqrt{2}を初期値にし、下回る場合に値を更新します。

乱数の大きさをサイン関数によって動かせば、特徴点が格子からずれるに従って、歪に動くことになります。

第1近傍距離
第1近傍距離

パフォーマンス改良#

先ほどのコードは 2 つ隣までのマスを端からしらみ潰しに探索してますが、この方法では52=255^2 = 25マスの探索が必要です。探索方法を効率化して、探索対象のマスを減らしてみます。改良版のコードは次のようになります。

第1近傍距離の計算の改良
float fdist21(vec2 p) {
  vec2 n = floor(p + 0.5); // 最も近い格子点
  float dist = sqrt(2.0);
  for (float j = 0.0; j <= 2.0; j++) {
    vec2 grid; // 近くの格子点
    grid.y = n.y + sign(mod(j, 2.0) - 0.5) * ceil(j * 0.5);
    if (abs(grid.y - p.y) - 0.5 > dist) {
      continue;
    }
    for (float i = -1.0; i <= 1.0; i++) {
      grid.x = n.x + i;
      vec2 jitter = hash22(grid) - 0.5;
      dist = min(dist, length(grid + jitter - p));
    }
  }
  return dist;
}

まずは、y 方向(行)の探索を見てみます。

for (float j = 0.0; j <= 2.0; j++) {
  vec2 grid; // チェックする格子点の座標
 
  // Y方向に探索する範囲を決める
  grid.y = n.y + sign(mod(j, 2.0) - 0.5) * ceil(j * 0.5);
 
  // 縦方向で既に "dist" より離れていたらスキップ
  if (abs(grid.y - p.y) - 0.5 > dist) {
    continue;
  }
}

grid.yjの値により次のようになります。

  • j = 0 のとき、grid.y = n.y
  • j = 1 のとき、grid.y = n.y + 1.0
  • j = 2 のとき、grid.y = n.y - 1.0

近傍点は中央近くのマスにある確率が高いので、このように中心から離れるように探索するほうが効率的になります。また、改良前のコードは 2 つ隣のマスまで探索しましたが、2 つ隣のマスに第 1 近傍点が含まれるのは特殊な場合なので 1 つ隣までに制限しています。

このように中心から探索して、2\sqrt{2}よりも離れていたら、その行の特徴点の計算をスキップすることができます。

for (float i = -1.0; i <= 1.0; i++) {
  grid.x = n.x + i;
  vec2 jitter = hash22(grid) - 0.5;
  dist = min(dist, length(grid + jitter - p));
}

先述とおり 1 つ隣まで探索するので、x 方向は、n.x - 1, n.x, n.x + 1を調べます。あとは同様にランダムな特徴点とpとのユークリッド距離を計算し、最短距離の場合更新します。

第 1 近傍距離をとる関数の勾配#

第1近傍距離をとる関数の勾配の可視化
第1近傍距離をとる関数の勾配の可視化

第 1 近傍距離をとる関数F1F_1の勾配を可視化したものが上図になります。F1F_1は特徴点で値が 0 になりますが、特徴点の周りで勾配の向きも回転しています。また、特徴点と特徴点との間の境界線上で色が反転しているので、勾配の向きが切り替わっています。この境界線が次に紹介するボロノイ分割を与えます。

ボロノイ分割#

ボロノイ分割とは、簡単に言えばある領土を公平に分配するための分割です。例えば、隣の集落との境界線を引くのに最も公平な方法は、隣り合う集落同士の中心点からそれぞれ線を引いて、境界線は垂直二等分線となるように線を引くことです。

次はボロノイ分割したセルの塗り分けのコードになります。

ボロノイ分割
vec2 voronoi2(vec2 p) {
  vec2 n = floor(p + 0.5);
  float dist = sqrt(2.0);
  vec2 id; // ボロノイ胞体のID変数
  for (float j = 0.0; j <= 2.0; j++) {
    vec2 grid;
    grid.y = n.y + sign(mod(j, 2.0) - 0.5) * ceil(j * 0.5);
    if (abs(grid.y - p.y) - 0.5 > dist) {
      continue;
    }
    for (float i = -1.0; i <= 1.0; i++) {
      grid.x = n.x + i;
      vec2 jitter = hash22(grid) - 0.5;
 
      // 第1近傍距離が更新される場合
      if (length(grid + jitter - p) <= dist) {
        dist = length(grid + jitter - p); // 距離を更新
        id = grid; // IDとして格子点をとる
      }
    }
  }
  return id; // IDを返す
}

先ほどのF1F_1は第 1 近傍距離を返していましたが、ここでは第 1 近傍点の ID(格子点)を返すように変更し、その情報に対して色を決定するようにしてます。この ID から乱数を使って色を対応させると、モザイクタイルのようにタイル張りされます。3 変数に拡張されたF1F_1を改変すれば、同様に 3 次元ボロノイ分割もつくることができます。

ボロノイ分割の塗り分け
ボロノイ分割の塗り分け

胞体ノイズの構成#

ここまでは、1 番近い特徴点のみを計算しましたが、さらに 2 番目以降に近い特徴点の距離を計算してみましょう。i 番目に近い特徴点を第 i 近傍点、第 i 近傍点との距離を第 i 近傍距離とし、第 i 近傍距離を返す関数をFiF_iとします。

次のコードはF1,F2,F3,F4F_1, F_2, F_3, F_4の実装(第 4 近傍距離までの探索)になります。

第4近傍距離までの探索
// 暫定4位までの値を成分するlistと値vを比較して並べ替え
vec4 sort(vec4 list, float v) {
  bvec4 res = bvec4(step(v, list)); // 比較結果の真偽値
  return res.x ? vec4(v, list.xyz) :
    res.y ? vec4(list.x, v, list.yz) :
    res.z ? vec4(list.xy, v, list.z) :
    res.w ? vec4(list.xyz, v):
    list;
}
 
vec4 fdist24(vec2 p) {
  vec2 n = floor(p + 0.5);
  vec4 dist4 = vec4(length(1.5 - abs(p - n))); // 第4近傍距離の上限
  for (float j = 0.0; j <= 4.0; j++) {
    vec2 grid;
    grid.y = n.y + sign(mod(j, 2.0) - 0.5) * ceil(j * 0.5);
    if (abs(grid.y - p.y) - 0.5 > dist4.w) {
      continue;
    }
    for (float i = -2.0; i <= 2.0; i++) {
      grid.x = n.x + i;
      vec2 jitter = hash22(grid) - 0.5;
      dist4 = sort(dist4, length(grid + jitter - p)); // 近傍距離の更新
    }
  }
  return dist4;
}

第 1~4 近傍点を見つけるには、2 つ隣のマスまでを探索します。第 1 近傍点を探索したように、第 4 近傍距離の上限値を初期値とした 4 次元ベクトルdist4を順に更新し、近傍距離を求めます。特徴点との距離を計算したら、dist4との値を比較し、値が小さい場合は更新します。

第1~4近傍距離
第1~4近傍距離

特徴点の近傍#

近傍距離の意味は階段関数で二値化すると分かります。F1,F2,F3,F4F_1, F_2, F_3, F_4の値を二値化してみます。

近傍距離のRGBへの割り当て
void main() {
  // ...
  float thr = 0.7; // しきい値
  bvec4 dist4b = bvec4(step(thr, fdist24(pos))); // しきい値を上回るとき真となる真偽値ベクトル
 
  gl_FragColor = dist4b.x ? vec4(1.0, 1.0, 1.0, 1.0) : // しきい値 < F1のとき白
    dist4b.y ? vec4(1.0, 0.0, 0.0, 1.0) : // F1 < しきい値 < F2のとき赤
    dist4b.z ? vec4(0.0, 1.0, 0.0, 1.0) : // F2 < しきい値 < F3のとき緑
    dist4b.w ? vec4(0.0, 0.0, 1.0, 1.0) : // F3 < しきい値 < F4のとき青
    vec4(0.0, 0.0, 0.0, 1.0); // F4 < しきい値のとき黒
}

第1~4近傍距離の二値化と塗り分け
第1~4近傍距離の二値化と塗り分け

結果を見てみると、F1,F2,F3,F4F_1, F_2, F_3, F_4の値のしきい値処理が円盤の重なり方と対応します。円盤と重ならない部分は白、1 枚重なる部分は赤、2 枚重なる部分は緑、3 枚重なる部分は青、4 枚重なる部分は黒になります。つまり多く重なる部分はFiF_iの値が小さくなります。

この円盤は特徴点からの距離がしきい値以内の領域を表していますが、このようなある点から一定距離以内にある領域を近傍と呼びます。

胞体ノイズ#

値ノイズと勾配ノイズはサーフレットをもとにつくられていましたが、胞体ノイズはF1,...,FnF_1, ... , F_nをもとにノイズをつくります。ここではF1,...,FnF_1, ... , F_nに重みa1,...,ana_1, ... , a_nをつけて、総和a1F1+...+anFna_1F_1 + ... + a_nF_nの絶対値をとってみます。重みを変えることで、様々なテクスチャがあらわれます。

胞体ノイズ
vec4 wt; // 重み
float cnoise21(vec2 p) {
  return abs(dot(wt, fdist24(p)));
}

重みwの胞体ノイズ
重みwの胞体ノイズ

次回リンク#

後で詳しく調べるものリスト#

参考書籍#

Related Post
WebGLで使う行列演算の備忘録

はじめに

ライブラリを使用せず素の WebGL で書けるようになりたいと思い、最近はいろいろ勉強してます。WebGL を触るうえで行列演算は避けては通れません。WebGL 用の行列演算の既存のライブラリだと glMatrix がありますが、今回は線形代数の勉強も兼ねて自作で作ってみました。(自作といっても、ほぼほぼ OGL の数学演算系をもって来ているだけですが)

足し算などの簡単なのは飛ばして、何をやってるか忘れそうな処理について備忘録的に書いていきます。長くなるので、主に 3x3 行列をみていきます。

数学演算系のコードは以下に置いてます。

https://github.com/nono-k/webgl-study-note/tree/main/src/lib/webgl/math

3x3 行列(Mat3)

3x3 (Mat3)行列のクラスは次のようにしてます。 演算系の関数はMat3Funcで書いています。

import * as Mat3Func from "./functions/Mat3Func";

export class Mat3 extends Array<number> {
  constructor(
    m00 = 1,
    m01 = 0,
    m02 = 0,
    m10 = 0,
    m11 = 1,
    m12 = 0,
    m20 = 0,
    m21 = 0,
    m22 = 1
  ) {
    super(m00, m01, m02, m10, m11, m12, m20, m21, m22);
  }
  // ...
}

数式で書くと次のような行列表記になります。

Meta Data
公開日:2025-12-20
Tags:
#数学
「リアルタイムグラフィックスの数学」勉強ログ - 第8章 3Dレンダリング
image

はじめに

「リアルタイムグラフィックスの数学」の第 8 章の 3D レンダリングについての勉強ログです。

<AmazonLink imageId="61CP8Asy52L.SY522" linkId="3Iy0agT" title="リアルタイムグラフィックスの数学 ― GLSLではじめるシェーダプログラミング" author="巴山竜来" />

テクスチャマッピング

地面とレイの交点を計算し、地面に市松模様をテクスチャとしてマッピングします。市松模様のテクスチャは次のようになります。

float text(vec2 st) {
  return mod(floor(st.s) + floor(st.t), 2.0);
}

ここで 3D 空間のカメラの設定をします。カメラの向きを$\bm{x} = (0, 0, -1)$とし、カメラの上方向を$\bm{y} = (0, 1, 0)$とします。この値に対して外積をとることで、撮影する向きに対する水平方向$(1, 0, 0)$を得ることができます。実際の計算は次のようになります。

Meta Data
公開日:2025-10-11
Tags:
#GLSL
#数学
#勉強ログ
「リアルタイムグラフィックスの数学」勉強ログ - 第7章 距離とSDF
image

はじめに

「リアルタイムグラフィックスの数学」の第 7 章の距離と SDF についての勉強ログです。

<AmazonLink imageId="61CP8Asy52L.SY522" linkId="3Iy0agT" title="リアルタイムグラフィックスの数学 ― GLSLではじめるシェーダプログラミング" author="巴山竜来" />

2 次元 SDF

胞体ノイズでは近傍点との距離を返す関数でしたが、近くの「点」ではなく「図形」との距離を返す関数を考えます。ここでは図形との負の値もありうる距離を返す関数を導入します。これがSDF(Signed Distance Function, 符号付き距離関数)となります。

円の SDF

円の SDF について考えてみます。円は中心点 C と半径 r から決まりますが、C からの距離が r より小さければ円の「内部」、r より大きければ円の「外部」となり、r と等しい場合は円の「境界」です。平面上の点 P に対して、円の外部では値は正、内部では値が負とします。これが円の SDF を定めます。

円のSDF

Meta Data
公開日:2025-10-05
Tags:
#GLSL
#数学
#勉強ログ
「リアルタイムグラフィックスの数学」勉強ログ - 第5章ノイズの調理法
image

はじめに

「リアルタイムグラフィックスの数学」の第 5 章のノイズの調理法についての勉強ログです。

<AmazonLink imageId="61CP8Asy52L.SY522" linkId="3Iy0agT" title="リアルタイムグラフィックスの数学 ― GLSLではじめるシェーダプログラミング" author="巴山竜来" />

再帰

再帰関数とは、関数の中で自分自身の関数を呼び出すような関数のことです。GLSL では再帰関数は使えませんが、for 文を使って再帰的な処理を行うことができます。

非整数ブラウン運動(fBM)

1 以下の定数 G に対し、1 変数ノイズ関数 noise(x)の周波数を 2 倍するごとに値を G 倍して、それを足し合わせてみましょう。式にすると次のようになります。

$$ noise(x) + Gnoise(2x) + G^2noise(4x) + ... + G^knoise(2^kx) $$

ここで素材となるノイズ関数 noise(x)はどのような関数でもいいですが、値の範囲を$[-0.5, 0.5]$区画にずらしておきます。このように加工されたノイズ関数は非整数ブラウン運動(fractional Brownian motion, fBM)と呼ばれます。

Meta Data
公開日:2025-09-24
Tags:
#GLSL
#数学
#勉強ログ
「リアルタイムグラフィックスの数学」勉強ログ - 第4章勾配ノイズ
image

はじめに

「リアルタイムグラフィックスの数学」の第 4 章の勾配ノイズについての勉強ログです。

<AmazonLink imageId="61CP8Asy52L.SY522" linkId="3Iy0agT" title="リアルタイムグラフィックスの数学 ― GLSLではじめるシェーダプログラミング" author="巴山竜来" />

勾配ノイズとは?

前回は値ノイズについて見てきました。値ノイズは計算量も少なく、簡単につくれるノイズですが、格子で区切っているのでブロック状に見えてしまいますし、ムラが現れやすいです。値ノイズを改善したノイズが勾配ノイズになります。

値ノイズの場合は、格子点での乱数の「値」を使うのに対し、勾配ノイズは乱数のベクトル値を「勾配」として使用します。ノイズ関数でよく使用されるパーリンノイズは、この勾配ノイズの一種であります。この章では、勾配ノイズと 2002 年の Perlin による改良版勾配ノイズ(パーリンノイズ)について見ていきます。

勾配ノイズの構成法

1 変数

値ノイズは乱数値をエルミート補間してつくりました。ここで別の見方として、$h(0) = 0$,$h(1) = 1$,$h'(0) = h'(1) = 0$ を満たすエルミート補間関数$h(x)$に対して、$w(x)$を次のように定義します。

Meta Data
公開日:2025-09-20
Tags:
#GLSL
#数学
#勉強ログ
「リアルタイムグラフィックスの数学」勉強ログ - 第3章値ノイズ
image

はじめに

「リアルタイムグラフィックスの数学」の第 3 章の値ノイズについての勉強ログです。

<AmazonLink imageId="61CP8Asy52L.SY522" linkId="3Iy0agT" title="リアルタイムグラフィックスの数学 ― GLSLではじめるシェーダプログラミング" author="巴山竜来" />

値ノイズの構成法

まずは 2 次元の値ノイズの構成法について見てみましょう。

平面上の正方形の4つの頂点(格子点)

平面上の正方形の 4 つの頂点(格子点)を用いて、値ノイズを構成します。各格子点での乱数値、または乱数ベクトルを使って生成されるノイズを格子ノイズと呼びます。格子点の成分は整数になるようにします。点$(x, y)$に対して床値$[]$を使って、$(x, y)$を取り囲むマスの格子点は次の 4 つのベクトルで表します。

Meta Data
公開日:2025-09-16
Tags:
#GLSL
#数学
#勉強ログ
「リアルタイムグラフィックスの数学」勉強ログ - 第2章疑似乱数
image

はじめに

「リアルタイムグラフィックスの数学」の第 2 章の疑似乱数についての勉強ログです。

<AmazonLink imageId="61CP8Asy52L.SY522" linkId="3Iy0agT" title="リアルタイムグラフィックスの数学 ― GLSLではじめるシェーダプログラミング" author="巴山竜来" />

レガシー乱数

GLSL ES 1.0 ではビット演算が使えないため、ハッシュ関数ではなくサイン関数を使用した乱数生成が行われていました。The Book of Shaders ではサイン関数を使用した乱数生成の方法が紹介されています。

https://thebookofshaders.com/10/?lan=jp

サイン関数自体には乱数性はないですが、サイン関数の値に大きい値をかけて桁を上げ、fractで小数部分を取り出すことで乱数のように見えることができます。

レガシー乱数の 1 変数と 2 変数のコードは次のようになります。

// 1変数のレガシー乱数
float fractSin11(float x) {
  return fract(1000.0 * sin(x));
}

// 2変数のレガシー乱数
float fractSin21(vec2 xy) {
  return fract(sin(dot(xy, vec2(12.9898, 78.233))) * 43758.5453123);
}
Meta Data
公開日:2025-09-10
Tags:
#GLSL
#数学
#勉強ログ
「リアルタイムグラフィックスの数学」勉強ログ - 第1章補間
image

はじめに

「リアルタイムグラフィックスの数学」の第 1 章の補間についての勉強ログです。

<AmazonLink imageId="61CP8Asy52L.SY522" linkId="3Iy0agT" title="リアルタイムグラフィックスの数学 ― GLSLではじめるシェーダプログラミング" author="巴山竜来" />

線形補間とグラデーション

mix 関数について

GLSL における mix 関数は以下のような数式になります。

$$ \bm{a} + x\overrightarrow{AB} = \bm{a} + x(\bm{b} - \bm{a}) = (1 - x)\bm{a} + x\bm{b} $$

これは、線分 AB があり、その線分上を動く 0 以上 1 以下の x に対して、線分 AB を x: (1 - x)に内分する点の位置ベクトルを表しています。上記の式のベクトルをmix(a, b, x)として定義しています。このように 2 点間をつなぐことを補間と呼び、上記の式は線形補間(Linear Interpolation)の公式です。

コード 1.1 のように、赤(1, 0, 0)と青(0, 0, 1)を mix 関数でつなぐことで 2 色のグラデーションを作ることができます。

vec2 RED = vec3(1.0, 0.0, 0.0);
vec3 BLUE = vec3(0.0, 0.0, 1.0);
vec3 col = mix(RED, BLUE, pos.x);

赤と青のグラデーション

Meta Data
公開日:2025-09-07
Tags:
#GLSL
#数学
#勉強ログ
勉強のために「リアルタイムグラフィックスの数学」のGLSLコードを確認できるサイトを作った

はじめに

シェーダに再挑戦したいと思い、「リアルタイムグラフィックスの数学」の本をまた読み直してます。

<AmazonLink imageId="61CP8Asy52L.SY522" linkId="3Iy0agT" title="リアルタイムグラフィックスの数学 ― GLSLではじめるシェーダプログラミング" author="巴山竜来" />

勉強する際に、本に書いているサンプルコードを Web サイトですぐに確認できるように、Astro と Three.js を使用して GLSL コードを表示するサイトを作成しました。サイトはこちらになります。

リポジトリはこちらになります!

https://github.com/nono-k/book-of-realtime-graphics-math

勉強方法の方針

いったん、本を読みながらローカルの VSCode 上で GLSL コードを書いて確認して、Web サイトに GLSL のコードと実行結果のサムネをアップしていきたいと思います。

また、このブログで勉強した内容を章ごとに残していきたいと思います。以下はリンクになります。

Meta Data
公開日:2025-09-06
Tags:
#GLSL
#数学
Info