Pum-Pum Level3

Hi!

Here something I’ve started to work on the 5th of July…
So, one month on it :face_with_spiral_eyes:

  • All drawing made on Krita (it was loooong)
  • Old monitor VFX effect made on Natron (possible to do it on Krita but with rendering time it’s unfortunately not possible to use Krita for that…)

:warning: UNEXPLICIT CONTENT :warning:

Please be aware that the following video contains no sausages!!

Available on Peertube

Available on Viméo

Grum999

10 Likes

Cute! :+1:t2::grin:

It’ll take a long time to get that tune out of my head :slight_smile:

2 Likes

@CrazyCatBird
Thanks :slight_smile:

@AhabGreybeard
Lol yes :sweat_smile:
You don’t even imagine on my side, it took me a long time to be able to compose this version of popcorn song (I’m absolutely not a musician :upside_down_face:) and then the number of time I had to heard results of what I did :sweat_smile:

Grum999

1 Like

You deserve a medal! :crazy_face:

3 Likes

Few word about process…

First thing was to draw a map of maze in which Pum-Pum is moving
maze-map-tunnel
You have here a global oveview of maze, Pum-Pum is not moving across all passages, but I tried different paths :slight_smile:

Then, I’ve wrote a small python script and executed it in scripter :

# https://fahad-haidari.medium.com/build-a-pseudo3d-game-engine-with-javascript-using-raycasting-4ab9c3d22bc1

from PyQt5.Qt import *
from math import (sqrt, sin, cos, radians, ceil, floor)

import time


class MazeMap(QObject):

    def __init__(self):
        super(MazeMap, self).__init__(None)

        self.width=0
        self.height=0

        self.__map=[]

        img=QImage("/home/grum/maze-map-tunnel.png")
        self.width=img.width()
        self.height=img.height()

        self.colorNone=QColor("#00000000")

        white=QColor("#ffffff")
        for y in range(self.height):
            row=[None]*self.width
            for x in range(self.width):
                color=img.pixelColor(x, y)
                if color.alpha()!=0:
                    #if color.name().lower() in ("#fea711", "#ffff00"):
                    #    color=white
                    row[x]=color
            self.__map.append(row)

    def color(self, x, y):
        try:
            return self.__map[y][x]
        except:
            return None


class MazeConfig:
    wallHeight=30
    frameRate=30
    renderPrecision=60/((1920+400)/2)
    renderRayPrecision=0.25
    cellSize = 20
    halfCellSize = 10
    fov = 60
    speed = 1.75

    renderWidth=1920+400
    renderHeight=1080+400

    antialiasing=0 # QPainter.Antialiasing

    saveFrames=True

    mapRenderFactor=4

    COLOR_BG = QColor("#000000")
    COLOR_RAYS = QColor("#88FFFFFF")
    COLOR_POS = QColor("#FF00FF")
    COLOR_WORLD_BOUND = QColor("#000000")


class Camera:

    def __init__(self):
        self.__halfCellSize=MazeConfig.cellSize/2
        self.col=0
        self.row=0
        self.pX=0
        self.pY=0
        self.rotation=0
        self.angle=radians(-90)

    def setColRow(self, col, row):
        self.col=col
        self.row=row
        self.pX=self.col * MazeConfig.cellSize + self.__halfCellSize
        self.pY=self.row * MazeConfig.cellSize + self.__halfCellSize

    def setPxPy(self, pX, pY):
        self.pX=pX
        self.pY=pY
        self.col=floor(self.pX / MazeConfig.cellSize)
        self.row=floor(self.pY / MazeConfig.cellSize)

    def update(self):
        self.rotation*=0.5
        self.angle+=self.rotation


class MazePosition:
    tmp="""
    POSITIONS = [
        # x, y, speed (nbSeconds)
        # -- GREEN AREA --
        [50, 199,1.5,   0],
        [50, 85, 0.5,   0],
        [50, 39, 4.5,   0],
        [50, 33, 0.3,  30],
        [51, 32, 0.3,  30],
        [52, 31, 0.3,  30],
        [72, 31, 2.5, -90],
        [72, 17, 1.5,  90],
        [79, 17, 1.5,  90],
        [79, 22,   2,  45],

        # -- BLUE AREA --
        [79, 23, 0.3, -90],
        [80, 24, 0.3,  45],
        [80, 27,   1,  45],
        [78, 29, 0.3, -45],

        # -- PINK AREA --
        [78, 48, 2.5,  45],
        [77, 49, 0.3,  45],
        [73, 49, 1.5, -90],
        [73, 50, 0.5,  90],
        [71, 50, 0.5,  90],
        [71, 49, 0.5, -90],
        [69, 49, 0.5, -90],
        [69, 50, 0.5,  90],
        [67, 50, 0.5,  90],
        [67, 49, 0.5, -90],

        # -- YELLOW AREA --
        [11, 49, 3.5,  30],
        [10, 48, 0.3,  30],
        [10, 47, 0.3,  30],
        [10, 33,   2,  30],
        [11, 32, 0.3,  30],
        [12, 31, 0.3,  30],

        # -- ORANGE AREA --
        [17, 31, 0.6,  -30],
        [19, 30, 0.6,   30],
        [21, 30, 0.6,   45],
        [24, 33, 0.6,  -45],
        [26, 33, 0.6,  -45],
        [29, 30, 0.6,   45],
        [31, 30, 0.6,   30],
        [33, 31, 0.6,  -30],


        # -- RED AREA --
        [47, 31,   2, -45],
        [50, 28, 0.5, -45],

        [50, 19,   1, -20],
        [49, 14,   1,  40],
        [50,  9,   1, -20],

        [50, 1,  1.5,   0],


        [50, 1, 1, 0],
        [50, 1, 1, 0],
    ]
    """
    POSITIONS = [
        # x, y, speed (nbSeconds)
        # -- GREEN AREA --
        [50, 199, 0.5,  0],
        [50, 139, 1.6,-15],

        [48, 130, 0.5, 15],
        [48, 128, 0.3, 15],


        [52, 114, 0.8,-15],
        [52, 112, 0.3,-15],

         
        [50, 102, 0.5, 15],
        [50,  85, 0.5,  0],
         
         # -- end
        [50, 39, 4.5,   0],
        [50, 33, 0.3,  30]
    ]
    

    def __init__(self):
        self.index=0

        self.col=MazePosition.POSITIONS[0][0]
        self.row=MazePosition.POSITIONS[0][1]
        self.nextCol=MazePosition.POSITIONS[1][0]
        self.nextRow=MazePosition.POSITIONS[1][1]

        self.stepX=0
        self.stepY=0

        self.targetX=0
        self.targetY=0
        self.stepRotation=0
        self.rotation=0
        self.nbFrames=0

        self.halfCellSize=MazeConfig.cellSize/2

        self.camera=Camera()
        self.camera.setColRow(self.col, self.row)

        totalTime=0
        for p in MazePosition.POSITIONS:
            totalTime+=p[2]
        print(f'Total time: {totalTime}')


    def move(self):
        if self.index<len(MazePosition.POSITIONS)-2:
            if round(self.camera.pX)==round(self.targetX) and round(self.camera.pY)==round(self.targetY) or self.index==0:
                # target reached or starting
                self.index+=1
                self.col=self.nextCol
                self.row=self.nextRow

                self.nextCol=MazePosition.POSITIONS[self.index][0]
                self.nextRow=MazePosition.POSITIONS[self.index][1]

                self.targetX=self.nextCol * MazeConfig.cellSize + self.halfCellSize
                self.targetY=self.nextRow * MazeConfig.cellSize + self.halfCellSize

                deltaX=self.targetX - self.camera.pX
                deltaY=self.targetY - self.camera.pY

                self.rotation=MazePosition.POSITIONS[self.index][3]
                self.nbFrames=MazeConfig.frameRate*MazePosition.POSITIONS[self.index][2]
                self.stepX=deltaX/self.nbFrames
                self.stepY=deltaY/self.nbFrames
                self.stepRotation=0
            elif self.stepRotation==0:
                d=sqrt((self.camera.pX - self.targetX)**2 + (self.camera.pY - self.targetY)**2)
                if d<=(1.25 * MazeConfig.cellSize):
                    self.stepRotation=radians(self.rotation)/self.nbFrames

            self.camera.setPxPy(self.camera.pX+self.stepX, self.camera.pY+self.stepY)
            self.camera.rotation+=self.stepRotation
            self.nbFrames-=1
            return True
        return False


class Maze(QObject):

    def __init__(self, autoStart=True):
        super(Maze, self).__init__(None)
        self.__mazeMap=MazeMap()

        self.__halfCellSize=MazeConfig.cellSize/2
        self.__mapWidth=floor(self.__mazeMap.width * MazeConfig.cellSize)
        self.__mapHeight=floor(self.__mazeMap.height * MazeConfig.cellSize)
        self.__pixmapBgMap=QPixmap(QSize(self.__mapWidth//MazeConfig.mapRenderFactor, self.__mapHeight//MazeConfig.mapRenderFactor))

        self.__halfHeight=MazeConfig.renderHeight/2

        self.__lblMap = QLabel("")
        self.__lblMap.setFixedHeight(self.__mapHeight//MazeConfig.mapRenderFactor)
        self.__lblMap.setFixedWidth(self.__mapWidth//MazeConfig.mapRenderFactor)

        self.__lbl3DView = QLabel("")
        self.__lbl3DView.setFixedHeight(MazeConfig.renderHeight)
        self.__lbl3DView.setFixedWidth(MazeConfig.renderWidth)

        self.__sceneData=[]
        self.__halfFOV=MazeConfig.fov/2
        self.__position=MazePosition()
        self.__frameNumber=0

        self.__timer = QTimer()
        self.__timer.timeout.connect(self.__update)

        self.__timer.setInterval(floor(1000 / MazeConfig.frameRate))
        print(floor(1000 / MazeConfig.frameRate))
        #self.__timer.setInterval(2)

        self.__initializeMapBg()

        self.__lastRenderTime=0

        self.__listAngles=[]
        angle=-self.__halfFOV
        while angle<=self.__halfFOV:
            self.__listAngles.append(radians(angle))
            angle+=MazeConfig.renderPrecision

        self.__timer1=0
        self.__timer2=0

        self.__refHeight=self.__halfHeight * MazeConfig.wallHeight

        if autoStart:
            self.start()

    def start(self):
        dlg = QDialog(Application.activeWindow().qwindow())
        layout = QHBoxLayout(dlg)

        layout.addWidget(self.__lbl3DView)
        layout.addWidget(self.__lblMap)

        dlg.showEvent=self.__show
        dlg.closeEvent=self.__close
        dlg.exec()

    def __show(self, e):
        self.__lblMap.setPixmap(self.__pixmapBgMap)
        self.__timer.start()
        print("--Start!--")

    def __close(self, e):
        self.__timer.stop()
        print("--Stop!--", self.__frameNumber)

    def __initializeMapBg(self):
        self.__pixmapBgMap.fill(MazeConfig.COLOR_BG)
        canvas = QPainter()
        canvas.begin(self.__pixmapBgMap)

        cellSize=MazeConfig.cellSize//MazeConfig.mapRenderFactor
        for col in range(self.__mazeMap.width):
            for row in range(self.__mazeMap.height):
                color=self.__mazeMap.color(col, row)
                if not color is None:
                    canvas.fillRect(QRectF(col * cellSize, row * cellSize, cellSize, cellSize), color)

        canvas.end()

    def __drawMap(self):
        pixmapMap=QPixmap(self.__pixmapBgMap)
        canvas = QPainter()
        canvas.begin(pixmapMap)
        canvas.setPen(QPen(MazeConfig.COLOR_RAYS))
        for data in self.__sceneData:
            # each data item is a tuple
            # x, y, rayX, rayY, h, colorIndex
            canvas.drawLine(QPointF(data[0]/MazeConfig.mapRenderFactor, data[1]/MazeConfig.mapRenderFactor), QPointF(data[2]/MazeConfig.mapRenderFactor, data[3]/MazeConfig.mapRenderFactor))

        canvas.setBrush(QBrush(MazeConfig.COLOR_POS ))
        canvas.drawEllipse(QPointF(self.__position.camera.pX/MazeConfig.mapRenderFactor, self.__position.camera.pY/MazeConfig.mapRenderFactor), 4, 4)

        canvas.drawText(20,25,f"Frame:  {self.__frameNumber}")
        canvas.drawText(20,50,f"Timer1: {self.__timer1:.5f}")
        canvas.drawText(20,75,f"Timer2: {self.__timer2:.5f}")

        canvas.end()
        self.__lblMap.setPixmap(pixmapMap)

    def __draw3DView(self):
        pixmap3DView=QPixmap(QSize(MazeConfig.renderWidth, MazeConfig.renderHeight))
        pixmap3DView.fill(Qt.transparent)
        canvas = QPainter()
        canvas.begin(pixmap3DView)
        canvas.setRenderHint(MazeConfig.antialiasing)

        for data in self.__sceneToRender:
            # each data item is a tuple
            if data[1].name()!="#000000":
                canvas.fillRect(data[0], data[1])

        canvas.end()
        self.__lbl3DView.setPixmap(pixmap3DView)

        if MazeConfig.saveFrames:
            pixmap3DView.save(f"/home/grum/Temp/frame-start-{self.__frameNumber:04}.png", b'PNG')

    def __update(self):
        if self.__position.move():
            self.__frameNumber+=1
            self.__position.camera.update()
            self.__calculateSceneData()
            self.__drawMap()
            self.__draw3DView()

            QApplication.processEvents()

    def __calculateSceneDataAngle(self, pX, pY, angle):
        dst = 0
        mapHMax=self.__mapHeight-4
        mapWMax=self.__mapWidth-4

        while dst < MazeConfig.renderWidth:
            dst += MazeConfig.renderRayPrecision
            rayX = pX + cos(angle) * dst
            rayY = pY + sin(angle) * dst

            row = floor(rayY / MazeConfig.cellSize)
            col = floor(rayX / MazeConfig.cellSize)

            a = self.__position.camera.angle - angle
            # z = dst * cos(a)
            h = self.__refHeight / (dst * cos(a))

            try:
                if rayX > mapHMax or rayX < 4 or rayY < 4 or rayY > mapWMax:
                    # no world boundaries
                    return (pX, pY, rayX, rayY, h, MazeConfig.COLOR_WORLD_BOUND)
                else:
                    color=self.__mazeMap.color(col, row)
                    if not color is None:
                        return (pX, pY, rayX, rayY, h, color)
            except:
                pass
        return None


    def __calculateSceneData(self):
        # each item is a tuple
        # x, y, rayX, rayY, h, color
        ts=time.time()
        self.__sceneData=[None]*len(self.__listAngles)
        for index, angle in enumerate(self.__listAngles):
            self.__sceneData[index]=self.__calculateSceneDataAngle(self.__position.camera.pX, self.__position.camera.pY, angle + self.__position.camera.angle)
        self.__timer1=time.time()-ts

        self.__sceneData=[data for data in self.__sceneData if not data is None]

        width=MazeConfig.renderWidth / len(self.__sceneData)

        ts=time.time()
        self.__sceneToRender=[None] * len(self.__sceneData)
        x=0
        for index, data in enumerate(self.__sceneData):
            # x, y, w, h, alpha, color
            self.__sceneToRender[index]=(QRectF(x, self.__halfHeight-data[4]/2, width, data[4]), data[5])
            x+=width
        self.__timer2=time.time()-ts



Maze()

Script is not optimized for performance neither for output quality.
It just generate 3D render from 2D maze moves.

Here result of 2 rendered frames


  • Background is transparent
  • Wall color match with color defined on 2D maze map: colors here help to get a vision of distance, sizes, moves

Once all frames are generated, import them in animated layer, and start to draw one by one, wall lines and some miscelleanous effects like lasers and others :slight_smile:

Sames frames drawn in Krita (used brush d) Ink-4 Pen Rought, 7px size)


As you can see, I’m working here in greyscale…

Pum-Pum is drawn on another document as small loops (15 to 30 frames per loop)

Then Pum-Pum loops layers are added to final document, and synchronized with movement in maze (turn left, turn right, jump, …) with cloned frames

When rendered, I render as PNG sequences with transparent background
Render is made in 2 steps:

  • 3D walls frames
  • Pum-Pum frames

This allows me to separately apply some blurs effects on Pum-Pum and walls in some sequence part

After, post-processing is made in Natron; here’s my project’s nodes:

Basically 3 parts:

  • Sequences are mounted in the left purple/gray box
  • Green monitor VFX effect is made in middle green box
  • Outpur rendering is made in right yellow box

Music (popocorn) has been composed in LMMS
Here sequences, with some oscillators settings I’ve defined to try get this 8bit computer music style

Finally, video + music + sounds are assembled within KDEnlive

2 medals no? :sweat_smile:

Grum999

4 Likes

Wow!! :open_mouth: That’s a lot of work!! :upside_down_face::open_mouth: :woozy_face: