GLTF to J3O Texture Filtering Issue

I’m just giving a personal suggestion here, in this case it should keep consistent behavior with other engines’ GLTF importers by default, basically most other engines’ GLTF viewers or importers use linear filtering as the option (because it really is the priority in most cases), even some tools like gltfViewers on WebGL also default to Linear…
It’s fine if others prefer the nearest neighbor filtering option too, just like I said earlier, maybe adding an independent config tab in the SDK for setting texture filtering options when converting to j3o (so everyone can choose their own default option) could work, but as I’ve said, referencing other engines using Linear as default when converting to j3o would make more sense.

4 Likes

There is one more thing, after conversion to J3O, the filtering option ignores mipmaps, which leads to high texture bandwidth overhead.

In the two images above, the first image has correct mipmap filtering, optimized for texture cache bandwidth (low bandwidth overhead), the second image uses nearest neighbor filtering, no proper mipmap, unfriendly to texture cache bandwidth.

Well, i would prefer option with default to Linear filtering in every case, allowing to change to non-linear filtering if just someone need.

Lets say many new users try it out, and they do not even know about this “hidden” setting and they might think this is engine issue(i mean they might think thats how engine work).

But since i might not know real reasons behind this, i think other people who think opposite should respond here. Every opinion is important here. Also in both cases, this should be same for every j3o/gltf/etc loaders.

4 Likes

I agree with your perspective. This is also the default configuration for GLTF importers in other engines.

I have the same concern: For Java beginners who are new to JME or even new to 3D game engines, they may think this is the rendering performance of the engine and feel JME does not perform as well as other engines…

1 Like

So are you saying filter settings are correct when directly loading Gltf file but get changed after exported to j3o?

If so, then this is an engine bug. You might need to open an issue on JME GitHub page and provide a minimal gltf test file for us to test.

2 Likes

GLTF probably depend on file setting that usually do not export filtering or something is not working well. (thats odd since Blender gltf imo should export as linear)

While j3o converted most probably convert to linear no matter what settings were in GLTF one - thats my guess.

Here is from GLTFLoader, but im really not sure if getAsInteger(sampler, “minFilter”) can be taken from file or it is missing in file or its in different place or its set as non-linear one.

    public Texture2D readSampler(int samplerIndex, Texture2D texture) throws IOException {
        if (samplers == null) {
            throw new AssetLoadException("No samplers defined");
        }
        JsonObject sampler = samplers.get(samplerIndex).getAsJsonObject();
        Texture.MagFilter magFilter = getMagFilter(getAsInteger(sampler, "magFilter"));
        Texture.MinFilter minFilter = getMinFilter(getAsInteger(sampler, "minFilter"));
        Texture.WrapMode wrapS = getWrapMode(getAsInteger(sampler, "wrapS"));
        Texture.WrapMode wrapT = getWrapMode(getAsInteger(sampler, "wrapT"));

        if (magFilter != null) {
            texture.setMagFilter(magFilter);
        }
        if (minFilter != null) {
            texture.setMinFilter(minFilter);
        }
        texture.setWrap(Texture.WrapAxis.S, wrapS);
        texture.setWrap(Texture.WrapAxis.T, wrapT);

        texture = customContentManager.readExtensionAndExtras("texture.sampler", sampler, texture);

        return texture;
    }

in GLTFLoader it goes like:
readTexture → readImage → readSampler(when getAsInteger(textureData, “sampler”) is not null) → set sampler if not null.

Maybe its about:

        if (samplerIndex != null) {
            texture2d = readSampler(samplerIndex, texture2d);
        } else {
            texture2d.setWrap(Texture.WrapMode.Repeat);
        }

Where when samperIndex is null it should setup linear one.(not just wrap)

@JhonKkk Could you see content of GLTF and check if there is “sampler” Json data anywhere and what it got?

1 Like

Yes, actually many GLTF models have issues when converted to J3O. I just randomly picked a model from Sketchfab, such as this one:
test_model
You just need to download the gltf format, then directly preview the gltf in SDK or other engines (note, don’t convert to j3o, but directly preview the gltf in SDK), it has normal linear filtering, but once converted to J3O, it becomes wrong.
It is normal for the SDK to directly preview gltf files, and the results are consistent with gltfViewer. J3o should also be consistent with these gltfViewers, rather than forcibly converting to nearest neighbor filtering.

1 Like

I will look into this part of the code later. Thank you for pointing this out. :wink:

edit:

ok i managed to login.

Here is data from model:

  "samplers": [
    {
      "magFilter": 9729,
      "minFilter": 9987,
      "wrapS": 10497,
      "wrapT": 10497
    }
  ],

and:

  "textures": [
    {
      "sampler": 0,
      "source": 0
    },
    {
      "sampler": 0,
      "source": 1
    },
    {
      "sampler": 0,
      "source": 2
    },
    {
      "sampler": 0,
      "source": 3
    },
    {
      "sampler": 0,
      "source": 4
    },
    {
      "sampler": 0,
      "source": 5
    },
    {
      "sampler": 0,
      "source": 6
    },
    {
      "sampler": 0,
      "source": 7
    },
    {
      "sampler": 0,
      "source": 8
    },
    {
      "sampler": 0,
      "source": 9
    },
    {
      "sampler": 0,
      "source": 10
    },
    {
      "sampler": 0,
      "source": 11
    },
    {
      "sampler": 0,
      "source": 12
    },
    {
      "sampler": 0,
      "source": 13
    },
    {
      "sampler": 0,
      "source": 14
    },
    {
      "sampler": 0,
      "source": 15
    },
    {
      "sampler": 0,
      "source": 16
    },
    {
      "sampler": 0,
      "source": 17
    },
    {
      "sampler": 0,
      "source": 18
    },
    {
      "sampler": 0,
      "source": 19
    },
    {
      "sampler": 0,
      "source": 20
    },
    {
      "sampler": 0,
      "source": 21
    },
    {
      "sampler": 0,
      "source": 22
    },
    {
      "sampler": 0,
      "source": 23
    },
    {
      "sampler": 0,
      "source": 24
    },
    {
      "sampler": 0,
      "source": 25
    },
    {
      "sampler": 0,
      "source": 26
    },
    {
      "sampler": 0,
      "source": 27
    },
    {
      "sampler": 0,
      "source": 28
    },
    {
      "sampler": 0,
      "source": 29
    },
    {
      "sampler": 0,
      "source": 30
    },
    {
      "sampler": 0,
      "source": 31
    }
  ]

so if code works correctly it should be:

https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-sampler

  "magFilter": 9729, -> LINEAR
  "minFilter": 9987, -> LINEAR
  "wrapS": 10497,
  "wrapT": 10497

JME code:

public static Texture.MagFilter getMagFilter(Integer value) {
    if (value == null) {
        return null;
    }
    switch (value) {
        case 9728:
            return Texture.MagFilter.Nearest;
        case 9729:
            return Texture.MagFilter.Bilinear;
    }
    return null;
}

public static Texture.MinFilter getMinFilter(Integer value) {
    if (value == null) {
        return null;
    }
    switch (value) {
        case 9728:
            return Texture.MinFilter.NearestNoMipMaps;
        case 9729:
            return Texture.MinFilter.BilinearNoMipMaps;
        case 9984:
            return Texture.MinFilter.NearestNearestMipMap;
        case 9985:
            return Texture.MinFilter.BilinearNearestMipMap;
        case 9986:
            return Texture.MinFilter.NearestLinearMipMap;
        case 9987:
            return Texture.MinFilter.Trilinear;

    }
    return null;
}

So GLTFLoader seems to have proper values coded, but maybe it read wrong field for sampler.

1 Like

I actually just randomly found a model on sketchfab, like this one:test_model
I currently have no way to upload it somewhere public for you to download, so you may need to find your account and password… :joy:

1 Like

sorry for confusion, i could guess password and edited my previous post :slight_smile:

@Ali_RS @tonihele @oxplay2
So this is an issue with j3o’s conversion? The SDK previewing gltf directly is normal, I opened an issue here: GLTF to J3O Texture Filtering Issue · Issue #544 · jMonkeyEngine/sdk · GitHub

2 Likes


I just randomly downloaded another model, its texture filtering mode is linear, previewing the GLTF directly with the SDK is normal:

However after converting to J3O:


I believe this is a J3O issue, the 15 models I tested a few days ago all had problems after converting to J3O… What surprises me is, has no one else noticed this before :joy:

3 Likes

i did check and it seems like this GLTF model setup exact filters:

I did messup, since i thought you mean its opposite were GLTF have nearest filter and j3o linear.

But not i re-watch screenshots and j3o given you nearest filtering, so it seems like it indeed loose filtering in convert process.

Myself i load only GLTF currently, thought about converting to j3o later to speed-up loading, so i didnt seen this issue (and also anyway i did setup filtering via code manually anyway per load)

So yes, i think there is some issue with j3o converting loosing filtering mode. So i guess we should make github issue like you did. (@pspeed you made jmec before so maybe might know where exatly is the issue, im very unfamiliar to converter code)

