GLTF to J3O Texture Filtering Issue


result:


result:

(please open full view images to see real result, in miniatures its broken)

im confused. Is it near visible or linear one in both here.

For me both of them looks fine as linear right?

debug show ofc proper one:

---------------------------------------
Geom name:#CAM0001_Film_Advance_#CAM0001_Textures_0
Mat Name:CAM0001_Textures
MinFilter:Trilinear
MagFilter:Bilinear
---------------------------------------
Geom name:#CAM0001_Shutter_#CAM0001_Textures_0
Mat Name:CAM0001_Textures
MinFilter:Trilinear
MagFilter:Bilinear
---------------------------------------

edit:

if i set nearest manually i see difference:

        SceneGraphVisitor visitor = (Spatial spatTraversal) -> {
            if (spatTraversal instanceof Geometry) {
                for (String name : Arrays.asList(new String[]{"ColorMap", "DiffuseMap", "BaseColorMap", "NormalMap", "MetallicRoughnessMap", "MetallicMap", "RoughnessMap"})) {
                    MatParamTexture tex = ((Geometry)spatTraversal).getMaterial().getTextureParam(name);
                    if (tex != null && tex.getTextureValue() != null) {
                        tex.getTextureValue().setMinFilter(Texture.MinFilter.NearestNoMipMaps);
                        tex.getTextureValue().setMagFilter(Texture.MagFilter.Nearest);
                        ((Geometry)spatTraversal).getMaterial().setTexture(name, tex.getTextureValue());
                    }
                }
            }
        };
        model.breadthFirstTraversal(visitor);

Also what was funny for me that when i had:

        settings.setSamples(16);

Then even when manually setting Nearest it were looking same like Trilinear

Anyway both with or without
model = BinaryExporter.saveAndLoad(assetManager, model);
always looks same for me.

1 Like

It looks like both have the same image result, and are correct linear texture filtering. But why is this? I saw different results in SDK 3.6.1.

ah i think i messed up. i had assetManager.clearCache(); in wrong place. give me moment

edit:

ok replicated:


result:


result:



After executing settings.setSamples(16);, the second image has the line uncommented: model = BinaryExporter.saveAndLoad(assetManager, model);
Please take a closer look at the red boxed area, these two images still have different results, and the difference becomes more obvious when the camera moves further away.

ye, already replicated and edited earlier post. just had assetManager.clearCache(); in wrong place.

Im trying to check now if i can find issue or not

Exporter:



so Exporter seems to export as Trilinear fine.

Importer:





So everything looks fine.

So next i tried things like:

        SceneGraphVisitor visitor = (Spatial spatTraversal) -> {
            if (spatTraversal instanceof Geometry) {
                for (String name : Arrays.asList(new String[]{"ColorMap", "DiffuseMap", "BaseColorMap", "NormalMap", "MetallicRoughnessMap", "MetallicMap", "RoughnessMap"})) {
                    MatParamTexture tex = ((Geometry)spatTraversal).getMaterial().getTextureParam(name);
                    if (tex != null && tex.getTextureValue() != null) {
                        Material mat = ((Geometry)spatTraversal).getMaterial().clone();
                        mat.setTexture(name, tex.getTextureValue().clone());
                        ((Geometry)spatTraversal).setMaterial(mat);
                    }
                }
            }
        };
        model.breadthFirstTraversal(visitor);

Because i thought maybe shader need re-send it or something.
But this change nothing.

While when doing:

        SceneGraphVisitor visitor3 = (Spatial spatTraversal) -> {
            if (spatTraversal instanceof Geometry) {
                for (String name : Arrays.asList(new String[]{"ColorMap", "DiffuseMap", "BaseColorMap", "NormalMap", "MetallicRoughnessMap", "MetallicMap", "RoughnessMap"})) {
                    MatParamTexture tex = ((Geometry)spatTraversal).getMaterial().getTextureParam(name);
                    if (tex != null && tex.getTextureValue() != null) {
                        tex.getTextureValue().setMinFilter(Texture.MinFilter.Trilinear);
                        tex.getTextureValue().setMagFilter(Texture.MagFilter.Bilinear);
                        ((Geometry)spatTraversal).getMaterial().setTexture(name, tex.getTextureValue());
                    }
                }
            }
        };
        model.breadthFirstTraversal(visitor3);

after model is saved and re-loaded it help. So im very confused.

So after each one i start compare result, thinking its some other param affecting filters:



So then i started checking mipmaps. and look this:

and after i setup trilinear/bilinear manually like:

                        tex.getTextureValue().setMinFilter(Texture.MinFilter.Trilinear);
                        tex.getTextureValue().setMagFilter(Texture.MagFilter.Bilinear);

its:

So suddenly “needGeneratedMips = true” instead of false.

When i use:

tex.getTextureValue().getImage().setUpdateNeeded();

this will not change into “needGeneratedMips = true”

Tho Texture have:

    public void setMinFilter(MinFilter minificationFilter) {
        if (minificationFilter == null) {
            throw new IllegalArgumentException(
                    "minificationFilter can not be null.");
        }
        this.minificationFilter = minificationFilter;
        if (minificationFilter.usesMipMapLevels() && image != null && !image.isGeneratedMipmapsRequired() && !image.hasMipmaps()) {
            image.setNeedGeneratedMipmaps();
        }
    }

