1
$\begingroup$

The Image Texture node offers a box projection mode with optional blending. But the Normal Map node doesn't have a matching mode to allow functioning box-projected tangent space normal maps out of the box. As a result, most surfaces receive wrong light direction.

I have calculated the output normals manually: by taking the input normal map in world space, splitting it into components, and using them as coefficients for normals, tangents and bitangents. It looks correct for surfaces pointed along the XYZ axes, but on oblique angles it's still incorrect. Note the horizontal red lines on the bevelled edge of the middle object - should be pointing right instead.

nodes

Could someone check my math please. Or maybe there's an easier way to go about doing this?

$\endgroup$
6
  • 2
    $\begingroup$ I'm not sure if I feel like taking a stab at a full answer-- it's a big job-- but there are a few pieces to the puzzle. One is that if you want to use box blend, you need to build your own box mapped (triplanar projection) texture lookups, because you're not getting anything remotely usable on a blended normal map lookup. The other is that I would want to recreate the normal mapping in nodes, which means some base baking incompatibility with Blender (because it uses interpolated vertex normal and tangent, not orthonormal.) $\endgroup$
    – Nathan
    Jul 27, 2022 at 14:57
  • $\begingroup$ So you're saying setting blending above zero produces nonsense vectors and there's no way around this? It would have to be flat-mapped either way? If that's the case, seems like it might be easier to just build six instances of the same shader rotated for each direction and blend between those. That way at least I won't have to deal with manually doing the normals. $\endgroup$
    – chedap
    Jul 27, 2022 at 15:05
  • $\begingroup$ Well, one of the issues is that the blended normals are really two or three different normals with different tangents. I'm not sure if you'd need to do the normals manually after all, it might work to just rotate them in 90 degree chunks, mix normals by blend weight, then renormalize. $\endgroup$
    – Nathan
    Jul 27, 2022 at 15:12
  • $\begingroup$ I was hoping blending tangents would compensate for blended normals, but I guess that was wishful thinking. I'm not sure what you mean by rotating in chunks - by that point, don't we just have a vector defined by blending colors "as colors", i.e. nonsense? $\endgroup$
    – chedap
    Jul 27, 2022 at 15:20
  • $\begingroup$ Well, if we can get an accurate normal for the +Z face, we can rotate that normal 90 degrees in Y to get an accurate normal for the +X face. But in any case, I still think that we'd need to do manual normal mapping for the general solution (not just a beveled cube.) I will probably take a stab at this after all, I guess my curiosity has been aroused, but it will be a while because I've got some other work for right now. $\endgroup$
    – Nathan
    Jul 27, 2022 at 15:44

1 Answer 1

2
$\begingroup$

It's easy! Just do manual box mapping and manual normal map calculation and then mix the normals by blend factor!

enter image description here

That should be perfectly clear, right? Right?

So our basic plan here is to lookup our normal map from generated coordinates, depending on the direction of the particular sample's normal, then create three modified normal vectors from those lookups, and then to combine those with weights, on the basis of how much our base normal agrees with our 3 basis object vectors and on an arbitrary, 0-1 blend factor.

Let's go through our node groups. We'll start with the idea of a triplanar projection, in our "triplanar lookup" node group:

enter image description here

Here, I'm doing three lookups on a texture, using generated coordinates. Each translates a particular pair of our XYZ coordinates into two UV coordinates. We'll manage these three lookups separately, by moving on to manual normal mapping, in our "NormalMapNode" node group:

enter image description here

This node group is sent a world-space normal and an arbitrary, object space tangent. We repair the tangent to be orthogonal to our normal with a pair of cross products and generate a binormal vector in our "repair tangent" node group:

enter image description here

Returning to our NormalMapNode group, each of our lookups is going to be associated with a different object space tangent. We will remap the normal map color from the 0,1 space to the -1,1 space, then we'll scale our normal, tangent, and binormal vectors by the amounts given to us by our normal map and add those vectors back up to generate a normal in an arbitrary, object space tangent.

We now have three normals for every sample. We need to know how much of each to use, so lets look at the normDotObj node group:

enter image description here

We start by checking our object space normals against our three object space basis vectors. To get the same value regardless of which direction the normal is pointing-- eg, to get the same value on +x and -x faces-- we take the absolute value of the dot product.

We'll then use those in our "weightTriplanar" node group to mix with our arbitrary "blend" value. We want this value to emulate Blender's, so we'll use a 0-1 range, although 0 (and other very low numbers) will be buggy, because division by zero. (For more work, we could check for very low numbers and have a different path, but I don't think it's necessary, not compared to just living with very tiny amounts of blending.)

enter image description here

