ゲーム開発
Unity
UnrealEngine
C++
Blender
Houdini
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • Houdini
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • ラーメン日記
    • 四コマ漫画
      • その他
        • おすすめアイテム
        • おもしろコラム
      1. ホーム
      2. 20250622_01_wave_function_collapse

      【プロシージャル】Pythonで学ぶ波動関数崩壊アルゴリズム(Wave Function Collapse)

      PythonHoudiniグラフィックスプロシージャルアルゴリズム
      2025-06-22

      マイケル
      マイケル
      みなさんこんにちは! マイケルです!
      エレキベア
      エレキベア
      こんにちクマ~~~
      マイケル
      マイケル
      今日はとある面白いアルゴリズムを見つけたので実装してみました。 その名も「波動関数崩壊アルゴリズム(Wave Function Collapse)」です!!
      エレキベア
      エレキベア
      か、かっこいい・・・・!!
      マイケル
      マイケル
      今回実装する内容はGitHubリポジトリに上げています! 細かい処理内容についてはこちらをご参照ください!

      GitHub - python-wfc-generater
      ▲今回実装したリポジトリ

      20250623_01_01

      アルゴリズム概要

      マイケル
      マイケル
      今回紹介する波動関数崩壊アルゴリズムは、量子力学の波動関数の収縮(崩壊)から着想を得て作られたものなんだ。 ※考え方が少し似ているだけで、実装内容自体は物理的に関係がありません
      エレキベア
      エレキベア
      実際は関係ないのクマか・・・ しかしかっこよくて言いたくなるクマ
      マイケル
      マイケル
      名前もかっこいいけど、処理の結果も面白いんだ! 簡単にいうと 入力された画像からパターンを抽出して画像生成する という処理になっていて、例えば下記のわかめ画像を入力にすると大量のわかめが生産されます!
      20250623_01_in_wakame
      ▲わかめっぽい入力画像

      20250623_01_out_wfc_wakame
      ▲処理を実行すると大量のわかめが生産された

      エレキベア
      エレキベア
      おお~~~これが自動で生成されるのクマか 最近の画像生成AIみたいクマね
      マイケル
      マイケル
      この処理がシンプルなアルゴリズムで行えるというのが面白いんだ! また、Wave Function Collapseに似てシンプルな考え方として WangTiles というものもあります。 理解の助けになると思うので、まずWangTilesについて解説した後にWave Function Collapseの解説を行おうと思います。

      WangTilesとは

      マイケル
      マイケル
      WangTilesの考え方は非常にシンプルで、各タイルの辺に対して隣接情報を定義して繋ぎ合わせるというものになっています。 このつなぎ合わせる部分に関しては特に方法は制限されていません。
      • 数種のタイルからなる集合を選び、隣合う辺の色が一致するようにタイルのコピーを並べるという概念
        • タイルの各辺に色やラベルがついており、隣り合うタイル同士は同じ色でなければならない
        • 同じ色、ラベル同士を繋げることで新たな画像を生成する
      エレキベア
      エレキベア
      繋ぐことが出来るタイルをパズルのように組み合わせるというわけクマね 非常にシンプルクマ
      マイケル
      マイケル
      WangTilesの実装として使いやすいものが下記論文で紹介されている方法で、 各行、列で同じルールのものが並ぶよう定義されています。

      参考:
      Tile-Based Texture Mapping on Graphics Hardware

      20250623_01_02
      ▲各行、列が同じルールのタイルを並べる

      マイケル
      マイケル
      今回はこちらの方法を使用して実装を試してみます。

      Wave Function Collapseとは

      マイケル
      マイケル
      そして本題の Wave Function Collapse についてですが、こちらも自動で画像生成を行うアルゴリズムで下記リポジトリにC#での実装が公開されています。

      公式のGitHubリポジトリ GitHub - WaveFunctionCollapse

      マイケル
      マイケル
      WangTilesと異なるところは 生成処理のプロセスが明確に定義されている点 で、考え方も異なるものとなっています。 よくある説明としては下記のようなものが見られます。
      • タイルマップ画像を入力として、パターンを選択して新たな画像を生成するアルゴリズム
        • 全ての存在可能な状態(エントロピー)があるセルから1つに収束(崩壊)させ、伝搬させることで確定させていく
      エレキベア
      エレキベア
      エントロピー・・・?収束・・・? よく分からないクマ・・・
      マイケル
      マイケル
      量子力学の波動関数の収縮から着想を得ているから専門用語が使われているけどやっていることは単純で、 エントロピー=「各タイルが存在可能かの状態」を表すもの として、可能性が絞られたものから順にセルを確定していく といったものになっています。
      • エントロピー
        • 熱力学及び統計力学において定義される状態量
        • WFCの場合、「各タイルが存在可能かの状態」を表す尺度となる
      マイケル
      マイケル
      下記の記事で紹介されているような 数独を解いていく感覚に近い のと、 実際にデモなどを触ってみるとイメージが掴みやすいと思います。

      5分で学ぶ RustとWave Function Collapse (波動関数崩壊アルゴリズム)の旅 - Qiita
      ▲Rustで実装しながら解説いただいている記事、分かりやすい!

      Wave Function Collapse - Mixed Initiative Demo by Martin Donald
      ▲Wave Function Collapseのデモ

      20250623_01_03
      ▲初めは全てのセルがどのタイルになる可能性持っている

      20250623_01_04
      ▲一つのセルが確定されると、その近辺のセルにも情報が伝搬して確定されていく

      エレキベア
      エレキベア
      へ~~面白いクマね セルを決めるとその近くのセルの候補も絞られていくクマね
      マイケル
      マイケル
      そして公式で用意されている方法として、下記2つのモデルが存在しています。 Simple Tile Modelが事前にタイルパターンを定義する必要があるのに対して、Overlapping Modelはパターン抽出も自動で行います。
      • Simple Tile Model
        • あらかじめ定義されたセットのタイルを使用して画像を生成する
        • タイルは通常、辺や角が他のタイルと一致するよう設計される
      • Overlapping Model
        • 1枚の画像からタイル画像、重み、ルールを自動生成する
        • 画像内の全てのNxNピクセルのパターンを分析する
      エレキベア
      エレキベア
      ということはただ画像入力するだけで画像生成してくれるクマね なんか最近のAI見たいクマ
      マイケル
      マイケル
      今回はせっかくなのでOverlapping Modelの方を実装して、パターン自動抽出も試してみました。 簡単に実装の解説も行いますので、お楽しみに!
      マイケル
      マイケル
      なお、着想元となった量子力学の波動関数の収縮については、ヨビノリさんの動画で丁寧に解説されているのでおすすめです! 気になった方はぜひチェックしてみてください。
      • 量子力学の波動関数の収縮(崩壊)から着想されているが、直接的に関係があるわけではない
        • 存在可能性がある状態から観測することで確定する、という現象が今回のアルゴリズムの着想となっている(恐らく)

      数式なしでもしっかり学ぶ量子力学
      ▲ヨビノリさんによる解説動画

      20250623_01_06

      エレキベア
      エレキベア
      ヨビノリさんの動画はいつもお世話になってるクマ~~

      画像の自動生成例

      マイケル
      マイケル
      それでは具体的な実装内容に入る前に、それぞれどのような画像生成結果になるのかを軽く見てみます。

      WangTiles

      マイケル
      マイケル
      まずWangTilesについてですが、こちらは分かりやすく事前に定義したパターンを組み合わせて生成されます。
      WangTilesにおける画像自動生成例
      入力画像
      画像サイズ
      出力画像
      画像サイズ
      20250623_01_in_tile_patterns
      96x96
      20250623_01_out_wang_tile_patterns_240
      240x240
      エレキベア
      エレキベア
      ちゃんと道が繋がっているクマね

      Wave Function Collapse (Overlapping Model)

      マイケル
      マイケル
      次にWaveFunctionCollapseによる結果を見てみます。 今回生成したのはOverlapping Modelで、3x3のパターンで自動抽出しています。
      WaveFunctionCollapseにおける画像自動生成例
      入力画像
      画像サイズ
      出力画像
      画像サイズ
      20250623_01_in_tile_patterns
      96x96
      20250623_01_out_wfc_tile_patterns
      240x240
      20250623_01_in_road_small
      24x24
      20250623_01_out_wfc_road_small
      15x24
      20250623_01_in_wakame
      96x96
      20250623_01_out_wfc_wakame
      240x240
      マイケル
      マイケル
      処理時間はかかりますが、それらしい画像が生成されていることが確認できました。 1枚目の入力画像はWangTilesの入力画像と同じですが、3x3で自動抽出しているため、2本で引かれた道ではなく1本の線が組み合わされて生成されている のが面白いですね。
      エレキベア
      エレキベア
      結構それっぽくなったクマね 3枚目のわかめ画像もいい感じに繋がっていてすごいクマね

      WangTilesの処理内容

      マイケル
      マイケル
      それでは実際に実装したスクリプトと共に処理内容について軽く触れていきます。 今回実装したWangTilesのスクリプトは下記になります。

      GitHub - python-wfc-generater - wang_tiles_random.py
      ▲今回実装したWangTilesの処理

      タイルパターン画像を用意する

      マイケル
      マイケル
      まずはタイルパターン画像を用意します。 今回は冒頭で紹介した下記論文の4x4パターンで用意しました。
      20250623_01_in_tile_patterns
      ▲4x4のタイルパターン画像を用意する

      Tile-Based Texture Mapping on Graphics Hardware
      ▲今回実装した論文

      マイケル
      マイケル
      スクリプトでの定義部分は下記になります。 0、1でパターンを定義して、それぞれのタイルに対して付与して読み込んでいます。
          # 下記論文を参考にした4x4のタイルIDパターン
          # https://graphics.stanford.edu/papers/tile_mapping_gh2004/
          TILE_ID_STR_PATTERN_LIST = [
              "0010", "0110", "0111", "0011",
              "1010", "1110", "1111", "1011",
              "1000", "1100", "1101", "1001",
              "0000", "0100", "0101", "0001",
          ]
      
          @dataclass
          class TileId:
              """タイルIDデータクラス"""
      
              top: int
              right: int
              bottom: int
              left: int
      
      
          @staticmethod
          def get_tile_id_pattern_list() -> List[WangTilesUtility.TileId]:
              """タイルIDパターンリストを取得する
      
              Returns:
                  List[WangTilesUtility.TileId]: タイルIDパターンリスト
              """
              tile_id_list = []
              for s in WangTilesUtility.TILE_ID_STR_PATTERN_LIST:
                  tile_id_list.append(
                      WangTilesUtility.TileId(
                          top=int(s[0]), right=int(s[1]), bottom=int(s[2]), left=int(s[3])
                      )
                  )
              return tile_id_list
      
      ▲タイルパターンン画像に合わせて隣接ルールを定義したもの
      エレキベア
      エレキベア
      この辺は単純クマね

      タイルパターン画像を元に組み合わせて出力する

      マイケル
      マイケル
      あとは各タイルのパターンをチェックしながら、接続できるタイルを並べていきます。
          @staticmethod
          def create_random_tile_grids(
              tile_count_x: int,
              tile_count_y: int,
              tile_id_pattern_list: List[TileId],
              random_seed: int = 0,
          ) -> List[List[WangTilesUtility.TileId]]:
              """指定された情報からランダムにタイルIDのGrid情報を生成する
      
              Args:
                  tile_count_x (int): X方向のタイル数
                  tile_count_y (int): Y方向のタイル数
                  tile_id_pattern_list (List[TileId]): タイルIDパターンリスト
                  random_seed (int, optional): 乱数シード値. Defaults to 0.
      
              Returns:
                  List[List[WangTilesUtility.TileId]]: タイルIDのGrid情報
              """
              random.seed(random_seed)
      
              # grid初期化
              id_tile_list: List[List[WangTilesUtility.TileId]] = [
                  [None for _ in range(tile_count_x)] for _ in range(tile_count_y)
              ]
      
              # タイルを左辺、上辺どちらかが繋がるようにランダムに抽出
              for y in range(tile_count_y):
                  for x in range(tile_count_x):
                      filter_pattern_id_list = []
                      # 最初のタイルはランダムに選択
                      if x == 0 and y == 0:
                          id_tile_list[y][x] = random.choice(tile_id_pattern_list)
                          continue
                      # 左辺 == 左タイルの右辺
                      left_neighbor_tile_id = id_tile_list[x][y - 1]
                      if left_neighbor_tile_id:
                          for tile_id in tile_id_pattern_list:
                              if tile_id.left == left_neighbor_tile_id.right:
                                  filter_pattern_id_list.append(tile_id)
                      # 上辺 == 上タイルの下辺
                      top_neighbor_tile_id = id_tile_list[x - 1][y]
                      if top_neighbor_tile_id:
                          for tile_id in tile_id_pattern_list:
                              if tile_id.top == top_neighbor_tile_id.bottom:
                                  filter_pattern_id_list.append(tile_id)
      
                      id_tile_list[x][y] = random.choice(filter_pattern_id_list)
      
              return id_tile_list
      
      ▲隣接ルールを元にランダムに画像を生成する
      マイケル
      マイケル
      生成処理は以上になります。 処理自体の速度も速く、解像度を上げてもいい感じに生成してくれます。
      20250623_01_out_wang_tile_patterns_240
      ▲タイルパターン画像を組み合わせた状態(240x240)

      20250623_01_out_wang_tile_patterns_960
      ▲更に大きな画像を生成した例(960x960)

      エレキベア
      エレキベア
      すごいクマ~~~ 無限に広がる道が生成できるクマね

      Wave Function Collapseの処理内容

      マイケル
      マイケル
      それではWave Function Collapseの処理内容についても見てみます。 今回実装したのは簡易的なOverlapping Model で、 画像からパターン抽出も行う実装としています。

      GitHub - python-wfc-generater - wfc_overlapping_model.py
      ▲今回実装したWaveFunctionCollapseの処理

      • 実装内容
        • 3x3のパターンを自動抽出する
        • 隣接方向は上下左右のみで、回転の考慮もしていません
      マイケル
      マイケル
      なお下記のデモの結果がすごく分かりやすいので、こちらの画像を見ながら説明させていただく形にしています。

      参考:
      Wave Function Collapse - Mixed Initiative Demo by Martin Donald
      ▲Wave Function Collapseのデモ

      エレキベア
      エレキベア
      どんな実装内容なのか楽しみクマ~~~~

      入力画像からパターンを抽出し、隣接情報を生成する

      マイケル
      マイケル
      まず入力画像からのパターン抽出をどのように行うかというと、 NxNのパターンの組み合わせを全て確認して抽出しています。 今回は3x3で抽出していますが、これが4x4、5x5となるとパターンが膨大な数になり処理時間もその分増えるので注意が必要です。
      20250623_01_in_wakame
      ▲入力した画像のNxNの組み合わせをチェックし、パターンを抽出する

          def _create_pattern_info_list(
              self, image_grid: ImageGridType, check_pattern_size: int
          ) -> List[TilePatternInfo]:
              """
              入力グリッドからユニークなパターンを抽出し、頻度もカウントする
      
              Args:
                  image_grid (ImageGridType): 画像の2次元リスト
                  check_pattern_size (int): チェックするパターンの1辺サイズ
      
              Returns:
                  List[PatternInfo]: パターン情報のリスト
              """
              image_grid_count_x = len(image_grid[0])
              image_grid_count_y = len(image_grid)
      
              # 画像グリッドからパターンを抽出
              # 抽出範囲のサイズが画像グリッドのサイズを超えないようにする
              pattern_info_dict: Dict[str, WFCSimpleOverlappingModel.TilePatternInfo] = {}
              for y in range(image_grid_count_y - check_pattern_size + 1):
                  for x in range(image_grid_count_x - check_pattern_size + 1):
                      # 抽出範囲での全てのパターンを追加
                      pattern_row_list = []
                      for dy in range(check_pattern_size):
                          row = []
                          for dx in range(check_pattern_size):
                              ix = x + dx
                              iy = y + dy
                              row.append(image_grid[iy][ix])
                          pattern_row_list.append(tuple(row))
                      # パターンの1次元タプルをキーとして値を設定
                      pattern_values = tuple(chain.from_iterable(pattern_row_list))
                      if pattern_values not in pattern_info_dict:
                          pattern_info_dict[pattern_values] = self.TilePatternInfo(
                              pattern_values,
                              frequency=1,
                              size=check_pattern_size,
                          )
                      else:
                          # 重複していたらfrequencyとしてカウント
                          pattern_info_dict[pattern_values].frequency += 1
              return list(pattern_info_dict.values())
      
      ▲パターン抽出処理
      エレキベア
      エレキベア
      おおう・・・結構力技クマね・・・
      マイケル
      マイケル
      そしてここからどのように隣接情報を定義するかというと、各辺同士の2列が同じなら隣接可能と定義しています。 ここは生成したい画像やタイルのパターンによって調整してもいいかもしれません。
          # 左, 右, 上, 下
          CHECK_DIRECTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
      
          class TilePatternInfo:
              """
              タイルのパターン情報をまとめたクラス
              size x size のタイルパターンをvaluesとして1次元タプルで保持する
      
              例:3x3のパターンなら
              values = ((0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0))
              のようにRGB情報が9つ分入る形になる
      
              Args:
                  patterns: パターン本体(1次元タプル)
                  frequency: 出現頻度
                  size: パターンの1辺サイズ
              """
      
              def __init__(
                  self,
                  patterns: Tuple[int, ...],
                  frequency: int = 1,
                  size: int = 3,
              ):
                  self.values = patterns
                  self.frequency = frequency
                  self.size = size
      
              def right_except(self) -> Tuple[int, ...]:
                  """右端以外の2列を1次元で返す"""
                  return tuple(
                      n for i, n in enumerate(self.values) if i % self.size != (self.size - 1)
                  )
      
              def left_except(self) -> Tuple[int, ...]:
                  """左端以外の2列を1次元で返す"""
                  return tuple(n for i, n in enumerate(self.values) if i % self.size != 0)
      
              def bottom_except(self) -> Tuple[int, ...]:
                  """下端以外の2行を1次元で返す"""
                  return self.values[: (self.size * self.size) - self.size]
      
              def top_except(self) -> Tuple[int, ...]:
                  """上端以外の2行を1次元で返す"""
                  return self.values[self.size :]
      
      ・・・略・・・
      
          def _create_allowed_pattern_adjacencies_dict(
              self,
          ) -> AllowedPattenAdjacenciesType:
              """
              各パターンごとに隣接可能なパターンインデックス辞書を作成する
      
              Returns:
                  AllowedPattenAdjacenciesType: 隣接可能なパターンインデックスリストの辞書
              """
              patten_info_count = len(self._pattern_info_list)
              allowed_pattern_adjacencies_list = {
                  i: ([], [], [], []) for i in range(patten_info_count)
              }
      
              # 各パターンの隣接可能なパターンを上下左右で探索
              for i in range(patten_info_count):
                  pattern_i = self._pattern_info_list[i]
                  for j in range(patten_info_count):
                      pattern_j = self._pattern_info_list[j]
                      # 左端以外と右端以外の2列が一致
                      if pattern_i.left_except() == pattern_j.right_except():
                          allowed_pattern_adjacencies_list[i][1].append(j) # 右隣接
                          allowed_pattern_adjacencies_list[j][0].append(i) # 左隣接
                      # 上端以外と下端以外の2行が一致
                      if pattern_i.top_except() == pattern_j.bottom_except():
                          allowed_pattern_adjacencies_list[i][3].append(j) # 下隣接
                          allowed_pattern_adjacencies_list[j][2].append(i) # 上隣接
      
      ▲隣接情報の定義
      エレキベア
      エレキベア
      ここがWangTilesでいうところの事前に行うタイルパターン定義に該当するクマね

      各セルのエントロピーを最大で初期化する

      マイケル
      マイケル
      そしてここからエントロピー計算の処理に入っていきます。 まずは全てのセルの状態について、どのタイルになる可能性を持っているとして初期化します。
      20250623_01_03
      ▲全てのセルの状態をどのタイルになる可能性を持っているとして初期化する

          def _initialize_entropy_grid(self) -> Dict[int, int]:
              """エントロピー計算用グリッドを初期化する
              初期値は全てのセルでパターン数と同じ値に設定し、開始セルのエントロピーのみ1減らした状態にする
      
              Returns:
                  Dict[int, int]: エントロピー計算用グリッド(key: インデックス value: エントロピー)
              """
              entropy_grid = {}
              for x in range(self._output_grid_size[0] * self._output_grid_size[1]):
                  entropy_grid[x] = len(self._pattern_info_list)
              # 開始セルのエントロピーを1減らしておく
              start_index = random.randint(0, len(entropy_grid.keys()) - 1)
              entropy_grid[start_index] = len(self._pattern_info_list) - 1
              return entropy_grid
      
      ▲各セルのエントロピー初期化
      マイケル
      マイケル
      この時、開始セルのエントロピーだけ1減らしておくことで、最小セルとして扱えるようにします。

      処理を繰り返してセルを確定させていく

      マイケル
      マイケル
      そしてここからは下記のように処理を繰り返すことでセルを確定させていきます。
      • 最小エントロピーのセルをランダムに確定する
      • 隣接するセルに伝搬して、隣接情報からエントロピーを再計算して絞り込む
      20250623_01_04
      ▲一つのセルが確定されると、その近辺のセルにも情報が伝搬して確定されていく

      エレキベア
      エレキベア
      存在が絞られたセルから確定して、周りのセルも絞っていくクマね この部分は確かに数独みたいクマ
          def _get_lowest_entropy_cell_index(self) -> int:
              """エントロピーが最小のセルのインデックスを返す
      
              Returns:
                  int: エントロピーが最小のセルのインデックス
              """
              return min(self._entropy_grid, key=self._entropy_grid.get)
      
          def _assign_random_pattern_to_cell(self, cell_index: int):
              """セルにパターンをランダムに1つ設定して確定する
      
              Args:
                  cell_index (int): 対象セルのインデックス
              """
              # 該当セルで許容されているパターンを取得
              # 出現頻度に基づいてウェイトを設定する
              allowed_patterns = self._output_grid[cell_index]
              weights = [
                  self._pattern_info_list[pattern_index].frequency
                  for pattern_index in allowed_patterns
              ]
              pattern_index = random.choices(allowed_patterns, weights=weights)[0]
              # セルにパターンを適用してグリッドから削除
              self._output_grid[cell_index] = [pattern_index]
              del self._entropy_grid[cell_index]
      
          def _propagate_cells(self, cell_index: int) -> bool:
              """セルの確定を周囲に伝播してエントロピーを更新する
              セルの隣接パターン(上下左右)を全てチェックし、隣接セルの許容パターンを更新していく
      
              Args:
                  cell_index (int): 対象セルのインデックス
      
              Returns:
                  bool: 処理が成功したかどうか
              """
              to_update_cell_index_stack = {cell_index}
      
              while len(to_update_cell_index_stack) != 0:
                  cell_index = to_update_cell_index_stack.pop()
                  # セルの隣接パターン(上下左右)を全てチェック
                  for direction, transform in enumerate(self.CHECK_DIRECTIONS):
                      # 隣接セルのインデックスを計算
                      neighbor_x = (
                          cell_index % self._output_grid_size[0] + transform[0]
                      ) % self._output_grid_size[0]
                      neighbor_y = (
                          cell_index // self._output_grid_size[0] + transform[1]
                      ) % self._output_grid_size[1]
                      neighbor_cell_index = (
                          neighbor_x + neighbor_y * self._output_grid_size[0]
                      )
                      # 隣接セルが未確定の場合
                      if neighbor_cell_index in self._entropy_grid:
                          # セルの許容パターンを全て取得
                          allowed_pattern_indices_in_all_cell = set()
                          for pattern_index in self._output_grid[cell_index]:
                              allowed_pattern_indices: List[int] = (
                                  self._allowed_pattern_adjacencies_dict[pattern_index][
                                      direction
                                  ]
                              )
                              for allowed_patten_index in allowed_pattern_indices:
                                  allowed_pattern_indices_in_all_cell.add(allowed_patten_index)
                          # 隣接セルの許容パターンを更新
                          allowed_pattern_indices_in_neighbor_cell = self._output_grid[
                              neighbor_cell_index
                          ]
                          # 隣接セルのパターンがセルの許容パターンに含まれていない場合
                          # (セルの更新が発生して矛盾が発生した場合)
                          if not set(allowed_pattern_indices_in_neighbor_cell).issubset(
                              allowed_pattern_indices_in_all_cell
                          ):
                              # 隣接セルとセルに共通するパターンに絞り込んで設定する
                              shared_pattern_indices = [
                                  x
                                  for x in allowed_pattern_indices_in_neighbor_cell
                                  if x in allowed_pattern_indices_in_all_cell
                              ]
                              if len(shared_pattern_indices) == 0:
                                  return False
                              shared_pattern_indices.sort()
                              # 共通パターン数をエントロピーに設定する
                              self._output_grid[neighbor_cell_index] = shared_pattern_indices
                              self._entropy_grid[neighbor_cell_index] = len(
                                  shared_pattern_indices
                              )
                              # 更に伝搬する
                              to_update_cell_index_stack.add(neighbor_cell_index)
              return True
      
      ▲最小エントロピーのセル確定、隣接セルへの伝搬
      マイケル
      マイケル
      あとはこの処理を、全てのセルが確定するまで繰り返せば完了です!
      20250623_01_05
      ▲全てのセルが確定されると画像生成が完了する

      マイケル
      マイケル
      なるほどクマ~~~ 一つ一つ見ていけば結構シンプルクマね

      (参考) HoudiniのWFCノードについて

      マイケル
      マイケル
      アルゴリズムに関しての説明は以上になります! あとは余談にはなりますが、実はHoudiniにもWFC、WangTilesのノードが存在しています。 これらを活用したチュートリアルとノードのURLを下記に記載しておきます。
      マイケル
      マイケル
      チュートリアルを見ると、Wave Function Collapseで処理した結果を WangTilesのノードに接続しているので一瞬なぜだろうとおもいますが、 WangTilesノードはあらかじめ3Dタイルがいくつか用意されており、あくまで処理結果から3D化するために使用しているようでした。
      20250623_01_07
      ▲Wave Function Collapseで生成した結果

      20250623_01_09
      ▲WangTilesノードのタイルセット情報

      20250623_01_08
      ▲色情報(画像だとB値)を見てタイルを配置している

      エレキベア
      エレキベア
      Houdiniに用意されているのは気軽に使用できそうでいいクマね ダンジョン生成やってみたいクマ~~~

      おわりに

      マイケル
      マイケル
      というわけで今回はWave Function Collapseについての解説でした! どうだったかな??
      エレキベア
      エレキベア
      名前だけ聞くと難しそうな印象だったクマが、内容を見てみると思ったよりシンプルで使いやすそうだったクマ
      マイケル
      マイケル
      昨今は画像生成AIも流行っているけど、こういったアルゴリズムは中身を理解して使用することである程度の制御が可能な点がメリットだと思います。 いろいろ遊びつつ、画像生成手法の選択肢の一つとして持っておけるとよさそうですね!
      マイケル
      マイケル
      それでは今日はこの辺で! アデューー!!
      エレキベア
      エレキベア
      クマ~~~~

      【プロシージャル】Pythonで学ぶ波動関数崩壊アルゴリズム(Wave Function Collapse)~完~


      PythonHoudiniグラフィックスプロシージャルアルゴリズム
      2025-06-22

      関連記事
      【UE5.5】Nanite、Lumen、VSMの概要についてまとめる
      2025-05-12
      【Houdini20.5】主要な環境変数と設定方法についてまとめる【Windows/Mac】【Steam版】
      2025-05-05
      【Houdini20.5】観覧車をプロシージャルに作ってみる【SOP・VEX】
      2025-01-03
      【書籍紹介】「コンピュータグラフィックス」に出てくる用語をまとめる【CGエンジニア検定】
      2024-07-13
      【UE5】Niagara SimulationStageによるシミュレーション環境構築
      2024-05-30
      【Unity】Boidsアルゴリズムを用いて魚の群集シミュレーションを実装する
      2024-05-28
      【Unity】第二回 シェーダーライティング入門 〜テクスチャマップを使用したライティング〜(法線マップ、スペキュラマップ、AOマップ)【シェーダー】
      2023-03-14
      【Unity】第一回 シェーダーライティング入門 〜基本のライティング〜(Lambert、Phong、HalfLambert、Blinn-Phong、リムライト)【シェーダー】
      2023-02-28