Multisampling messes up pixel-perfect GUI

Above is my button image in Panda3D, below in an image editor:

You can notice that the edges of the image get feathered in Panda.
After some trial and error, I noticed that if I disable multisampling in my program, the problem is fixed.
So If I just comment out these lines the problem is gone:

load_prc_file_data("", "framebuffer-multisample 1")
load_prc_file_data("", "multisamples 8")
base.render.setAntialias(AntialiasAttrib.MMultisample)

But I want multisampling, I guess just not at the GUI.
I tried disabling it with the two below ways, but neither seems to work, or at least neither works the same way as not enabling multisampling to begin with.

base.pixel2d.setAntialias(AntialiasAttrib.MNone)
base.pixel2d.clearAntialias()

What should be done?

What happens if you manually load your GUI-textures (i.e. by calling something like “loader.loadTexture”), and in doing so pass in a second parameter of “minfilter = SamplerState.FTNearest”? (First importing “SamplerState”, of course.)

Something like this:

# In your importations:
from panda3d.core import SamplerState

# Elsewhere:
texture = myShowBaseObj.loader.loadTexture(
                                 "someTextureFile.png",
                                 minfilter = SamplerState.FTNearest
                                          )
button = DirectButton(frameTexture = texture, <other parameters here>)

[edit] In fact, the use of SamplerState might not be required when loading a texture manually–I think that “Nearest” might be the default for that. So it might be worth first trying manual loading without the “SamplerState”, and seeing whether that works, before trying the version with the “SamplerState”.

If you set up render-to-texture for the main scene (such as via FilterManager), you can set multisampling for that buffer only, while leaving it off for the main window.

1 Like

Nothing changes.

I’m already loading texture manually, here’s the code for that:

tex = loader.loadTexture(filepath, minfilter = SamplerState.FTNearest)
tex.setBorderColor(Vec4(0,0,0,0))
tex.setWrapU(Texture.WMBorderColor)
tex.setWrapV(Texture.WMBorderColor)
cm = CardMaker(filepath + ' card')
cm.setFrame(-tex.getOrigFileXSize()/2, tex.getOrigFileXSize()/2, -tex.getOrigFileYSize()/2, tex.getOrigFileYSize()/2)
card = NodePath(cm.generate())
card.setTexture(tex)
card.setTransparency(TransparencyAttrib.MAlpha)
card.flattenLight()
card.reparentTo(pixel2d)

I’ve made sure the sizing is correct, by overlaying a png of the texture over the screenshot of a panda3d window with the image loaded in it.

Doesn’t sound like an easy solution.

CommonFilters won’t be usable on it, need to keep track of various things manually…

Well, if you are using CommonFilters already (which uses FilterManager under the hood), in fact, it is a waste of memory to have multisampling enabled on the main window at all, especially if you don’t want your GUI to be multisampled.

The only issue is that CommonFilters does not currently allow overriding the framebuffer properties requested from its FilterManager. Hmm. For now, a solution would be to copy-paste CommonFilters.py into your game’s source code and change the fbprops that are passed to renderSceneInto to include multisample bits. I will look into making this possible for the next Panda release.

1 Like

I’m not currently using CommonFIlters, but was planning to to allow outline rendering for 3d models in my 3d viewer.

I think that’s only one issue with your proposal. From the looks of it if I render to my own buffer than I need to take care of host of other things, like managing resolution changes after window resizing, change of aspect ratio of the window, etc, etc.

Sounds like a better fix for Panda would be to just allow to disable multisampling for GUI, otherwise pixel2d node does not do what it claims: " It can be used if pixel-perfect layout of UI elements is desired." Built-in Global Variables — Panda3D Manual
Currently it seems like you need to jump through a bunch of hoops to get it to work as claimed, if you want something as basic as multisampling in your program alongside pixel2d.

Nope, this is all automated by FilterManager, it’s just set-up and forget, that’s the whole point of that class.

