2025年7月11日金曜日

縄文遺構実測図の縁取線(地形変換線)の高度線形補間Blenderアドオン

 Blender add-on for linear interpolation of the border lines (terrain transformation lines) of Jomon ruins survey maps


The border lines (terrain transformation lines) of Jomon ruins survey maps are not contour lines, but change in altitude. I created a Blender add-on that linearly interpolates the altitude distribution of the entire border line from several known point altitudes and displays it in the Blender3D viewport. An essential basic tool for 3D modeling of Jomon ruins has been completed.


縄文遺構実測図の縁取線(地形変換線)は等高線ではなく、高度が変化します。判明している幾つかのポイント高度から縁取線全体の高度分布を線形補間して、Blender3Dビューポートに表示するBlenderアドオンを作成しました。縄文遺構3Dモデリングの必須基礎ツールが完成しました。

1 縁取線高度線形補間Blenderアドオンの機能

このアドオンでは、3Dビューポートの真上からのビュー(テンキー7)で描いた縁取線(べジェ曲線)の頂点インデックスに2つ以上の既知の高度数値を手入力します。アドオンは、入力された既知高度数値に基づき、それ以外の頂点インデックスに線形補間(内挿、外挿)で高度数値を生成し、縁取線頂点高度が3Dビューポートで移動します。

2 縁取線高度線形補間Blenderアドオンの使い方

2-1 縁取線の作成・表示

Blender3Dビューポートに縄文遺構実測図を真上からのビュー(テンキー7)でスケール通りに配置します。この実測図を下敷きにして目的の縁取線をBezier Toolkitを使ってクリックによりべジェ曲線で生成します。

生成したべジェ曲線を編集モードで選択しておきます。

なお、閉じたべジェ曲線の場合は「オブジェクトデータプロパティ→アクティブスプライン→ループのUチェックを外す」操作で開いたべジェ曲線にしておきます。

2-2 アドオンでの高度線形補間

2-2-1 3Dビューポートにおけるインデックス(番号)の表示

アドオンの「インデックス表示切替」をクリックして、3Dビューポートの縁取線(べジェ曲線)頂点にインデックス(番号)を表示します。

2-2-2 初期化

「初期化」をクリックして、ID欄(頂点番号欄)とZ値欄(標高欄)をアドオン画面に表示します。

ID欄は全て表示されていますが、Z値欄は空欄となっています。

2-2-3 既知高度の手入力

実測図から判明している既知高度を関連する頂点のZ値欄に手入力します。補間のために、2つ以上の値を入力する必要があります。


既知高度を入力した様子(54頂点に対して既知高度を4頂点に手入力)


既知高度を入力した様子(高度は全て0となっている)

2-2-4 補間とその反映

「補間して反映」をクリックすると、Z値欄の空欄部分に線形補間された数値(高度)が生成されるとともに、3Dビューポートでべジェ曲線頂点が補間高度に移動します。


線形補間した様子(X座標、Y座標は不動)


線形補間した様子(頂点のZ座標が補間高度に移動している)


参考 土坑実測図(土坑肩線の高度が読みとれるのは4点)

2-2-5 やり直し

不都合があれば、「初期化」をクリックして、Z値欄に既知高度を入力し直し、「補間して反映」をクリックします。

2-2-6 確定

不都合がなければ、「確定」で結果を確定します。

元が閉じたべジェ曲線であった場合は、「オブジェクトデータプロパティ→アクティブスプライン→ループのUをチェックする」で開いたべジェ曲線から閉じたべジェ曲線に戻します。

3 メモ

高度線形補間を行った縁取線を使うことによって、縄文遺構3Dモデリングの精度を上げることができます。

このアドオンはBlenderバージョン4.4.3を対象にしています。

4 感想

このアドオンは「べジェ曲線の頂点インデックス(番号)表示」BlenderPythonスクリプト及び、「内挿と外挿を含む線形補間」Pythonスクリプトを最初に作成し、その2つを主な要素として新たに構成したPythonスクリプトです。それぞれの段階でのChatGPTとのやりとりはいつも以上に回数が多くなりました。恐らく、このアドオンの機能を最初からChatGPTに求めても、完成に至らなかったと想像します。

5 Pythonスクリプト


bl_info = {
    "name": "Bezier Z Interpolator",
    "blender": (4, 4, 3),
    "category": "Curve",
}

import bpy
import blf
from bpy_extras import view3d_utils
import numpy as np

handler = None

def draw_callback_px(self, context):
    obj = context.active_object
    if not obj or obj.type != 'CURVE' or obj.mode != 'EDIT':
        return
    region = context.region
    rv3d = context.space_data.region_3d
    curve = obj.data

    font_id = 0
    blf.size(font_id, 14)
    for spline in curve.splines:
        if spline.type != 'BEZIER':
            continue
        for i, bez in enumerate(spline.bezier_points):
            co_world = obj.matrix_world @ bez.co
            screen_coord = view3d_utils.location_3d_to_region_2d(region, rv3d, co_world)
            if screen_coord:
                blf.position(font_id, screen_coord.x, screen_coord.y, 0)
                blf.draw(font_id, str(i))

def register_draw_handler():
    global handler
    if handler is None:
        handler = bpy.types.SpaceView3D.draw_handler_add(
            draw_callback_px, (None, bpy.context), 'WINDOW', 'POST_PIXEL'
        )

def unregister_draw_handler():
    global handler
    if handler is not None:
        bpy.types.SpaceView3D.draw_handler_remove(handler, 'WINDOW')
        handler = None

def interpolate_z(values):
    ids = np.array([i for i, v in enumerate(values) if v != ""])
    zs = np.array([float(v) for v in values if v != ""])
    if len(ids) < 2:
        return values
    f = np.interp(range(len(values)), ids, zs)
    return [str(round(z, 4)) for z in f]

class CurveZEntry(bpy.types.PropertyGroup):
    z: bpy.props.StringProperty(name="Z", default="")

class CURVE_PT_z_interpolator_panel(bpy.types.Panel):
    bl_label = "Z値補間ツール"
    bl_idname = "CURVE_PT_z_interpolator_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Bezier Z"

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        layout.operator("curve.toggle_index_display", text="インデックス表示切替")
        layout.operator("curve.z_initialize", text="初期化")

        layout.label(text="Z値入力:")
        for i, entry in enumerate(scene.bezier_z_entries):
            row = layout.row()
            row.label(text=f"ID {i}")
            row.prop(entry, "z", text="")

        layout.operator("curve.z_interpolate", text="補間して反映")
        layout.operator("curve.z_confirm", text="確定")

class CURVE_OT_z_initialize(bpy.types.Operator):
    bl_idname = "curve.z_initialize"
    bl_label = "Zエントリ初期化"

    def execute(self, context):
        obj = context.active_object
        if not obj or obj.type != 'CURVE':
            self.report({'WARNING'}, "カーブオブジェクトを選択してください")
            return {'CANCELLED'}
        context.scene.bezier_z_entries.clear()
        for spline in obj.data.splines:
            if spline.type == 'BEZIER':
                for _ in spline.bezier_points:
                    context.scene.bezier_z_entries.add()
        self.report({'INFO'}, "Zエントリを初期化しました")
        return {'FINISHED'}

class CURVE_OT_z_interpolate(bpy.types.Operator):
    bl_idname = "curve.z_interpolate"
    bl_label = "Z値補間"

    def execute(self, context):
        entries = context.scene.bezier_z_entries
        z_list = [e.z for e in entries]
        z_interp = interpolate_z(z_list)

        obj = context.active_object
        if obj and obj.type == 'CURVE':
            index = 0
            mw = obj.matrix_world
            imw = mw.inverted()

            for spline in obj.data.splines:
                if spline.type != 'BEZIER':
                    continue

                bez_points = spline.bezier_points
                n = len(bez_points)

                for i, bez in enumerate(bez_points):
                    if index >= len(z_interp):
                        break

                    # グローバル座標でZ補正
                    world_co = mw @ bez.co
                    world_co.z = float(z_interp[index])
                    bez.co = imw @ world_co
                    entries[index].z = z_interp[index]
                    index += 1

                # 始点・終点のハンドルをたたむ
                if n >= 2:
                    p_start = spline.bezier_points[0]
                    p_end = spline.bezier_points[-1]

                    p_start.handle_left = p_start.co.copy()
                    p_start.handle_right = p_start.co.copy()
                    p_end.handle_left = p_end.co.copy()
                    p_end.handle_right = p_end.co.copy()

        return {'FINISHED'}

class CURVE_OT_z_confirm(bpy.types.Operator):
    bl_idname = "curve.z_confirm"
    bl_label = "Z値確定"

    def execute(self, context):
        self.report({'INFO'}, "Z値を確定しました")
        return {'FINISHED'}

class CURVE_OT_toggle_index_display(bpy.types.Operator):
    bl_idname = "curve.toggle_index_display"
    bl_label = "インデックス表示切替"

    def execute(self, context):
        if handler is None:
            register_draw_handler()
            self.report({'INFO'}, "インデックス表示を開始")
        else:
            unregister_draw_handler()
            self.report({'INFO'}, "インデックス表示を停止")
        return {'FINISHED'}

classes = (
    CurveZEntry,
    CURVE_PT_z_interpolator_panel,
    CURVE_OT_z_initialize,
    CURVE_OT_z_interpolate,
    CURVE_OT_z_confirm,
    CURVE_OT_toggle_index_display,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.bezier_z_entries = bpy.props.CollectionProperty(type=CurveZEntry)
    # register() では active_object にアクセスしない!

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    unregister_draw_handler()
    del bpy.types.Scene.bezier_z_entries

if __name__ == "__main__":
    register()

0 件のコメント:

コメントを投稿