Magpie Hoard

Applying scale factors

I've spent a decent chunk of time first playing Payday 2 and then working on a modding tool for it, to convert between GLTF and the game's native mesh format. However, partly because it was easier and partly because just didn't remember that line of the spec, I neglected to scale models by a factor of 100, PD2 being done to a 1cm scale, and GLTF mandating a 1m scale.

Unfortunately, Blender also has a slight preference for a 1m scale, so everything has to have a scale factor applied manually at some point, and/or you have to put up with objects being uncomfortably big on the Blender side of things, especially if you happen to be making building-scale objects. Solution: modify the converter to apply this scale factor manually.

Further problem: The math isn't nice and tidy.

For rotations and translations it is: adjacent ones of the same type can collapse together, and for every rotation followed by translation, it's possible to create a translation followed by rotation that has the same effect. Thus we can push the rotation impelled by Diesel (the PD2 engine) and Blender having different notions of which axis is up (Y and Z respectively) to wherever we like fairly easily.

For translate-rotate-scale, however, it's not so pleasant: translate-rotate forms a group, but adding scale to the mix means it's no longer a group (you can make a shear out of a combination of rotates and non-uniform scales).

Actually, what I want to do here is push a uniform scale down the tree of objects until it ends up butting against the actual vertex buffers. With that in mind the obvious approach is creating rules to accomplish this motion in steps. Probably uniform scale and non-uniform scale behave a bit differently, so let's say there are four kinds of operation we want to consider: translate, rotate, scale, and stretch.

translate(vector3)
rotate(quaternion)
scale(number) // uniform scale
stretch(vector3) // non-uniform scale

All the operations commute with themselves:

translate(A)translate(B)=translate(B)translate(A)rotate(A)rotate(B)=rotate(B)rotate(A)scale(A)scale(B)=scale(B)scale(A)stretch(A)stretch(B)=stretch(B)stretch(A){`\\begin{array}{ccc} \\operatorname{translate}(A) \\cdot \\operatorname{translate}(B) & = & \\operatorname{translate}(B) \\cdot \\operatorname{translate}(A) \\\\ \\operatorname{rotate}(A) \\cdot \\operatorname{rotate}(B) & = & \\operatorname{rotate}(B) \\cdot \\operatorname{rotate}(A) \\\\ \\operatorname{scale}(A) \\cdot \\operatorname{scale}(B) & = & \\operatorname{scale}(B) \\cdot \\operatorname{scale}(A) \\\\ \\operatorname{stretch}(A) \\cdot \\operatorname{stretch}(B) & = & \\operatorname{stretch}(B) \\cdot \\operatorname{stretch}(A) \\end{array}`}

As does uniform scaling with stretching and rotation

scale(A)stretch(B)=stretch(B)scale(A)scale(A)rotate(B)=rotate(B)scale(A){`\\begin{array}{lcl} \\operatorname{scale}(A) \\cdot \\operatorname{stretch}(B) & = & \\operatorname{stretch}(B) \\cdot \\operatorname{scale}(A) \\\\ \\operatorname{scale}(A) \\cdot \\operatorname{rotate}(B) & = & \\operatorname{rotate}(B) \\cdot \\operatorname{scale}(A) \\end{array}`}

Stretches and rotations can't be moved past each other without introducing shears or additional nodes, which I don't want to do:

stretch(A)rotate(B)=rotate(A)stretch(B)={`\\begin{array}{lcl} \\operatorname{stretch}(A) \\cdot \\operatorname{rotate}(B) & = & \\bot \\\\ \\operatorname{rotate}(A) \\cdot \\operatorname{stretch}(B) & = & \\bot \\end{array}`}

But stretches and scales go nicely with translates (here {`*`} is elementwise multiply):

translate(A)scale(B)=scale(B)translate(A/B)translate(A)stretch(B)=stretch(B)translate(A/B)translate(A)rotate(B)=rotate(B)translate((B)A)stretch(A)translate(B)=translate(BA)stretch(A)scale(A)translate(B)=translate(BA)scale(A)rotate(A)translate(B)=translate(BA)rotate(A){`\\begin{array}{lcl} \\operatorname{translate}(A) \\cdot \\operatorname{scale}(B) & = & \\operatorname{scale}(B) \\cdot \\operatorname{translate}(A/B) \\\\ \\operatorname{translate}(A) \\cdot \\operatorname{stretch}(B) & = & \\operatorname{stretch}(B) \\cdot \\operatorname{translate}(A/B) \\\\ \\operatorname{translate}(A) \\cdot \\operatorname{rotate}(B) & = & \\operatorname{rotate}(B) \\cdot \\operatorname{translate}((-B) \\cdot A) \\\\ \\operatorname{stretch}(A) \\cdot \\operatorname{translate}(B) & = & \\operatorname{translate}(B * A) \\cdot \\operatorname{stretch}(A) \\\\ \\operatorname{scale}(A) \\cdot \\operatorname{translate}(B) & = & \\operatorname{translate}(B * A) \\cdot \\operatorname{scale}(A) \\\\ \\operatorname{rotate}(A) \\cdot \\operatorname{translate}(B) & = & \\operatorname{translate}(B \\cdot A) \\cdot \\operatorname{rotate}(A) \\end{array}`}

Since translations and rotations are themselves groups, it should be obvious that translate-rotate matrices form a group, since the above rules allow arranging for all the rotating to happen first, then all the translating, and being groups lets you collapse that down to one of each.

The lack of closure means translate-rotate-scale isn't even a monoid, let alone a group, but fortunately this isn't a problem here. Most of these rules aren't even needed here since we're only moving a uniform scale down the object tree.

Given

trs(T,R,S)=translate(T)rotate(R)stretch(S){`\\operatorname{trs}(T,R,S) = \\operatorname{translate}(T) \\cdot \\operatorname{rotate}(R) \\cdot \\operatorname{stretch}(S)`}

We can thus do:

scale(U)trs(T,R,S)=scale(U)translate(T)rotate(R)stretch(S)=translate(TU)scale(U)rotate(R)stretch(S)=translate(TU)rotate(R)scale(U)stretch(S)=translate(TU)rotate(R)stretch(S)scale(U)=trs(TU,R,S)scale(U){`\\operatorname{scale}(U) \\cdot \\operatorname{trs}(T,R,S) \\newline = \\operatorname{scale}(U) \\cdot \\operatorname{translate}(T) \\cdot \\operatorname{rotate}(R) \\cdot \\operatorname{stretch}(S) \\newline = \\operatorname{translate}(TU) \\cdot \\operatorname{scale}(U) \\cdot \\operatorname{rotate}(R) \\cdot \\operatorname{stretch}(S) \\newline = \\operatorname{translate}(TU) \\cdot \\operatorname{rotate}(R) \\cdot \\operatorname{scale}(U) \\cdot \\operatorname{stretch}(S) \\newline = \\operatorname{translate}(TU) \\cdot \\operatorname{rotate}(R) \\cdot \\operatorname{stretch}(S) \\cdot \\operatorname{scale}(U) \\newline = \\operatorname{trs}(TU, R, S) \\cdot \\operatorname{scale}(U)`}

which is what we actually need to push scale factors all the way down to the actual vertex buffers. In addition, U{`U`} is not affected, so the only thing to worry about in regard to instancing is not to apply it twice (the current implementation makes instances unique, admittedly).