MortonNumberTable.hsp

HSPでモートン番号(Z-order曲線)テーブルを作成して空間インデックスを扱うTips

こんにちは。HSP(Hot Soup Processor)でゲームやツールを作っている皆さん、今日はモートン番号(Morton number)を使った空間分割のテクニックをご紹介します。モートン番号とは、多次元座標(2Dや3Dなど)を1次元の整数に変換する手法で、Z-order曲線(Z字曲線)とも呼ばれます。座標の各軸のビットを交互に並べ替える(ビットインターリーブ)ことで実現され、空間的に近い点が1次元上でも近くになるという局所性の良い性質を持っています。これにより、四分木(quadtree)や八分木(octree)などの空間インデックスを効率的に扱えるようになります。特に、範囲検索(ある矩形領域内の点を素早く探す)で威力を発揮します。モートン番号の範囲を計算すれば、該当する葉ノードを連続した番号として取得できるからです。

今回のコードは、事前にモートン番号テーブルを作成しておき、高速に座標からモートン番号を取得したり、範囲のモートン番号を計算したりできるモジュールです。1次元?4次元まで対応し、分割レベル(空間の細かさ)も柔軟に設定可能です。主な機能テーブル作成:MakeMortonNoTable 次元数, 分割レベル, テーブル変数
個別空間番号取得:テーブルから直接インデックスでアクセス(例: table(x, y))
範囲空間番号取得:GetLinerNo2d(table, x1, y1, x2, y2) などで、矩形範囲の最小?最大モートン番号を計算(範囲検索に便利)
次元数・分割レベル取得:GetDimensionality(table)、GetDivisionLevel(table)

//--"MortonNo.hsp"
#ifndef MortonNo
#module MortonNo mtable
#modinit int dimensionality, int division_level
    max = 1 << division_level ; 一つの軸上にある空間の数
    LeafSpace = 1 << division_level * dimensionality ; 総葉空間数
    dim bt, max
    for i,, max
        repeat 30 / dimensionality
            if i >> cnt & 1: bt(i) | 1 << cnt * dimensionality
        loop
    next
    if dimensionality == 1{
        dim mtable, max
        repeat max
            mtable(cnt) = LeafSpace | bt(cnt)
        loop
    }else: if dimensionality == 2{
        dim mtable, max, max
        repeat max
            b = LeafSpace | bt(cnt) << 1
            dup D, mtable(0, cnt)
            repeat max
                D(cnt) = b | bt(cnt)
            loop
        loop
    }else: if dimensionality == 3{
        dim mtable, max, max, max
        for i,, max
            b1 = LeafSpace | bt(i) << 2
            repeat max
                b = b1 | bt(cnt) << 1
                dup D, mtable(0, cnt, i)
                repeat max
                    D(cnt) = b | bt(cnt)
                loop
            loop
        next
    }else: if dimensionality == 4{
        dim mtable, max, max, max, max
        for j,, max
            b2 = LeafSpace | bt(j) << 3
            for i,, max
                b1 = b2 | bt(i) << 2
                repeat max
                    b = b1 | bt(cnt) << 1
                    dup D, mtable(0, cnt, i, j)
                    repeat max
                        D(cnt) = b | bt(cnt)
                    loop
                loop
            next
        next
    }
    mref thisID, 2
    return thisID
#modfunc __duptable array a
    __duparray a, mtable
    return
#deffunc __duparray array a, array c
    dup a, c
    i = __get_pval(a)
    dupptr S, i + 8 , 4: S = length(c)
    dupptr S, i + 12, 4: S = length2(c)
    dupptr S, i + 16, 4: S = length3(c)
    dupptr S, i + 20, 4: S = length4(c)
    return
#defcfunc __get_pval var
#deffunc ______ int pval
    return pval
#defcfunc __MSB32 int, double w
#deffunc ___MSB32 double, int w
if w <= 0: return (w < 0) * 32
return (w >> 20) - 1022
#deffunc __MakeMortonNoTable int dimensionality, int division_level, array a
    if (dimensionality < 1) | (dimensionality > 4) | (division_level * dimensionality > 30): return 1
    dup S, index(dimensionality, division_level)
    if S < 0: newmod table, MortonNo, dimensionality, division_level: S = stat ; 作成済みでなければ作成
    __duptable table(S), a 
    return 0
#deffunc __DelMortonNotable int dimensionality, int division_level
    dup S, index(dimensionality, division_level)
    if S >= 0: delmod table(S): S = -1
    return
#defcfunc __GetDimensionality array a
    if length4(a): return 4
    if length3(a): return 3
    if length2(a): return 2
    return 1
#deffunc __InitMortonNo
    dim index, 5, 30
    for i,, 600, 4
        lpoke index, i, -1
    next
    return
