|
| 1 | +bl_info = { |
| 2 | + "name": "Import QV-Pen Line Data to Blender", |
| 3 | + "author": "Hiyakake/ひやかけ", |
| 4 | + "version": (0, 1), |
| 5 | + "blender": (2, 80, 0), |
| 6 | + "location": "View3D > Sidebar > QV Pen Importer", |
| 7 | + "description": "This plugin imports QV-Pen line data to Blender. Export QV-Pen data from the world where Dolphiiiin's \"QvPen Exporter / Importer\" (https://booth.pm/ja/items/6499949) is installed, and use \"QvPen Export Formatter\" (https://dolphiiiin.github.io/qvpen-export-formatter/) to convert it into a JSON file. This tool can import the converted JSON file as a mesh path. The color and thickness are inherited. The coordinates correspond to the location where the line is drawn on the VRC world.", |
| 8 | + "category": "Import-Export", |
| 9 | +} |
| 10 | + |
| 11 | +import bpy |
| 12 | +import json |
| 13 | +import os |
| 14 | + |
| 15 | +# シーンに各種プロパティを追加 |
| 16 | +bpy.types.Scene.json_filepath = bpy.props.StringProperty( |
| 17 | + name="JSON File Path", |
| 18 | + description="Path to the JSON file containing stroke data", |
| 19 | + subtype='FILE_PATH', |
| 20 | + default="" |
| 21 | +) |
| 22 | + |
| 23 | +# JSON に width がない場合のデフォルト値(カーブの extrude と bevel 用) |
| 24 | +bpy.types.Scene.json_extrude = bpy.props.FloatProperty( |
| 25 | + name="Default Extrude", |
| 26 | + description="Default extrude amount if JSON stroke does not include width (in meters)", |
| 27 | + default=0.005, |
| 28 | + unit='LENGTH' |
| 29 | +) |
| 30 | + |
| 31 | +# ソリッド化モディファイアの厚み用のパネルプロパティ(JSON に width がない場合) |
| 32 | +bpy.types.Scene.json_solidify_thickness = bpy.props.FloatProperty( |
| 33 | + name="Default Solidify Thickness", |
| 34 | + description="Default solidify thickness if JSON stroke does not include width (in meters)", |
| 35 | + default=0.005, |
| 36 | + unit='LENGTH' |
| 37 | +) |
| 38 | + |
| 39 | +bpy.types.Scene.json_add_solidify = bpy.props.BoolProperty( |
| 40 | + name="Add Solidify Modifier", |
| 41 | + description="Apply a Solidify modifier to the generated curves", |
| 42 | + default=False |
| 43 | +) |
| 44 | + |
| 45 | +# JSON ファイルを選択するオペレーター |
| 46 | +class IMPORT_OT_JSONFile(bpy.types.Operator): |
| 47 | + bl_idname = "import.json_file" |
| 48 | + bl_label = "Select JSON File" |
| 49 | + |
| 50 | + filepath: bpy.props.StringProperty(subtype="FILE_PATH") |
| 51 | + |
| 52 | + def execute(self, context): |
| 53 | + context.scene.json_filepath = self.filepath |
| 54 | + self.report({'INFO'}, f"Selected file: {self.filepath}") |
| 55 | + return {'FINISHED'} |
| 56 | + |
| 57 | + def invoke(self, context, event): |
| 58 | + context.window_manager.fileselect_add(self) |
| 59 | + return {'RUNNING_MODAL'} |
| 60 | + |
| 61 | +# JSON からパス(カーブ)を生成するオペレーター |
| 62 | +class OBJECT_OT_GeneratePaths(bpy.types.Operator): |
| 63 | + bl_idname = "object.generate_json_paths" |
| 64 | + bl_label = "Generate Paths from JSON" |
| 65 | + |
| 66 | + def execute(self, context): |
| 67 | + filepath = context.scene.json_filepath |
| 68 | + if not os.path.exists(filepath): |
| 69 | + self.report({'ERROR'}, "JSON file not found or invalid path") |
| 70 | + return {'CANCELLED'} |
| 71 | + |
| 72 | + # JSONファイル読み込み |
| 73 | + try: |
| 74 | + with open(filepath, 'r') as f: |
| 75 | + data = json.load(f) |
| 76 | + except Exception as e: |
| 77 | + self.report({'ERROR'}, f"Failed to load JSON: {e}") |
| 78 | + return {'CANCELLED'} |
| 79 | + |
| 80 | + exported_data = data.get("exportedData", []) |
| 81 | + collection = context.collection |
| 82 | + |
| 83 | + # 16進数カラーを RGBA タプル (0~1) に変換する関数 |
| 84 | + def hex_to_rgba(hex_str): |
| 85 | + r = int(hex_str[0:2], 16) / 255.0 |
| 86 | + g = int(hex_str[2:4], 16) / 255.0 |
| 87 | + b = int(hex_str[4:6], 16) / 255.0 |
| 88 | + return (r, g, b, 1.0) |
| 89 | + |
| 90 | + # マテリアルキャッシュ(キー:カラーの16進文字列) |
| 91 | + material_cache = {} |
| 92 | + |
| 93 | + for i, stroke in enumerate(exported_data): |
| 94 | + positions = stroke.get("positions", []) |
| 95 | + if len(positions) < 6: # 点が2点未満の場合はスキップ |
| 96 | + continue |
| 97 | + |
| 98 | + color_info = stroke.get("color", {}) |
| 99 | + color_values = color_info.get("value", []) |
| 100 | + color_hex = color_values[0] if color_values else "FFFFFF" |
| 101 | + rgba = hex_to_rgba(color_hex) |
| 102 | + |
| 103 | + num_points = len(positions) // 3 |
| 104 | + |
| 105 | + # JSON に含まれる width があればその値を使用し、なければパネルの値を使用 |
| 106 | + stroke_width_for_curve = stroke.get("width", context.scene.json_extrude) |
| 107 | + |
| 108 | + # 新しいカーブデータの作成 |
| 109 | + curve_data = bpy.data.curves.new(name=f"StrokeCurve_{i}", type="CURVE") |
| 110 | + curve_data.dimensions = '3D' |
| 111 | + # JSON の width を extrude と bevel_depth に反映 |
| 112 | + curve_data.extrude = stroke_width_for_curve |
| 113 | + curve_data.bevel_depth = stroke_width_for_curve |
| 114 | + |
| 115 | + # POLY スプラインを追加(必要に応じて BEZIER などに変更可能) |
| 116 | + spline = curve_data.splines.new(type="POLY") |
| 117 | + spline.points.add(num_points - 1) |
| 118 | + |
| 119 | + for j in range(num_points): |
| 120 | + x = positions[j*3] |
| 121 | + y = positions[j*3 + 1] |
| 122 | + z = positions[j*3 + 2] |
| 123 | + spline.points[j].co = (x, y, z, 1) |
| 124 | + |
| 125 | + # カーブオブジェクトの作成およびシーンへのリンク |
| 126 | + curve_obj = bpy.data.objects.new(name=f"Stroke_{i}", object_data=curve_data) |
| 127 | + collection.objects.link(curve_obj) |
| 128 | + |
| 129 | + # ソリッド化モディファイアの追加 |
| 130 | + if context.scene.json_add_solidify: |
| 131 | + mod = curve_obj.modifiers.new(name="Solidify", type='SOLIDIFY') |
| 132 | + # JSON に width があればその値を、なければパネルの値を使用 |
| 133 | + mod.thickness = stroke.get("width", context.scene.json_solidify_thickness) |
| 134 | + |
| 135 | + # 同じ色のマテリアルがあれば再利用、なければ新規作成 |
| 136 | + if color_hex in material_cache: |
| 137 | + mat = material_cache[color_hex] |
| 138 | + else: |
| 139 | + mat = bpy.data.materials.new(name=f"StrokeMaterial_{color_hex}") |
| 140 | + mat.use_nodes = True |
| 141 | + nodes = mat.node_tree.nodes |
| 142 | + bsdf = nodes.get("Principled BSDF") |
| 143 | + if bsdf: |
| 144 | + bsdf.inputs["Base Color"].default_value = rgba |
| 145 | + bsdf.inputs["Metallic"].default_value = 0.0 |
| 146 | + bsdf.inputs["Roughness"].default_value = 1.0 |
| 147 | + bsdf.inputs["IOR"].default_value = 0.5 |
| 148 | + if "Alpha" in bsdf.inputs: |
| 149 | + bsdf.inputs["Alpha"].default_value = 1.0 |
| 150 | + material_cache[color_hex] = mat |
| 151 | + |
| 152 | + # オブジェクトにマテリアルを割り当て |
| 153 | + if curve_obj.data.materials: |
| 154 | + curve_obj.data.materials[0] = mat |
| 155 | + else: |
| 156 | + curve_obj.data.materials.append(mat) |
| 157 | + |
| 158 | + self.report({'INFO'}, "Paths generated successfully") |
| 159 | + return {'FINISHED'} |
| 160 | + |
| 161 | +# Nパネルに表示するパネルクラス |
| 162 | +class VIEW3D_PT_QVPenPanel(bpy.types.Panel): |
| 163 | + bl_label = "QV Pen Importer" |
| 164 | + bl_idname = "VIEW3D_PT_qv_pen_importer" |
| 165 | + bl_space_type = 'VIEW_3D' |
| 166 | + bl_region_type = 'UI' |
| 167 | + bl_category = "QV Pen" |
| 168 | + |
| 169 | + def draw(self, context): |
| 170 | + layout = self.layout |
| 171 | + scene = context.scene |
| 172 | + |
| 173 | + layout.prop(scene, "json_filepath") |
| 174 | + layout.operator("import.json_file", text="Select JSON File") |
| 175 | + layout.separator() |
| 176 | + layout.prop(scene, "json_extrude") |
| 177 | + layout.prop(scene, "json_add_solidify") |
| 178 | + if scene.json_add_solidify: |
| 179 | + layout.prop(scene, "json_solidify_thickness") |
| 180 | + layout.separator() |
| 181 | + layout.operator("object.generate_json_paths", text="Generate Paths") |
| 182 | + |
| 183 | +# 登録するクラスのリスト |
| 184 | +classes = ( |
| 185 | + IMPORT_OT_JSONFile, |
| 186 | + OBJECT_OT_GeneratePaths, |
| 187 | + VIEW3D_PT_QVPenPanel, |
| 188 | +) |
| 189 | + |
| 190 | +def register(): |
| 191 | + for cls in classes: |
| 192 | + bpy.utils.register_class(cls) |
| 193 | + |
| 194 | +def unregister(): |
| 195 | + for cls in classes: |
| 196 | + bpy.utils.unregister_class(cls) |
| 197 | + del bpy.types.Scene.json_filepath |
| 198 | + del bpy.types.Scene.json_extrude |
| 199 | + del bpy.types.Scene.json_add_solidify |
| 200 | + del bpy.types.Scene.json_solidify_thickness |
| 201 | + |
| 202 | +if __name__ == "__main__": |
| 203 | + register() |
0 commit comments