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)


 Posted by at 13:42  2 Responses »
Sep 242010


That ugly word is actually a very useful tool for reconstructing geometric information from 2D images. Using a collection of similar photographs of a given subject, you can use matrix math to recompute the 3D structure of that object from the 2D images. Not by hand, mind you. That would take way more brainpower and patience than pretty much anyone has any desire to lend to the task. Computers, however, make great photogrammetric calculators.

Why is this relevant to anything? Well, it’s pretty important when you want to accurately recreate something in the world in a 3D modeling environment and you don’t have access to A) the thing you want to create and B) a 3D scanner. Specifically, I’m talking about modeling spaceships. Most 3D hobbyists just wing it, eying the proportions and getting pretty close. But let’s be honest: when have I ever been satisfied with getting “pretty close” when I could use math to be exact?

I started out modeling a Star Destroyer last year, trying to take very accurate measurements in Photoshop and extrapolating the “right” values by averaging several of these measurements together. I was putting together what looked like a fairly accurate model. Then I read about photogrammetry. This had two effects: the first was that my progress on the Star Destroyer model ground to a halt; the second was a period of intense research into the fundamental math behind photogrammetry. This included (re-)teaching myself matrix math, learning about projection matrices1, and so on. I googled university lectures, dissertations, and dissected open-source projects to understand how this process was done.

Sadly, none of the open-source projects I found would do quite what I want. It seems that the hot thing in photogrammetry is reconstructing terrain surface detail with as many recreated vertices as the resolution of the source images would allow. I wanted to define just a handful of points each image and have a mesh reconstructed from them. From there, I would do the fine detail work on my own. So, I started writing my own program (in Python) to do it. Losing my job, getting a new job, and getting married all conspired to prevent much progress on this front, though, so it hasn’t progressed very far yet.

Assuming I can get something I’m happy with, though, it will alleviate one of the biggest sticking points I’ve always had when modeling technical things: accurate blueprints. Just about every set of blueprints for every technical thing2 I’ve tried to model has had errors in it. Not little, nitpicky errors, either, but major, mismatched proportions between orthographic views. In one image, a component would be X pixels long but in another image—from the same set of diagrams, mind you—it would be Y pixels long. In some cases, you can just split the difference and get something decent. Most of the time, these compromises compound until you’ve got an irreconcilable problem.

Anyway, this is probably one of those topics that will prompt most people who read this to smile, nod, and pat me on my math nerd head. All the same, it’s interesting to me, so maybe it’ll strike your interest to.

  1. A projection matrix describes the conversion of a 3D coordinate to a 2D coordinate through a camera lens, essentially. []
  2. Okay, okay, spaceship. []

Spam: Defeated

 Posted by at 18:46  No Responses »
Aug 192009

I finally took the bull by the horns and did something about my excessive spam problem today.

I have a two-tiered spam filter.  The first is server-level, with SpamAssassin.  The second is client-level, with Thunderbird (formerly Evolution, which I ditched in favor of Thunderbird after many months — years? — of use for several reasons).  One of the drawbacks to my server-level approach is that when SpamAssassin marks mail as spam, it gets shunted into a spam folder on the server, thus keeping it from flooding my inbox.   This spam folder, left unchecked, grows and grows.  Between my two mail accounts, mcc@ and mcc3d@, I had about 25,000 messages built up from June.

Enter cron and Python.  As I’ve gotten more comfortable with Python, I’ve pushed it to do more and more, particularly at work.  I used some of that knowledge to write a Python script that looks for any message in a spam folder older than 7 days and purge it.  I’ve also written another script that goes through and flat-out deletes messages containing several key regex searches from the spam folder.  With any luck, this should prevent my server-side mail folders from ballooning too much.

On the client side, I was pleased with Thunderbird’s ability to recognize spam, but I hadn’t learned enough about the specifics of the app to get it to do anything about that spam.  I did some quick googling and got that correctly configured, so I think I’ll start seeing a marked improvement on that front.

Jan 262009

At work, I make very frequent use of 3ds Max’s native scripting language, Max Script, to create a number of tools for the art team and to facilitate my own work.  Before coming to Blue Fang, I didn’t know any Max Script at all.  Now, it’s part of my daily routine.  On the flip side, I’ve made occasional use of Maya’s native scripting language, MEL, to accomplish a few things in the past.  I openly admit that I haven’t done nearly as much with MEL as I have with Max Script.

Last night, I ran into a situation in Maya where I cut the position coordinates of every object I had selected by a factor of 0.05 (i.e. 1.0 cm becomes 0.05 cm).  Here’s what it would look like if I needed to write this script in Max Script:
for o in selection do o.pos = o.pos * 0.05

Here’s what it takes to do it in MEL:
string $selection[] = `ls -selection`;
int $i;
for ($i = 0; $i < size($selection); $i++) {
    float $translate[] = getAttr($selection[$i]+".translate");
    $translate[0] = $translate[0] * 0.05;
    $translate[1] = $translate[1] * 0.05;
    $translate[2] = $translate[2] * 0.05;

It’s no wonder Maya started to include Python support! Of course, while I’m very familiar with Python, I’m not at all familiar with the Python-Maya interface, so I stuck with muddling through via MEL. Still, talk about an unintuitive approach.