Repository navigation
Weirdness with deluxe or normal mapping with the linear pipeline, reproduced with Xonotic maps #1905
Description
Activity
Xonotic maps use normal+height files, instead of separate normal file and height file like we do, so I want to check if that's us wrongly loading the Xonotic files, or if we have a bug in the rendering part of the code.
But it's like if something overflows, and the buggy dark zones perfectly fit the light frontiers, so I doubt it's a normal format loading bug, it looks much more a rendering bug.
Forcing Xonotic maps to only load the normal part of the file doesn't fix the bug, so the bug shouldn't be in the loading.
Our deluxe code is that (I don't know if that's correct or not):
float NdotL = dot( normal, lightDir ); NdotL = clamp( NdotL, 0.0, 1.0 ); color.rgb += lightColor.rgb * NdotL * diffuseColor.rgb;
So I guess if
NdotLis0the surface would be black.Here is me painting
NdotLas color:Here is me doing
clamp( NdotL, 0.1, 1.0 ):Here is me doing
clamp( NdotL, 0.5, 1.0 ):Here is without deluxe mapping:
I wouldn't be surprised if the computation is incorrect to begin with.
- removing the
lightColor /= clamp(dot(normalize(var_Normal), lightDir), 0.3, 1.0);doesn't cancel the bug, so if I understand the topic of Good lightmap data unnecessarily tainted by bad deluxemaps #1424, it's unrelated. - Wrong deluxemaps as mentioned in Deluxe maps are wrong on and near transparent surfaces close to a skybox. Unvanquished/Unvanquished#2908 interact with this bug, but it's a different bug (in that other issue those deluxe maps are wrong to begin with, unlike there).
- removing the
- added a commit that references this issue
on Feb 19, 2026 This trick workarounds the bug:
I forgot that the fix from #1424 only applies if there is no normal map like a flat pane of glass. If there is a different
var_Normalfrom the BSP geometry andnormalfrom a normal map, then it is not a pointless round trip. So we can say this bug is distinct from #1424 and harder to fix.- removing the
lightColor /= clamp(dot(normalize(var_Normal), lightDir), 0.3, 1.0);doesn't cancel the bug, so if I understand the topic of Good lightmap data unnecessarily tainted by bad deluxemaps #1424, it's unrelated.
According to your research, the problem comes from a non-positive
NdotL, which I believe is the same as #1424.those deluxe maps are wrong to begin with, unlike there
Maybe the problem is not so much that the deluxe maps are wrong, as that the whole concept of a deluxe map is bogus to begin with. As soon as you have anything more than a single point source, it's not really valid. If you have light coming from all different directions, there is no value you can select for the "light direction" that will produce correct results from the equations. So you may have ambient light from all directions, but if you are unlucky, the "light direction" at the surface may differ by more than 90 degrees from the normal map direction.
- removing the
Maybe what we could do is compute the angle between the BSP geometry-based normal (of the flat triangle), and the normal map normal. Then say, well, if the angle is only 5 degrees, the amount of incoming light shouldn't change that much. Well, in theory the light could have all come in at a 179 degree angle and it changes by 100%... but we could assume that doesn't happen, and make a heuristic for how much it is allowed to change based on the angle difference. The good part is it would 100% solve the case from #1424, since the angle is always 0 if there is no normal map.
The deluxe maps themselves do look somewhat suspicious to me. In the first it appears there are very oblique angles everywhere under the light. Why isn't the direction more or less +z at least under the middle of it?
There's two types of deluxemaps. Worldspace ones and tangent space ones. Make sure Xonotic doesn't use tangentspace ones while you only support worldspace ones.
Both Xonotic and Unvanquished use the same deluxe maps, and it's world space as far as I know (edit: the fluo ones).
For knowledge, q3map2 also knows how to produce tangent space deluxe maps, so if that technology is better (I don't know), we would only have to implement it engine side.
Oh, its potentially the normal map format. I didn't read the introduction to the issue... If it disappears with disabled normal mapping, it might just be related to the normal map format.
Oh, maybe some of those specific normal maps are messed up…
Supposedly we use X -Y Z and Xonotic uses X Y Z but we supposedly flip them accordingly.Just checked some of your normal mapping code and it assumes that there's a z component stored in the b channel. This might be an issue as many engines just calculate the z component from the xy ones. Xonotic might as well, so the b channel might be unused or used otherwise which might lead to this issue. Deluxemaps should never produce NdotL <= 0.0. And I haven't seen them do that in our JKA renderer at least.
We compute the z component from the xy ones when the normal map file only contains normals (as we even use a CRN format with a DXT variant that shuffle some channels), but when the normal map file stores normals in rgb and heightmap in alpha channel (what Xonotic does), we read xyz from rgb.
Daemon/src/engine/renderer/glsl_source/reliefMapping_fp.glsl
Lines 51 to 83 in af74c1c
#if defined(r_normalMapping) #if defined(USE_HEIGHTMAP_IN_NORMALMAP) // alpha channel contains the height map so do not try to reconstruct normal map from it normal = texture2D(u_NormalMap, texNormal).rgb; normal = 2.0 * normal - 1.0; #else // !USE_HEIGHTMAP_IN_NORMALMAP // the Capcom trick abusing alpha channel of DXT1/5 formats to encode normal map // https://github.com/DaemonEngine/Daemon/issues/183#issuecomment-473691252 // // the algorithm also works with normal maps in rgb format without alpha channel // but we still must be sure there is no height map in alpha channel hence the test // // crunch -dxn seems to produce such files, since alpha channel is abused such format // is unsuitable to embed height map, then height map must be distributed as loose file normal = texture2D(u_NormalMap, texNormal).rga; normal.x *= normal.z; normal.xy = 2.0 * normal.xy - 1.0; // In a perfect world this code must be enough: // normal.z = sqrt(1.0 - dot(normal.xy, normal.xy)); // // Unvanquished texture known to trigger black normalmap artifacts // when doing Z reconstruction: // textures/shared_pk02_src/rock01_n // // Although the normal vector is supposed to have a length of 1, // dot(normal.xy, normal.xy) may be greater than 1 due to compression // artifacts: values as large as 1.27 have been observed with crunch -dxn. // https://github.com/DaemonEngine/Daemon/pull/260#issuecomment-571010935 // // This might happen with other formats too. So we must take care not to // take the square root of a negative number here. normal.z = sqrt(max(0, 1.0 - dot(normal.xy, normal.xy))); #endif // !USE_HEIGHTMAP_IN_NORMALMAP - addedA-Assetsrelating to asset creation pipelinerelating to asset creation pipeline
on Feb 23, 2026 I don't know what was the bug, but it was an image loading bug, this PR fixes it:

















Spotted on some Xonotic maps, this disappears when I disable either
r_deluxeMappingorr_normalMapping. It doesn't disappear when I disabler_specularMappingorr_reliefMapping. Some very lit surfaces are turned black.xoylent,setviewpos -700 -750 600 30 10Enabled:
Disabled:
xoylent,setviewpos 1100 30 880 170 40Enabled:
Disabled: