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()