Skip to content

Weirdness with deluxe or normal mapping with the linear pipeline, reproduced with Xonotic maps #1905

Description

@illwieckz

Spotted on some Xonotic maps, this disappears when I disable either r_deluxeMapping or r_normalMapping. It doesn't disappear when I disable r_specularMapping or r_reliefMapping. Some very lit surfaces are turned black.

  • Xonotic map xoylent, setviewpos -700 -750 600 30 10

Enabled:

Image

Disabled:

Image

  • Xonotic map xoylent, setviewpos 1100 30 880 170 40

Enabled:

Image

Disabled:

Image

Activity

  1. illwieckz commented on Feb 19, 2026

    @illwieckz
    MemberAuthor

    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.

  2. illwieckz commented on Feb 19, 2026

    @illwieckz
    MemberAuthor

    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.

  3. illwieckz commented on Feb 19, 2026

    @illwieckz
    MemberAuthor

    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.

  4. illwieckz commented on Feb 19, 2026

    @illwieckz
    MemberAuthor

    Showing deluxe maps, light maps and normal maps, the input data looks to be correct, so the compute is likely borken:

    Image
    Image
    Image
    Image
    Image
    Image

  5. illwieckz commented on Feb 19, 2026

    @illwieckz
    MemberAuthor

    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 NdotL is 0 the surface would be black.

    Here is me painting NdotL as color:

    Image

    Here is me doing clamp( NdotL, 0.1, 1.0 ):

    Image

    Here is me doing clamp( NdotL, 0.5, 1.0 ):

    Image

    Here is without deluxe mapping:

    Image

    I wouldn't be surprised if the computation is incorrect to begin with.

  6. illwieckz commented on Feb 19, 2026

    @illwieckz
    MemberAuthor

    Here is normal:

    Image

    Here is lightDir:

    Image

  7. slipher commented on Feb 19, 2026

    @slipher
    Member

    It's possible that this is a duplicate of #1424. Luckily #1424 is not really complex to solve; it just requires a bit of tedious refactoring to reshuffle the code.

  8. illwieckz commented on Feb 19, 2026

    @illwieckz
    MemberAuthor
    1. 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.
    2. 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).
  9. illwieckz commented on Feb 19, 2026

    @illwieckz
    MemberAuthor
  10. slipher commented on Feb 19, 2026

    @slipher
    Member

    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_Normal from the BSP geometry and normal from 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.

    1. 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.

  11. slipher commented on Feb 19, 2026

    @slipher
    Member

    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.

  12. slipher commented on Feb 20, 2026

    @slipher
    Member

    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?

  13. SomaZ commented on Feb 20, 2026

    @SomaZ

    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.

  14. illwieckz commented on Feb 20, 2026

    @illwieckz
    MemberAuthor

    Both Xonotic and Unvanquished use the same deluxe maps, and it's world space as far as I know (edit: the fluo ones).

  15. illwieckz commented on Feb 20, 2026

    @illwieckz
    MemberAuthor

    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.

  16. SomaZ commented on Feb 20, 2026

    @SomaZ

    This just looks wrong with worldspace deluxemaps in mind. I mean, it's probably possible, but looks wrong on first glance. Someone should just test multiplying the tbn mat with the lightvector on this map and see if that fixes it.

    Image
  17. SomaZ commented on Feb 20, 2026

    @SomaZ

    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.

  18. illwieckz commented on Feb 20, 2026

    @illwieckz
    MemberAuthor

    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.

  19. SomaZ commented on Feb 20, 2026

    @SomaZ

    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.

  20. illwieckz commented on Feb 20, 2026

    @illwieckz
    MemberAuthor

    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.

    #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

  21. slipher commented on Feb 20, 2026

    @slipher
    Member

    Visualization of dot(normal, lightDir) where green is positive and red negative. So one or the other is probably wrong.

    Image

  22. slipher commented on Feb 20, 2026

    @slipher
    Member

    Here's a nice way to see that the normals are wrong. r_showNormalMaps with normal mapping off vs. on. If turning on normal mapping makes the color totally different from the flat version, the normals must be wrong.

    Image
    Image

  23. illwieckz commented on Feb 28, 2026

    @illwieckz
    MemberAuthor

    I don't know what was the bug, but it was an image loading bug, this PR fixes it:

    unvanquished_2026-02-28_151750_000

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions