Display glitches with multiple windows of different aspect ratio

I’m trying to display multiple windows. To this end, I open a second window using this snippet:

    win = self.openWindow(
        type='onscreen', keepCamera=False, makeCamera=False)
    self.graphicsLens.setAspectRatio(self.getAspectRatio(win))
    self.makeCamera(win, camName='grapĥics', lens=self.graphicsLens)
    camera2d = self.makeCamera2d(win)
    self.setupMouse(win, fMultiWin=False)
    dr = win.makeDisplayRegion()
    dr.setCamera(camera2d)'

Additionally, I have overloaded windowEvent method in order to call self.graphicsLens.setAspectRatio(self.getAspectRatio(win)) for any window not being the main one.

Everything it is not working fine, as long as the aspect ratio is the same for both window. However, as soon as it changes, only the latest window resized display properly when the camera is moved around the scene. Then if I resize the other one, the behavior is the same but reversed. I cannot attach pictures to illustrate this behavior because I don’t have the right to do so, but in practice the ground, the grid and position of the bodies are wrong, and in some cases even their respective positions are corrupted, making the model looks dislocated.

N.B.: I’m trying the resize a offscreen window, but setSize is raising an exception. To get around this limitation, I’m doing

    self.closeWindow(self.win, keepCamera=False)
    self.win = self.openWindow(
        type='offscreen', keepCamera=True, makeCamera=False,
        size=(width, height))

Is this the right way to do it ? Note that I’m doing it very seldomly.

Hi, welcome to the forums!

ShowBase attaches to the window-event handler to watch for a resize event, and then calls self.windowEvent, which in turn calls adjustWindowAspectRatio that updates the lens aspect ratio and the aspect2d render root.

You’re taking the right approach to override windowEvent with your own aspect ratio handling. However, calling self.getAspectRatio() will return you with a single ratio, so I think you just need to replace that with your own calculation, which is simply a matter of dividing the window width by the height.

If you create an offscreen buffer, make sure to to pass in a host window, via the host=self.win flag. That will allow it to create a more efficient kind of buffer that shares its graphics context with the window. I believe that this type of buffer should then also be resizeable.

Hi ! Thank you for your help !

However, calling self.getAspectRatio() will return you with a single ratio

Are you sure about that ? Since I’m passing the window in argument, it should return the right one ? I don’t have any issue regarding the aspect ratio itself. The rendering itself is not distorded, but the models on the scene are not at the right position (including the ground).

If you create an offscreen buffer, make sure to to pass in a host window, via the host=self.win flag. That will allow it to create a more efficient kind of buffer that shares its graphics context with the window. I believe that this type of buffer should then also be resizeable.

Do you have any information /documentation regarding the intended use of host flag ? I did not found anything more than GraphicsEngine.makeOutput.

Here attached screenshot showing the glitches.

Oh, I didn’t realise that getAspectRatio took a window argument. You are right.

The host flag should be set to any already created window, and it will inherit the graphics context from that host if possible. It is just passed into the lower-level makeOutput call that I see you have already found.

In your screenshot it looks almost as if the grid and the model are scaled differently. Are they attached to different scene graphs? It would help to better understand your set-up.

In your screenshot it looks almost as if the grid and the model are scaled differently.

Yes indeed, it is very weird…

Are they attached to different scene graphs? It would help to better understand your set-up.

I don’t really know :confused: I’m using panda3d_viewer, which is adding the grid, ground and axes automatically. It can be installed very easily using pip install panda3d_viewer. The grid, ground and axes are added on the scene using something like this

        model = GeomNode('grid')
        model.add_geom(geometry.make_grid())
        node = self.render.attach_new_node(model)
        node.set_light_off()
        node.set_render_mode_wireframe()
        node.set_antialias(AntialiasAttrib.MLine)
        node.hide(self.LightMask)

Here is a minimal example to reproduce the issue:

from panda3d.core import  PerspectiveLens
from panda3d_viewer.viewer_app import ViewerApp
from panda3d_viewer import ViewerConfig 
    
