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

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

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

マイケル
マイケル
みなさんこんにちは! マイケルです!
エレキベア
エレキベア
こんにちクマ~~~
マイケル
マイケル
今日は、とある面白いアルゴリズムを見つけたので紹介しようと思います。 その名も「波動関数崩壊アルゴリズム(Wave Function Collapse)」です!!
エレキベア
エレキベア
か、かっこいい・・・・!!
マイケル
マイケル
入力画像からパターン抽出して画像生成するアルゴリズムなのですが、実際にPythonで実装してみました! 内容は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とは
  • 数種のタイルからなる集合を選び、隣合う辺の色が一致するようにタイルのコピーを並べるという概念
    • タイルの各辺に色やラベルがついており、隣り合うタイル同士は同じ色でなければならない
    • 同じ色、ラベル同士を繋げることで新たな画像を生成する
エレキベア
エレキベア
繋ぐことが出来るタイルをパズルのように組み合わせるというわけクマね 非常にシンプルクマ
マイケル
マイケル
WangTilesの実装として使いやすいものが下記論文で紹介されている方法で、 各行、列で同じルールのものが並ぶよう定義されています。

参考:
Tile-Based Texture Mapping on Graphics Hardware

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

エレキベア
エレキベア
同じ色の辺が接続できるのクマね
マイケル
マイケル
今回はこちらの方法を使用して実装を試してみます。

Wave Function Collapseとは

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

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

マイケル
マイケル
WangTilesと異なるところは 生成処理のプロセスが明確に定義されている点 で、考え方も異なるものとなっています。 よくある説明としては下記のようなものが見られます。
✔ WaveFunctionCollapseとは
  • タイルマップ画像を入力として、パターンを選択して新たな画像を生成するアルゴリズム
    • 全ての存在可能な状態(エントロピー)があるセルから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はパターン抽出も自動で行います。
✔ WaveFunctionCollapseのモデル
  • 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を下記に記載しておきます。
エレキベア
エレキベア
へぇ~~WFCをダンジョン生成に活用しているのクマね
マイケル
マイケル
チュートリアルを見ると、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

関連記事
【Houdini21.0】Solaris徹底入門:USD構成を意識した基本的な作業フローについてまとめる
2025-12-31
【OpenUSD】USD入門 その2:PythonでUSDを生成・操作してみる
2025-12-29
【OpenUSD】USD入門 その1:概要とデータ構造についてまとめる
2025-12-25
【Houdini21.0】Otisによる筋肉シミュレーションと筋肉情報転送機能の使い方【Otis Muscle and Tissue Simulation】
2025-11-29
【Houdini】Mardini2025の振り返りと制作物解説
2025-11-24
【Houdini Indie】通常版とSteam版の挙動の違いをまとめる【2025年版】
2025-11-03
【ゲーム数学】第十回 p5.js(+α)で学ぶゲーム数学「複素数とフラクタル」
2025-11-02
【UE5.5】Nanite、Lumen、VSMの概要についてまとめる
2025-05-12