We'll move on to our weightedMix3 node group:

enter image description here

Our weights aren't normalized, so we'll start by normalizing them, so that everything adds up. To mix three, we're basically multiplying each by its normalized weight and adding them up.

It's okay that we're using color nodes to deal with vectors. Blender doesn't know the difference. We can vector math/scale colors, we can mixRGB vectors, it's fine.

At the end, to deal with imprecisions, we do a final normalization operation on our normal and send it into a shader.

There is something to take note of here, which is this is not exactly how Blender computes normals from normal maps. This treats normals maps as existing in an orthonormal tangent space, whereas Blender does not. (I know, that's total Greek, but you don't have to understand if you don't want to.) However, it's better than how Blender computes normals, and it's how almost everybody else computes normals. If your plan is to normal map from popular texture packs, this normal mapping technique is appropriate, but you will notice differences between this and Blender's default.

It is possible to bake orthonormal normal maps in Blender, despite Blender's defaults, but I don't think it's worth getting into that for this question, where you are most likely interested in using normal maps that were baked as orthonormal to begin with.

Addendum based on comments: Well, I said it was a first draft :) You're absolutely right that it doesn't handle the negative faces correctly. That's because the binormal is going the wrong direction for those faces.

Let's fix that by first editing our normDotObj node group to output facing with regards to our basis vecs:

enter image description here

We're checking whether these are positive or negative faces by getting Greater Than 0 before we absolute value the dot product, then we're remapping that to a -1, 1 value: -1 if the values are opposite, 1 if the values are the same direction.

We can then edit our normalMapNode to have an option to flip the binormal from a new Scale input:

enter image description here

We can plug the facing into the scale like so:

enter image description here

After that, our binormals are pointing the right direction on our normal map node.

As for editing three different image texture nodes being cumbersome, I feel that this is the least cumbersome issue here. This is a complicated structure; we're not really supposed to use normal maps like this. (We're not really supposed to use normal maps independent of meshes to begin with, but this is the world we've found ourselves in. Maybe in some parallel world, texture artists are distributing packs with 16bpc .png bump maps rather than normal maps, and it's my own bad karma that landed me in this world.) However, you can link changes to a texture node by using linked node groups:

enter image description here

Here, I've just put the image texture node into a node group, then duplicated that node group and used it in lieu of the image texture node. I've also put an unconnected copy into my base node tree. Now, to change the texture image used, I only have to edit it once, in any instance of those linked groups. Because they're linked, editing the group affects all instances of that group, changing it everywhere.

Edit 2: Again, you're correct in comments that the +Y face is wrong (backwards tangent.) Actually, both Y faces are wrong. I was designing things around the +Y face, but the +Y face is actually pointing in -Y, so we're flipping that tangent instead of the face opposite it.

We can fix this easily just by reversing the sign of our tangent for the +-Y lookups. Instead of sending that middle normal map node a 1,0,0 tangent, we can send it a -1,0,0 tangent:

enter image description here

In our viewport, we can see a box-blended Suzanne and bevelled, subdivided cube, and a basic cube on the right (well, UVs rotated/scale to provide appropriate tangents) getting the normal map via Blender's basic normal mapping. I've set the view to display material normals.

Updated file:

Images aren't packed, so you'll have to provide your own normal map.

$\endgroup$
7
  • $\begingroup$ This will take me some time to fully take in, but on a quick test this currently doesn't handle flipsides correctly (some colors are inverted): i.imgur.com/VvOzoGi.png The other three sides seem to be in order though. Ideally of course, one wouldn't have to change three textures deep inside the graph for every material. Maybe feeding in unblended box-projected texture could be a way to avoid that. $\endgroup$
    – chedap
    Jul 27, 2022 at 20:53
  • 1
    $\begingroup$ You're right about the flipsides. See edit, including updated file. $\endgroup$
    – Nathan
    Jul 27, 2022 at 22:29
  • $\begingroup$ I forgot to reply, but in the updated file the green channel is flipped on faces pointing along Y. It's an easy fix, but I'm not 100% sure what the actual reason is. $\endgroup$
    – chedap
    Aug 13, 2022 at 0:52
  • 2
    $\begingroup$ @chedap Okay, I think I got it now. The fix broke it :) Both Y faces were wrong, but they're right now. Added edit and updated file. $\endgroup$
    – Nathan
    Aug 15, 2022 at 16:25
  • 1
    $\begingroup$ @Nathan Just solved it: simply add a mapping node into the "TextureLookup" Group and connect the "Vector" Group Input into the "Vector" input of the mapping node. This is simply fantastic!!! Thanks again! $\endgroup$ May 9, 2023 at 20:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .