tipam / pi3d

Simple, yet powerful, 3D Python graphics library for beginners and school children running on the Raspberry Pi.
Other
283 stars 76 forks source link

Blue Screen (Not BSOD) on relaunch of pi3d #254

Open Shando opened 2 years ago

Shando commented 2 years ago

I have tried to launch pi3d multiple times from the same code and every time it works fine the 1st time around and then I get a blue pi3d screen on subsequent launches (i.e. when pressing the '0' key) and the mouse only allows movement in a very small area in the middle of the screen?

Each launch follows calls to: mykeys.close() mymouse.stop() DISPLAY.destroy() as I need to close the pi3d window before loading another map.

See code here: MyTest.zip

This is part of a larger codebase (my GUI for WorldEngine) and I am probably doing something wrong?

Any help would be greatly appreciated

Thanks in advance

Shando

PS: I'm using the following: python 3.6.8 pi3d 2.49 pygame 2.1.1 PyOpenGL 3.1.5

Shando commented 2 years ago

And this is the def for world3dA:

def world3dA(inHeightmap, inWidth, inDepth, inHeight, inTextureMap, inBumpMap, inSeaLevel):

DISPLAY = pi3d.Display.create(0, 0, 1280, 1024)
CAMERA = pi3d.Camera.instance()
base_tex = np.array(Image.open(inTextureMap))
# texture for land
base_gr = base_tex.copy()
ix = np.where(base_gr[:,:,2] > 20)  # i.e. was blue
base_gr[ix[0], ix[1], 1] += 50  # increase green
base_gr[ix[0], ix[1], 2] = 0  # reduce blue
texg = pi3d.Texture(base_gr)
# texture for water
base_bl = base_tex.copy()
base_bl[:,:] = [0, 0, 60, 170]  # uniform slightly transparent
texb = pi3d.Texture(base_bl)
grass_tex = pi3d.Texture('textures/grasstile_n.jpg')
w_norm = pi3d.Texture('textures/n_norm000.png')

shader = pi3d.Shader("uv_bump")
rshader = pi3d.Shader("uv_reflect")
mapwidth = inWidth
mapdepth = inDepth
mapheight = inHeight

mymap = pi3d.ElevationMap(inBumpMap, width=mapwidth, depth=mapdepth, height=mapheight, divx=199, divy=199,
                          ntiles=1, name="sub")
mymap.set_draw_details(shader, [texg, grass_tex], 200.0)
wmap = pi3d.ElevationMap(inBumpMap, width=mapwidth, depth=mapdepth, height=mapheight * 0.1, divx=199, divy=199,
                         ntiles=1, name="water", y=inSeaLevel)
wmap.set_draw_details(rshader, [texb, w_norm, texg], 50.0, 0.2)
rot = 0.0
tilt = 0.0
height = 20.0
viewHeight = 4
sky = 200
xm, ym, zm = 0.0, height, 0.0
onGround = False

mykeys = pi3d.Keyboard()
mymouse = pi3d.Mouse(restrict=False)
mymouse.start()

omx, omy = mymouse.position()

while DISPLAY.loop_running():
    mx, my = mymouse.position()
    rot -= (mx - omx) * 0.2
    tilt -= (my - omy) * 0.2
    omx = mx
    omy = my

    CAMERA.reset()
    CAMERA.rotate(-tilt, rot, 0)
    CAMERA.position((xm, ym, zm))

    mymap.draw()
    wmap.draw()

    k = mykeys.read()

    if k > -1:
        if k == 48:  # ESCAPE key - '0' Key
            mykeys.close()
            mymouse.stop()
            DISPLAY.destroy()
            break
        elif k == 87 or k == 119:  # FORWARDS key - 'W' Key
            xm -= sin(radians(rot))
            zm += cos(radians(rot))
        elif k == 83 or k == 115:  # BACKWARDS key - 'S' Key
            xm += sin(radians(rot))
            zm -= cos(radians(rot))
        elif k == 82 or k == 114:  # UPWARDS key - 'R' Key
            ym += 2
            onGround = False
        elif k == 84 or k == 116:  # DOWNWARDS key - 'T' Key
            ym -= 2

    ym -= 0.1
    xm = limit(xm, -(mapwidth / 2), (mapwidth / 2))
    zm = limit(zm, -(mapdepth / 2), (mapdepth / 2))

    if ym >= sky:
        ym = sky

    ground = mymap.calcHeight(xm, zm) + viewHeight

    if (onGround is True) or (ym <= ground):
        ym = mymap.calcHeight(xm, zm) + viewHeight
        onGround = True
paddywwoof commented 2 years ago

Hi, I will try too have a look at this later today. What hardware are you running on?

paddywwoof commented 2 years ago

Hi, I've had a quick look and there are a couple of things: 1. The argument naming (the diffuse texture is called '..elevation' and the elevation texture is called '..normal')! 2. I haven't really understood why you want to close and recreate the display window rather than just regenerate mymap and wmap, which would be much quicker and wouldn't cause any errors.

world3dA is effectively the main() function and calling that from inside itself seem fraught with unnecessary problems. Maybe explain a bit more why you need to do that. If it turns out to be the only way they you probably should open and close each instance as a subprocess.

Paddy

Shando commented 2 years ago

Hey Paddy, thanks for the quick reply.

I probably didn't explain myself too well :(

1) I'm running on a Ryzen 7 with inbuilt Radeon graphics on Windows 10 (I do also have a GeForce GTX1650 in the same machine, but haven't tried with that yet) 2) The naming is due to me creating a Grayscale Heightmap, a Normal Map and an Elevation Map (a coloured view of the world). These maps are generated (except the Normal Map) to be displayed in my graphical WorldEngine GUI, which generates worlds using plate tectonics etc. 3) The 3D View is generated when the user clicks a button in the GUI and is meant to give them an idea what the world would look like in 3D. This is the reason I need to close and reopen the pi3d instance, as I can't leave the 3D view running when they are generating a different world.

The file I attached was just a test that I did to ensure that the blue screen always happened on the 2nd and subsequent attempts to create a new view. In my actual code, it's called from a main file where all my gui code is.

Hope that explains a bit more?

Thanks again

Shando

paddywwoof commented 2 years ago

OK. It sounds to be an interesting idea. I think you will have problems trying to close and reopen pi3d.Display. It might be possible to do but the OpenGL side is all C function calls and that means 'unsafety' is built in! Especially calling from python through ctypes, which tends to have a go at doing what it thinks you want... fine until it stops working.

I think my approach would be to have my main() function creating and holding an instance of the pi3d scene stuff as a class which would have the key checking as a public method and the texture and environment map creation as a public method. I will hack the code into an approximation of what I mean, as an example. If you don't want the pi3d window showing you can have a method to move it out of the way so you can't see it.

paddywwoof commented 2 years ago

Hi, this is a superficial re-structure of your zipped file. I've not done too much and not even tried running it so there are probably typos and other bugs/mistakes. However it gives you the idea of the approach I might take. You mention a button on the GUI so it will be critical what event loop you are using for that. Generally speaking I find it much better to hang pi3d off the GUI event loop rather than getting the two fighting. i.e. the while world.loop_running(): below would be a tk or pygame loop.

from math import sin, cos, radians
import pi3d
from PIL import Image
import numpy as np

WORLDS = (
    ('Maps/seed_32829_grayscale.png', 1024, 1024, 200, 'Maps/seed_32829_elevation.png', 'Maps/seed_32829_normal.png', 100),
    ('Maps/seed_11111_grayscale.png', 1024, 1024, 200, 'Maps/seed_11111_elevation.png', 'Maps/seed_11111_normal.png', 100),
    ('Maps/seed_42958_grayscale.png', 1024, 1024, 200, 'Maps/seed_42958_elevation.png', 'Maps/seed_42958_normal.png', 100),
    ('Maps/seed_2593_grayscale.png', 1024, 1024, 200, 'Maps/seed_2593_elevation.png', 'Maps/seed_2593_normal.png', 100),
    ('Maps/seed_32829_grayscale.png', 1024, 1024, 200, 'Maps/seed_32829_elevation.png', 'Maps/seed_32829_normal.png', 100)
    )

def limit(value, inMin, inMax):
    if value < inMin:
        value = inMin
    elif value > inMax:
        value = inMax
    return value

