Using simplepbr renders native blender geometry and textures black

Hello guys,

I have a problem with native (non blender-exported) geometry showing up pitch black. When transporting my blender models to Panda3D, I would like to avoid egg-files in favor of the more native export of glTF files.

After import of the simplepbr module, the glTF models CAN be loaded (without error), and after calling it’s .init method in the Base subclass constructor, the textures even look correct.

The problem is that if I call the .init method, the geometry generated by panda3d shows up as pitch black. No texture, no basic shading (although there is a directional light from above).

I have created a dedicated small program to show the issue.

from panda3d.core import loadPrcFileData, DirectionalLight
from direct.showbase.ShowBase import ShowBase

from panda3d.core import GeoMipTerrain, Filename
import simplepbr

loadPrcFileData("", """
win-size 1280 720
window-title My Game
""")

    
class MyGame(ShowBase):

    def __init__(self):
        super().__init__()
        #simplepbr.init()        ##  <== Here lies the problem! Commented out, it shows the texture.
        FILENAME = r"d:\python38\work\panda3d\01_start\textures\height129.png"

        dlight = DirectionalLight("sun")
        dlnp = self.render.attachNewNode(dlight)
        dlnp.setHpr(0, -90, 0)
        self.render.setLight(dlnp)

        tex = self.tex = self.loader.loadTexture(Filename.fromOsSpecific(FILENAME))
        self.terrain = GeoMipTerrain("mySimpleTerrain")
        self.terrain.setHeightfield(Filename.fromOsSpecific(FILENAME))
        self.terrain.getRoot().reparentTo(self.render)
        self.terrain.getRoot().setScale(1, 1, 30)
        self.terrain.getRoot().setPos(-60, 30, 0)
        self.terrain.getRoot().setTexture(tex)
        self.terrain.generate()

        self.cam.setPos(0, -100, 120)
        self.cam.setHpr(0, -30, 0)
    
game = MyGame()
game.run()

Here’s the referenced heightmap “height129.png”:
grafik

With the .init commented out, it looks like this:

WITH .init(), it looks like this:

I’m sorry, I didn’t find the simplepbr documentation and maybe it is a simple flag-setting issue. But I wonder, why the simplepbr works so exclusively against panda’s native geometry generation and texturing and I don’t find it mentioned, anywhere. I feel like I will smash my table with my forehead at the first answer to this question. :wink:

Many thanks if you could tell me how to get blender exported glTF models and panda generated geometry to coexist peacefully next to each other. :slight_smile:

BR
Michael

I imagine that the problem is that simplepbr assumes that its various textures are present, and the fallback values that result when they aren’t produce the output that you’re seeing.

One way to fix this might be to just add appropriate textures to your Panda-generated geometry, in the appropriate order. (Even if you just use simple solid-colour textures as stand-ins.)

1 Like

Thanks a lot for this instant response! :slight_smile:

But I’m not sure what you imply by “appropriate textures […] in the appropriate order.”?
Shouldn’t I load the texture with loader.loadTexture? Shouldn’t I use .setTexture on the nodepath or just at another place?

I didn’t mention it, but I get the same result for polygons generated by the panda CardMaker.

The “loadTexture” and “setTexture” methods are correct, I believe!

Essentially this: Going by the documentation, simplepbr requires that a model have four textures, applied to the model in the following order:

  • Colour
  • “MetalRoughness”
  • Normals
  • and Emission

I suspect that “Emission” can be omitted, but without all three of the rest, simplepbr (or Panda) may be falling back on default values, and those default values may be resulting in the effect that you’re seeing.

Now, there may be other requirements to getting this working–I don’t know, being relatively-unfamiliar with simplepbr myself. However, at the least it looks like you’re currently only applying a single texture, which seems likely to be one problem, at least.

[edit]
After that, you may have to set up a proper material for your surface–but let’s see whether just adding the above-mentioned textures helps, first.

Ok, that kind of makes sense, if the first texture is not regarded as diffuse/albedo and the remaining textures as not available/needed.

In the simplepbr git’s readme, I just read that these textures are expected in the respective slots 0-3. Do these “slots” refer to panda’s texture stages? So, I would assign simple black textures to the other stages for the geometry generated by GeoMipMapping?
I’ll have a try, then. Thank you!