and Image have:

    public void setUpdateNeeded() {
        super.setUpdateNeeded();
        if (isGeneratedMipmapsRequired() && !hasMipmaps()) {
            // Mipmaps are no longer valid, since the image was changed.
            setMipmapsGenerated(false);
        }
    }

hasMipmaps is:

    public boolean hasMipmaps() {
        return mipMapSizes != null;
    }

So cant use setUpdateNeeded because it have marked as mipmaps are generated already.

And i cant use setNeedGeneratedMipmaps:

    public boolean isMipmapsGenerated() {
        return mipsWereGenerated;
    }
    
    /**
     * (Package private) Called by {@link Texture} when 
     * {@link #isMipmapsGenerated() } is false in order to generate
     * mipmaps for this image.
     */
    void setNeedGeneratedMipmaps() {
        needGeneratedMips = true;
    }

because setNeedGeneratedMipmaps is like only not public method. Like someone forgot to put there “public” or it is not intended to be public one.

But setMinFilter() is using it via:

        if (minificationFilter.usesMipMapLevels() && image != null && !image.isGeneratedMipmapsRequired() && !image.hasMipmaps()) {
            image.setNeedGeneratedMipmaps();
        }

So if we will look again at Exporter start object:

It do have marked that Image need generate Mips.

Tho it will not save Image because its not in list for saving it seems:

while lets look Importer:

It have image, tho it export as not need to generate mips.

just to say Image contructor or methods changing it as i seen were not exected during import.

Im currently trying to find out why Importer is loosing “needGeneratedMips = true”
Because i understand even new image should have it. But maybe it is because of TextureKey that have generateMips as false;

Normally if texture would use .setImage() it would auto-correct this, but capsule is not executing this method.

1 Like

After further investigation

As i notice, Image moment after import it do seems to start having minimaps, so idk if its real case:

But still why executing again:

tex.getTextureValue().setMinFilter(Texture.MinFilter.Trilinear);

helps the issue? where it only affect mipmap param really.

What if you change this:

…to call the setter instead of setting the field directly? Same for mag filter.

These fields seem to have additional setup in setMinFilter()/setMagFilter() that this code is bypassing.

2 Likes

im currently using Intellij de-code/source class to debug, i would need clone JME and move all my test from my project into new TestCase to change anything.

But from Debug it is proper:

IMO something related to mipmaps is breaking filtering.

For example:
tex.getTextureValue().setMinFilter(Texture.MinFilter.Trilinear);
is not changing value because its already Trilinear, but it fix issue, and all it do is:

    public void setMinFilter(MinFilter minificationFilter) {
        if (minificationFilter == null) {
            throw new IllegalArgumentException(
                    "minificationFilter can not be null.");
        }
        this.minificationFilter = minificationFilter;
        if (minificationFilter.usesMipMapLevels() && image != null && !image.isGeneratedMipmapsRequired() && !image.hasMipmaps()) {
            image.setNeedGeneratedMipmaps();
        }
    }

That would tell me that image.setNeedGeneratedMipmaps(); fix the issue when executed.

Edit:

doing exactly same what @JhonKkk did in beginning but showing more params its like:

Geom name:#CAM0001_Shutter_Speed_#CAM0001_Textures_0
Mat Name:CAM0001_Textures
MinFilter:Trilinear
MagFilter:Bilinear
AnisotropicFilter:0
texture.getImage().isGeneratedMipmapsRequired():true
texture.getImage().isMipmapsGenerated():false
texture.getImage().isUpdateNeeded():true
---------------------------------------
!!! RELOADING MODEL (BinaryExporter.saveAndLoad) !!!
---------------------------------------
---------------------------------------
Geom name:#CAM0001_Body_#CAM0001_Textures_0
Mat Name:CAM0001_Textures
MinFilter:Trilinear
MagFilter:Bilinear
AnisotropicFilter:0
texture.getImage().isGeneratedMipmapsRequired():false
texture.getImage().isMipmapsGenerated():false
texture.getImage().isUpdateNeeded():true
---------------------------------------
Geom name:#CAM0001_Battery_Check_#CAM0001_Textures_0
Mat Name:CAM0001_Textures
MinFilter:Trilinear
MagFilter:Bilinear
AnisotropicFilter:0
texture.getImage().isGeneratedMipmapsRequired():false
texture.getImage().isMipmapsGenerated():false
texture.getImage().isUpdateNeeded():true
---------------------------------------

Loading GLTF it setup “texture.getImage().isGeneratedMipmapsRequired()” as true
SaveAndReload binary setup “texture.getImage().isGeneratedMipmapsRequired()” as false

Also Image as i noticed is not really Saved(Exported). Texture is, but not image itself.

So even when Importer import Texture, the image is probably just loaded by assetmanager.

1 Like

And if read() called setMinFilter() instead of setting the field directly then wouldn’t that end up calling setNeedGeneratedMipmaps()?