# Mock windowEvent method
windowEventOld = ViewerApp.windowEvent
def windowEvent(self, win):
    if win != self.win:
        aspectRatio = self.getAspectRatio(win)
        self.graphicsLens.setAspectRatio(aspectRatio)
        self.aspect2d.setScale(1.0 / aspectRatio, 1.0, 1.0)
    windowEventOld(self, win)
ViewerApp.windowEvent = windowEvent

# Instantiate viewer
config = ViewerConfig()
config.set_window_size(500, 500)
config.set_window_fixed(False)
config.enable_antialiasing(True, multisamples=4)
config.enable_shadow(True)
config.enable_lights(True)
config.enable_hdr(True)
config.enable_fog(False)
config.show_axes(True)
config.show_grid(True)
config.show_floor(True)
base = ViewerApp(config)

# Initialize second display
base.graphicsLens = PerspectiveLens()
base.trackball.node().set_forward_scale(
    base.config.GetFloat('trackball-scale', 0.01))
win = base.openWindow(
    type='onscreen', keepCamera=False, makeCamera=False)
base.graphicsLens.setAspectRatio(base.getAspectRatio(win))
cam = base.makeCamera(win, camName='grapĥics', lens=base.graphicsLens)
camera2d = base.makeCamera2d(win)
base.setupMouse(win, fMultiWin=False)
dr = win.makeDisplayRegion()
dr.setCamera(camera2d)

# Run
base.run()

It looks like there’s still only one lens that you use for both windows. That won’t work, because each window needs a lens with different aspect ratio. You need to have two different lens objects, one for each window, rather than set the same lens on both windows.

It looks like there’s still only one lens that you use for both windows.

Hum, I don’t think so. self.camLens is used internally for self.win, and I implemented self.graphicsLens for the additional window.

It is hard to say then. I do not know how the grid is being rendered.

Here is a more complete example, adding the grid manually.

import numpy as np
from panda3d.core import (
    Geom, GeomNode, GeomVertexFormat, GeomVertexData, GeomVertexWriter, 
    GeomLines, AntialiasAttrib, PerspectiveLens)
from panda3d_viewer.viewer_app import ViewerApp
from panda3d_viewer import ViewerConfig 

# Mock windowEvent method
windowEventOld = ViewerApp.windowEvent
def windowEvent(self, win):
    if win != self.win:
        aspectRatio = self.getAspectRatio(win)
        self.graphicsLens.setAspectRatio(aspectRatio)
        self.aspect2d.setScale(1.0 / aspectRatio, 1.0, 1.0)
    windowEventOld(self, win)
ViewerApp.windowEvent = windowEvent

# Instantiate viewer
config = ViewerConfig()
config.set_window_size(500, 500)
config.set_window_fixed(False)
config.enable_antialiasing(True, multisamples=4)
config.enable_shadow(True)
config.enable_lights(True)
config.enable_hdr(True)
config.enable_fog(False)
config.show_axes(True)
config.show_grid(False)
config.show_floor(True)
base = ViewerApp(config)

# Create grid
ticks = np.arange(-5, 6)
vformat = GeomVertexFormat.get_v3()
vdata = GeomVertexData('vdata', vformat, Geom.UHStatic)
vdata.uncleanSetNumRows(len(ticks) * 4)
vertex = GeomVertexWriter(vdata, 'vertex')
for t in ticks:
    vertex.addData3(t, ticks[0], 0)
    vertex.addData3(t, ticks[-1], 0)
    vertex.addData3(ticks[0], t, 0)
    vertex.addData3(ticks[-1], t, 0)
prim = GeomLines(Geom.UHStatic)
prim.addNextVertices(len(ticks) * 4)
grid = Geom(vdata)
grid.addPrimitive(prim)

# Add grid
model = GeomNode('grid')
model.add_geom(grid)
node = base.render.attach_new_node(model)
node.set_light_off()
node.set_render_mode_wireframe()
node.set_antialias(AntialiasAttrib.MLine)
node.hide(base.LightMask)
node.show()