This I honestly don’t know, I’m afraid. I would guess as much–and it seems work trying!

I’m not sure about black textures, specifically.

For normals black likely won’t work well–perhaps use a colour with mid-values for red and green, and full value for blue.

For “metallicity-roughness” it might be safer to use mid-grey as a testing-value, at least until you confirm that the approach is working.

Emission can safely be black however, I think.

Ok, thanks for the hints.
Just as a remark: If I read correctly, the normal maps are by default turned off, so I thought it was just about filling the slot. But you may be right to not take chances and just use a section of an existing normal map.

Ah, right–I’d forgotten that. Normal-maps are indeed disabled by default. But on the other hand, I don’t know what simplepbr does when they’re not present at all, so as you say, it might be wiser to include them anyway while testing.

I conducted a test with the software creation of texture modes based on the post @Thaumaturge

https://discourse.panda3d.org/t/simplepbr-no-normals-or-emission

And I found out that it does not work as expected. I repeated the values from the Egg file.

Modified. pbrTest.egg (6.5 KB)

from direct.showbase.ShowBase import ShowBase
from panda3d.core import PandaNode, NodePath, DirectionalLight, TextureStage, SamplerState, Texture

import simplepbr

class Game(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        simplepbr.init(use_normal_maps=True)

        self.accept("escape", base.userExit)

        self.updateTask = taskMgr.add(self.update, "update")

        self.model = loader.loadModel("pbrTest")
        self.model.reparentTo(render)
        
        tex0 = loader.loadTexture("tex/colour.png")
        tex0.setMagfilter(SamplerState.FT_linear_mipmap_linear)
        tex0.setMinfilter(SamplerState.FT_linear_mipmap_linear)
        tex0.setWrapU(Texture.WM_repeat)
        tex0.setWrapV(Texture.WM_repeat)

        colour = TextureStage('colour')
        colour.setMode(TextureStage.MModulate)
        self.model.setTexture(colour, tex0)

        tex1 = loader.loadTexture("tex/normals.png")
        tex1.setMagfilter(SamplerState.FT_linear_mipmap_linear)
        tex1.setMinfilter(SamplerState.FT_linear_mipmap_linear)
        tex1.setWrapU(Texture.WM_repeat)
        tex1.setWrapV(Texture.WM_repeat)

        normals = TextureStage('normals')
        normals.setMode(TextureStage.MNormal)
        self.model.setTexture(normals, tex1)
        
        tex2 = loader.loadTexture("tex/metalRoughness.png")
        tex2.setMagfilter(SamplerState.FT_linear_mipmap_linear)
        tex2.setMinfilter(SamplerState.FT_linear_mipmap_linear)
        tex2.setWrapU(Texture.WM_repeat)
        tex2.setWrapV(Texture.WM_repeat)

        metalRoughness = TextureStage('metalRoughness')
        metalRoughness.setMode(TextureStage.MSelector)
        self.model.setTexture(metalRoughness, tex2)

        tex3 = loader.loadTexture("tex/emission.png")
        tex3.setMagfilter(SamplerState.FT_linear_mipmap_linear)
        tex3.setMinfilter(SamplerState.FT_linear_mipmap_linear)
        tex3.setWrapU(Texture.WM_repeat)
        tex3.setWrapV(Texture.WM_repeat)

        emission = TextureStage('emission')
        emission.setMode(TextureStage.MEmission)
        self.model.setTexture(emission, tex3)

        light = DirectionalLight("mew")
        light.setColor((1, 1, 1, 1))
        lightNP = render.attachNewNode(light)
        lightNP.setHpr(5, -5, 0)
        render.setLight(lightNP)

    def update(self, task):
        dt = globalClock.getDt()

        self.model.setH(self.model, dt*17)

        return task.cont

app = Game()
app.run()

result:

Maybe I missed something. Also, for GeoMipTerrain, you will need to take care of tangents.

I think that it might be the order in which you added your textures: I believe that “metallicity-roughness” should be in the second slot, and normals in the third, not the other way around.

And indeed, a quick experiment seems to indicate that it is so: simply swapping around the chunks of code for “metallicity-roughness” and normals seems to result in the expected output.

That is, this code:

from direct.showbase.ShowBase import ShowBase
from panda3d.core import PandaNode, NodePath, DirectionalLight, TextureStage, SamplerState, Texture

import simplepbr

class Game(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        simplepbr.init(use_normal_maps=True)

        self.accept("escape", base.userExit)

        self.updateTask = taskMgr.add(self.update, "update")

        self.model = loader.loadModel("pbrTest")
        self.model.reparentTo(render)
        
        tex0 = loader.loadTexture("tex/colour.png")
        tex0.setMagfilter(SamplerState.FT_linear_mipmap_linear)
        tex0.setMinfilter(SamplerState.FT_linear_mipmap_linear)
        tex0.setWrapU(Texture.WM_repeat)
        tex0.setWrapV(Texture.WM_repeat)

        colour = TextureStage('colour')
        colour.setMode(TextureStage.MModulate)
        self.model.setTexture(colour, tex0)
        
        # The change is here, I believe: this section, between
        # the comment labelled "start 1" and the comment
        # labelled "end 1", has been swapped with the 
        # next section, between the comment labelled "start 2"
        # and the comment labelled "end 2"

        # start 1
        tex2 = loader.loadTexture("tex/metalRoughness.png")
        tex2.setMagfilter(SamplerState.FT_linear_mipmap_linear)
        tex2.setMinfilter(SamplerState.FT_linear_mipmap_linear)
        tex2.setWrapU(Texture.WM_repeat)
        tex2.setWrapV(Texture.WM_repeat)

        metalRoughness = TextureStage('metalRoughness')
        metalRoughness.setMode(TextureStage.MSelector)
        self.model.setTexture(metalRoughness, tex2)
        # end 1

        # start 2
        tex1 = loader.loadTexture("tex/normals.png")
        tex1.setMagfilter(SamplerState.FT_linear_mipmap_linear)
        tex1.setMinfilter(SamplerState.FT_linear_mipmap_linear)
        tex1.setWrapU(Texture.WM_repeat)
        tex1.setWrapV(Texture.WM_repeat)

        normals = TextureStage('normals')
        normals.setMode(TextureStage.MNormal)
        self.model.setTexture(normals, tex1)
        # end 2

        tex3 = loader.loadTexture("tex/emission.png")
        tex3.setMagfilter(SamplerState.FT_linear_mipmap_linear)
        tex3.setMinfilter(SamplerState.FT_linear_mipmap_linear)
        tex3.setWrapU(Texture.WM_repeat)
        tex3.setWrapV(Texture.WM_repeat)

        emission = TextureStage('emission')
        emission.setMode(TextureStage.MEmission)
        self.model.setTexture(emission, tex3)

        light = DirectionalLight("mew")
        light.setColor((1, 1, 1, 1))
        lightNP = render.attachNewNode(light)
        lightNP.setHpr(5, -5, 0)
        render.setLight(lightNP)

    def update(self, task):
        dt = globalClock.getDt()

        self.model.setH(self.model, dt*17)

        return task.cont

app = Game()
app.run()

And the output is like so:

1 Like

A short example of how to configure textures for SimplePBR.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import DirectionalLight, TextureStage

import simplepbr

class Game(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        simplepbr.init(use_normal_maps=True)

        self.accept("escape", base.userExit)

        self.updateTask = taskMgr.add(self.update, "update")

        self.model = loader.loadModel("pbrTest")
        self.model.reparentTo(render)
        
        tex0 = loader.loadTexture("tex/colour.png")
        colour = TextureStage('colour')
        colour.setMode(TextureStage.MModulate)
        self.model.setTexture(colour, tex0)

        tex1 = loader.loadTexture("tex/metalRoughness.png")
        metalRoughness = TextureStage('metalRoughness')
        metalRoughness.setMode(TextureStage.MSelector)
        self.model.setTexture(metalRoughness, tex1)
        
        tex2 = loader.loadTexture("tex/normals.png")
        normals = TextureStage('normals')
        normals.setMode(TextureStage.MNormal)
        self.model.setTexture(normals, tex2)

        tex3 = loader.loadTexture("tex/emission.png")
        emission = TextureStage('emission')
        emission.setMode(TextureStage.MEmission)
        self.model.setTexture(emission, tex3)

        light = DirectionalLight("mew")
        light.setColor((1, 1, 1, 1))
        lightNP = render.attachNewNode(light)
        lightNP.setHpr(5, -5, 0)
        render.setLight(lightNP)

    def update(self, task):
        dt = globalClock.getDt()

        self.model.setH(self.model, dt*17)

        return task.cont

app = Game()
app.run()

For the generated models, you need to configure the material, as well as create tangents. This applies to CardMaker and GeoMipTerrain.

1 Like

All this has to be done for one auto-generated card or geomipmap? I’d need several hundred or thousands of them, to simulate the patchwork map. Is there a limit in materials simplepbr can handle like that?

Does it mean that the Panda3D internal shader takes care for all that, including the generation and application of tangents for the generated map, simply by calling the setTexture method?
It appears to be exponentially more complicated to use simplepbr just to handle the glTF models.

Is there another, simpler way to load the glTF models (and only with their diffuse and maybe normal maps)? Or another up-to-date way to get blender models into my game without having to install extra exporters or importers? Best with animations and bones, but also if I don’t need them (like for this game).

This means the generated model programmatically requires this data, it is only needed for geometry. You can get it through .gltf by default if you will export from the blender.

Regarding the configuration of textures in the blender, the output may be as follows: to insert dummy textures for unused shader techniques with a size of 2*2. However, I have no idea what the texture order will look like after exporting. Need to check I think.

I would post an announcement about my flight simulator project, soon. But for now, to clarify:
In my simulator, I would have models like the plane that are designed in and exported from blender.

And in parallel, I would dynamically load map tile images from google maps, create/update terrain tiles and apply the loaded satellite textures.

For the first, I would like to use glTF format to transfert the models to Panda3D. For the latter, I would like to auto-create geometry and just apply the diffuse texture, simple as that.

Can I somehow get the best from both worlds?

In this option, you just need to assign the textures and their stubs in the correct order. You don’t need to adjust the textures, as I showed above in the example. In fact, for example, if the material was not installed, panda uses the default material. The same is the case with texture parameters.

However, the material still needs to be configured, since the default model is displayed incorrectly. It’s too dark here, as you noticed above. But you don’t need to create a material for each geometry separately, you can have one instance for all.

I added an archive with the necessary textures. The only thing you need to check with @Moguri is which colors to use for dummy textures.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import DirectionalLight, TextureStage, Material

import simplepbr

class Game(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        simplepbr.init(use_normal_maps=True)

        myMaterial = Material()

        model = loader.loadModel("pbrTest")
        model.setMaterial(myMaterial)
        model.reparentTo(render)

        colour = TextureStage('colour')
        colour.setMode(TextureStage.MModulate)
        model.setTexture(colour, loader.loadTexture("tex/colour.png"))

        metalRoughness = TextureStage('metalRoughness')
        metalRoughness.setMode(TextureStage.MSelector)
        model.setTexture(metalRoughness, loader.loadTexture("tex/metalRoughness.png"))

        normals = TextureStage('normals')
        normals.setMode(TextureStage.MNormal)
        model.setTexture(normals, loader.loadTexture("tex/normals.png"))

        light = DirectionalLight("mew")
        light.setColor((1, 1, 1, 1))
        lightNP = render.attachNewNode(light)
        lightNP.setHpr(5, -5, 0)
        render.setLight(lightNP)

app = Game()
app.run()

pbrTest.zip (110.0 KB)

I think I’m confused. This will also be enough.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import DirectionalLight, TextureStage, Material

import simplepbr

class Game(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        simplepbr.init(use_normal_maps=True)

        model = loader.loadModel("pbrTest")
        model.setMaterial(Material())
        model.reparentTo(render)

        colour = TextureStage('colour')
        colour.setMode(TextureStage.MModulate)
        model.setTexture(colour, loader.loadTexture("tex/colour.png"))

        light = DirectionalLight("mew")
        light.setColor((1, 1, 1, 1))
        lightNP = render.attachNewNode(light)
        lightNP.setHpr(5, -5, 0)
        render.setLight(lightNP)

app = Game()
app.run()

Empty textures are not needed if there is one texture.

1 Like

That would be just perfect! Thank you so much. I’ll test it as soon as possible. :slight_smile:

Just one question: You’ve set the normal maps usage to one, in the init of both examples. So I can omit it, when I don’t have a normal tex and can add the 3 lines of the prev. last example (with the normal texture stage), if I want to add one?

[edit]
Oh, and I can reuse the TextureStage objects for different model/texture combinations? That would be great.