Skip to content

Commit 2bb7b91

Browse files
author
hiyakake
committed
init
1 parent 6621136 commit 2bb7b91

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# QV ペンで描いた絵を Blender に読み込むやつ / Import QV-Pen Line Data to Blender
2+
3+
## 概要
4+
5+
### 日本語
6+
7+
このプラグインは QV-Pen のデータを Blender にインポートするためのものです。
8+
9+
QV ペンのデータを Dolphiiiin 様作成の「QvPen Exporter / Importer」(https://booth.pm/ja/items/6499949)が設置されたワールドでエクスポートし、「QvPen Export Formatter」(https://dolphiiiin.github.io/qvpen-export-formatter/)を使ってJSONファイルに変換したものを本ツールに読み込ませるとメッシュ化したパスとしてインポートできます。
10+
11+
色と太さが引き継がれます。
12+
13+
座標は、VRC のワールド上で描かれた場所と対応します。
14+
15+
### English
16+
17+
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.

main.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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

Comments
 (0)