NAME

Model3D::WavefrontObject - Perl extension for reading, manipulating and writing polygonal Alias Wavefront 3D models

SYNOPSIS

use Model3D::WavefrontObject;
my $model = Model3D::WavefrontObject->new;
$model->ReadObj('blMilWom_v3.obj');
$model->Rotate(x => 45, y => 15);
$model->Scale(135%);
$model->WriteObj('V3_modified.obj');

DESCRIPTION

Model3D::WavefrontObject allows a polygonal Alias Wavefront Object file to be loaded into memory, manipulated, analysed, and re-output.

At this time the model only supports the polygon functions of the Wavefront Object format, not bezier splines, procedural surfaces, and so on. It is currently coded only far enough to support the sorts of Wavefront Object meshes that are also supported by the Poser 3D animation program, and of the sort exported by 3DSMax by using the HABWare exporter.

It supports groups and materials, but not multi-grouped polygons (only the first group is recognised if a polygon is declared to be in multiple groups).

Polygons with greater numbers of vertices are supported, even though these are not supported by the Poser software.

The models will also recognise (and support) the Region extension to the format as defined by Steve Cox's UVMapper and UVMapper Pro program. As a result, it may well be the only piece of code that writes Wavefront Objects without leaving these out.

METHODS

Constructor

new()

The new() method returns a new Model3D::WavefrontObject object.

You may additionally supply other parametres to the constructor, including, if you so choose, all the data needed to construct an object (following the format of the object as shown in PROPERTIES, below).

The most common parametre to send into the constructor would probably be the objfile parametre. This can be set either to a file path or name, or to a reference to a filehandle. If this is done, the ReadObj() method will be called automatically by the internal _init() method when the object is created and before it's returned. Uhm, that is to say, you don't need to call it yourself.

It all depends on how specific you want to be.

Public IO Methods

ReadObj(filehandle or filename)

Use the ReadObj() method to read a Wavefront Object file into memory. Unless you are building one from scratch, you'll most likely want to do this.

