Cyli N'Der and Other Meshes on a Stick

April 4th, 2020 by Diana Coman

The work on Eulora's client during those past 10 days1 went mostly as planned, towards setting meshes on sticks ("bones" if you absolutely must) and therefore on cal3d sprites rather than just plain CS meshes. The main unplanned part included sorting out some additional wtf related to the way in which textures are set for Cal3d meshes2 and otherwise the more pleasant fact that I finally got to a point where I had enough of an idea of the whole pipeline so I could consolidate the scripts and the client into what is now a much quicker way of going from "try this" to "here's how it looks in Eulora": basically I can make changes to the surface generated directly and then run one single script that recompiles the code, runs the polygoniser, then the formatting for cs and cal3d, then the setting of meshes on a skeleton and then finally moves them to where the client expects them, launches the client and let's you gaze at the horror -or beauty, whatever- shown. And to illustrate this, here's Cyli N'Der, the rather cute robot-like set of same-mesh-on-various-bones that photo-bombed Cally one day:
mesh2bone_9_1024.png

mesh2bone_10_640.png

While there are quite a few new bits and pieces done over those past 10 days, from a practical point of view it makes most sense to look first at the whole resulting pipeline instead of what happens to be new and where. So here's what pipeline I have currently:

  1. Surface definition and polygonization (C).
  2. Formatting to CS and Cal3d mesh formats (.xml and .xmf, respectively; awk).
  3. Setting of a mesh to a given bone, with some simplifying assumptions (awk).
  4. A basic "skeleton" creation (and mesh setting on each bone) based on given width, depth and height of the bounding box for the creature (awk).
  5. Loading and showing of a Cal3d sprite from Eulora's client bypassing all the PS soup and based simply on *number of parts* and naming convention (aka sanely in a for loop instead of idiotically listing each, ffs) (C++).
  6. A bash script calling in turn *all* the above, including running the client so that it's enough to change the surface definition, decide on desired parameters for the polygonizer and then just run this script to have everything done and actually see the resulting sprite directly in Eulora3.

Surface definition and polygonization

This is based on the original polygonizer to which I added however quite a few bits to try out and experiment with different surfaces and ways to define them. Essentially it turns out that the results are at any rate best for CS/Cal3d when the surfaces are indeed closed - while the polygonizer is perfectly fine with any open surface just as well, the resulting mesh & sprite trips CS every which way and it's really quite annoying because it vanishes unexpectedly (bits and parts become invisible at various angles) and it generally ends up even difficult to have a proper look around.

Having read some more on the topic of implicit surfaces and modeling with them, I experimented with a few types (e.g. revolution surfaces and "charges"-based ones) but the one that seems most promising currently -as a base surface so to be messed with, not used straight as such- might in fact be the cylinder with rounded ends. There are ways to make branching cylinders (the name ends up misleading since the result really is supposedly looking like a trunk with branches growing out of it rather than a "cylinder" at all), to blend them reasonably well at joints and moreover, to make them even into generalised cylinders - meaning "cylinders" defined around an arbitrary surface rather than just around a segment of a line. The Cyli sprite shown above is the result of play a bit around with this rounded cylinder shap - all its parts are in fact the same mesh, only set on the various bones and the mesh itself is a cylinder but with the half-sphere at one end with a bigger radius than the cylinder itself and set the "wrong" way. For the "correct" plain cylinders (around line segments since those are the "bones") and their mixing and setting about on various "bones", here are some screenshots - the first 7 are of single-end bounded cylinders and some experimenting with slight bulge in that area, while the rest are of fully bounded cylinders with matching half-spheres on both ends:
mesh2bone_2_1024.png
mesh2bone_3_1024.png
mesh2bone_7_1024.png
mesh2bone_8_1024.png
mesh2bone_15_1024.png
mesh2bone_20_1024.png
mesh2bone_21_1024.png
mesh2bone_18_1024.png
mesh2bone_23_1024.png

Formatting to CS and Cal3d mesh formats (.xml and .xmf, respectively)

This takes the output of the polygonizer (which is plain text), assumes a fixed size for whatever texture may be used, goes through all the input data collecting everything needed, does the mapping of vertices to corresponding points in the texture and then spits out 2 files (one for CS non-moving mesh and one for Cal3d mesh that is part of a Cal3d sprite) that represent the same mesh and with the same texture mapping. At this point the mesh is still as obtained from the polygonizer so without any scaling/translation/rotation applied. Some additional screenshots for this:
mesh2bone_11_1024.png
mesh2bone_12_1024.png
mesh2bone_14_1024.png

Setting of a mesh to a given bone, with some simplifying assumptions.

The trouble addressed by this part is that Cal3d/CS expect the various meshes that make up a character to be set at the position (including rotated as needed) in which they are meant to "fit" as part of a whole. Basically Cal3d/CS do *not* provide any way to simply take a mesh and fit it to a bone/skeleton but expect instead to receive - yet again, as everywhere else - the mesh already set aka moved and rotated and scaled or whatever else exactly as it is to be shown on the screen as part of the character. So I wrote a mesh2bone.awk script4 that takes as parameters the start+end of a bone and then scales+translates+rotates a mesh to match that bone. It's still relatively crude but it does the job perfectly fine.