3 Likes

I believe that after our analysis, there is indeed an issue with j3o conversion, but what surprises me is: it seems this issue has existed for a long time? Perhaps just as you said before “other JME3 newbies thought this is how jme renders visuals, differently from other engines…”, so others have not raised this issue, but this is indeed wrong…

2 Likes

Yes, and based on my own example, its probably why i have in-code manual edit for texture filtering each model load, while currently i load working GLTF so i do not even need that anymore in fact. (until loading j3o ofc)

But probably again its about hisotry, because j3o can be generated from multiple loader loaded models, so maybe there was reason behind it earlier. We will not know until we could find exact code in j3o converter that cause it.

JMEC is using private static J3MExporter j3mExporter = new J3MExporter();
that do j3mExporter.save(material, file);

There i could find J3MOutputCapsule in same directory that have:

    protected static String formatMatParamTexture(MatParamTexture param) {
        StringBuilder ret = new StringBuilder();
        Texture tex = (Texture) param.getValue();
        TextureKey key;
        if (tex != null) {
            key = (TextureKey) tex.getKey();

            if (key != null && key.isFlipY()) {
                ret.append("Flip ");
            }

            ret.append(formatWrapMode(tex, Texture.WrapAxis.S));
            ret.append(formatWrapMode(tex, Texture.WrapAxis.T));
            ret.append(formatWrapMode(tex, Texture.WrapAxis.R));

            //Min and Mag filter
            if (tex.getMinFilter() != Texture.MinFilter.Trilinear) {
                ret.append("Min").append(tex.getMinFilter().name()).append(" ");
            }

            if (tex.getMagFilter() != Texture.MagFilter.Bilinear) {
                ret.append("Mag").append(tex.getMagFilter().name()).append(" ");
            }

            ret.append("\"").append(key.getName()).append("\"");
        }

        return ret.toString();
    }