# Initialize second display
base.graphicsLens = PerspectiveLens()
base.trackball.node().set_forward_scale(
    base.config.GetFloat('trackball-scale', 0.01))
win = base.openWindow(
    type='onscreen', keepCamera=False, makeCamera=False)
base.graphicsLens.setAspectRatio(base.getAspectRatio(win))
cam = base.makeCamera(win, camName='grapĥics', lens=base.graphicsLens)
camera2d = base.makeCamera2d(win)
base.setupMouse(win, fMultiWin=False)
dr = win.makeDisplayRegion()
dr.setCamera(camera2d)

# Run
base.run()

After some trial and error, I finally managed to determine which line of code is causing the issue:

node.hide(base.LightMask)
node.show()

Commenting them fix it, but I don’t understand why so far. This mask is used when creating light source:

light = DirectionalLight('Directional Light{:02d}'.format(index))
light.set_color(Vec3(*color))
light.set_camera_mask(self.LightMask)
light.set_shadow_buffer_size(
    (self._shadow_size, self._shadow_size))
lens = light.get_lens()
lens.set_film_size(5.5, 5.5)
lens.set_near_far(10, 30)
node = self.render.attach_new_node(light)
node.set_pos(*pos)
node.look_at(*target)

Commenting light.set_camera_mask(self.LightMask) is also fixing the gliches, but obviously it makes it impossible to disable shadow casting for some object, which is undesirable.

As you can see, it messes up with the perspective if I uncomment them:

Even more surprising, disabling the lights using clear_light also messes up with the perspective. It looks like only the object casting shadow are properly rendered:

make sure to to pass in a host window, via the host=self.win flag.

Apparently, doing this fixes the glitches, but it is not clear to me why.

Regarding the other part my question, namely:

I’m trying the resize a offscreen window, but setSize is raising an exception.

You replied me this:

If you create an offscreen buffer, make sure to to pass in a host window, via the host=self.win flag. That will allow it to create a more efficient kind of buffer that shares its graphics context with the window. I believe that this type of buffer should then also be resizeable.

Unfortunately, it is not possible, since the offscreen is the host window itself in my current usecase :confused:

Here is a self-contained snippet of what I tried:

from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
    loadPrcFileData, GraphicsPipe, WindowProperties, 
    FrameBufferProperties)

loadPrcFileData('', """
    print-pipe-types 0
    notify-level error
    win-fixed-size 0
    framebuffer-multisample 0
    multisamples 0
""")

base = ShowBase(windowType="none")

base.makeDefaultPipe()
pipe = base.pipe
props = WindowProperties()
props.setSize(500, 1000)
fbprops = FrameBufferProperties()
name = 'windowOff'
flags = GraphicsPipe.BFRefuseWindow | GraphicsPipe.BFResizeable
win = base.graphicsEngine.makeOutput(
    pipe, name, 0, fbprops, props, flags)

print(win)  # It returns None...

# Run
base.run()

You can still create a new resizable offscreen buffer, passing in your main non-resizable buffer as a host “buffer”.

This is because the “main” buffer must be a pbuffer as opposed to an FBO. FBOs are very flexible, pbuffers are not.

Ah I see, very interesting approach. Do you think it is better / more efficient than closing the current buffer and creating a new one with the appropriate window size ? I only need to be able to resize the offscreen buffer very seldomly, I would say no more than one time during the whole execution of the program.

Creating a new FBO with the same host pointer or resizing an existing FBO is a relatively fast operation, and the same context is retained. Any graphics resources that have already been uploaded will not need to be reuploaded.

Reopening your “main” buffer will create a new graphics context, so any textures and such will need to be reuploaded to the GPU. If it’s rare, and you don’t mind a little chug here, then that’s a fine approach.

Ok it is very clear. Thank you again for your time. I will try both approach to see how it goes.

For the record, passing in the host GSG via the gsg=self.win.getGsg() flag instead of the host window itself also fixés the glitches, with the additional bonus to make sure a subsequent offscreen buffer is not a ParasiteBuffer and properly support anti-aliasing amont other things. It may be useful for someone to know this.