When a setter does additional work, anywhere else the field is set directly is automatically a code smell… especially without a “we are doing this on purpose because” comment.

I don’t know what you mean be “proper” here… since the problem data isn’t even shown there.

This is a weird statement to me. How is your code setup? Is it a separate project?

Do folks not know about using “gradle install” to make custom JME builds available to their local projects? Or is there something else preventing you from doing that? Or do you just not have a JME clone at all?

Maybe it’s just weird to me because I always have a local clone.

1 Like

Knowing it and using it - are 2 different things. like i said 99 times on this hub, i am not using fork or anything modified.

Maybe it’s just weird to me because I always have a local clone.

Indeed, myself im not using any clone because i want always be sure to use correct base-code.
If i will end up having local clone i would probably end-up like many people with own forks/custom codes within engine.

Ofc i could download and edit engine temporarly, but currently im just debugging so if i catch issue, then anyone having local clone can quickly verify it. (like Jhonkkk could just change line and verify it was this)

For me, the local clone has a different version than the official versions. So Lemur, etc. when I build those they are always against public releases while my own projects can refer to the local build.

Just never ever use wild-cards in your dependencies. :slight_smile:

ok, so

→ For GLTF it start with:
step 1) Texture tex = info.getManager().loadTexture(key);
Calls setMinFilter() Where initially
isGeneratedMipmapsRequired is False and
minificationFilter.usesMipMapLevels() is False
because it reads BilinearNoMipMaps.

step2) GLTFLoader setup texture.setMinFilter(minFilter);
Calls setMinFilter() again
isGeneratedMipmapsRequired is False and
minificationFilter.usesMipMapLevels() is True
because this time it reads Trilinear

Later it read textures from cache.

→ for BinaryExporter.saveAndLoad(assetManager, model);

step 1) here it also use loadTexture(key)
Calls setMinFilter() Where initially
isGeneratedMipmapsRequired is False and
minificationFilter.usesMipMapLevels() is False
because it reads BilinearNoMipMaps.

step 2) It reads calpsule public void read(JmeImporter importer) for Texture
But here difference is it do not call setMinFilter() again.

Summary:

  • GLTF loader execute additional line via texture.setMinFilter()
        if (minificationFilter.usesMipMapLevels() && image != null && !image.isGeneratedMipmapsRequired() && !image.hasMipmaps()) {
            image.setNeedGeneratedMipmaps();
        }
  • BinaryImporter do not execute additional line via texture.setMinFilter()
    it just read value:
        anisotropicFilter = capsule.readInt("anisotropicFilter", 1);
        minificationFilter = capsule.readEnum("minificationFilter",
                MinFilter.class,
                MinFilter.BilinearNoMipMaps);
        magnificationFilter = capsule.readEnum("magnificationFilter",
                MagFilter.class, MagFilter.Bilinear);

Possible Solution:
Move:

        if (minificationFilter.usesMipMapLevels() && image != null && !image.isGeneratedMipmapsRequired() && !image.hasMipmaps()) {
            image.setNeedGeneratedMipmaps();
        }

into private method executed for both setMinFilter() and read(JmeImporter importer) not just first one.

@JhonKkk Can you please verify if this might help? (you had fork to test so i belive you could)

Texture.java
just add:

        if (minificationFilter.usesMipMapLevels() && image != null && !image.isGeneratedMipmapsRequired() && !image.hasMipmaps()) {
            image.setNeedGeneratedMipmaps();
        }

below:

        anisotropicFilter = capsule.readInt("anisotropicFilter", 1);
        minificationFilter = capsule.readEnum("minificationFilter",
                MinFilter.class,
                MinFilter.BilinearNoMipMaps);
        magnificationFilter = capsule.readEnum("magnificationFilter",
                MagFilter.class, MagFilter.Bilinear);

i guess there should be new ticket in JME not for JMEC

created one:

2 Likes

If a class has a setter for a field then the setter should be used to set that field.

Possible solution: fix read() to call setMinFilter() and setMagFilter() instead of setting fields directly.

Won’t that also fix the problem and also get rid of the code smell?

3 Likes

yes, you are right. Maybe you can add this comment into github too :slight_smile:

i still hope someone verify my theory that this issue cause OP issue.

1 Like

Thank you @oxplay2 @pspeed , by adjusting the code to directly read j3o I got correct texture filtering + mipmap results:


6 Likes

There is another issue. I replaced the jme3-core.jar in the SDK and got correct texture filtering + mipmapping results when running TestMiinmMapMatParams.java. However, when viewing in SceneComposer it is still incorrect. It seems like the SDK needs separate fixes for the SceneComposer part?
I wonder if you could provide some help on this part?@tonihele

2 Likes

I’ll look into it. I’m not sure if replacing the jar would work on an installed SDK. Depending on how you did it of course.

3 Likes

I have fixed it here(But I think the SDK SceneComposer should be fixed separately…):

3 Likes

By default, SceneComposer is built using the released version of the jme3-core library, which is currently v3.6.1-stable.

If you install the Engine locally, using ./gradlew install then you can specify that local version (i.e. v3.7.0-SNAPSHOT) in the SDK buildscipts.

3 Likes