#global
__InitMortonNo
//以下インターフェース ( このモジュールで使用可能になる全ての機能です )
//Most Significant Bit 取得
#define global ctype MSB32(%1) __MSB32(,%1)
//多次元配列シャローコピー
#define global DupArray(%1,%2) __duparray %1, %2
//テーブル作成
#define global MakeMortonNoTable(%1, %2, %3) __MakeMortonNoTable %1, %2, %3
//テーブル削除
#define global DelMortonNoTable(%1, %2) __DelMortonNoTable %1, %2
//テーブルから、その次元を取得
#define global ctype GetDimensionality(%1) __GetDimensionality(%1)
//テーブルから、その分割レベルを取得
#define global ctype GetDivisionLevel(%1) MSB32(length(%1) - 1)
//テーブルと範囲を指定して空間番号を取得
#define global ctype GetLinerNo1d(%1,%2,%3                  ) (%1(%2         ) >> MSB32(%1(%2         ) ^ %1(%3         )))
#define global ctype GetLinerNo2d(%1,%2,%3,%4,%5            ) (%1(%2,%3      ) >> MSB32(%1(%2,%3      ) ^ %1(%4,%5      )))
#define global ctype GetLinerNo3d(%1,%2,%3,%4,%5,%6,%7      ) (%1(%2,%3,%4   ) >> MSB32(%1(%2,%3,%4   ) ^ %1(%5,%6,%7   )))
#define global ctype GetLinerNo4d(%1,%2,%3,%4,%5,%6,%7,%8,%9) (%1(%2,%3,%4,%5) >> MSB32(%1(%2,%3,%4,%5) ^ %1(%6,%7,%8,%9)))
#endif
//--"MortonNo.hsp" --End Of File
//-------二次元(四分木)での空間番号テスト-------- 
//対象範囲
sx = 100
sy = 60
ex = ginfo_sx - 100
ey = ginfo_sy - 10
//分割レベル
divlevel = 2
font "", 20
レベル変更 = 1
*main
    if レベル変更{
        divlevel = limit(divlevel + レベル変更 / 120, 0, 8)
        divsizex = double(ex - sx) / (1 << divlevel)
        divsizey = double(ey - sy) / (1 << divlevel)
        max = (1 << divlevel) - 1
        //テーブル作成    次元数, 分割レベル, このテーブルを指す変数
        MakeMortonNoTable 2     , divlevel  , jtable
        DupArray table, jtable ; コピーで使ってみるテスト
    }
    title "ドラッグしてください ホイールで分割レベル変更"  + " 次元数 = " + GetDimensionality(table) + " 分割レベル = " + GetDivisionLevel(table)
    レベル変更 = mousew
    x = double(mousex)
    y = double(mousey)
    dragging = key
    getkey key, 1
    color: boxf
    if key{
        if dragging = 0{
            mx = x
            my = y
        }
        x1 = limit((mx - sx) / divsizex, 0, max)
        y1 = limit((my - sy) / divsizey, 0, max)
        x2 = limit(( x - sx) / divsizex, 0, max)
        y2 = limit(( y - sy) / divsizey, 0, max)
        color 200, 200, 200
        boxf mx, my, x, y
        color 0, 50, 255
        circle mx - 4, my - 4, mx + 4, my + 4
        circle  x - 4,  y - 4,  x + 4,  y + 4
        pos 0, 0
        color 200, 0, 255
        mes "点1の空間番号 = " + table(x1, y1) 
        mes "点2の空間番号 = " + table(x2, y2)
        mes "範囲の空間番号 = " + GetLinerNo2d(table, x1, y1, x2, y2)
    }
    for j, divlevel, -1, -1
        hsvcolor j * 191 / 11, 220, 220
        m = 1 << j
        for i, m , -1 , -1
            _x = sx + i * (ex - sx) / m
            _y = sy + i * (ey - sy) / m
            line sx, _y , ex, _y
            line _x, sy, _x , ey
        next
    next
    redraw 
    redraw 2
    await 16
goto *main

なぜテーブル化するのか?

通常のモートン番号計算はビット操作でリアルタイムに可能ですが、HSPのようなスクリプト言語ではループ処理が遅くなりがちです。そこで、事前にテーブルを作成しておくことで、座標→番号の変換を配列参照だけで超高速にできます。特に大量の点を扱うゲーム(衝突判定、空間ソートなど)で有効です。このモジュールは1?4次元に対応しており、3Dゲームのoctreeや、4Dの特殊処理にも拡張可能です。HSPは軽快に動く言語なので、こうした空間インデックスを簡単に試せて楽しいですよね。衝突検出の最適化や、大規模な粒子シミュレーションなどにぜひ活用してみてください!質問や改良案があれば、コメントでお知らせください。皆さんのHSP作品を楽しみにしています!

ソースコードに寄せてAI(Grok)さんに文章を生成してもらいました(2025/12)

コメント

タイトルとURLをコピーしました