I’m mostly writing this for my own notes, but on the off-chance my incoherent notes are useful to others, I decided to put it here. Most of this is going to be devoid of context, but for reference’s sake, I’m using a combination of XWA Opt Editor, Blender, XWA Texture Replacer (XTR), and finally OPTech to create the XvT/TIE-compatible OPTs. I’ll probably add more to this as I go.
Clean Up Unused Materials
There’s an addon that ships with Blender but is dormant by default called Material Utils that has a function to remove unused materials from an object (Clean Material Slots (Material Utils)). Use this once you’ve finished futzing with materials.
Clean Up UVTextures
These garbage up the exported OBJ with bad materials long after you’ve done any material editing. The following script obliterates them:
import bpy objects = bpy.context.selected_objects if (objects is None): print("You must select at least one object") # This warning will only show in the Blender console quit() for ob in objects: uvTexData = ob.data.uv_textures.active.data[:] print("Active UV on %s has %s faces of data" % (ob.name, len(uvTexData))) # Purely informational; can be omitted if desired for i in range(0, len(uvTexData)): if (uvTexData[i].image is not None): # We do not want ANY uv textures! print("Face %s: %s" % (i, uvTexData[i].image.name)) # Purely informational; what face has what UV texture uvTexData[i].image = None print("Cleaned UV texture from face")
Material and Texture Naming
Materials and Textures (the Blender concept of a Texture, not the actual filename) must be named TEX*, with a 5-digit numeric identifier (starting at 00000 and incrementing by 1) in order to behave properly. I tried a bunch of different naming schemes in the hopes that I could keep human-meaningful names applied to either Materials or Textures, but this inevitably caused problems once trying to check the model in XTR or OPTech. XWA Opt Editor handles it fine, though. I wrote several python scripts to do this, based on whatever previous iteration of material naming I had. Here was the most recent:
import bpy, re materials = bpy.data.materials idx = 0 for mat in materials: if mat.name == 'X': # Detecting whether a material was prefixed with X, which was the previous naming scheme for my top-level LOD newName = "TEX%s" % format(idx,'05') # 0-pad to 5 digits print("Renaming %s to %s" % (mat.name, newName)) # Informational mat.name = newName # Rename the material imgEx = mat.active_texture.image.name[-4:] # Get the extension on the Texture print("Renaming %s to %s%s" (mat.active_texture.image.name, newName, imgEx)) # Informational mat.active_texture.image.name = "%s%s" % (newName, imgEx) # Rename the texture; NOT the file, though idx += 1 # Only increment if we matched above
Make sure Selected Only is enabled if you only want to export your selection (which I did/do, since I had multiple LODs in the same Blender file) and make sure Triangulate Faces is turned on. Optionally, turn off Include Edges, which I think will keep the OBJ from having two-vertex mesh objects treated as full faces (if you have these, you probably did something wrong).
Texture Format Doesn’t (Seem To) Matter
One thing I tried was converting all the PNGs exported by XWA OPT Editor to BMPs before loading them into Blender, but this didn’t ultimately make a difference when then re-importing the exported OBJ back to XWA OPT Editor; they still came in as 32-bit images and had to be internally converted to 8-bit. Irritating limitation of the tool, I guess. One issue I’ve variously encountered is garbage material/palette names that I thought might be connected to this in some way. The solution here, though, seemed to simply be saving the imported OPT as soon as it was imported from the OBJ, then running the 32 -> 8-bit conversion. That resulted in non-garbage palette names. Of course, this may also be related to the previous note about naming and have nothing to do with the conversion order of operations.
Look, Up, Right Vectors
I’m not actually sure about any of this yet, because I haven’t tested it, but I wrote the following script to compute my best-guess for the OPT convention for what “Look”, “Up,” and “Right” vectors should be, based on an input selection of vertices and the average of their normals. The idea here is to use it to define rotational axes and such for rotary gun turrets and other moving model parts. For most parts, this isn’t necessary.
import bpy from mathutils import Vector selVerts = [i.index for i in bpy.context.active_object.data.vertices if i.select == True] retNormal = Vector((0,0,0)) # The resulting vector we'll calculate from the selection for i in selVerts: vertNormal = bpy.context.object.data.vertices[i].normal retNormal += vertNormal # Add to the calculated normal retNormal = retNormal / len(selVerts) # Average the summed normals by the number of vertices involved retNormal = retNormal * bpy.context.active_object.matrix_world * 32767 # Scale to the OPT convention and multiply by the world matrix to get global normals instead of local # ALL OF THIS IS SPECULATIVE! # The idea is to take the computed average normal from Blender's coordsys and convert it to the OPT coordsys displayed in XWA Opt Editor lookVector = Vector((retNormal.y, retNormal.z, retNormal.x)) upVector = Vector((retNormal.z, retNormal.x*-1, retNormal.y)) rightVector = Vector((retNormal.x, retNormal.y*-1, retNormal.z*-1)) print("Look: %s\nUp: %s\nRight: %s\n------" % (lookVector, upVector, rightVector))
Getting a Coordinate for a Hardpoint
Rather than manually copying every vertex I wanted to use as a hardpoint, I wrote this script.
import bpy, os objLoc = bpy.context.active_object.location objWorldMatrix = bpy.context.active_object.matrix_world objVerts = bpy.context.active_object.data.vertices selVerts = [i.index for i in verts if i.select == True] for i in selVerts: # Need to do the following vector/matrix math to get the value # actually reported as a Global coordinate by Blender for a # selected vertex # # (Local vertex coordinate + (object location * object world matrix)) * inverse object world matrix vertLocalPos = objVerts[i].co vertGlobalPos = (vertLocalPos + (objLoc * objWorldMatrix)) * objWorldMatrix.inverted() # Flip the y value to match OPT coordinate space vertGlobalPos.y = vertGlobalPos.y * -1 # Dump the string to the clipboard optStr = "%s; %s; %s" % (vertPos.x, vertPos.y, vertPos.z) print(optStr) # Informational os.system("echo %s | clip" % optStr)