You can provide a filename (either absolute or relative path, which means that both 'C:\Program Files\Curious Labs\Poser 4\Runtime\Geometries\XFXPeople\grpAeonTeenF.obj' and '../whatever/someObj.obj' will work.

If you precede this method call by setting the object's '_no_calc_vpos' it will be a little bit faster. Not much, but a bit. It will also take less memory. Not terribly much, but a bit.

If you DO let it calculate vpos, it will add a sub-hashref keyed by numbers 1 to 10, each of which will contain a hashref keyed by "vpos" -- a string that identifies a vertex by its XYZ coordinates. The value is a reference to this vertex.

In turn, inside each vertex in the {v} array, and likewise each vertex in {verts}->{v} inside each {f} (polygon) struct, there will be a hash of vpos strings.a( The number is the precision.)

What good is this? Welll, it's great for calculating stuff between two separate OBJ files when they are otherwise identical, but they've gotten the vrtices reordered. The reason for the multiple precisions is that they cover for possible rounding error differences and formatting/precision changes based on what software saved them out.

This means that you can map between vertex orders. This is actually kind of a big deal.

To do this, try something like: for my $tv (0..#${$messedup->{v}}) { for my $prec (reverse 1..10) { $vpos = $messedup->{v}->[$tv]->{vpos}->{$prec}; if ( $fixed->{vpos}->{$prec}->{$vpos} and $fixed->{vpos}->{$prec}->{$vpos}->[0] and $fixed->{vpos}->{$prec}->{$vpos}->[0]->{id}) { $tv->{other_index} = $fixed->{vpos}->{$prec}->{$vpos}->{pid}; last; } } }

If you know what you're doing with these models, I'm sure this can be use use to you. One example would be to take a Poser morph. Assign a delta property to each vertex, then find the matching vertex in the other object and adjust its X, y and z appropriately to apply the morph, or, alternatively, assign the delta to it and then loop through and save them out after.

Another great example is if you've generated a morph or reshape, but you you discover that your annoying software decided to split the groups. You can solve this because two unwelded vertices occupying the same position will have the same vpos.

It should be noted that Poser-relative paths will NOT work at this time. I.e., the string ':Runtime:Geometries:XFXCritters:grpChickenThing.obj' will not be parsed. This may change in a future release, but right now it's a bit tricky to tell whether the path is a relative one, or a path on an older Macintosh, as both have the same directory seperators (for reasons obvious to those familiar with the history of Poser).

Returns 1 on success, undef on failure. Check {errstr} for why.

WriteObj(outfile)

Writes the Wavefront Object to outfile. If no outfile is provided, writes to STDOUT.

WriteObj() can accept either a filename or a filehandle reference, as ReadObj(), above. The difference is that WriteObj() doesn't require the file in question already exist.

Another new addition is the gid and gpid property in each vertex. These are the index of the vertex *within the group* and therefore skip intervening vertices which are NOT in the same group. This is useful for interacting with Poser morph targets, or even to extract a specific group, manipulate it by itself, and then read in two OBJ files with separate instances of this module and apply the deformation of one to the other (see the bit about vpos above, too, as this may be relevant to you).

Returns 1 on success, undef on failure. Check {errstr} for why.

ReadMtlLib(mtllib)

The ReadMtlLib() method reads a material library into memory and associates the material lib data with the materials defined in the object mesh. If, while reading the Wavefront Obj, a mtllib directive is encountered, this method is automatically called (though in experience, it will rarely actually find the lib, as people almost never provide it with).

Public Manipulation Methods

Translate(translations)

The Translate() method moves the object to the left, right, up, down, and/or forwards and back. Specify the translations you want to perform when you call the method in hash format. For instance: $model->Translate(x => .1) # Translates 0.1 units to the object's left. $model->Translate(x => -0.3) # Translates -0.3 units to the object's right. $model->Translate(y => 2) # Translates the model 2 units upward. $model->Translate(z => -5) # Translates the model 5 units back.

Rotate(rotations, optional centre)

The Rotate() method rotates the object. You specify the rotations per axis in hash format, i.e. {x => val, y => val, z => val}.

You may also specify an optional centre to perform the rotations around, by setting a centre => property when you call the method. This centre may be specified with a hashref ({x => val, y => val, z => val}), an arrayref ([x, y, z]), or a string in two formats: either x,y,z or x:val,y:val,z:val. A scalar reference to such a string is also acceptable.

Additionally, the strings 'natural' and 'apparent' can be provided. If the centre is specified as 'apparent', the centre will be positioned in the centre of the bounding box that would surround the object. If 'natural' is specified, the centre will be positioned at the absolute average co-ordinate of the object. I.e., the average Y position of all of the vertices, and so on. This will cause objects that have, for instance, a heavy concentration of smaller, higher-resolution polygons in one location to have a centre closer to that concentration.

Note that if the arrayref method or the x,y,z string method is used, the order cannot be specified; it will always be in X,Y,Z order. However, as the rotation actually modifies the object and is not local to the object's axis, the rotations are not subject to gimbal lock. The order of rotation will, however affect the result, moreso the further the centre is from the actual object.

As a concession to the rebel colonies, the centre may also be supplied with the popular but improper spelling of 'center'.

Scale(scale/scales, optional centre)

The Scale() method scales the object. You can specify the scales in hash format, as per Rotate() and Translate() above, or as a single value which will be applied to all three axes. You may also supply a centre, as in Rotate(), above.

Since it is conceivable that you don't want to specify all three scales, but you do want to specify a centre, and since the one-argument method above does not provide for the option of a centre to be specified (since it needs to be just one argument), you may also provide an argument of scale, which will suffice as the scale to apply to all three. As a side effect of this approach, you can specify a single axis for one scaling factor, as per the normal hash format above (x => 110%), and then use the centre argument to specify both the other two at once.

You may specify the scaling factor as an absolute factor of 1 (i.e., .9, 1.7, and so on), as a percentage factor (90%, 170%), or as a relative amount (-.1, +.7). You may also combine relative and percentage approaches (-10%, +70%).

Any axis not specified defaults to a factor of '1', not 0, which means that unspecified axes do not flatten along that axis, as this would be unbearably annoying.

Mirror(x||y||z)

The Mirror() method causes an object to be mirrored along the axis specified. For instance, if you Mirror along the X axis, translate forward along the z axis, and rotate 180 degrees on the y axis, you will get a version of the figure that would be looking out of a mirror at the original figure.

Mirror() does not mirror UVW coordinates. To do that, call FlipUVs (see below). Note that this is important to know, as depending on the method used to interpret standard bumpmaps, and always in the case of Poser 4 'greenscale' bumpmaps, if an object is mirrored but the UVs and images used as maps are not inverted, the effect of the bumpmap will be inverted (causing white to indicate low areas and black to indicate high areas).

It should be additionally noted that mirroring the contents of a P4 BUM bumpmap will NOT work. You need to invert the greyscale bumpmap, then convert *that* to a greenscale bumpmap.

If no axis is specified, X is used.

FlipUVs('u' or 'v')

This method flips the UVs of the model. If 'v' is specified, the UVs are flipped vertically. If 'u' is specified, or if no axis is specified, the UVs are flipped horizontally.

ReverseWinding

The ReverseWinding() method reverses the winding order of the polygons' vertices. In effect (and in Poser), this reverses the normals of those polygons, effectively flipping a model inside-out or outside-in. The Mirror() method automatically calls this method after mirroring, to preserve the original appearance of the surfaces.

Note that Poser 4 and ProPack, and the Poser renderer in Poser 5 and 6 do not pay any attention to the normals and treat all polygons as 2-sided. It does, however, affect the preview mode, as well as affecting P5/6 Firefly renders by default.

Object files exported from ZBrush will usually have inverted normals because ZBrush inverts the Z coordinate internally (if you've used ZBrush and imported an Object, you've most likely noticed that you had to turn it around after loading it into your canvas). This method will fix that handily.

Utility Methods

GetVertex(id)

This method returns a hashref containing the x, y, and z coordinates of the specified vertex, as well as the id and 'pid' (Poser ID) of it.

The vertex specified is the 0-indexed vertex. It should be noted that this is not the id as given in any given polygon (f) specification in an Alias Wavefront Object file, as the file format uses an index od 1 rather than 0.

The vertex retrived will have an id property set to the 1-indexed value, as well as a pid property set to the 0-indexed value. 'pid' stands for Poser ID, as Poser refers to vertices by their 0-indexed elements (i.e., in a TargetGeom delta directive in a Poser file).

If you want to select by the ID that the file format itself uses, simply subtract one from the value you want to specify: my $vert = $model->GetVertex($vertid - 1);

FindEdges()

Builds an additional property hash, "{edges}" which includes all the edges. They are keyed by the vertex IDs (the 1-indexed ones, not the 0-indexed pid (which can stand for Perl ID or Poser ID, as you prefer, but this is the other one, because that's how the file format works).

Each edge will contain what polygons ($obj->{f}) (by pid) it's part of via $obj->{edge}->{$edgeid}->{f} = [...]. They will also contain an id, which can be used to order then in the order they were found (0-indexed), and two possible analysis properties: {border}, if 1, means it's a border. {bad}, if true, means it's a "bad" edge, meaning more than two polygons connect to it. This is generally frowned upon in an OBJ and many programs will flake out when trying to render such an edge.

calcVertexFacets()

Adds an array of facets in which vertices take part to each vertex. These are by reference, so you can walk through a vertex to the properties of the facets it is a part of.

calcSurfaceNormals()

Supposed to calculate the surface normals for each polygon. May work. I'm not sure if it's right.

calcVertexNormals()

Supposed to calculate the vertex normals for each polygon. DOES NOT WORK RIGHT. I am as of yet unsure why not. But the ones Blender makes are right and these are all sorts of messed up. Feel free to fix and patch and send it to me if you know what's wrong. Otherwise I'll figure it out eventually.

calcPolyAreas()

Calculates the areas of each polygon using Heron's equation.

getTriArea({x, y, z}, {x, y, z}, {x, y, z})

Calculates the area of any triangle (doesn't need to be one in the object). Used internally. but exposed because it's useful.

calcVertexAreas()

This name is kind of misleading, because every vertex, being a one-dimensional thing, has 0 area. However, this totals up the areas of the polygons in which the vertex takes part. Can be handy for modifying weights of morph deltas between different shapes of the same mesh, etc.

getDistance({x, y, z}, {x, y, z})

Returns the distance between the two given points. Works with vertices in {v} or whoever you get to them, or you can pass it arbitrary objects with x, y and z properties and it will do those, too.

areaFromLengths(length, length, length)

Just does Heron's formula. Gives the area of a triangle with these lengths.

GetVertexSpherical(id)

This method works just like GetVertex(), above, except that it returns the spherical co-ordinates of the vertex requested, in the order: Radial (rho), Azimuthal (theta), Polar (phi).

GetNaturalCentre

This method returns the natural centre of the model, in the form of a hashref containing the keys x,y and z with those values applied. If the model does not have any vertices when the method is called, the values are all 0.

The natural centre is the average x, y and z coordinate of all vertices, as explained in Rotate(), above.

This method is not spelled in American.

GetApparentCentre

This method returns the apparent centre of the model, in the form of a hashref as with GetNaturalCentre, above. The difference, as explained under the Rotate() method, is that the apparent centre is the midway point between the top, bottom, left, right, front anc back vertices.

MinMax

This method, primarily used internally but made to be accessible publically as well, returns two variables. The first is a hashref containing the minimum x, y and z co-ordinates, and the second is a hashref containing the maximum x, y and z co-ordinates of the model.

These values can be used to construct a bounding box, of course.

Because it's only meant to be called in a list context, if called in a scalar context the method will always return 2. Don't use it like that.

Top

This method returns the highest Y value in the model.

Bottom

This method returns the lowest Y value in the model.

Left

This method returns the highest (leftmost) X value in the model.

This method returns the lowest (rightmost) X value in the model.

Front

This method returns the highest (foremost) Z value in the model.

Back

This method returns the highest (rearmost) Z value in the model.

RenameGroup(group, new name)

Renames a group to the new name you just gave it.

RenameMaterial(material, new name)

Renames a material to the new name you just gave it.

Private Methods

There are (currently) three private methods, _getScaleVal, _getTransCentre, and _init. These are only for internal convenience parsing and are very unlikely to do an API user any good, so leave them alone. If you must know, read the source. It hasn't been bleached or eyedropped or anything.

AUTHOR

Sean 'Dodger' Cannon qbqtre@ksk3q.arg =~ tr/a-mn-z/n-za-m/ http://www.xfx3d.net

BUGS

  • Does not handle beziers/splines

  • Does not handle procedural geometry

  • Normal generation doesn't work right. Don't use it. I'll try to work out what Blender is doing differently. For now, generate normals in Blender, because the method in this is broken. Surface normals may be right. Since surface normals aren't stored in an OBJ, I can't tell for sure. Vertex normals are quite hosed though I do not know why.

  • Does not yet write out MTLLibs

GOOD INTENTIONS

  • A Clone() method that duplicates the object without the innards being references to the first innards (allowing seperate manipulation of each)

  • Figure out how to handle beziers/splines and poly-fy them

  • Figure out how to handle procedural geometry and poly-fy it

  • Save out mtllibs (material libraries)

  • Apply morphs by group, material, or region

  • Extract geometry by group

  • Extract geometry by material

  • Extract geometry by region

  • Insert/append geometry

  • Increase mesh resolution (all or by group, material, region)

  • Project UVWs from one model onto another (I have an idea...)

  • Decrease mesh resolution. Dream on.

  • Apply boolean functions to geometry. Hahahaha! Yeah right! *splutter*

  • Interface with other 3D model modules. So far I think VMRL is all there is

  • Build in several utility scripts I have (as file processors) as methods

  • The cat has finally been fed. Now she wants greenies.

SEE ALSO

perl(1)

Model3D::Poser (in progress)