This script takes as input the Cal3d mesh file produced by the previous script and the two endings of a "bone". It makes the assumption that the mesh is set in the origin and oriented "up" on the y axis - while this is a simplification, there's no loss to it as far as I can see: for one thing my surface & polygonizer can generate the mesh exactly like that without any trouble and for the other thing, once this part is set, the mesh can *then* be moved wherever and however needed.

With the above assumption, the script simply calculates the length of the given bone and uses an assumed length of the mesh5 to figure out the required scaling factor. It uses the start position of the bone to calculate the required translation (3D). It uses then the bone as a vector to calculate the rotation (3D) required to bring the mesh's own assumed axis (aka 0,1,0 since it's the y axis, positive direction) to that of the bone (the axis of rotation is given by the cross product of the bone & Y axis while the angle is obtained from the dot product). Scaling, translation and rotation are applied then to vertices and normals as read from the input file and the result is written as another Cal3d mesh file that can be read directly from Eulora's client. To illustrate, here's a screenshot of setting some sort of egg-shapes on a skeleton:
mesh2bone_13_1024.png

A basic "skeleton" creation

This is currently very basic indeed and will need to get expanded later on. It takes width, depth and height as inputs6 and then it calculates the starts/ends of a 5 bones skeleton with 2 upper limbs, 2 lower limbs and a connecting vertical "spine", just about as simple as it can possibly get. It then calls the mesh2bones script to set existing meshes on each of those "bones" but so far it does not write out the skeleton file itself (mainly because there's no need for it yet - it will be required only at animation time and there's still some wtf to be sorted about what calculations are needed to provide the data the exact way that cs/cal3d expect it).

A bash script calling in turn *all* the above

The title says it all really but the great thing is that now I have this in place, I can finally start properly experimenting and trying stuff out so we get to see what and how works or not. For starters, I added to the surface/polygonizer the simple Perlin noise (the 2012 updated version though as it fixes some issues) and two variants (exponential and polynomial) of the fbm7 to see what they can do to a poor cylinder surface. Using the fbm to change -fractally, essentially- the radius of the cylinder certainly has a visible effect indeed although it seems that it's in all cases almost too fine, basically a sort of effect that gets perceived more as texture than as shape changing. Anyways, here are some screenshots, to get an idea of what is going on with this particular approach (the spiky cubes are obtained if the "deformation" is way too big so that the result ends up more of a visualisation of perlin noise fractally composed than of anything else):

mesh2bone_16_1024.png
mesh2bone_17_1024.png
mesh2bone_19_1024.png
mesh2bone_22_1024.png
mesh2bone_24_1024.png
mesh2bone_25_1024.png
mesh2bone_26_1024.png
mesh2bone_27_1024.png
mesh2bone_28_1024.png
mesh2bone_30_1024.png

The most important next part seems to me currently the further exploration of shapes (branching cylinders and generalised cylinders are currently on the list to try out at least) and of better ways to apply fractal deformation/surface definition to get both textures and working character variations.


  1. There is still quite a lot of reading required and so the write-up interval doesn't quite stabilize that easily - I could make it every week and then end up with one week reporting mainly reading and very little concrete to show for it as I'm still digesting what went in; or I could go for 2 weeks and then at times I have a ton done and pending write-up and slowing me down. So ugh, so far I kept to the "report as you need", hence the rather unpredictable intervals, sorry. If it does create a problem, let me know though and I'll stick to a fixed interval one way or another. At the moment I'm going for at most 2 weeks but reporting it earlier if it accumulated and it requires the write-up. 

  2. Since it turns out that CS relies on Cal3d's own format for the texture mapping although it does *not* use Cal3d's format for materials. So if previously I had figured out how to write a CS mesh file including texture coordinates, now I got to further figure out also how to write the same texture coordinates in a way that the Cal3d code understands, from its own mesh format. The document describing the format is useful to some extent but not fully since it's more an indication as to what you may find in a file of that type rather than the exact precise way to specify it, sigh. Anyways, reverse engineering for the win - basically I converted back to xml one of the existing mesh files and figure it out from there, what more can I say. It works. 

  3. It's *such* a relief to even have this working as smoothly as it does. 

  4. Because fuck the c/cpp that is 10 times more cumbersome even for this while delivering only the great benefit that I also need to re-run gcc every time I make a change, such benefits that I can't begin to wish for more of them, right? 

  5. This can be in principle calculated from the input data but so far I didn't implement that - it would require again reading all data upfront just to calculate this; it probably will get done but it wasn't absolutely needed just yet. 

  6. The idea being that the ratio height/width should even be enough to tell if the skeleton is meant to be biped or quadruped really. 

  7. Fractal Brownian Motion