This is outside our control! Panda is already doing all it can to honour your setAntialias request, but the driver is stubbornly choosing to enable antialiasing on everything instead. All Panda can do for you is automate setting up a buffer for render-to-texture, which most games will end up having to do anyway, and this is provided by FilterManager. :slight_smile:

I’m not sure if we’ve tested everything to come to this conclusion.
What would be the simplest test to identify this?
As a reminder, right now I’m enabling multisampling globally via the PRC settings, and then trying to disable it on one node (pixel2d).

If I enable antialias only on a child node of render, then parent another node to render, would this mean that the driver works correctly if the second node attached to render has antialiasing disabled and only the first child node has it enabled?

In OpenGL, it takes two things to enable multisample antialiasing: first, the window framebuffer needs to be allocated with the desired number of samples (which is what the PRC settings do), and secondly, you need to enable the multisample antialiasing modes (setAntialias) to take advantage of this.

The thing is, Panda doesn’t do the second automatically at all. On my NVIDIA card, I get multisample antialiasing just from enabling a multisample framebuffer, without even requesting it with an antialias mode. I verified using apitrace that Panda is indeed making glEnable(GL_MULTISAMPLE) and glDisable(GL_MULTISAMPLE) calls before rendering objects with the appropriate antialias mode set.

I tried playing with this setting in the NVIDIA Control Panel:


But setting that to “Off” just makes OpenGL report to Panda that no multisampling is supported at all.

This appears to be a long-standing “bug” in the NVIDIA drivers:

In that thread, the suggestion is also offered of using render-to-texture to achieve what you want.

In the next Panda release (1.10.13), I am adding a setMSAA method to CommonFilters that enables multisample antialiasing on the offscreen buffer. This is intended as a foolproof “just works” way to set up MSAA for your main scene only. It requires leaving framebuffer-multisample off, since then you’re applying it to the main window as well, which you don’t want.

2 Likes

Just update regarding this, I’ve gotten the latest 1.11.0 experimental build to try this.

Should I just be doing

base.render.set_antialias(panda3d.core.AntialiasAttrib.M_multisample)
filters = CommonFilters(base.win, base.cam)
filters.setMSAA(8)

and commenting out “multisamples 8” from the RPC file?
Doesn’t seem to work.

And I’m not able to do base.set_background_color() after setting up CommonFilters.

Can you clarify “doesn’t seem to work”?

Sure, I mean I don’t see the anitaliasing effect this way.
And base.set_background_color() doesn’t affect the background color anymore.

Not sure how else to verify that it doesn’t work, if there is a better way let me know and I’ll share the sample code for that and the resulting screenshots.

I took Roaming Ralph, changed the background color to white, took this screenshot:
image
Then I added these lines:

        cf = CommonFilters(base.win, base.cam)
        cf.setMSAA(8)

And now the edges of geometry are far less jagged:
image

So, the MSAA working for me. I need some more info about your system configuration (OS, GPU, etc.) to be able to dig further.

Issue filed. There’s a workaround listed in the issue description:

Oh I’m sorry, I think I see what the issue is.

I use my own code for generating geoms from images for my GUI (I see no reason why Panda by default expects an egg or other 3d file for UI images), and also for my 3d grid. And for these, magfilter = panda3d.core.SamplerState.FT_nearest makes sense for their textures, otherwise gui graphics get feathered at their native resolution, and the grid lines also get feathered. But magfilter = panda3d.core.SamplerState.FT_nearest seems to mess up any antialiasing.

here is a test code:

import panda3d.core
from direct.showbase.ShowBase import ShowBase
from direct.filter.CommonFilters import CommonFilters

#panda3d.core.load_prc_file_data("", "framebuffer-multisample 1")
#panda3d.core.load_prc_file_data("", "multisamples 8")

base = ShowBase()

base.render.set_antialias(panda3d.core.AntialiasAttrib.M_multisample)
filters = CommonFilters(base.win, base.cam)
filters.setMSAA(8)
filters.manager.buffers[0].setClearColor((0,0,0,1))


