-
-
Notifications
You must be signed in to change notification settings - Fork 35.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GLTFLoader: Normal-Tangent Test model result is incorrect #11438
Comments
Thanks for writing this up @cx20. Just for more context, here are some related info & issues:
|
Good writeup, thanks! The model appears to render correctly with the addition of this line: materialParams.normalScale = new THREE.Vector2(-1, 1); But I'm not sure I understand the issue well. Understanding #11315 might help here. |
@donmccurdy Thanks for your advice. |
@cx20 agreed, this fix is now merged into THREE.GLTF2Loader. |
I have confirmed that it is being fixed. |
See #15749 — the regression is intentional, and can be avoided by including tangents in the model. Ideally we would have a JS implementation for generating mikktspace tangents, to fully solve this, but that is fairly complex. |
I wasn't aware of #15749 until now. I'm caught off guard by this, I had thought we did a good enough job defining the tangents in glTF that they could be at least approximated at runtime. Note that the Blender exporter won't export any glTF tangents by default, as it helps keep the file size down, and the major implementations of glTF were all passing this test without tangents. I suspect this change may have broken normal mapping for a majority of glTF models in ThreeJS. I'll need some time to read through all the linked issues to get a deeper understanding of what's happened and why. But I think the glTF community should consider this high priority to get models without tangents rendering correctly again, as I believe most models are in that category by default. |
Reopening 😅 |
I don't believe it's anything that severe – we've always generated tangents realtime in the shader with derivatives, and we still do. We previously included a hack (
See above. In general, I believe we do render models without tangents adequately. We do not, however, generate mikktspace tangents as required by the glTF spec. To my knowledge, no JS implementation of that exists, and our derivatives-based shader implementation is simply a "mostly good enough" approximation. This sample model is an intentionally extreme case that demonstrates the limits of that approximation. We'd be glad to have a JS mikktspace tangent generation implementation; that would be good addition to THREE.BufferGeometryUtils. But the official (native) mikktspace code is fairly long, and I haven't dug in enough yet to see how much of that is required for generating tangents. |
Did it break other glTF models specifically, or just examples in general? There are two different types of tangent-space normal maps out in the wild. Substance Painter calls these "DirectX Normals" and "OpenGL Normals", which is not the greatest name for it. The difference is specifically the What I suspect is happening is that when ThreeJS auto-calculates tangents, it expects the normal map to have been authored with the flipped Y (DirectX) style, and gets that channel backwards for glTF, so the flip is needed. However, when the tangents are supplied, no such flip is needed. The question of mikktspace I think is separate from this. It's unfortunate that the spec calls for mikktspace and most implementations approximate that with screen-space derivatives. I don't know how similar the two are, but, normal maps generated in mikktspace appear to work reasonably well when shown with the approximation, so long as the left/right-handedness of the map is done correctly. (There's also some discussion of this from last year in KhronosGroup/glTF-Sample-Models#174) |
I'd like to call out one sentence from the glTF spec on normal maps in particular:
This sentence is what allows models to be shipped without tangent vectors, saving space. Let's test it. Here I've made a lousy height map (bump map) out of a splotch in a paint program: Let's define this height field as an outward bump, where white pixels are closer to the viewer, and black pixels are further away. Using an online converter (of questionable quality, but we'll examine the result in a moment), I've converted this from a height map to a normal map. Keep in mind there's no 3D model here, no UV coordinates or mikktspace calculations or any geometry. Just a height map converted to a normal map. I had to manually configure the online converter per glTF's instructions, such that X is right, Y is up, and Z faces the viewer. This is the result: Let's bring that back into a paint program and bust out the color channels to see where these vectors point. Below, each color channel has been separated into a grayscale image of its own. Remember that these will be interpreted such that black means So I think the online converter did what glTF asked, at least after configuring it correctly. In the Red (X) image, we can see the slope on the right has white pixels (+1.0), pointing the X vector at the right edge of the image. On the left side of the Red image, black pixels (-1.0) point the X vector at the left side of the image. In the Green (Y) image, white pixels along the top slope of the bump point the Y vector at the top of the image. The Z values are the least intuitive, but remember that the tip of the bump and the back plate itself both point at the viewer, and the slopes on all sides point away, so are all evenly darker. What if we load this into Blender Eevee, which (just like glTF) accepts OpenGL-style normal maps? What happens if the UV map is rotated, or even scaled to be inverted? Turns out, this works just fine. Indeed, the whole point of defining the tangent space this way is not to enable software to go crazy with the vectors, it's to allow texture artists some sanity by ensuring that their normal maps will be right-side up regardless of the geometry. But, not all software uses the OpenGL convention. Some uses a different convention (sometimes called the DirectX convention), where the Y vectors point at the bottom of the image instead of the top. Here's the decomposed Y channel of my image in this form. The lighter pixels are the ones facing the bottom of the image. If I load one of these DirectX-style normal maps into Blender Eevee, can I still expect it to work? No. Blender was expecting +Y up. The math is wrong, and the reflected horizon line spins all around. This is what the NormalTangentTest model is attempting to test. Each row spins the UV coordinates into a different orientation, trying to make sure that the reflections remain right-side up in these different orientations. |
There still needs to be a concrete formula in the specification for how to compute a tangent for a lone primitive, given a primitive and its UV coordinates, and which W sign to use for bitangent. "OpenGL normals" and "DX normals" is not precise enough to derive the formula. They might refer to conventions, but I have no idea as an implementer what to do with that. What I currently do is emit flipped TangentW from MikkTSpace to match up with this particular sample, but that was just what happened to work. |
Reported bugs were related to glTF models, specifically. That said, I doubt there's enough confidence in material export via FBX or COLLADA to say those normal map conventions were ever thoroughly understood and tested either.
Thanks, this is a much better justification of our "hack" than we had when we implemented it. 😇
The spec is right that MikkTSpace is the most robust way to generate tangents, I think, it's just not universally the right choice to do this automatically at runtime. If cheaper alternatives look correct for a particular model, there's no reason to do something more expensive that doesn't look any better. The spec language could be loosened to allow for approximations but I don't feel strongly about this.
I'm not sure the MikkTSpace algorithm is so easy to represent as a discrete formula... are you asking for an alternative to the canonical MikkTSpace code? Or some additional information beyond the instruction to use MikkTSpace? @Themaister For the original issue, it sounds like we ought to restore the
If threejs is really using the DirectX convention and e.g. Blender is not, I could see a case for (c). In the interest of a quick and safe solution, though, I am inclined to go with (a). |
No, three.js does not assume that... three.js assumes +Y is "up" for tangent space, and increasing-V is "up" for uv space. That is, three.js assumes uv ( 0, 0 ) is in the lower-left corner of the texture, while the glTF spec assumes the upper-left. This is why When tangents are not present, three.js uses screen-space derivatives to estimate tangents. It does so using the chain rule. An assumption in that calculation is that tangent space and uv space have the same orientation. For properly-authored glTF models, that assumption is not correct. However, you should be able to compensate by setting It would also seem to me we could fix this automatically by honoring the If setting |
Thanks for this clarification. I think we have a path forward here.
Yes, with the exception of cases where the glTF supplies its own tangent vectors, right? I would expect only the auto-computed tangents to need the
Edit: I believe I've confirmed the correctness of the exported tangent vectors, and they do not need any Y flipping. I can post more detail on that if needed. But it seems like the correct action is to flip |
In my previous post, I explained how to manually compensate for inverted normals when attribute tangents are not present. There should be nothing to compensate for when tangents are present because screen-space derivatives are not being used. I also suggested we may be able to fix this automatically by honoring the In any event, before we go down that path, I think it is imperative that we verify this hypothesis:
We must have an explanation for every model that is not being rendered correctly. |
Yes, apologies, I did not intend to be dictating that sort of implementation detail. I'm just trying to make sure there's not a misunderstanding of what honoring the
That sounds like that could potentially be a large set. If there are indeed models out there that are doing something radically different than the official test models, and yet could still be considered valid uses of normal maps in glTF, that would be important to discover. The test models are intended to cover valid uses of normal maps on static (non-transformed) geometry. There shouldn't be a way, particularly when relying on viewer-generated tangent vectors, to construct the model in some other coordinate system and yet claim that it's valid glTF. |
I assume this is closely related to this issue? KhronosGroup/glTF-Sample-Models#174 |
https: //github.com/mrdoob/three.js/issues/11438 Change-Id: Ib85c4ed8fbd0b936216cf4a7e129687c0d28d53d Reviewed-on: https://kuesa-codereview.kdab.com/c/kuesa/kuesa/+/140 Reviewed-by: Paul Lemire <[email protected]>
@mrdoob @Mugen87 @donmccurdy This issue has resurfaced in r125, I would bet due to #21076. I still think the benefits of that change far outweigh this regression, but it would be nice if we could fix it all. |
It would be interesting to know if #21076 is really the cause. Have you tested after the corrections for I'll comment further in #20997 (comment) – tl;dr i think we should probably revert #21076 for several reasons, but am not sure yet what else should be done for the various issues. |
Instead of reverting #21076 I vote to just remove the following code section in three.js/examples/js/loaders/GLTFLoader.js Lines 3242 to 3255 in a159495
Having |
There are two similar samples in the Khronos glTF Sample model: |
@Mugen87 Thanks. I updated from |
That's great! Can you please double check |
@Mugen87 I checked |
@cx20 Would you be willing to fix the Normal Tangent Test demo? The normal map has tangents largely parallel to the plane and reflections are unreasonable. (This is your model in my testbed.) You will need to "flatten" the hemispheres and bake new normal maps. I achieve the same effect by hacking the loader callback: mesh.scale.z = 0.25;
mesh.material.normalScale.multiplyScalar( 0.25 ); In your demo, it also helps if your environment map doesn't look practically the same in every direction. Maybe you can use a different one. There is also no need for a spinning scene. OrbitControls alone is sufficient. Also, it would be helpful to fix the near-plane clipping so the scene is visible when zooming. Thanks. |
@WestLangley That's an intentional effect. The model's README mentions that the test is only valid when viewed face-on. It's intended to test the broadest range of normal vectors, not viewing angles. |
@emackey Thank you for your reply. Without changes, however, I would have to remind the three.js devs to be suspect when using that model to test three.js code. |
@WestLangley Thank you for the improvement plan. |
Description of the problem
I tried to display the Normal-Tangent Test model.
However, the displayed result seems to be different from Khronos' sample.
Three.js + Normal-Tangent Test model result:
Khronos sample loader + Normal-Tangent Test model result:
I think that this sample model should have the same left and right results.
Related : https://emackey.github.io/testing-pbr/normal-tangent-readme.html
/cc @emackey
Three.js version
Browser
OS
Hardware Requirements (graphics card, VR Device, ...)
ThinkPad X260 + Windows 10 + Intel HD Graphics 520
The text was updated successfully, but these errors were encountered: