Jul 252015

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
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[0] == '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

Export Settings

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

# 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)