def load_image_as_plane(filepath):
	"""
	Load image as textured 3d plane, return Panda3D NodePath
	"""
	
	tex = base.loader.load_texture(filepath, magfilter = panda3d.core.SamplerState.FT_nearest)	
	tex.set_border_color(panda3d.core.Vec4(0,0,0,0))
	tex.set_wrap_u(panda3d.core.Texture.WMBorderColor)
	tex.set_wrap_v(panda3d.core.Texture.WMBorderColor)
	cm = panda3d.core.CardMaker(filepath + ' card')
	cm.set_frame(-tex.get_orig_file_x_size()/2, tex.get_orig_file_x_size()/2, -tex.get_orig_file_y_size()/2, tex.get_orig_file_y_size()/2)
	card = panda3d.core.NodePath(cm.generate())
	card.set_texture(tex)
	card.set_transparency(panda3d.core.TransparencyAttrib.M_dual)
	
	return card

model = load_image_as_plane("image.png")
model.reparent_to(base.render)
model.set_r(45)


base.run()

This is the image.png used:

https://i.imgur.com/DYDCSWr.png

This is the result without doing magfilter = panda3d.core.SamplerState.FT_nearest :

And here is the result with it:

I’m not sure if there’s any workaround here, sort of just using a very high resolution grid image so that the lines will both look sharp but won’t also severely alias.

Antialiasing applies to geometry edges, not to texture alpha cutout edges. This is because the texture lookup is done per fragment, and not per sample.

There are modes that enable multisampling for transparency. These will convert an alpha value sampled from the texture into a sample mask that will be combined with the normal geometry-edge sample mask:

    card.set_transparency(panda3d.core.TransparencyAttrib.M_multisample)
    #card.set_transparency(panda3d.core.TransparencyAttrib.M_multisample_mask)

The problem with this (you’ll note neither mode does anything here) is that you’ll never sample an alpha value other than 0 or 1 if you don’t have any kind of texture filtering, so each fragment is either in or out. With linear sampling, you would definitely need a higher-resolution texture. Though I would suggest picking a black-white greyscale texture and setting its format to F_alpha so that the colour channel is entirely white, so you don’t get odd blending at the corners.

What you would really need to make this work with multisampling at any zoom level is a shader that calculates the appropriate sample mask based on multiple texture lookups, one for each sample. There is also an OpenGL extension that makes the fragment shader be run for every sample instead of for every fragment. Panda doesn’t currently implement it, but it would be easy to add, so I would be happy to entertain a feature request for this. It is inefficient, though, at that point it would be far more efficient to just generate your grid as geometry instead of a texture.

1 Like

Another good way to create an antialiased grid is using a shader, by the way. The fwidth function can be used to produce decent antialiasing using the partial derivatives.

https://madebyevan.com/shaders/grid/

This is an aside, but I daresay that it’s because even UI widgets are technically 3D objects in Panda. Indeed, one can embed them into the 3D world if one desires. (Although I think that doing so complicates interacting with them, as the default interaction system is built around aspect2d, if I recall correctly.)

That said, depending on what you’re doing, you don’t necessarily need to construct this 3D geometry yourself. You can specify either the “frameTexture” keyword-argument or the “image” keyword-argument, providing them with either textures or paths to images (or tuples thereof–at least in the case of “frameTexture”; I’m not sure offhand about “image”).

Both of those approaches have their own bits of trickiness to get used to, but (again depending on your specific purposes) they may allow you to apply images to your widgets without constructing geometry yourself.

Like so:

btn = DirectButton(text = "Kittens",
                   scale = 0.1,
                   frameSize = (-1, 1, -0.5, 0.5),
                   relief = DGG.FLAT,
                   frameTexture = (
                     "path/normal.png",
                     "path/clicked.png",
                     "path/highlighted.png",
                     "path/disabled.png"
                   )
                 )
1 Like

The manual pages on Direct GUI should really tell more. “image” is described there as one texture object, and frameTexture is not explained, and samples seem to use geom argument instead.