class World3dA:
    def__init__(self, inHeightmap, inWidth, inDepth, inHeight, inTextureMap, inBumpMap, inSeaLevel):
        self.DISPLAY = pi3d.Display.create(0, 0, 1280, 1024)
        self.CAMERA = pi3d.Camera.instance()
        self.grass_tex = pi3d.Texture('textures/grasstile_n.jpg')
        self.w_norm = pi3d.Texture('textures/n_norm000.png')
        self.shader = pi3d.Shader("uv_bump")
        self.rshader = pi3d.Shader("uv_reflect")
        self.viewHeight = 4
        self.sky = 200
        self.mykeys = pi3d.Keyboard()
        self.mymouse = pi3d.Mouse(restrict=False)
        self.mymouse.start()

        self.create_world(inHeightmap, inWidth, inDepth, inHeight, inTextureMap, inBumpMap, inSeaLevel)
        self.keep_looping = True

    def create_world(self, inHeightmap, inWidth, inDepth, inHeight, inTextureMap, inBumpMap, inSeaLevel):
        base_tex = np.array(Image.open(inTextureMap))
        # texture for land
        base_gr = base_tex.copy()
        ix = np.where(base_gr[:,:,2] > 20)  # i.e. was blue
        base_gr[ix[0], ix[1], 1] += 50  # increase green
        base_gr[ix[0], ix[1], 2] = 0  # reduce blue
        texg = pi3d.Texture(base_gr)
        # texture for water
        base_bl = base_tex.copy()
        base_bl[:,:] = [0, 0, 60, 170]  # uniform slightly transparent
        texb = pi3d.Texture(base_bl)
        self.mapwidth = inWidth
        self.mapdepth = inDepth
        self.mapheight = inHeight

        self.mymap = pi3d.ElevationMap(inBumpMap, width=self.mapwidth, depth=self.mapdepth, height=self.mapheight, divx=199, divy=199,
                                  ntiles=1, name="sub")
        self.mymap.set_draw_details(self.shader, [texg, grass_tex], 200.0)
        self.wmap = pi3d.ElevationMap(inBumpMap, width=self.mapwidth, depth=self.mapdepth, height=self.mapheight * 0.1, divx=199, divy=199,
                                 ntiles=1, name="water", y=inSeaLevel)
        self.wmap.set_draw_details(self.rshader, [texb, w_norm, texg], 50.0, 0.2)
        self.rot = 0.0
        self.tilt = 0.0
        self.height = 20.0
        self.xm, self.ym, self.zm = 0.0, self.height, 0.0
        self.onGround = False
        self.omx, self.omy = self.mymouse.position()

    def loop_running(self):
        if self.keep_looping and self.DISPLAY.loop_running():
            mx, my = mymouse.position()
            self.rot -= (mx - self.omx) * 0.2
            self.tilt -= (my - self.omy) * 0.2
            self.omx = mx
            self.omy = my

            self.CAMERA.reset()
            self.CAMERA.rotate(-self.tilt, self.rot, 0)
            self.CAMERA.position((self.xm, self.ym, self.zm))

            self.mymap.draw()
            self.wmap.draw()
            return True
        else:
            return False

    def stop_looping(self):
        self.keep_looping = False
        self.mykeys.close()
        self.mymouse.stop()
        self.DISPLAY.destroy()

    def read_key_and_move(self):
        k = self.mykeys.read()

        if k > -1:
            if k == 48:  # ESCAPE key - '0' Key
                return k # return early and do scene change or stop
            elif k == 87 or k == 119:  # FORWARDS key - 'W' Key
                self.xm -= sin(radians(rot))
                self.zm += cos(radians(rot))
            elif k == 83 or k == 115:  # BACKWARDS key - 'S' Key
                self.xm += sin(radians(rot))
                self.zm -= cos(radians(rot))
            elif k == 82 or k == 114:  # UPWARDS key - 'R' Key
                self.ym += 2
                self.onGround = False
            elif k == 84 or k == 116:  # DOWNWARDS key - 'T' Key
                self.ym -= 2
            # only do this if a move key was pressed?
            self.ym -= 0.1
            self.xm = limit(self.xm, -(self.mapwidth / 2), (self.mapwidth / 2))
            self.zm = limit(self.zm, -(self.mapdepth / 2), (self.mapdepth / 2))

            if self.ym >= self.sky:
                self.ym = self.sky

            self.ground = mymap.calcHeight(self.xm, self.zm) + self.viewHeight

            if self.onGround or (self.ym <= self.ground):
                self.ym = mymap.calcHeight(self.xm, self.zm) + self.viewHeight
                self.onGround = True
        return None

def main():
    my_var = 0
    world = World3dA(*WORLDS[my_var])
    my_var += 1
    while world.loop_running():
        if world.read_key_and_move() == 48:
            if my_var >= len(WORLDS):
                world.stop_looping()
            else:
                world.create_world(*WORLDS[my_var])
                my_var += 1

if __name__ == '__main__':
    main()
Shando commented 2 years ago

Hey Paddy,

Thanks for the help. It certainly pointed me in the right direction and, with some messing about with 'win32gui' and 'win32con' I now have the 3D window not only pausing and updating correctly every time the button is pressed, but also being moved in front of the main GUI window when running and back behind it when not :)

Thanks again for the help

Shando

paddywwoof commented 2 years ago

Great, look forward to seeing the finished product.

Shando commented 2 years ago

Hey Paddy,

Not sure if you're a Windoze person, but my WorldEngine GUI is now complete (as far as I can tell!). You can access the GitHub repository here https://github.com/Shando/WorldEngine_UI. In the 'WorldEngine/worldengine/dist/wegui' folder there is the 'wegui.exe' file that should run the whole shebang without needing to set up any special environments.

There is one prebuilt "World" included (seed_11111.world), which you can load up to view the Maps / 3D. Unfortunately, due to its size, I've had to use the GitHub LFS, so not sure if I still have any bandwidth left? If it doesn't download, you can also download it from here https://1drv.ms/u/s!Au7DMGV6totzgsA7TIAjmXee_3HegA?e=CIm8Kt

Thanks for your help with this.

Shando