mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
103.01k stars 35.4k forks source link

GLTFExporter does not export models with normal maps correctly #22165

Open WestLangley opened 3 years ago

WestLangley commented 3 years ago

GLTFExporter does not export models with normal maps correctly. This is a known-issue. Perhaps this post can help sort it out.

//

three.js is highly flexible. It supports both OpenGL-style and DirectX-style normal maps.

three.js also supports geometries for which uv (0, 0) corresponds to the lower-left corner, or upper-left corner, of a texture.

To achieve this flexibility, three.js provides texture.flipY, and the bivariate material property normalScale.

Conversely, the glTF spec is much more restrictive: material.normalScale is univariate, and the spec supports only the OpenGL-style normal maps.

This means that when a model is exported by GLTFExporter, the more restrictive glTF convention must be honored.

Solution:

  1. If texture.flipYis true, then the exporter must either flip the uv's of the geometry (likely not advisable), or reorder the rows of the texture. I believe this is currently implemented correctly in the exporter.

  2. If either material.normalScale.x or material.normalScale.y is negative, then the exporter must modify the red or green channels of the normal map like so:

color = 255 - color;

I think if these rules were followed, then any three.js model having a normal map can be exported in a glTF-compliant manner.

Here is a simple test case that should be helpful in validating the exporter. The test is based on the existing glTF exporter example.

https://raw.githack.com/westlangley/three.js/dev-gltf-normalMap/examples/misc_exporter_gltf_normalMap.html

/ping @donmccurdy

three.js r130

donmccurdy commented 3 years ago

Thanks @WestLangley, this is a helpful writeup and examples.

To achieve this flexibility, three.js provides texture.flipY, and the bivariate material property normalScale.

I'll comment separately in https://github.com/mrdoob/three.js/pull/22146 as to how these examples might relate to keeping or removing .vertexTangents. But I think it is worth pointing out that a bivariate normalScale property is no longer necessary if we merge https://github.com/mrdoob/three.js/pull/22146 — we can support both GL- and DirectX-style normal maps with just a scalar normalScale property, unless there is some other reason for it?

This means that when a model is exported by GLTFExporter, the more restrictive glTF convention must be honored.

As for what GLTFExporter should do, I don't think the proposed solution is quite complete. For simplicity I'll omit the option of flipping geometry UVs; I agree it's inadvisable.

  1. If texture.flipY === true, flip the texture before export. (already implemented)
  2. If normalScale.x is NEGATIVE, rewrite the texture's R channel.
  3. If the model has tangents and normalScale.Y is NEGATIVE, or does not have tangents and normalScale.y is POSITIVE, rewrite the texture's G channel.

We currently attempt to just log a warning about (2) and (3), although that code looks out of date compared to what GLTFLoader currently constructs.

Practically speaking, enabling GLTFExporter to convert any plausible three.js input to valid glTF is a large task, and not one that I'm personally interested in. As it is now, using GLTFExporter will require some understanding of the features of glTF and which three.js inputs conform to those requirements. If any three.js users are interested in improving GLTFExporter to do more robust input conversions, new contributors are certainly welcome. 🙂