# ***** BEGIN GPL LICENSE BLOCK ***** # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # ***** END GPL LICENCE BLOCK ***** bl_info = { 'name': 'VDrift tools', 'description': 'Import-Export to VDrift track files', 'author': 'NaN, port of VDrift blender24 scripts', 'version': (0, 9), 'blender': (2, 6, 3), 'location': 'File > Import-Export', 'warning': '', 'wiki_url': 'http://', 'tracker_url': 'http://', 'category': 'Import-Export'} import bpy from bpy.props import StringProperty, BoolProperty from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy_extras.image_utils import load_image from struct import Struct from os import path from mathutils import Vector, Matrix class joe_vertex: bstruct = Struct('<fff') # read list of 3-tuples @staticmethod def read(num, file): values = [] for i in range(num): data = file.read(joe_vertex.bstruct.size) v = joe_vertex.bstruct.unpack(data) values.append(v) return values # write a list of 3-tuples @staticmethod def write(values, file): for v in values: data = joe_vertex.bstruct.pack(v[0], v[1], v[2]) file.write(data) class joe_texcoord: bstruct = Struct('<ff') # read a list of 2-tuples @staticmethod def read(num, file): values = [] for i in range(num): data = file.read(joe_texcoord.bstruct.size) v = joe_texcoord.bstruct.unpack(data) values.append((v[0], 1 - v[1])) return values # write a list of 2-tuples @staticmethod def write(values, file): for v in values: data = joe_texcoord.bstruct.pack(v[0], 1 - v[1]) file.write(data) class joe_face: __slots__ = 'vertex_index', 'normal_index', 'texture_index' bstruct = Struct('<3h3h3h') def __init__(self): self.vertex_index = [0, 0, 0] self.normal_index = [0, 0, 0] self.texture_index = [0, 0, 0] def load (self, file): data = file.read(joe_face.bstruct.size) v = joe_face.bstruct.unpack(data) self.vertex_index = [v[0], v[1], v[2]] self.normal_index = [v[3], v[4], v[5]] self.texture_index = [v[6], v[7], v[8]] return self def save(self, file): data = joe_face.bstruct.pack( self.vertex_index[0],self.vertex_index[1],self.vertex_index[2], self.normal_index[0],self.normal_index[1],self.normal_index[2], self.texture_index[0],self.texture_index[1],self.texture_index[2]) file.write(data) class joe_frame: __slots__ = 'num_vertices', 'num_normals', 'num_texcoords',\ 'faces', 'verts', 'texcoords', 'normals' bstruct = Struct('<3i') def __init__(self): self.num_vertices = 0 self.num_texcoords = 0 self.num_normals = 0 self.faces = [] self.verts = [] self.texcoords = [] self.normals = [] def load(self, file): # header data = file.read(joe_frame.bstruct.size) v = joe_frame.bstruct.unpack(data) self.num_vertices = v[0] self.num_texcoords = v[1] self.num_normals = v[2] # mesh data self.verts = joe_vertex.read(self.num_vertices, file) self.normals = joe_vertex.read(self.num_normals, file) self.texcoords = joe_texcoord.read(self.num_texcoords, file) return self def save(self, file): # header data = joe_frame.bstruct.pack(self.num_vertices, self.num_texcoords, self.num_normals) file.write(data) # mesh data joe_vertex.write(self.verts, file) joe_vertex.write(self.normals, file) joe_texcoord.write(self.texcoords, file) def from_mesh(self, obj): mesh = obj.data if obj.matrix_world != Matrix.Identity(4): mesh = obj.data.copy() mesh.transform(obj.matrix_world) if not mesh.tessfaces: mesh.calc_tessface() normals = util.indexed_set() vertices = util.indexed_set() texcoords = util.indexed_set() # get vertices and normals mvertices = mesh.vertices mtexcoords = mesh.tessface_uv_textures[0].data for fi, f in enumerate(mesh.tessfaces): uv = mtexcoords[fi].uv_raw if f.vertices_raw[3] != 0: # split quad in two tris d0 = (Vector(mvertices[2].co) - Vector(mvertices[0].co)).length_squared d1 = (Vector(mvertices[3].co) - Vector(mvertices[1].co)).length_squared if d0 < d1: vi1, vi2 = (0, 1, 2), (2, 3, 0) else: vi1, vi2 = (1, 2, 3), (3, 0, 1) jf = joe_face() jf.vertex_index = [vertices.get(mvertices[f.vertices_raw[i]].co) for i in vi2] if f.use_smooth: jf.normal_index = [normals.get(mvertices[f.vertices_raw[i]].normal) for i in vi2] else: jf.normal_index = [normals.get(f.normal)] * 3 jf.texture_index = [texcoords.get((uv[i * 2], uv[i * 2 + 1])) for i in vi2] self.faces.append(jf) else: vi1 = (0, 1, 2) jf = joe_face() jf.vertex_index = [vertices.get(mvertices[f.vertices_raw[i]].co) for i in vi1] if f.use_smooth: jf.normal_index = [normals.get(mvertices[f.vertices_raw[i]].normal) for i in vi1] else: jf.normal_index = [normals.get(f.normal)] * 3 jf.texture_index = [texcoords.get((uv[i * 2], uv[i * 2 + 1])) for i in vi1] self.faces.append(jf) self.normals = normals.list self.verts = vertices.list self.texcoords = texcoords.list self.num_normals = len(self.normals) self.num_texcoords = len(self.texcoords) self.num_vertices = len(self.verts) return self # remove faces consisting less then 3 vertices def remove_degenerate_faces(self): faces = [] for f in self.faces: vi = f.vertex_index if vi[0] != vi[1] and vi[1] != vi[2] and vi[0] != vi[2]: faces.append(f) self.faces = faces # blender only supports one normal per vertex def duplicate_verts_with_multiple_normals(self): face_vert = {} verts = [] for f in self.faces: for i in range(3): vn = f.vertex_index[i], f.normal_index[i] if vn not in face_vert: verts.append(self.verts[f.vertex_index[i]]) vi = len(verts) - 1 f.vertex_index[i] = vi face_vert[vn] = vi else: f.vertex_index[i] = face_vert[vn] self.verts = verts # in blender 2.5 the last vertex index shall not be 0 def swizzle_face_vertices(self): for f in self.faces: vi = f.vertex_index ni = f.normal_index ti = f.texture_index if vi[2] == 0: vi[0], vi[1], vi[2] = vi[2], vi[0], vi[1] ni[0], ni[1], ni[2] = ni[2], ni[0], ni[1] ti[0], ti[1], ti[2] = ti[2], ti[0], ti[1] def to_mesh(self, name, image): # cleanup joe self.remove_degenerate_faces() self.swizzle_face_vertices() self.duplicate_verts_with_multiple_normals() # new mesh mesh = bpy.data.meshes.new(name) mesh.vertices.add(len(self.verts)) mesh.tessfaces.add(len(self.faces)) # set vertices for i, v in enumerate(self.verts): mesh.vertices[i].co = v for f in self.faces: for i in range(3): mesh.vertices[f.vertex_index[i]].normal = self.normals[f.normal_index[i]] # set faces for i, f in enumerate(self.faces): mesh.tessfaces[i].vertices = (f.vertex_index[0], f.vertex_index[1], f.vertex_index[2], 0) mesh.tessfaces[i].use_smooth = True # set texture coordinates if self.num_texcoords == 0: print("Warning! Mesh has no texture coordinates.") else: mesh.tessface_uv_textures.new() for i, f in enumerate(self.faces): mf = mesh.tessface_uv_textures[0].data[i] mf.uv1 = self.texcoords[f.texture_index[0]] mf.uv2 = self.texcoords[f.texture_index[1]] mf.uv3 = self.texcoords[f.texture_index[2]] if (image): mf.image = image mesh.validate() mesh.update() object = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(object) return object class joe_obj: __slots__ = 'ident', 'version', 'num_faces', 'num_frames', 'frames' bstruct = Struct('<4i') def __init__(self): self.ident = 844121161 self.version = 3 self.num_faces = 0 self.num_frames = 0 self.frames = [] def load(self, file): # header data = file.read(joe_obj.bstruct.size) v = joe_obj.bstruct.unpack(data) self.ident = v[0] self.version = v[1] self.num_faces = v[2] self.num_frames = v[3] # frames for i in range(self.num_frames): self.frames.append(joe_frame()) for j in range(self.num_faces): self.frames[i].faces.append(joe_face().load(file)) self.frames[i].load(file) return self def save(self, file): # header data = joe_obj.bstruct.pack(self.ident, self.version, self.num_faces, self.num_frames) file.write(data) # frames for i in range(self.num_frames): for j in range(self.num_faces): self.frames[i].faces[j].save(file) self.frames[i].save(file) def to_mesh(self, name, image, num_frames=1): frames = [] if name.endswith('.joe'): name = name[:-4] for i in range(num_frames): bpy.context.scene.frame_set(i) frames.append(self.frames[i].to_mesh(name, image)) return frames[0] def from_mesh(self, mesh_obj, num_frames=1): for i in range(num_frames): bpy.context.scene.frame_set(i) frame = joe_frame() frame.from_mesh(mesh_obj) self.frames.append(frame) self.num_frames = num_frames self.num_faces = len(self.frames[0].faces) return self class joe_pack: version = b'JPK01.00' bstruct = Struct('<2i') def __init__(self): self.numobjs = 0 self.maxstrlen = 0 self.joe = {} self.list = {} self.images = {} self.surfaces = [] @staticmethod def read(filename): # don't change call order jpk = joe_pack() jpk.load_list(filename) jpk.load_images(filename) try: if not filename.endswith('.jpk'): dir = path.dirname(filename) filename = path.join(dir, 'objects.jpk') jpk.load(filename) except: jpk.load_joes(filename) return jpk @staticmethod def write(filename, write_list, write_jpk): jpk = joe_pack().from_mesh() if write_jpk: jpk.save(filename) if write_list: jpk.save_list(filename) def to_mesh(self): trackobject.create_groups() for name, joe in self.joe.items(): image = None trackobj = self.list.get(name) if trackobj: imagename = trackobj.values[1] image = self.images[imagename] obj = joe.to_mesh(name, image) trackobj.to_obj(obj) else: print(name + ' not imported. Not in list.txt.') def from_mesh(self): objlist = bpy.context.scene.objects trackobject.set_groups() for obj in objlist: if obj.type != 'MESH': continue if obj.name.startswith('~'): continue if not obj.data.tessfaces: obj.data.calc_tessface() if len(obj.data.tessfaces) == 0: print(obj.name + ' not exported. No faces.') continue if not obj.data.tessface_uv_textures: print(obj.name + ' not exported. No texture coordinates.') continue image = None if obj.data.tessface_uv_textures[0].data[0].image: image = obj.data.tessface_uv_textures[0].data[0].image else: for mat_slot in obj.material_slots: for mtex_slot in mat_slot.material.texture_slots: if mtex_slot and hasattr(mtex_slot.texture, 'image'): image = mtex_slot.texture.image break if not image: print(obj.name + ' not exported. No texture linked.') continue objname = obj.name trackobj = trackobject().from_obj(obj, path.basename(image.filepath)) # override obj name if len(trackobj.values[0]): objname = trackobj.values[0] # loader expects a joe file if not objname.endswith('.joe'): objname = objname + '.joe' trackobj.values[0] = objname self.list[objname] = trackobj self.joe[objname] = obj self.maxstrlen = max(self.maxstrlen, len(objname)) self.numobjs = len(self.joe) return self # fallback if no jpk def load_joes(self, filename): dir = path.dirname(filename) for name in self.list: joe_path = path.join(dir, name) file = open(joe_path, 'rb') joe = joe_obj().load(file) self.joe[name] = joe def load(self, filename): file = open(filename, 'rb') # header version = file.read(len(joe_pack.version)) if version != joe_pack.version: raise Exception(filename + ' unknown jpk version: ' + str(version) + ' expected: ' + str(joe_pack.version)) data = file.read(joe_pack.bstruct.size) v = joe_pack.bstruct.unpack(data) self.numobjs = v[0] self.maxstrlen = v[1] # fat fat = [] for i in range(self.numobjs): data = file.read(joe_pack.bstruct.size) v = joe_pack.bstruct.unpack(data) offset = v[0] length = v[1] data = file.read(self.maxstrlen) # strip trailing zeros for i in range(self.maxstrlen): if data[i] == 0: data = data[:i] break name = data.decode('ascii') fat.append((offset, length, name)) # data for offset, length, name in fat: pos = file.tell() delta = offset - pos if delta < 0: print('Error reading: ', name, offset) return elif delta > 0: file.read(delta) joe = joe_obj().load(file) self.joe[name] = joe file.close() def save(self, filename): try: file = open(filename, 'rb+') except IOError: file = open(filename, 'wb') # header file.write(self.version) data = joe_pack.bstruct.pack(self.numobjs, self.maxstrlen) file.write(data) # allocate fat fat_offset = file.tell() for i in range(self.numobjs): data = joe_pack.bstruct.pack(0, 0) file.write(data) name = util.fillz('', self.maxstrlen) file.write(name.encode('ascii')) # write data / build fat fat = [] for name, obj in self.joe.items(): offset = file.tell() joe = joe_obj().from_mesh(obj) joe.save(file) length = file.tell() - offset fat.append((offset, length, name)) # fill fat file.seek(fat_offset) for offset, length, name in fat: data = joe_pack.bstruct.pack(offset, length) file.write(data) name = util.fillz(name, self.maxstrlen) file.write(name.encode('ascii')) file.close() def load_list(self, filename): dir = path.dirname(filename) list_path = path.join(dir, 'list.txt') try: list_file = open(list_path) except IOError: print(list_path + ' not found.') return # read objects line = list_file.readline() while line != '': if not line.startswith('#') and '.joe' in line: object = trackobject() name = line.strip() line = object.read(name, list_file) self.list[object.values[0]] = object else: line = list_file.readline() if len(self.list) == 0: print('Failed to load list.txt.') list_file.close() def save_list(self, filename): dir = path.dirname(filename) list_path = path.join(dir, 'list.txt') file = open(list_path, 'w') file.write('17\n\n') i = 0 for name, object in self.list.items(): file.write('#entry ' + str(i) + '\n') object.write(file) i = i + 1 file.close() def load_images(self, filename): dir = path.dirname(filename) for name, object in self.list.items(): imagename = object.values[1] if imagename not in self.images: imagepath = path.join(dir, imagename) self.images[imagename] = load_image(imagepath) class trackobject: names = ('model', 'texture', 'mipmap', 'lighting', 'skybox', 'blend',\ 'bump length', 'bump amplitude', 'drivable', 'collidable',\ 'non treaded', 'treaded', 'roll resistance', 'roll drag',\ 'shadow', 'clamp', 'surface') namemap = dict(zip(names, range(17))) @staticmethod def create_groups(): trackobject.grp_surf = [] trackobject.grp = {} for name in ('mipmap', 'nolighting', 'skybox', 'transparent',\ 'doublesided', 'collidable', 'shadow', 'clampu', 'clampv'): grp = bpy.data.groups.get(name) if grp == None: grp = bpy.data.groups.new(name) # need to link an object to group to make it visible obj = bpy.data.objects.get('0') if not obj: obj = bpy.data.objects.new('0', None) grp.objects.link(obj) trackobject.grp[name] = grp.objects @staticmethod def set_groups(): trackobject.create_groups() trackobject.is_surf = [] for grp in bpy.data.groups: if grp.name == 'mipmap': trackobject.is_mipmap = set(grp.objects) elif grp.name == 'nolighting': trackobject.is_nolighting = set(grp.objects) elif grp.name == 'skybox': trackobject.is_skybox = set(grp.objects) elif grp.name == 'transparent': trackobject.is_transparent = set(grp.objects) elif grp.name == 'doublesided': trackobject.is_doublesided = set(grp.objects) elif grp.name == 'collidable': trackobject.is_collidable = set(grp.objects) elif grp.name == 'shadow': trackobject.is_shadow = set(grp.objects) elif grp.name == 'clampu': trackobject.is_clampu = set(grp.objects) elif grp.name == 'clampv': trackobject.is_clampv = set(grp.objects) elif grp.name.startswith('surface'): trackobject.is_surf.append((grp.name.split('-')[-1], set(grp.objects))) def __init__(self): self.values = ['none', 'none', '1', '0', '0', '0',\ '1.0', '0.0', '0', '0',\ '1.0', '0.9', '1.0', '0.0',\ '0', '0', '0'] def read(self, name, list_file): i = 0 self.values[i] = name while True: line = list_file.readline() if line == '' or '.joe' in line: return line elif line.startswith('#') or line.startswith('\n'): continue else: i = i + 1 self.values[i] = line.strip() return line def write(self, list_file): for v in self.values: list_file.write(v + '\n') list_file.write('\n') def to_obj(self, object): object['model'] = self.values[0] object['texture'] = self.values[1] if self.values[2] == '1': trackobject.grp['mipmap'].link(object) if self.values[3] == '1': trackobject.grp['nolighting'].link(object) if self.values[4] == '1': trackobject.grp['skybox'].link(object) if self.values[5] == '1': trackobject.grp['transparent'].link(object) if self.values[5] == '2': trackobject.grp['doublesided'].link(object) if self.values[8] == '1' or self.values[9] == '1': trackobject.grp['collidable'].link(object) if self.values[14] == '1': trackobject.grp['shadow'].link(object) if self.values[15] == '1' or self.values[15] == '3': trackobject.grp['clampu'].link(object) if self.values[15] == '2' or self.values[15] == '3': trackobject.grp['clampv'].link(object) surfid = int(self.values[16]) while surfid >= len(trackobject.grp_surf): surfnum = len(trackobject.grp_surf) surfname = 'surface-'+str(surfnum) grp = bpy.data.groups.get(surfname) if grp == None: grp = bpy.data.groups.new(surfname) trackobject.grp_surf.append(grp.objects) trackobject.grp_surf[surfid].link(object) return self # set from object def from_obj(self, object, texture): model = object.name self.values[0] = object.get('model', model) self.values[1] = object.get('texture', texture) self.values[2] = '1' if object in trackobject.is_mipmap else '0' self.values[3] = '1' if object in trackobject.is_nolighting else '0' self.values[4] = '1' if object in trackobject.is_skybox else '0' if object in trackobject.is_transparent: self.values[5] = '1' elif object in trackobject.is_doublesided: self.values[5] = '2' else: self.values[5] = '0' self.values[9] = '1' if object in trackobject.is_collidable else '0' self.values[14] = '1' if object in trackobject.is_shadow else '0' self.values[15] = '1' if object in trackobject.is_clampu else '0' if object in trackobject.is_clampv: self.values[15] = '2' if self.values[15] == '0' else '3' for name, grp in self.is_surf: if object in grp: self.values[16] = name break return self class roads: @staticmethod def load(path): file = open(path, 'r') roadnum = int(file.readline()) file.readline() for i in range(roadnum): roads.load_road(file, 'road.' + str(i)) @staticmethod def save(path): file = open(path, 'w') meshes = [] i = 0 while 'road.' + str(i) in bpy.data.objects: obj = bpy.data.objects['road.' + str(i)] mesh = obj.data if obj.matrix_world != Matrix.Identity(4): mesh = obj.data.copy() mesh.transform(obj.matrix_world) meshes.append(mesh) i = i + 1 file.write(str(len(meshes)) + '\n\n') for m in meshes: roads.save_road(file, m) @staticmethod def load_road(file, name): patchnum = int(file.readline()) file.readline() # new mesh mesh = bpy.data.meshes.new(name) mesh.vertices.add(patchnum * 4 + 4) mesh.tessfaces.add(patchnum * 3) mesh.tessface_uv_textures.new() # parse road lines = [None] * 16 for p in range(patchnum): # road is stored reversed for n in range(15, -1, -1): lines[n] = file.readline() file.readline() # vertices first row, other rows are interpolated on export for n in range(4): i = p * 4 + n xyz = [float(s) for s in lines[n].split()] mesh.vertices[i].co = (xyz[2], xyz[0], xyz[1]) # faces for n in range(3): i = p * 3 + n vi = p * 4 + n mesh.tessfaces[i].vertices_raw = (vi, vi + 4, vi + 5, vi + 1) mesh.tessfaces[i].use_smooth = True u, v = 1 - n/3.0, float(p) mesh.tessface_uv_textures[0].data[i].uv_raw = (u, v, u, v + 1, u - 1/3.0, v + 1, u - 1/3.0, v) # last row for n in range(4): i = patchnum * 4 + n xyz = [float(s) for s in lines[n + 12].split()] mesh.vertices[i].co = (xyz[2], xyz[0], xyz[1]) # new object mesh.validate() mesh.update(calc_tessface = True) object = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(object) @staticmethod def save_road(file, mesh): if not mesh.tessfaces: mesh.calc_tessface() patchnum = int(len(mesh.vertices) / 4 - 1) #print('patches from facenum ' + str(len(mesh.tessfaces) / 3)) #print('patches from vertnum ' + str(patchnum)) road = [None] * 16 * patchnum if len(mesh.tessface_uv_textures) == 0 or len(mesh.tessface_uv_textures[0].data) == 0: raise NameError("Road mesh %s has no uv coordinates" % mesh.name) # get first, last patch rows from faces for i, f in enumerate(mesh.tessfaces): tf = mesh.tessface_uv_textures[0].data[i] for n in range(4): pointid = int(round(tf.uv_raw[2 * n] * 3)) patchid = int(round(tf.uv_raw[2 * n + 1])) id = patchid * 16 + pointid if patchid < patchnum: road[id] = mesh.vertices[f.vertices[n]].co if patchid > 0: road[id - 4] = mesh.vertices[f.vertices[n]].co # debug #for i, p in enumerate(road): # if p: # file.write(str(i) + ' %.4f %.4f %.4f\n' % (p[1], p[2], p[0])) # else: # file.write(str(i) + '\n') # calculate middle rows for i in range(patchnum - 1): roads.attach_patches(road, i, i + 1) # closed/open road if (road[0] - road[-1]).length < 1E-3: roads.attach_patches(road, -1, 0) else: roads.set_middlerow(road, 0, 1) roads.set_middlerow(road, -1, 2) # write road file.write(str(patchnum) + '\n\n') for i in range(patchnum): for n in range(3, -1, -1): for m in range(4): p = road[i * 16 + n * 4 + m] file.write('%.4f %.4f %.4f\n' % (p[1], p[2], p[0])) file.write('\n') # p0: first patch index # p1: second patch index @staticmethod def attach_patches(road, p0, p1): r0 = p0 * 16 r1 = p1 * 16 for n in range(4): i0 = r0 + n i1 = r1 + n slope = (road[i1 + 12] - road[i0]).normalized() len0 = (road[i0 + 12] - road[i0]).length len1 = (road[i1 + 12] - road[i1]).length scale = min(len1, len0) / 3.0 #old: (len1 + len0) / 6.0 road[i0 + 8] = road[i0 + 12] - slope * scale road[i1 + 4] = road[i1] + slope * scale # pi: patch index [0, patchnum) # ri: middle row index 1, 2 @staticmethod def set_middlerow(road, pi, ri): scale = ri / 3 for n in range(4): i = pi * 16 + n road[i + ri * 4] = road[i] + (road[i + 12] - road[i]) * scale class track: @staticmethod def load(path): start_position = {} obj = track.get_info() file = open(path, 'r') for line in file: line = line.rstrip('\n') if not line: continue name, value = line.split(' = ', 1) # generic properties (as strings for now) if name in obj: #if value == 'on' or value == 'yes': value = True #elif value == 'off' or value == 'no': value = False #ob[name] = type(ob[name])(value) obj[name] = value # lap sequences (as strings for now) elif name.startswith('lap sequence '): road, patch, unused = value.split(',', 2) obj[name] = road.split('.', 1)[0] + ':' + patch.split('.', 1)[0] # start positions elif name.startswith('start position '): x, y, z = value.split(',', 2) track.get_box(name).location = (float(z), float(x), float(y)) elif name.startswith('start orientation '): rad = 0.0174532925 x, y, z = value.split(',', 2) x, y, z = float(x) * rad, float(y) * rad, float(z) * rad name = 'start position ' + name.rsplit(' ', 1)[1] track.get_box(name).rotation_euler = (z, x, y) file.close() @staticmethod def save(path): file = open(path, 'w') obj = track.get_info() lap_sequence = [] for k, v in obj.items(): if k.startswith('lap sequence'): lap_sequence.append((k, v)) else: file.write(k + ' = ' + str(v) + '\n') file.write('lap sequences = ' + str(len(lap_sequence)) + '\n') for v in lap_sequence: name = v[0] road, patch = v[1].split(':', 1) file.write(name + ' = ' + road + ',' + patch + ',0\n') n = 0 while True: name = 'start position ' + str(n) obj = bpy.data.objects.get(name) if not obj: break x, y, z = obj.location file.write('start position %s = %.4f,%.4f,%.4f\n' % (str(n), y, z, x)) deg = 57.2957795 x, y, z = obj.rotation_euler x, y, z = x * deg, y * deg, z * deg file.write('start orientation %s = %.2f,%.2f,%.2f\n' % (str(n), y, z, x)) n = n + 1 file.close() @staticmethod def get_info(): obj = bpy.data.objects.get('track_info') if not obj: obj = bpy.data.objects.new('track_info', None) obj['cull faces'] = 'on' obj['vertical tracking skyboxes'] = 'no' obj['non-treaded friction coefficient'] = '1.0' obj['treaded friction coefficient'] = '0.9' bpy.context.scene.objects.link(obj) return obj; @staticmethod def get_box(name): obj = bpy.data.objects.get(name) if not obj: verts = [(1,-1,-1), (1,-1,1),(-1,-1,1),(-1,-1,-1),(1,1,-1),(1,1,1),(-1,1,1),(-1,1,-1)] edges = [(0,1),(1,2),(2,3),(3,7),(4,7),(5,6),(6,7),(0,3),(4,5),(1,5),(2,6),(0,4)] mesh = bpy.data.meshes.new("cube") mesh.from_pydata(verts, edges, []) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) obj.scale = (2.0, 0.9, 0.5) obj.show_axis = True return obj class util: # helper class to filter duplicates class indexed_set(object): def __init__(self): self.map = {} self.list = [] def get(self, ob): # using float as key in dict fixed = tuple(round(n, 5) for n in ob) if not fixed in self.map: ni = len(self.list) self.map[fixed] = ni self.list.append(fixed) else: ni = self.map[fixed] return ni # fill trailing zeroes @staticmethod def fillz(str, strlen): return str + chr(0)*(strlen - len(str)) class export_joe(bpy.types.Operator, ExportHelper): bl_idname = 'export.joe' bl_label = 'Export JOE' filename_ext = '.joe' filter_glob = StringProperty( default='*.joe', options={'HIDDEN'}) def __init__(self): try: self.object = bpy.context.selected_objects[0] except: self.object = None def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) if len(bpy.context.selected_objects[:]) != 1: raise NameError('Please select one object!') object = self.object if object.type != 'MESH': raise NameError('Selected object must be a mesh!') try: file = open(filepath, 'wb') joe = joe_obj().from_mesh(object) joe.save(file) file.close() finally: self.report({'INFO'}, object.name + ' exported') return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self); return {'RUNNING_MODAL'} class import_joe(bpy.types.Operator, ImportHelper): bl_idname = 'import.joe' bl_label = 'Import JOE' filename_ext = '.joe' filter_glob = StringProperty( default='*.joe', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) try: image = None #load_image(filepath_img) file = open(filepath, 'rb') joe = joe_obj().load(file) joe.to_mesh(bpy.path.basename(filepath), image) file.close() finally: self.report({'INFO'}, filepath + ' imported') return {'FINISHED'} class import_image(bpy.types.Operator, ImportHelper): bl_idname = 'import.image' bl_label = 'Import texture' filename_ext = '.png' filter_glob = StringProperty( default='*.png', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) image = load_image(filepath) if image == None: raise NameError('Failed to load image!') if len(bpy.context.selected_objects[:]) != 1: raise NameError('Please select one object!') object = bpy.context.selected_objects[0] if object.type != 'MESH': raise NameError('Selected object must be a mesh!') if len(object.data.tessfaces) == 0: object.data.calc_tessface() if len(object.data.tessfaces) == 0: raise NameError('Selected object has no faces!') if len(object.data.tessface_uv_textures) == 0: raise NameError('Selected object has no texture coordinates!') for mf in object.data.tessface_uv_textures[0].data: mf.image = image return {'FINISHED'} class export_jpk(bpy.types.Operator, ExportHelper): bl_idname = 'export.jpk' bl_label = 'Export JPK' filename_ext = '.jpk' filter_glob = StringProperty( default='*.jpk', options={'HIDDEN'}) export_list = BoolProperty( name='Export properties (list.txt)', description='Export track objects properties', default=True) export_jpk = BoolProperty( name='Export objects (objects.jpk)', description='Export track objects as JPK', default=True) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) joe_pack.write(filepath, self.export_list, self.export_jpk) return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self); return {'RUNNING_MODAL'} class import_jpk(bpy.types.Operator, ImportHelper): bl_idname = 'import.jpk' bl_label = 'Import JPK' filename_ext = '.jpk' filter_glob = StringProperty( default='*.jpk', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) jpk = joe_pack.read(filepath) jpk.to_mesh() return {'FINISHED'} class import_joe_list(bpy.types.Operator, ImportHelper): bl_idname = 'import.list' bl_label = 'Import VDrift track objects' filename_ext = '.txt' filter_glob = StringProperty( default='*.txt', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) jpk = joe_pack.read(filepath) jpk.to_mesh() return {'FINISHED'} class export_trk(bpy.types.Operator, ExportHelper): bl_idname = 'export.trk' bl_label = 'Export Vdrift roads' filename_ext = '.trk' filter_glob = StringProperty( default='*.trk', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) roads.save(filepath) return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self); return {'RUNNING_MODAL'} class import_trk(bpy.types.Operator, ImportHelper): bl_idname = 'import.trk' bl_label = 'Import VDrift roads' filename_ext = '.trk' filter_glob = StringProperty( default='*.trk', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) roads.load(filepath) return {'FINISHED'} class export_track(bpy.types.Operator, ExportHelper): bl_idname = 'export.track' bl_label = 'Export Vdrift track' filename_ext = '.txt' filter_glob = StringProperty( default='track.txt', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) track.save(filepath) return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self); return {'RUNNING_MODAL'} class import_track(bpy.types.Operator, ImportHelper): bl_idname = 'import.track' bl_label = 'Import VDrift track' filename_ext = '.txt' filter_glob = StringProperty( default='track.txt', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) track.load(filepath) return {'FINISHED'} def read_vec(str): return tuple(float(i) for i in (str.split('#', 1)[0]).split(',')) def load_suspension(cfg, wheel_name): name = wheel_name+'.double-wishbone' if name in cfg: s = cfg[name] ucf = read_vec(s['upper-chassis-front']) ucr = read_vec(s['upper-chassis-rear']) uh = read_vec(s['upper-hub']) lcf = read_vec(s['lower-chassis-front']) lcr = read_vec(s['lower-chassis-rear']) lh = read_vec(s['lower-hub']) verts = [ucf, uh, ucr, lcf, lh, lcr] edges = [(0,1), (1,2), (2,0), (3,4), (4,5), (5,3), (1,4)] else: name = wheel_name+'.macpherson-strut' if name in cfg: s = cfg[name] e = read_vec(s['strut-end']) t = read_vec(s['strut-top']) h = read_vec(s['hinge']) verts = [h, e, t] edges = [(0,1), (1,2)] else: name = wheel_name+'.hinge' s = cfg[name] hw = read_vec(s['wheel']) hb = read_vec(s['chassis']) verts = [hw, hb] edges = [(0,1)] mesh = bpy.data.meshes.new(name) mesh.from_pydata(verts, edges, []) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) def load_wheel(cfg, wheel_name): tp = read_vec(cfg[wheel_name]['position']) ts = read_vec(cfg[wheel_name+'.tire']['size']) tw = ts[0] * 0.001 ta = ts[1] * 0.01 tr = ts[2] * 0.5 * 0.0254 + tw * ta bpy.ops.mesh.primitive_cylinder_add(vertices=16, radius=tr, depth=tw, location=tp, rotation=(0.0, 1.5708, 0.0)) load_suspension(cfg, wheel_name) class import_car(bpy.types.Operator, ImportHelper): bl_idname = 'import.car' bl_label = 'Import VDrift car' filename_ext = '.car' filter_glob = StringProperty( default='*.car', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) try: from configparser import ConfigParser cfg = ConfigParser() cfg.read(filepath) load_wheel(cfg, 'wheel.fl') load_wheel(cfg, 'wheel.fr') load_wheel(cfg, 'wheel.rl') load_wheel(cfg, 'wheel.rr') filepath = path.join(path.dirname(filepath), 'body.joe') file = open(filepath, 'rb') joe = joe_obj().load(file) joe.to_mesh(bpy.path.basename(filepath), None) file.close() finally: self.report({'INFO'}, filepath + ' imported') return {'FINISHED'} def menu_export_joe(self, context): self.layout.operator(export_joe.bl_idname, text = 'VDrift JOE (.joe)') def menu_import_joe(self, context): self.layout.operator(import_joe.bl_idname, text = 'VDrift JOE (.joe)') def menu_import_image(self, context): self.layout.operator(import_image.bl_idname, text = 'VDrift Texture (.png)') def menu_export_jpk(self, context): self.layout.operator(export_jpk.bl_idname, text = 'VDrift JPK (.jpk)') def menu_import_jpk(self, context): self.layout.operator(import_jpk.bl_idname, text = 'VDrift JPK (.jpk)') def menu_import_joe_list(self, context): self.layout.operator(import_joe_list.bl_idname, text = 'VDrift Track Objects (list.txt)') def menu_export_trk(self, context): self.layout.operator(export_trk.bl_idname, text = 'VDrift Roads (.trk)') def menu_import_trk(self, context): self.layout.operator(import_trk.bl_idname, text = 'VDrift Roads (.trk)') def menu_export_track(self, context): self.layout.operator(export_track.bl_idname, text = 'VDrift Track Info (track.txt)') def menu_import_track(self, context): self.layout.operator(import_track.bl_idname, text = 'VDrift Track Info (track.txt)') def menu_import_car(self, context): self.layout.operator(import_car.bl_idname, text = 'VDrift Car (.car)') def register(): bpy.utils.register_module(__name__) bpy.types.INFO_MT_file_export.append(menu_export_joe) bpy.types.INFO_MT_file_import.append(menu_import_joe) bpy.types.INFO_MT_file_import.append(menu_import_image) bpy.types.INFO_MT_file_export.append(menu_export_jpk) bpy.types.INFO_MT_file_import.append(menu_import_jpk) bpy.types.INFO_MT_file_export.append(menu_export_trk) bpy.types.INFO_MT_file_import.append(menu_import_trk) bpy.types.INFO_MT_file_export.append(menu_export_track) bpy.types.INFO_MT_file_import.append(menu_import_track) bpy.types.INFO_MT_file_import.append(menu_import_joe_list) bpy.types.INFO_MT_file_import.append(menu_import_car) def unregister(): bpy.utils.unregister_module(__name__) bpy.types.INFO_MT_file_export.remove(menu_export_joe) bpy.types.INFO_MT_file_import.remove(menu_import_joe) bpy.types.INFO_MT_file_import.remove(menu_import_image) bpy.types.INFO_MT_file_export.remove(menu_export_jpk) bpy.types.INFO_MT_file_import.remove(menu_import_jpk) bpy.types.INFO_MT_file_export.remove(menu_export_trk) bpy.types.INFO_MT_file_import.remove(menu_import_trk) bpy.types.INFO_MT_file_export.remove(menu_export_track) bpy.types.INFO_MT_file_import.remove(menu_import_track) bpy.types.INFO_MT_file_import.append(menu_import_joe_list) bpy.types.INFO_MT_file_import.append(menu_import_car) if __name__ == '__main__': register()