├── Blender └── io_import_unity.py ├── Editor ├── Mesh2BPY.cs └── UnpackAssets.cs ├── LICENSE.md └── README.md /Blender/io_import_unity.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Unity Model Script Importer", 3 | "author": "ata4", 4 | "version": (0, 2, 0), 5 | "blender": (2, 6, 0), 6 | "location": "File > Import-Export", 7 | "description": "Imports Unity model scripts exported by the Mesh2BPY script", 8 | "warning": "", 9 | "support": "COMMUNITY", 10 | "category": "Import-Export", 11 | } 12 | 13 | import bpy, bmesh, math, re 14 | 15 | # ImportHelper is a helper class, defines filename and 16 | # invoke() function which calls the file selector. 17 | from bpy_extras.io_utils import ImportHelper 18 | from bpy.props import StringProperty, BoolProperty, EnumProperty 19 | from bpy.types import Operator 20 | from mathutils import Vector, Quaternion 21 | 22 | class ImportPythonModel(Operator, ImportHelper): 23 | """This appears in the tooltip of the operator and in the generated docs""" 24 | bl_idname = "import_mesh.unity" 25 | bl_label = "Import Unity model script" 26 | 27 | # ImportHelper mixin class uses this 28 | filename_ext = ".py" 29 | 30 | def __init__(self): 31 | self.model = None 32 | 33 | def execute(self, context): 34 | # compile and execute provided script 35 | script = open(self.filepath).read() 36 | script_c = compile(script, self.filepath, 'exec') 37 | globals = {} 38 | 39 | exec(script_c, globals) 40 | 41 | self.model = globals.get('model') 42 | 43 | # test if the "model" variable is defined 44 | if not self.model: 45 | print("No model variable found!") 46 | return {'CANCELLED'} 47 | 48 | # get rid of "mesh" in the model name 49 | pattern = re.compile("mesh", re.IGNORECASE) 50 | self.model['name'] = pattern.sub("", self.model['name']) 51 | 52 | # build model 53 | self.build_model() 54 | 55 | return {'FINISHED'} 56 | 57 | def build_model(self): 58 | print("Building %s" % self.model['name']) 59 | 60 | # create empty 61 | ob = bpy.data.objects.new(self.model['name'], None) 62 | ob.rotation_euler = (math.radians(90), 0, 0) 63 | 64 | # build mesh 65 | me = self.build_geometry() 66 | 67 | # create mesh object 68 | ob_mesh = bpy.data.objects.new(self.model['name'] + " Mesh", me) 69 | ob_mesh.location = self.model['pos'] 70 | ob_mesh.rotation_quaternion = self.model['rot'] 71 | #ob_mesh.scale = self.model['scl'] 72 | ob_mesh.parent = ob 73 | 74 | # create armature 75 | amt = bpy.data.armatures.new(self.model['name']) 76 | amt.show_names = True 77 | 78 | # create armature object 79 | ob_amt = bpy.data.objects.new(self.model['name'] + " Armature", amt) 80 | ob_amt.show_x_ray = True 81 | ob_amt.draw_type = 'WIRE' 82 | ob_amt.parent = ob 83 | 84 | # Give mesh object an armature modifier, using vertex groups but 85 | # not envelopes 86 | mod = ob_mesh.modifiers.new('Armature', 'ARMATURE') 87 | mod.object = ob_amt 88 | mod.use_bone_envelopes = False 89 | mod.use_vertex_groups = True 90 | 91 | # link objects to scene 92 | bpy.context.scene.objects.link(ob) 93 | bpy.context.scene.objects.link(ob_mesh) 94 | bpy.context.scene.objects.link(ob_amt) 95 | 96 | # build armature 97 | bpy.context.scene.objects.active = ob_amt 98 | self.build_armature(ob_mesh, ob_amt) 99 | 100 | def build_geometry(self): 101 | # create mesh data and BMesh 102 | me = bpy.data.meshes.new(self.model['name']) 103 | bm = bmesh.new() 104 | 105 | # create vertices 106 | for vert in self.model['verts']: 107 | bm.verts.new(vert) 108 | 109 | # to avoid slow iterator lookups later / indexing verts is slow in bmesh 110 | bm_verts = bm.verts[:] 111 | 112 | # set of indices of duplicate faces 113 | dupfaces = set() 114 | 115 | for submesh_index, submesh in enumerate(self.model['submeshes']): 116 | # get name of material 117 | mat_name = self.model['materials'][submesh_index] 118 | 119 | # create and append material 120 | mtl = bpy.data.materials.get(mat_name) 121 | if not mtl: 122 | mtl = bpy.data.materials.new(name = mat_name) 123 | me.materials.append(mtl) 124 | 125 | # create faces 126 | for face_index, face in enumerate(zip(submesh[0::3], submesh[1::3], submesh[2::3])): 127 | try: 128 | bm_face = bm.faces.new((bm_verts[i] for i in face)) 129 | bm_face.smooth = True 130 | bm_face.material_index = submesh_index 131 | except ValueError as e: 132 | # duplicate face, save id for later 133 | print("Duplicate face: %d" % face_index) 134 | dupfaces.add(face_index) 135 | 136 | # create uv layers 137 | uv_lay = bm.loops.layers.uv.verify() 138 | face_index_ofs = 0 139 | for face_index, face_uv in enumerate(self.model['uv']): 140 | # skip duplicate faces and correct face index 141 | if face_index in dupfaces: 142 | face_index_ofs = face_index_ofs - 1 143 | continue 144 | 145 | for loop_index, loop_uv in enumerate(face_uv): 146 | bm.faces[face_index + face_index_ofs].loops[loop_index][uv_lay].uv = loop_uv 147 | 148 | # export BMesh to mesh data 149 | bm.to_mesh(me) 150 | me.update() 151 | 152 | return me 153 | 154 | def build_armature(self, ob_mesh, ob_amt): 155 | bpy.ops.object.mode_set(mode='EDIT') 156 | 157 | # create vertex groups, and add verts and weights 158 | # first arg in assignment is a list, can assign several verts at once 159 | for name, vgroup in self.model['vg'].items(): 160 | grp = ob_mesh.vertex_groups.new(name) 161 | for (v, w) in vgroup: 162 | grp.add([v], w, 'REPLACE') 163 | 164 | # first pass: create and position bones 165 | for bone_name, bone_data in self.model['bones'].items(): 166 | scale = 1 / self.get_bone_scale(bone_name, 1) 167 | bone = ob_amt.data.edit_bones.new(bone_name) 168 | bone.head = bone_data['pos'] 169 | bone.tail = bone.head + Vector((0, scale * -0.1, 0)) 170 | bone.roll = 0 171 | 172 | # second pass: build bone hierarchy 173 | for bone_name, bone_data in self.model['bones'].items(): 174 | bone = ob_amt.data.edit_bones.get(bone_name) 175 | bone_parent_name = bone_data.get('parent') 176 | if bone and bone_parent_name: 177 | bone_parent = ob_amt.data.edit_bones.get(bone_parent_name) 178 | if bone_parent: 179 | bone.parent = bone_parent 180 | 181 | bpy.ops.object.mode_set(mode='OBJECT') 182 | 183 | # create custom shape for bones 184 | bone_shape = bpy.data.objects.get("bone_shape") 185 | if not bone_shape: 186 | bone_shape = bpy.ops.object.empty_add(type='SPHERE') 187 | bone_shape = bpy.context.active_object 188 | bone_shape.name = "bone_shape" 189 | bone_shape.use_fake_user = True 190 | #bone_shape.empty_draw_size = 0.2 191 | bpy.context.scene.objects.unlink(bone_shape) # don't want the user deleting this 192 | bpy.context.scene.objects.active = ob_amt 193 | 194 | bpy.ops.object.mode_set(mode='POSE') 195 | 196 | # apply custom shape 197 | for bone in ob_amt.pose.bones: 198 | bone.custom_shape = bone_shape 199 | 200 | # third pass: apply bone scales 201 | for bone_name, bone_data in self.model['bones'].items(): 202 | bone = ob_amt.pose.bones.get(bone_name) 203 | bone.scale = bone_data['lscl'] 204 | 205 | bpy.ops.object.mode_set(mode='OBJECT') 206 | 207 | def get_bone_scale(self, bone_name, scale): 208 | bone_data = self.model['bones'][bone_name] 209 | scale *= (bone_data['lscl'][0] + bone_data['lscl'][1] + bone_data['lscl'][2]) / 3 210 | 211 | bone_parent_name = bone_data.get('parent') 212 | if bone_parent_name: 213 | scale = self.get_bone_scale(bone_parent_name, scale) 214 | 215 | return scale 216 | 217 | 218 | def menu_func_import(self, context): 219 | self.layout.operator(ImportPythonModel.bl_idname, text="Unity Python model script (.py)") 220 | 221 | def register(): 222 | bpy.utils.register_class(ImportPythonModel) 223 | bpy.types.INFO_MT_file_import.append(menu_func_import) 224 | 225 | def unregister(): 226 | bpy.utils.unregister_class(ImportPythonModel) 227 | bpy.types.INFO_MT_file_import.remove(menu_func_import) 228 | 229 | if __name__ == "__main__": 230 | register() 231 | 232 | # test call 233 | #bpy.ops.import_mesh.unity('INVOKE_DEFAULT') 234 | -------------------------------------------------------------------------------- /Editor/Mesh2BPY.cs: -------------------------------------------------------------------------------- 1 | /* 2 | ** 2012 September 25 3 | ** 4 | ** The author disclaims copyright to this source code. In place of 5 | ** a legal notice, here is a blessing: 6 | ** May you do good and not evil. 7 | ** May you find forgiveness for yourself and forgive others. 8 | ** May you share freely, never taking more than you give. 9 | **/ 10 | 11 | using System; 12 | using System.IO; 13 | using System.Collections.Generic; 14 | using System.Text.RegularExpressions; 15 | using UnityEngine; 16 | using UnityEditor; 17 | 18 | public class Mesh2BPY { 19 | 20 | [MenuItem("Custom/Export Mesh to Python")] 21 | public static void ExportMesh() { 22 | Transform[] selection = Selection.GetTransforms(SelectionMode.Unfiltered); 23 | 24 | if (selection.Length == 0) { 25 | EditorUtility.DisplayDialog(WindowTitle, "Please select a transform that contains a SkinnedMeshRenderer you wish to export.", "Ok"); 26 | return; 27 | } 28 | 29 | Transform root = selection[0]; 30 | SkinnedMeshRenderer mesh = (SkinnedMeshRenderer) root.GetComponentInChildren(typeof(SkinnedMeshRenderer)); 31 | //Animation anim = (Animation) root.GetComponent(typeof(Animation)); 32 | 33 | if (mesh == null) { 34 | EditorUtility.DisplayDialog(WindowTitle, "No SkinnedMeshRenderer found in the currently selected transform!", "Ok"); 35 | return; 36 | } 37 | 38 | string scriptName = mesh.name + ".py"; 39 | string scriptPath = EditorUtility.SaveFilePanel("Export to Python script", "", scriptName, "py"); 40 | 41 | if (scriptPath.Length == 0) { 42 | return; 43 | } 44 | 45 | StreamWriter w = new StreamWriter(scriptPath, false); 46 | 47 | using (w) { 48 | Mesh2BPY m2bpy = new Mesh2BPY(mesh, w); 49 | m2bpy.WriteHeader(); 50 | m2bpy.WriteGeometry(); 51 | m2bpy.WriteTransforms(); 52 | //m2bpy.WriteAnimations(anim); 53 | } 54 | 55 | EditorUtility.DisplayDialog(WindowTitle, "Finished writing " + scriptName, "Ok"); 56 | } 57 | 58 | private const string Version = "0.7"; 59 | private const string WindowTitle = "Mesh2BPY"; 60 | private const string S = " "; // indent space 61 | 62 | private string scriptName; 63 | private string scriptPath; 64 | private StreamWriter w; 65 | 66 | private Dictionary boneScales = new Dictionary(); 67 | 68 | private SkinnedMeshRenderer meshRenderer; 69 | private Mesh mesh; 70 | 71 | public Mesh2BPY(SkinnedMeshRenderer mr, StreamWriter sw) { 72 | meshRenderer = mr; 73 | mesh = mr.sharedMesh; 74 | w = sw; 75 | } 76 | 77 | private string ValueString(string str) { 78 | return "'" + str + "'"; 79 | } 80 | 81 | private string ValueVector2(UnityEngine.Vector2 vec) { 82 | return ValueVector2(vec.x, vec.y); 83 | } 84 | 85 | private string ValueVector2(float x, float y) { 86 | return "(" + x + ", " + y + ")"; 87 | } 88 | 89 | private string ValueVector3(UnityEngine.Vector3 vec) { 90 | return ValueVector3(vec.x, vec.y, vec.z); 91 | } 92 | 93 | private string ValueVector3(float x, float y, float z) { 94 | return "(" + x + ", " + y + ", " + z + ")"; 95 | } 96 | 97 | private string ValueQuaternion(UnityEngine.Quaternion q) { 98 | return ValueVector4(q.x, q.y, q.z, q.w); 99 | } 100 | 101 | private string ValueVector4(float x, float y, float z, float w) { 102 | return "(" + x + ", " + y + ", " + z + ", " + w + ")"; 103 | } 104 | 105 | public void WriteHeader() { 106 | w.WriteLine("# Raw model data Python script for \"" + meshRenderer.name + "\""); 107 | w.WriteLine("# Written by Mesh2BPY " + Version); 108 | w.WriteLine(); 109 | } 110 | 111 | public void WriteGeometry() { 112 | WriteMeshMeta(); 113 | WriteVertices(); 114 | WriteNormals(); 115 | WriteUV(); 116 | WriteSubmeshes(); 117 | WriteMaterials(); 118 | WriteVertexGroups(); 119 | } 120 | 121 | public void WriteMeshMeta() { 122 | w.WriteLine("model = {"); 123 | w.WriteLine(S + "'name': " + ValueString(meshRenderer.name) + ","); 124 | w.WriteLine(S + "'pos': " + ValueVector3(meshRenderer.transform.position) + ","); 125 | w.WriteLine(S + "'rot': " + ValueQuaternion(meshRenderer.transform.rotation) + ","); 126 | w.WriteLine(S + "'scl': " + ValueVector3(meshRenderer.transform.lossyScale)); 127 | w.WriteLine("}"); 128 | w.WriteLine(); 129 | } 130 | 131 | public void WriteVertices() { 132 | w.WriteLine("# List of vertex coordinates"); 133 | w.Write("model['verts'] = ["); 134 | 135 | Vector3[] verts = mesh.vertices; 136 | for (int i = 0; i < verts.Length; i++) { 137 | w.Write(ValueVector3(verts[i]) + ", "); 138 | WriteAutoLineBreak(i, 32); 139 | } 140 | 141 | w.WriteLine("]"); 142 | w.WriteLine(); 143 | } 144 | 145 | public void WriteNormals() { 146 | w.WriteLine("# List of normals"); 147 | w.Write("model['normals'] = ["); 148 | 149 | Vector3[] normals = mesh.normals; 150 | for (int i = 0; i < normals.Length; i++) { 151 | w.Write(ValueVector3(normals[i]) + ", "); 152 | WriteAutoLineBreak(i, 32); 153 | } 154 | 155 | w.WriteLine("]"); 156 | w.WriteLine(); 157 | } 158 | 159 | public void WriteTrianglesOld() { 160 | w.WriteLine("# Map of materials and face indices"); 161 | w.WriteLine("model['tris'] = {"); 162 | 163 | Material[] mats = meshRenderer.sharedMaterials; 164 | for (int i = 0; i < mesh.subMeshCount; i++) { 165 | Material material = mats[i]; 166 | string matName = material == null ? "null" : material.name; 167 | 168 | w.Write(S + ValueString(matName) + ": ["); 169 | 170 | int[] triangles = mesh.GetTriangles(i); 171 | 172 | for (int j = 0, n = 1; j < triangles.Length; j += 3, n++) { 173 | w.Write(ValueVector3(triangles[j], triangles[j + 1], triangles[j + 2]) + ", "); 174 | WriteAutoLineBreak(j, 32); 175 | } 176 | 177 | w.WriteLine("],"); 178 | } 179 | 180 | w.WriteLine("}"); 181 | w.WriteLine(); 182 | } 183 | 184 | public void WriteSubmeshes() { 185 | w.WriteLine("# List of triangle indices per submesh"); 186 | w.WriteLine("model['submeshes'] = ["); 187 | 188 | for (int i = 0; i < mesh.subMeshCount; i++) { 189 | w.Write(S + "["); 190 | 191 | int[] triangles = mesh.GetTriangles(i); 192 | 193 | for (int j = 0; j < triangles.Length; j++) { 194 | w.Write(triangles[j] + ", "); 195 | WriteAutoLineBreak(j, 128); 196 | } 197 | 198 | w.WriteLine("],"); 199 | } 200 | 201 | w.WriteLine("]"); 202 | w.WriteLine(); 203 | } 204 | 205 | public void WriteMaterials() { 206 | w.WriteLine("# List of material names per submesh"); 207 | w.Write("model['materials'] = ["); 208 | 209 | Material[] mats = meshRenderer.sharedMaterials; 210 | for (int i = 0; i < mesh.subMeshCount; i++) { 211 | Material material = mats[i]; 212 | string matName = material == null ? "null" : material.name; 213 | w.Write(ValueString(matName) + ", "); 214 | } 215 | 216 | w.WriteLine("]"); 217 | w.WriteLine(); 218 | } 219 | 220 | public void WriteUV() { 221 | w.WriteLine("# List of texture face UVs"); 222 | w.Write("model['uv'] = ["); 223 | 224 | int[] tris = mesh.triangles; 225 | Vector2[] uv = mesh.uv; 226 | for (int i = 0, n = 1; i < tris.Length; i += 3, n++) { 227 | w.Write("["); 228 | 229 | w.Write(ValueVector2(uv[tris[i]]) + ", "); 230 | w.Write(ValueVector2(uv[tris[i + 1]]) + ", "); 231 | w.Write(ValueVector2(uv[tris[i + 2]])); 232 | 233 | w.Write("], "); 234 | WriteAutoLineBreak(i, 16); 235 | } 236 | 237 | w.WriteLine("]"); 238 | w.WriteLine(); 239 | } 240 | 241 | public void WriteVertexGroups() { 242 | w.WriteLine("# List of vertex groups, formatted as: (vertex index, weight)"); 243 | w.WriteLine("model['vg'] = {"); 244 | 245 | Transform[] bones = meshRenderer.bones; 246 | BoneWeight[] boneWeights = mesh.boneWeights; 247 | for (int i = 0; i < bones.Length; i++) { 248 | List> vweightList = new List>(); 249 | 250 | for (int j = 0; j < boneWeights.Length; j++) { 251 | Dictionary vweight = new Dictionary(); 252 | 253 | if (boneWeights[j].boneIndex0 == i && boneWeights[j].weight0 != 0) { 254 | vweight.Add(j, boneWeights[j].weight0); 255 | } 256 | 257 | if (boneWeights[j].boneIndex1 == i && boneWeights[j].weight1 != 0) { 258 | vweight.Add(j, boneWeights[j].weight1); 259 | } 260 | 261 | if (boneWeights[j].boneIndex2 == i && boneWeights[j].weight2 != 0) { 262 | vweight.Add(j, boneWeights[j].weight2); 263 | } 264 | 265 | if (boneWeights[j].boneIndex3 == i && boneWeights[j].weight3 != 0) { 266 | vweight.Add(j, boneWeights[j].weight3); 267 | } 268 | 269 | if (vweight.Count > 0) { 270 | vweightList.Add(vweight); 271 | } 272 | } 273 | 274 | if (vweightList.Count > 0) { 275 | w.Write(S + ValueString(bones[i].name) + ": ["); 276 | 277 | int vgcount = 1; 278 | foreach (Dictionary vweight in vweightList) { 279 | foreach (KeyValuePair entry in vweight) { 280 | w.Write(ValueVector2(entry.Key, entry.Value) + ", "); 281 | vgcount++; 282 | WriteAutoLineBreak(vgcount, 32); 283 | } 284 | } 285 | 286 | w.WriteLine("],"); 287 | } else { 288 | w.WriteLine("# No weights for bone " + bones[i].name); 289 | } 290 | } 291 | 292 | w.WriteLine("}"); 293 | w.WriteLine(); 294 | } 295 | 296 | public void WriteTransforms() { 297 | HashSet bones = new HashSet(meshRenderer.bones); 298 | 299 | // save all connected bones in the set 300 | CollectTransforms(meshRenderer.transform.parent, bones); 301 | 302 | try { 303 | // reset local scale on all bones and save the original values in the dictionary, 304 | // this is required to unscale Transform.position 305 | boneScales.Clear(); 306 | foreach (Transform bone in bones) { 307 | boneScales.Add(bone, bone.localScale); 308 | bone.localScale = new Vector3(1, 1, 1); 309 | } 310 | 311 | w.WriteLine("model['bones'] = {"); 312 | 313 | foreach (Transform bone in bones) { 314 | WriteTransform(bone); 315 | } 316 | 317 | w.WriteLine("}"); 318 | w.WriteLine("model['root_bone'] = " + ValueString (meshRenderer.rootBone.name)); 319 | w.WriteLine(); 320 | } finally { 321 | // restore local scale 322 | foreach (Transform bone in bones) { 323 | bone.localScale = boneScales[bone]; 324 | } 325 | } 326 | } 327 | 328 | private void WriteTransform(Transform t) { 329 | w.WriteLine(S + ValueString(t.name) + ": {"); 330 | 331 | w.WriteLine(S + S + "'pos': " + ValueVector3(t.position) + ","); 332 | w.WriteLine(S + S + "'rot': " + ValueQuaternion(t.rotation) + ","); 333 | 334 | w.WriteLine(S + S + "'lpos': " + ValueVector3(t.localPosition) + ","); 335 | w.WriteLine(S + S + "'lrot': " + ValueQuaternion(t.localRotation) + ","); 336 | //w.WriteLine(S + S + "'lscl': " + ValueVector3(t.localScale) + ","); 337 | w.WriteLine(S + S + "'lscl': " + ValueVector3(boneScales[t]) + ","); 338 | 339 | if (t.parent != null) { 340 | w.WriteLine(S + S + "'parent': " + ValueString(t.parent.name) + ","); 341 | } 342 | 343 | if (t.childCount > 0) { 344 | w.Write(S + S + "'children': ["); 345 | 346 | Transform lastChild = t.GetChild(t.childCount - 1); 347 | foreach (Transform child in t) { 348 | w.Write(ValueString(child.name)); 349 | if (child != lastChild) { 350 | w.Write(", "); 351 | } 352 | } 353 | 354 | w.WriteLine("]"); 355 | } 356 | 357 | w.WriteLine(S + "},"); 358 | } 359 | 360 | private void CollectTransforms(Transform transform, HashSet tset) { 361 | tset.Add(transform); 362 | foreach (Transform child in transform) { 363 | CollectTransforms(child, tset); 364 | } 365 | } 366 | 367 | private void WriteAnimations(Animation anim) { 368 | if (anim == null) { 369 | return; 370 | } 371 | 372 | w.WriteLine("# Map of animation keyframes for every bone"); 373 | w.WriteLine("model['animations'] = {"); 374 | 375 | AnimationClip[] clips = AnimationUtility.GetAnimationClips(anim); 376 | 377 | foreach (AnimationClip clip in clips) { 378 | // map curves to bones first 379 | AnimationClipCurveData[] curves = AnimationUtility.GetAllCurves(clip); 380 | Dictionary> boneCurveMap = new Dictionary>(); 381 | 382 | foreach (AnimationClipCurveData curve in curves) { 383 | string boneName = Regex.Replace(curve.path, "^([^/]+/)+", ""); 384 | 385 | HashSet curveSet; 386 | 387 | if (boneCurveMap.ContainsKey(boneName)) { 388 | curveSet = boneCurveMap[boneName]; 389 | } else { 390 | curveSet = new HashSet(); 391 | boneCurveMap.Add(boneName, curveSet); 392 | } 393 | 394 | curveSet.Add(curve); 395 | } 396 | 397 | // now write data for each bone, curve and keyframe 398 | w.WriteLine(S + ValueString(clip.name) + ": {"); 399 | 400 | foreach (KeyValuePair> entry in boneCurveMap) { 401 | w.WriteLine(S + S + ValueString(entry.Key) + ": {"); 402 | 403 | foreach (AnimationClipCurveData curve in entry.Value) { 404 | w.Write(S + S + S + ValueString(curve.propertyName) + ": {"); 405 | 406 | foreach (Keyframe kf in curve.curve.keys) { 407 | w.Write(ValueVector2(kf.time, kf.value) + ","); 408 | } 409 | 410 | w.WriteLine("},"); 411 | } 412 | 413 | w.WriteLine(S + S + "},"); 414 | } 415 | 416 | w.WriteLine(S + "},"); 417 | } 418 | 419 | w.WriteLine("}"); 420 | } 421 | 422 | private void WriteAutoLineBreak(int n, int limit) { 423 | if ((n + 1) % limit == 0) { 424 | w.WriteLine(); 425 | } 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /Editor/UnpackAssets.cs: -------------------------------------------------------------------------------- 1 | /* 2 | ** 2013 March 9 3 | ** 4 | ** The author disclaims copyright to this source code. In place of 5 | ** a legal notice, here is a blessing: 6 | ** May you do good and not evil. 7 | ** May you find forgiveness for yourself and forgive others. 8 | ** May you share freely, never taking more than you give. 9 | **/ 10 | 11 | using System.Collections; 12 | using System.Collections.Generic; 13 | using System.IO; 14 | using System.Text; 15 | using System.Text.RegularExpressions; 16 | 17 | using UnityEngine; 18 | using UnityEditor; 19 | 20 | public class UnpackAssets : MonoBehaviour { 21 | 22 | private static string selectedDir = Application.dataPath; 23 | private static Dictionary> assetList = new Dictionary>(); 24 | private const char SEPARATOR = ':'; 25 | 26 | [MenuItem("Custom/Assets/Create asset bundle list")] 27 | public static void List() { 28 | string assetPackPath = EditorUtility.OpenFilePanel("Select asset bundle", selectedDir, "assets"); 29 | 30 | if (assetPackPath.Length == 0) { 31 | return; 32 | } 33 | 34 | string listName = Path.GetFileNameWithoutExtension(assetPackPath) + ".txt"; 35 | string listPath = EditorUtility.SaveFilePanel("Save asset bundle list", "", selectedDir + "/" + listName, "txt"); 36 | 37 | if (listPath.Length == 0) { 38 | return; 39 | } 40 | 41 | TextWriter tw = new StreamWriter(listPath); 42 | 43 | using (tw) { 44 | UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(assetPackPath); 45 | 46 | foreach (UnityEngine.Object asset in assets) { 47 | if (asset == null) { 48 | continue; 49 | } 50 | 51 | string assetName = GetFixedAssetName(asset); 52 | string assetType = asset.GetType().Name; 53 | 54 | if (assetName.Length == 0) { 55 | continue; 56 | } 57 | 58 | tw.WriteLine(assetType + SEPARATOR + assetName); 59 | } 60 | } 61 | 62 | EditorUtility.DisplayDialog("UnpackAssets", "Finished writing " + listPath, "Ok"); 63 | } 64 | 65 | [MenuItem("Custom/Assets/Unpack asset bundle")] 66 | public static void Unpack() { 67 | string assetPackPath = EditorUtility.OpenFilePanel("Select assets file to unpack", selectedDir, "assets"); 68 | 69 | if (assetPackPath.Length == 0) { 70 | return; 71 | } 72 | 73 | selectedDir = Path.GetDirectoryName(assetPackPath); 74 | 75 | string assetListPath = EditorUtility.OpenFilePanel("Select list of assets to unpack", selectedDir, "txt"); 76 | 77 | if (assetListPath.Length == 0) { 78 | return; 79 | } 80 | 81 | LoadAssetList(assetListPath); 82 | UnpackAssetBundle(assetPackPath); 83 | } 84 | 85 | private static void LoadAssetList(string assetListPath) { 86 | StreamReader reader = new StreamReader(assetListPath, Encoding.Default); 87 | 88 | using (reader) { 89 | for (string line; (line = reader.ReadLine()) != null;) { 90 | string[] parts = line.Split(SEPARATOR); 91 | 92 | if (parts.Length < 2) { 93 | continue; 94 | } 95 | 96 | string type = parts[0]; 97 | string name = parts[1]; 98 | 99 | HashSet value; 100 | 101 | if (assetList.TryGetValue(type, out value)) { 102 | value.Add(name); 103 | } else { 104 | assetList[type] = new HashSet(); 105 | } 106 | } 107 | } 108 | } 109 | 110 | private static void UnpackAssetBundle(string assetPackPath) { 111 | string assetPackName = Path.GetFileNameWithoutExtension(assetPackPath); 112 | string importDir = Application.dataPath + "/" + assetPackName; 113 | 114 | if (!Directory.Exists(importDir)) { 115 | Directory.CreateDirectory(importDir); 116 | } 117 | 118 | UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(assetPackPath); 119 | 120 | foreach (UnityEngine.Object asset in assets) { 121 | if (asset == null) { 122 | continue; 123 | } 124 | 125 | string assetName = GetFixedAssetName(asset); 126 | string assetType = asset.GetType().Name; 127 | 128 | HashSet value; 129 | 130 | if (assetList.TryGetValue(assetType, out value)) { 131 | if (!value.Contains(assetName)) { 132 | continue; 133 | } 134 | } else { 135 | continue; 136 | } 137 | 138 | string assetBasePath = importDir + "/" + assetType; 139 | 140 | if (!Directory.Exists(assetBasePath)) { 141 | Directory.CreateDirectory(assetBasePath); 142 | } 143 | 144 | string assetExt = "asset"; 145 | string assetPath = null; 146 | 147 | if (IsGameObject(asset)) { 148 | assetExt = "prefab"; 149 | } 150 | 151 | for (int i = 0; assetPath == null || File.Exists(assetPath); i++) { 152 | assetPath = "Assets/" + assetPackName + "/" + assetType + "/" + assetName + (i == 0 ? "" : "_" + i) + "." + assetExt; 153 | } 154 | 155 | UnpackAsset(asset, assetPath); 156 | } 157 | 158 | EditorUtility.DisplayDialog("UnpackAssets", "Finished unpacking " + assetPackName, "Ok"); 159 | } 160 | 161 | private static void UnpackAsset(Object asset, string assetPath) { 162 | if (IsGameObject(asset)) { 163 | GameObject gobj = (GameObject) asset; 164 | Object prefab = PrefabUtility.CreateEmptyPrefab(assetPath); 165 | PrefabUtility.ReplacePrefab(gobj, prefab); 166 | } else { 167 | Object assetInstance = asset; 168 | 169 | try { 170 | assetInstance = Object.Instantiate(asset); 171 | } catch { 172 | assetInstance = asset; 173 | } 174 | 175 | if (assetInstance != null) { 176 | AssetDatabase.CreateAsset(assetInstance, assetPath); 177 | } else { 178 | Debug.LogWarning("Can't unpack " + GetFixedAssetName(asset)); 179 | } 180 | } 181 | AssetDatabase.Refresh(); 182 | } 183 | 184 | private static bool IsGameObject(Object asset) { 185 | return asset.GetType() == typeof(GameObject) || asset.GetType().IsSubclassOf(typeof(GameObject)); 186 | } 187 | 188 | private static bool IsSubstanceArchive(Object asset) { 189 | return asset.GetType() == typeof(SubstanceArchive); 190 | } 191 | 192 | private static string GetFixedAssetName(Object asset) { 193 | return Regex.Replace(asset.name, "[^a-zA-Z0-9.-_ ]", ""); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | This is free and unencumbered software released into the public domain. 4 | 5 | Anyone is free to copy, modify, publish, use, compile, sell, or 6 | distribute this software, either in source code form or as a compiled 7 | binary, for any purpose, commercial or non-commercial, and by any 8 | means. 9 | 10 | In jurisdictions that recognize copyright laws, the author or authors 11 | of this software dedicate any and all copyright interest in the 12 | software to the public domain. We make this dedication for the benefit 13 | of the public at large and to the detriment of our heirs and 14 | successors. We intend this dedication to be an overt act of 15 | relinquishment in perpetuity of all present and future rights to this 16 | software under copyright law. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 22 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | For more information, please refer to 27 | 28 | ### Apache License 29 | 30 | BSPSource uses libraries of the [Apache Commons project](http://commons.apache.org): 31 | 32 | * [Apache Commons CLI](http://commons.apache.org/cli/) 33 | * [Apache Commons Compress](http://commons.apache.org/compress/) 34 | * [Apache Commons IO](http://commons.apache.org/io/) 35 | * [Apache Commons Lang3](http://commons.apache.org/lang/) 36 | 37 | which are licensed under the [Apache License version 2.0](http://www.apache.org/licenses/LICENSE-2.0). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Unity Editor Utils 2 | ================== 3 | 4 | A collection of scripts and programs for the Unity editor, mostly for content extraction. 5 | --------------------------------------------------------------------------------