While loading:

    private enum TextureOption {

        /**
         * Applies a {@link com.jme3.texture.Texture.MinFilter} to the texture.
         */
        Min {

            @Override
            public void applyToTextureKey(final String option, final TextureKey textureKey) {
                Texture.MinFilter minFilter = Texture.MinFilter.valueOf(option);
                textureKey.setGenerateMips(minFilter.usesMipMapLevels());
            }

            @Override
            public void applyToTexture(final String option, final Texture texture) {
                texture.setMinFilter(Texture.MinFilter.valueOf(option));
            }
        },

        /**
         * Applies a {@link com.jme3.texture.Texture.MagFilter} to the texture.
         */
        Mag {
            @Override
            public void applyToTexture(final String option, final Texture texture) {
                texture.setMagFilter(Texture.MagFilter.valueOf(option));
            }
        },

my question is…

why:

            //Min and Mag filter
            if (tex.getMinFilter() != Texture.MinFilter.Trilinear) {
                ret.append("Min").append(tex.getMinFilter().name()).append(" ");
            }

            if (tex.getMagFilter() != Texture.MagFilter.Bilinear) {
                ret.append("Mag").append(tex.getMagFilter().name()).append(" ");
            }

?

in Texture.MagFilter.valueOf() or Texture.MinFilter.valueOf() we got:

    public enum MinFilter {

        /**
         * Nearest neighbor interpolation is the fastest and crudest filtering
         * method - it simply uses the color of the texel closest to the pixel
         * center for the pixel color. While fast, this results in aliasing and
         * shimmering during minification. (GL equivalent: GL_NEAREST)
         */
        NearestNoMipMaps(false),

        /**
         * In this method the four nearest texels to the pixel center are
         * sampled (at texture level 0), and their colors are combined by
         * weighted averages. Though smoother, without mipmaps it suffers the
         * same aliasing and shimmering problems as nearest
         * NearestNeighborNoMipMaps. (GL equivalent: GL_LINEAR)
         */
        BilinearNoMipMaps(false),

        /**
         * Same as NearestNeighborNoMipMaps except that instead of using samples
         * from texture level 0, the closest mipmap level is chosen based on
         * distance. This reduces the aliasing and shimmering significantly, but
         * does not help with blockiness. (GL equivalent:
         * GL_NEAREST_MIPMAP_NEAREST)
         */
        NearestNearestMipMap(true),

        /**
         * Same as BilinearNoMipMaps except that instead of using samples from
         * texture level 0, the closest mipmap level is chosen based on
         * distance. By using mipmapping we avoid the aliasing and shimmering
         * problems of BilinearNoMipMaps. (GL equivalent:
         * GL_LINEAR_MIPMAP_NEAREST)
         */
        BilinearNearestMipMap(true),

        /**
         * Similar to NearestNeighborNoMipMaps except that instead of using
         * samples from texture level 0, a sample is chosen from each of the
         * closest (by distance) two mipmap levels. A weighted average of these
         * two samples is returned. (GL equivalent: GL_NEAREST_MIPMAP_LINEAR)
         */
        NearestLinearMipMap(true),

        /**
         * Trilinear filtering is a remedy to a common artifact seen in
         * mipmapped bilinearly filtered images: an abrupt and very noticeable
         * change in quality at boundaries where the renderer switches from one
         * mipmap level to the next. Trilinear filtering solves this by doing a
         * texture lookup and bilinear filtering on the two closest mipmap
         * levels (one higher and one lower quality), and then linearly
         * interpolating the results. This results in a smooth degradation of
         * texture quality as distance from the viewer increases, rather than a
         * series of sudden drops. Of course, closer than Level 0 there is only
         * one mipmap level available, and the algorithm reverts to bilinear
         * filtering (GL equivalent: GL_LINEAR_MIPMAP_LINEAR)
         */
        Trilinear(true);

        private final boolean usesMipMapLevels;

        private MinFilter(boolean usesMipMapLevels) {
            this.usesMipMapLevels = usesMipMapLevels;
        }

        public boolean usesMipMapLevels() {
            return usesMipMapLevels;
        }
    }

    public enum MagFilter {

        /**
         * Nearest neighbor interpolation is the fastest and crudest filtering
         * mode - it simply uses the color of the texel closest to the pixel
         * center for the pixel color. While fast, this results in texture
         * 'blockiness' during magnification. (GL equivalent: GL_NEAREST)
         */
        Nearest,

        /**
         * In this mode the four nearest texels to the pixel center are sampled
         * (at the closest mipmap level), and their colors are combined by
         * weighted average according to distance. This removes the 'blockiness'
         * seen during magnification, as there is now a smooth gradient of color
         * change from one texel to the next, instead of an abrupt jump as the
         * pixel center crosses the texel boundary. (GL equivalent: GL_LINEAR)
         */
        Bilinear;

    }

so MagFilter.Bilinear should work fine if not this lines, while MinFilter.Trilinear also should work fine, there is Trilinear enum.

1 Like

So in summary, i think i found the problem:

GLTF model we got have mag “Bilinear” and min “Trilinear”

While j3mExporter capsule via j3mOutputCapsule got:

            //Min and Mag filter
            if (tex.getMinFilter() != Texture.MinFilter.Trilinear) {
                ret.append("Min").append(tex.getMinFilter().name()).append(" ");
            }

            if (tex.getMagFilter() != Texture.MagFilter.Bilinear) {
                ret.append("Mag").append(tex.getMagFilter().name()).append(" ");
            }

so i think this lines here are the issue:

if (tex.getMinFilter() != Texture.MinFilter.Trilinear) {
Trilinear or Bilinear is treatet like exception here.

There was change from December 2022 (1 year ago almost) - it was needed to fix something

So my guess is that if image had no mipmaps then Bilinear caused some issues .

But because of this fix, it also caused j3o do not store Trilinear/Bilinear at all.

@Ali_RS maybe you will remember the topic, i know it was long time ago.

Is it possible to solve this another way? What was exact issue about this? Or was this just about Tests?

I belive we should change this fix to fix what it was fixing just some other way.

Ofc its still just a guess. To test this theory would need for example build JMEC with forked JME with disabled this conditional lines.

1 Like

i think

Both BilinearNoMipMaps and Trilinear can be the default, and thus non of them would be exported. That is stupid!

As you suggested I will default it to Trilinear then when the min filter is not specified.

ok so seems we have answer from this.

But… Bilinear or Trilinear are not default it seems. (can someone more verify with j3o loader?)

Also there are more than 2 of this like:

MinFilter (boolean usesMipMapLevels):

Trilinear(true)
NearestLinearMipMap(true)
BilinearNearestMipMap(true)
NearestNearestMipMap(true)
BilinearNoMipMaps(false)
NearestNoMipMaps(false)

MagFilter:

Nearest
Bilinear

So what about other ones? Even if we make MinFilter default one working as Trilinear there are still 3 more that require mipmaps.

@Ali_RS it seems we need your help again with this topic :slight_smile:

1 Like

i noticed few month ago however I didn’t have the skills to understand what the problem was, the solution that was proposed to me was “add a prob light”

1 Like