@AhabGreybeard
Lol yes
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 ) and then the number of time I had to heard results of what I did
First thing was to draw a map of maze in which Pum-Pum is moving
You have here a global oveview of maze, Pum-Pum is not moving across all passages, but I tried different paths
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.
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
Sames frames drawn in Krita (used brush d) Ink-4 Pen Rought, 7px size)