Flipped objects: Reversing normals and winding order

For a side-project that I’m currently working on, I have some simple quad-based objects that I want to be able to flip across their vertical axes. To that end, I’m setting their x-axis scales to -1 when I want them flipped. (Simply rotating them won’t work, I believe, as they’re not symmetrical.)

In and of itself, this works: the objects are flipped as intended.

However, this does mess up the lighting of the objects in question.

Now, as I understand it there are two components to this: (1) The objects’ now have a back-to-front winding order, leaving me seeing back-faces instead of front-faces, and (2) The objects’ normals are now the wrong way around.

Thus far the only element to a solution that I’ve found is to find the relevant GeomNodes, dig down to their primitives, and call “reverse” on those.

The following is my current approach to doing so:

            geomNP = self.currentPoseCard.find("**/+GeomNode")
            if geomNP is not None and not geomNP.isEmpty():
                for geom in geomNP.node().modifyGeoms():
                    for prim in geom.getPrimitives():
                        prim.reverse()

From what I see in the API entry for the “reverse” method, I think that I might further have to re-add the primitive to the Geom after the call. However, that aside, the API entry also indicates that it this method alone is not necessarily enough to fix lighting issues.

So, how might I fix this? Is there a way to flip the normals? Am I mistaken in what’s called for here? Is there perhaps a less-awkward solution? Or is there a less-troublesome method of flipping my objects?

(Oddly, in another program that same objects seem to be lit as desired–perhaps because the lights are applied after the objects are flipped?)

No need; you don’t even have to access the primitives. All you have to do is call geom.reverse_in_place().

Yes, you can simply call geom.get_vertex_data().reverse_normals() :slight_smile: .

So your code becomes:

            geomNP = self.currentPoseCard.find("**/+GeomNode")
            if geomNP is not None and not geomNP.isEmpty():
                for geom in geomNP.node().modifyGeoms():
                    geom.reverse_in_place()
                    vertex_data = geom.get_vertex_data().reverse_normals()
                    geom.set_vertex_data(vertex_data)

Hope this helps!

Probably because they (or an ancestor node) are being rendered two-sided there (check with model.ls() to see if there is a CullFaceAttrib applied to them; if so, it’s likely in M_cull_none mode; calling get_two_sided() could help too).

Ah, it does indeed–thank you very much! :smiley:

It’s possible: while the geometry is pretty much the same in both cases, there are a few differences (such as a “flatten” operation being performed in the case in which the lighting messes up).

But if rendering two-sided causes the lighting to work, then why did it not work when I called “setTwoSided” on the flipped model? o_0 Perhaps because I used the “setTwoSided” method, rather than directly applying a CullFaceAttrib?

Anyway, if what you gave above proves to work, then that’s good enough for me, I think!

Thank you again for your help! :slight_smile:

Actually, setTwoSided (or applying a CullFaceAttrib) doesn’t fix lighting normals; the back-faces would be rendered, but they would appear black due to the normals pointing the wrong way. Looks like I was thinking of Geom.doubleside which, if I’m not much mistaken, creates additional geometry, but I doubt that flattening or importing models could do this implicitly, hmmm…

Aah, fair enough! I thought that that was the case.

It is odd. Especially as I now recall that the flipped objects are, I think, not amongst those that are flattened.

Update: Hmm… The above doesn’t work for me, I’m afraid–at least, not when I scale only the x-dimension of the object by -1 (as I don’t want to change the y-dimension). If I scale both x and y, it does work–but flipping the y-dimension changes the model in ways that I don’t want.

Dropping the scaling by -1 and instead manually modifying the vertices and normals via a GeomVertexRewriter seems to work as intended–but it incurs handling of origin-offsets. However, unless a simpler solution presents itself (or is suggested), I’ll likely go with this.

Also, I stand corrected on the matter of the lighting working in the other application–it looks like I was mistaken on that matter! Sorry about that. ^^;

Ah, indeed! It turns out that if you scale by a negative amount in only one direction, you only need to reverse the vertex winding order and not flip the normals.
Can you confirm that it works if you leave out the lines of code dealing with the vertex_data?

FWIW, instead of using a GeomVertexRewriter, you could also use a scaling matrix to transform all vertices of the model (unless I’m mistaken, this also scales the normals):

mat = Mat4.scale_mat(-1., 1., 1.)
vertex_data.transform_vertices(mat)

To compensate for origin-offsets, you can combine the scaling matrix with two translation matrices:

x, y, z = origin_offset
# a matrix to describe the inverse origin-offset
inv_offset_mat = Mat4.translate_mat(-x, -y, -z)
# the matrix describing the scale
scale_mat = Mat4.scale_mat(-1., 1., 1.)
# a matrix describing the origin-offset
offset_mat = Mat4.translate_mat(x, y, z)
# the final matrix to transform the vertices with
mat = inv_offset_mat * scale_mat * offset_mat

The resulting matrix first moves the vertices to eliminate the offset, then scales the vertices about the desired origin position and finally moves their centre back to that offset.

None of this matrix manipulation is likely needed in this case, but it may come in handy in the future.

Ah, you’re quite right! I could have sworn that I tried that, having had a similar thought, and that it didn’t work–but work it does!

(It’s possible that I tried it before I realised that my model in fact has multiple GeomNodes, and that I should thus be getting them via “findAllMatches” and iterating over them, rather than just using “find” and operating on that first result.)

The final result then looks like this:

            geomNPs = self.currentPoseCard.findAllMatches("**/+GeomNode")
            for geomNP in geomNPs:
                if geomNP is not None and not geomNP.isEmpty():
                    for geom in geomNP.node().modifyGeoms():
                        geom.reverseInPlace()

I’m pretty sure that I recall trying that, and that it didn’t work. :/

But again, maybe that was before I started iterating over all GeomNodes? I don’t recall, offhand.

Thank you for that. :slight_smile:

Of course, with the reversal in place working now, it’s superfluous–but I do appreciate the help! :slight_smile: