Normal maps of custom MaterialVariant materials improperly reflect light on rotated parts

Below are 4 parts, each using the same custom MaterialVariant material. The only difference between the parts is their orientation. Each part is rotated at a different 90 degree angle. However, notice how the two parts on the right have stronger light reflections that the two on the left. The two parts on the left looks more ‘flat’, despite using the same normal map. Somehow, the orientation of the part relative to the direction the light is coming from influences how intensely light is being reflected.

image

The normal map used in the material is the following image:

image

The normal map is using OpenGL’s normal map format, as is required according to the documentation. I have gone back and forth, triple checking if maybe there is something wrong with my normal map, but I cannot find any errors in the normal map. The only conclusion I am able to draw is that the Roblox engine is calculating the light reflections incorrectly.

Expected behavior

I expect the light reflections on each of the 4 surfaces to be of equal intensity, independent of the part’s orientation.

9 Likes

Thanks for the report! We’ll follow up when we have an update for you.

2 Likes

Maybe, this’s intented behaviour? Because it’s how light falls on texture, so in pink/cyan parts of texture they will be brighter and when you rotate them by 90 degrees, they are diffirent?


(bas - relief image, shows how light falls maybe?)

If I understand your message correctly, what you are suggesting is that perhaps the pink/cyan parts of the texture have ‘steeper normals’ than the other sides, like the magenta and dark blue sides?

If so, I have made sure that the colors in the normal map are the correct shades. For example, take the following 6 pixels in this section of the normal map, which all fall on the edge of the tile where the normals would be pointing a little sideways:

The color values of these marked pixels are (starting at the top left, in clockwise order):

51, 201, 197
132, 237, 192
221, 181, 195
211, 60, 196
128, 24, 201
24, 90, 192

To convert a normal vector to a color, you add 1 to each color component and then divide by 2 (as per this article). So to convert a color back to a normal vector, you can multiply each color component with 2 and then subtract 1. Let’s do that for the 6 sampled colors and then print their size and Z-component:

local colors = {
	Color3.fromRGB(51, 201, 197);
	Color3.fromRGB(132, 237, 192);
	Color3.fromRGB(221, 181, 195);
	Color3.fromRGB(211, 60, 196);
	Color3.fromRGB(128, 24, 201);
	Color3.fromRGB(24, 90, 192);
}

for i = 1, #colors do
	local nx, ny, nz = colors[i].R * 2 - 1, colors[i].G * 2 - 1, colors[i].B * 2 - 1
	local normal = Vector3.new(nx, ny, nz)
	print(normal.Magnitude, normal.Z)
end

In the code above I print the magnitude of each vector (which should be 1 for each vector) and the ‘Z’ component of the normal vector, which corresponds to how much the vector is pointing ‘up’. In my case I sampled pixels on the edges of a stone tile which should all have the same amount of steepness. Their X and Y components in the normal vector will be different, but their Z component should all be equal. That said, not every pixel is exactly as far on the edge so there will be a slight difference.

The code above prints:

0.99471116065979 0.545098066329956
0.9973667860031128 0.5058823823928833 
0.9970583319664001 0.529411792755127
0.9989075660705566 0.5372549295425415
0.9956383109092712 0.5764706134796143
1.0006917715072632 0.5058823823928833

So each color represents a vector with a length between 0.99 and 1.01 (which makes sense, because there are some rounding issues when you work with floating point numbers. And the ‘steepness’ of each normal vector is between 0.505 and 0.576. So there is a very slight difference in how far ‘up’ these normal vectors are pointing, but that is mostly just because I sampled these pixels by hand, so some of my pixels fall a little more on the edge than others.

I generated this normal map texture with code, so the colors on the edges of every tile are calculated in the same way, so each tile should share this same characteristic where the normal vectors on the sides are all pointing the same amount of ‘up’.

Even if there were a slight difference of up to 7% in how much the normal vectors on one side are pointing up versus another side, I think it is fair to say that the reflection in the original pictures I posted indicate a far larger difference in ‘flatness’ versus ‘steepness’ than that 7% error.

2 Likes

I have narrowed down the problem from the original post even further. It seems that the brightness of the material color plays a large role in how ‘flat’ the texture looks as well. Consider the following example:

This image has the same 4 parts taken from the same angle, but with different part colors. In the top two cases, where the bricks are black or red, the light reflections look fine. In the bottom cases however, where the bricks are white and beige you will see the same behavior as mentioned in the original post. There, the top-right beige/white parts look more flat than the bottom-left beige/white parts.

1 Like

Any news on this? It’s been a year without a word.

Hi Zomebody,

Apologies for the delay on responding to you. This issue is on our radar, and we’ll reply here when we have an update for you!

2 Likes

This issue still exists, are there any updates on this? It’s mostly noticeable when porting old roblox material normal maps to modern roblox.