Skip to content

Commit d10c5e3

Browse files
committed
export mesh and skeleton
1 parent 836fb04 commit d10c5e3

1 file changed

Lines changed: 179 additions & 83 deletions

File tree

‎Exporter/Blender/gpmesh_export.py‎

Lines changed: 179 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,142 @@
1-
import bpy
21

3-
def write_gpmesh(context, filepath, vertFormat):
4-
# Get current object
5-
obj = bpy.context.scene.objects.active
6-
if obj.type == 'MESH':
7-
mesh = obj.data
8-
f = open(filepath, 'w', encoding='utf-8')
9-
f.write("{\n")
10-
f.write("\t\"version\":1,\n")
11-
f.write("\t\"vertexformat\":\"" + vertFormat + "\",\n")
12-
f.write("\t\"shader\":\"BasicMesh\",\n")
2+
3+
bl_info = {
4+
"name": "gpmesh Exporter",
5+
"blender": (3,00,0),
6+
"category": "Export"
7+
}
8+
9+
import bpy
10+
import json
11+
12+
def generate_gpmesh_json():
13+
mesh = bpy.context.active_object.data
14+
uv_layer = mesh.uv_layers.active.data
15+
16+
gpmesh = {
17+
"version": 1,
18+
"vertexformat": "PosNormSkinTex",
19+
"shader": "Skinned",
20+
"textures": [],
21+
"specularPower": 100.0,
22+
"vertices": [],
23+
"indices": []
24+
}
25+
26+
for vert in mesh.vertices:
27+
pos = vert.co
28+
normal = vert.normal
29+
uv = uv_layer[vert.index].uv
30+
gp_vert = []
31+
gp_vert.extend([pos.x, pos.y, pos.z])
32+
gp_vert.extend([normal.x, normal.y, normal.z])
33+
34+
# get bone indices and their weights that affect this vertex and sort them by weight from high to low
35+
boneToWeightTuples = []
36+
for group in vert.groups:
37+
u8_weight = int(group.weight * 255)
38+
boneToWeightTuples.append((group.group, u8_weight))
39+
boneToWeightTuples.sort(key=lambda boneToWeight : boneToWeight[1], reverse=True)
1340

14-
# For now, only one texture
15-
# figure out the file name...
16-
texname = filepath.split("\\")[-1].split("/")[-1].split(".")[0]
17-
# Make it a png
18-
texname += ".png"
19-
f.write("\t\"textures\":[\n")
20-
f.write("\t\t\"" + texname + "\"\n")
21-
f.write("\t],\n")
41+
# Only keep first 4 bones with their weights. As we sorted them by their bone weight (from high to low)
42+
# before, we only keep the once with highest influence.
43+
boneToWeightTuples = boneToWeightTuples[:4]
2244

23-
# specular power
24-
f.write("\t\"specularPower\":100,\n")
25-
26-
# vertices
27-
# We have to create our own storage for because uvs are stored separately
28-
verts = [dict() for x in range(len(mesh.vertices))]
29-
for v in mesh.vertices:
30-
verts[v.index]["pos"] = v.co
31-
verts[v.index]["norm"] = v.normal
45+
# The file format expects always 4 bones.
46+
while len(boneToWeightTuples) < 4:
47+
boneToWeightTuples.append((0, 0))
3248

33-
for l in mesh.loops:
34-
verts[l.vertex_index]["uv"] = mesh.uv_layers.active.data[l.index].uv
49+
boneIndices = []
50+
weights = []
51+
for boneIdx, weight in boneToWeightTuples:
52+
boneIndices.append(boneIdx)
53+
weights.append(weight)
54+
55+
gp_vert.extend(boneIndices)
56+
gp_vert.extend(weights)
57+
gp_vert.extend([uv.x, uv.y])
3558

36-
f.write("\t\"vertices\":[\n")
37-
first = True
38-
for v in verts:
39-
if first:
40-
f.write("\t\t[")
41-
first = False
42-
else:
43-
f.write(",\n\t\t[")
44-
f.write("%f,%f,%f," % (v["pos"].x, v["pos"].y, v["pos"].z))
45-
f.write("%f,%f,%f," % (v["norm"].x, v["norm"].y, v["norm"].z))
46-
f.write("%f,%f" % (v["uv"].x, -1.0 * v["uv"].y))
47-
f.write("]")
48-
f.write("\n\t],\n")
59+
gpmesh["vertices"].append(gp_vert)
60+
61+
for poly in mesh.polygons:
62+
tri = []
63+
for loop_index in poly.loop_indices:
64+
vertIndex = mesh.loops[loop_index].vertex_index
65+
tri.append(vertIndex)
4966

50-
# indices
51-
f.write("\t\"indices\":[\n")
52-
first = True
53-
for p in mesh.polygons:
54-
if first:
55-
f.write("\t\t")
56-
first = False
57-
else:
58-
f.write(",\n\t\t")
59-
f.write("[%d,%d,%d]" % (p.vertices[0], p.vertices[1], p.vertices[2]))
60-
f.write("\n\t]\n")
67+
gpmesh["indices"].append(tri)
68+
69+
textures = []
70+
materialSlots = bpy.context.active_object.material_slots
71+
for matSlot in materialSlots:
72+
if matSlot.material:
73+
if matSlot.material.node_tree:
74+
for node in matSlot.material.node_tree.nodes:
75+
if node.type == 'TEX_IMAGE':
76+
textures.append(node.image.name)
77+
78+
gpmesh["textures"] = textures
79+
80+
return gpmesh
81+
82+
def generate_gpskel_json():
83+
gpskel = {
84+
"version": 1,
85+
"bonecount": 0,
86+
"bones": []
87+
}
88+
boneInfos = []
89+
90+
armature = bpy.context.active_object
91+
while armature.type != 'ARMATURE':
92+
armature = armature.parent
93+
94+
if armature != bpy.context.active_object:
95+
armature = armature.data
96+
for i, bone in enumerate(armature.bones):
97+
parentBone = bone.parent
98+
parentIndex = -1
99+
if parentBone:
100+
parentIndex = armature.bones.find(parentBone.name)
101+
102+
local_matrix = bone.matrix_local
103+
trans = local_matrix.to_translation()
104+
rot = local_matrix.to_quaternion()
105+
boneInfo = {
106+
"name": bone.name,
107+
"index": i,
108+
"parent": parentIndex,
109+
"bindpose": {
110+
"rot": [rot.x, rot.y, rot.z, rot.w],
111+
"trans": [trans.x, trans.y, trans.z]
112+
}
113+
}
114+
boneInfos.append(boneInfo)
61115

62-
f.write("}\n")
116+
gpskel["bonecount"] = len(armature.bones)
117+
118+
gpskel["bones"] = boneInfos
119+
120+
return gpskel
121+
122+
def write_some_data(context, filepath, export_gpmesh, export_gpskel, export_gpanim):
123+
print("exporting to gpmesh...")
124+
125+
if export_gpmesh:
126+
gpmesh = generate_gpmesh_json()
127+
f = open(filepath, 'w', encoding='utf-8')
128+
# f.write("Hello World %s" % use_some_setting)
129+
f.write(json.dumps(gpmesh, sort_keys=False, indent=2))
130+
f.close()
131+
132+
if export_gpskel:
133+
gpskel = generate_gpskel_json()
134+
gpskel_filepath = filepath.split(".")[0] + '.gpskel'
135+
f = open(gpskel_filepath, "w", encoding="utf-8")
136+
f.write(json.dumps(gpskel, sort_keys=False, indent=2))
63137
f.close()
64-
else:
65-
raise ValueError("No mesh selected")
138+
139+
print("Done!")
66140

67141
return {'FINISHED'}
68142

@@ -74,49 +148,71 @@ def write_gpmesh(context, filepath, vertFormat):
74148
from bpy.types import Operator
75149

76150

77-
class ExportGPMesh(Operator, ExportHelper):
78-
"""Export to Game Programming in C++ mesh format"""
79-
bl_idname = "export_test.gpmesh" # important since its how bpy.ops.import_test.some_data is constructed
80-
bl_label = "Export Mesh"
151+
class ExportGPMESH(Operator, ExportHelper):
152+
"""Export mesh in gpmesh format."""
153+
bl_idname = "export.gpmesh" # important since its how bpy.ops.import_test.some_data is constructed
154+
bl_label = "Export as gpmesh"
81155

82156
# ExportHelper mixin class uses this
83157
filename_ext = ".gpmesh"
84158

85-
filter_glob = StringProperty(
86-
default="*.gpmesh",
87-
options={'HIDDEN'},
88-
maxlen=255, # Max internal buffer length, longer would be clamped.
89-
)
90-
91-
vertFormat = EnumProperty(
92-
name="Vertex Format",
93-
description="Choose the vertex format",
94-
items=(('PosNormTex', "PosNormTex", "Position, normal, tex coord"),
95-
('PosTex', "PosTex", "Position, tex coord")),
96-
default='PosNormTex',
97-
)
98-
159+
filter_glob: StringProperty(
160+
default="*.gpmesh",
161+
options={'HIDDEN'},
162+
maxlen=255, # Max internal buffer length, longer would be clamped.
163+
)
164+
165+
# List of operator properties, the attributes will be assigned
166+
# to the class instance from the operator settings before calling.
167+
export_gpmesh: BoolProperty(
168+
name="Export gpmesh",
169+
description="Writes the mesh as .gpmesh to disk",
170+
default=True
171+
)
172+
173+
export_gpskel: BoolProperty(
174+
name="Export gpskel",
175+
description="Writes .gpskel file to disk",
176+
default=True
177+
)
178+
179+
export_gpanim: BoolProperty(
180+
name="Export gpanim",
181+
description="Writes .gpanim file to disk",
182+
default=True
183+
)
184+
185+
type: EnumProperty(
186+
name="Example Enum",
187+
description="Choose between two items",
188+
items=(
189+
('OPT_A', "First Option", "Description one"),
190+
('OPT_B', "Second Option", "Description two"),
191+
),
192+
default='OPT_A',
193+
)
194+
99195
def execute(self, context):
100-
return write_gpmesh(context, self.filepath, self.vertFormat)
196+
return write_some_data(context, self.filepath, self.export_gpmesh, self.export_gpskel, self.export_gpanim)
101197

102198

103199
# Only needed if you want to add into a dynamic menu
104200
def menu_func_export(self, context):
105-
self.layout.operator(ExportGPMesh.bl_idname, text="GPMesh Exporter (.gpmesh)")
106-
201+
self.layout.operator(ExportGPMESH.bl_idname, text="gpmesh")
107202

203+
# Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access)
108204
def register():
109-
bpy.utils.register_class(ExportGPMesh)
110-
bpy.types.INFO_MT_file_export.append(menu_func_export)
205+
bpy.utils.register_class(ExportGPMESH)
206+
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
111207

112208

113209
def unregister():
114-
bpy.utils.unregister_class(ExportGPMesh)
115-
bpy.types.INFO_MT_file_export.remove(menu_func_export)
210+
bpy.utils.unregister_class(ExportGPMESH)
211+
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
116212

117213

118214
if __name__ == "__main__":
119215
register()
120216

121217
# test call
122-
bpy.ops.export_test.gpmesh('INVOKE_DEFAULT')
218+
bpy.ops.export.gpmesh('INVOKE_DEFAULT')

0 commit comments

Comments
 (0)