05fc3aef53
Replace the current license disclaimer in files by a SPDX-License-Identifier. Files that have to be modified by hand are modified. License files are organized under LICENSES directory. Task-number: QTBUG-67283 Change-Id: Id880c92784c40f3bbde861c0d93f58151c18b9f1 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Lars Knoll <lars.knoll@qt.io> Reviewed-by: Jörg Bornemann <joerg.bornemann@qt.io>
337 lines
11 KiB
C++
337 lines
11 KiB
C++
// Copyright (C) 2016 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
|
|
|
#include <QtCore>
|
|
#include <QtWidgets>
|
|
#include <qmath.h>
|
|
|
|
#define WORLD_SIZE 8
|
|
int world_map[WORLD_SIZE][WORLD_SIZE] = {
|
|
{ 1, 1, 1, 1, 6, 1, 1, 1 },
|
|
{ 1, 0, 0, 1, 0, 0, 0, 7 },
|
|
{ 1, 1, 0, 1, 0, 1, 1, 1 },
|
|
{ 6, 0, 0, 0, 0, 0, 0, 3 },
|
|
{ 1, 8, 8, 0, 8, 0, 8, 1 },
|
|
{ 2, 2, 0, 0, 8, 8, 7, 1 },
|
|
{ 3, 0, 0, 0, 0, 0, 0, 5 },
|
|
{ 2, 2, 2, 2, 7, 4, 4, 4 },
|
|
};
|
|
|
|
#define TEXTURE_SIZE 64
|
|
#define TEXTURE_BLOCK (TEXTURE_SIZE * TEXTURE_SIZE)
|
|
|
|
class Raycasting: public QWidget
|
|
{
|
|
public:
|
|
Raycasting(QWidget *parent = nullptr)
|
|
: QWidget(parent)
|
|
, angle(0.5)
|
|
, playerPos(1.5, 1.5)
|
|
, angleDelta(0)
|
|
, moveDelta(0)
|
|
, touchDevice(false) {
|
|
|
|
// http://www.areyep.com/RIPandMCS-TextureLibrary.html
|
|
textureImg.load(":/textures.png");
|
|
textureImg = textureImg.convertToFormat(QImage::Format_ARGB32);
|
|
Q_ASSERT(textureImg.width() == TEXTURE_SIZE * 2);
|
|
Q_ASSERT(textureImg.bytesPerLine() == 4 * TEXTURE_SIZE * 2);
|
|
textureCount = textureImg.height() / TEXTURE_SIZE;
|
|
|
|
watch.start();
|
|
ticker.start(25, this);
|
|
setAttribute(Qt::WA_OpaquePaintEvent, true);
|
|
setMouseTracking(false);
|
|
}
|
|
|
|
void updatePlayer() {
|
|
int interval = qBound(20ll, watch.elapsed(), 250ll);
|
|
watch.start();
|
|
angle += angleDelta * interval / 1000;
|
|
qreal step = moveDelta * interval / 1000;
|
|
qreal dx = cos(angle) * step;
|
|
qreal dy = sin(angle) * step;
|
|
QPointF pos = playerPos + 3 * QPointF(dx, dy);
|
|
int xi = static_cast<int>(pos.x());
|
|
int yi = static_cast<int>(pos.y());
|
|
if (world_map[yi][xi] == 0)
|
|
playerPos = playerPos + QPointF(dx, dy);
|
|
}
|
|
|
|
void showFps() {
|
|
static QElapsedTimer frameTick;
|
|
static int totalFrame = 0;
|
|
if (!(totalFrame & 31)) {
|
|
const qint64 elapsed = frameTick.elapsed();
|
|
frameTick.start();
|
|
int fps = 32 * 1000 / (1 + elapsed);
|
|
setWindowTitle(QString("Raycasting (%1 FPS)").arg(fps));
|
|
}
|
|
totalFrame++;
|
|
}
|
|
|
|
void render() {
|
|
|
|
// setup the screen surface
|
|
if (buffer.size() != bufferSize)
|
|
buffer = QImage(bufferSize, QImage::Format_ARGB32);
|
|
int bufw = buffer.width();
|
|
int bufh = buffer.height();
|
|
if (bufw <= 0 || bufh <= 0)
|
|
return;
|
|
|
|
// we intentionally cheat here, to avoid detach
|
|
const uchar *ptr = buffer.bits();
|
|
QRgb *start = (QRgb*)(ptr);
|
|
QRgb stride = buffer.bytesPerLine() / 4;
|
|
QRgb *finish = start + stride * bufh;
|
|
|
|
// prepare the texture pointer
|
|
const uchar *src = textureImg.bits();
|
|
const QRgb *texsrc = reinterpret_cast<const QRgb*>(src);
|
|
|
|
// cast all rays here
|
|
qreal sina = sin(angle);
|
|
qreal cosa = cos(angle);
|
|
qreal u = cosa - sina;
|
|
qreal v = sina + cosa;
|
|
qreal du = 2 * sina / bufw;
|
|
qreal dv = -2 * cosa / bufw;
|
|
|
|
for (int ray = 0; ray < bufw; ++ray, u += du, v += dv) {
|
|
// every time this ray advances 'u' units in x direction,
|
|
// it also advanced 'v' units in y direction
|
|
qreal uu = (u < 0) ? -u : u;
|
|
qreal vv = (v < 0) ? -v : v;
|
|
qreal duu = 1 / uu;
|
|
qreal dvv = 1 / vv;
|
|
int stepx = (u < 0) ? -1 : 1;
|
|
int stepy = (v < 0) ? -1 : 1;
|
|
|
|
// the cell in the map that we need to check
|
|
qreal px = playerPos.x();
|
|
qreal py = playerPos.y();
|
|
int mapx = static_cast<int>(px);
|
|
int mapy = static_cast<int>(py);
|
|
|
|
// the position and texture for the hit
|
|
int texture = 0;
|
|
qreal hitdist = 0.1;
|
|
qreal texofs = 0;
|
|
bool dark = false;
|
|
|
|
// first hit at constant x and constant y lines
|
|
qreal distx = (u > 0) ? (mapx + 1 - px) * duu : (px - mapx) * duu;
|
|
qreal disty = (v > 0) ? (mapy + 1 - py) * dvv : (py - mapy) * dvv;
|
|
|
|
// loop until we hit something
|
|
while (texture <= 0) {
|
|
if (distx > disty) {
|
|
// shorter distance to a hit in constant y line
|
|
hitdist = disty;
|
|
disty += dvv;
|
|
mapy += stepy;
|
|
texture = world_map[mapy][mapx];
|
|
if (texture > 0) {
|
|
dark = true;
|
|
if (stepy > 0) {
|
|
qreal ofs = px + u * (mapy - py) / v;
|
|
texofs = ofs - floor(ofs);
|
|
} else {
|
|
qreal ofs = px + u * (mapy + 1 - py) / v;
|
|
texofs = ofs - floor(ofs);
|
|
}
|
|
}
|
|
} else {
|
|
// shorter distance to a hit in constant x line
|
|
hitdist = distx;
|
|
distx += duu;
|
|
mapx += stepx;
|
|
texture = world_map[mapy][mapx];
|
|
if (texture > 0) {
|
|
if (stepx > 0) {
|
|
qreal ofs = py + v * (mapx - px) / u;
|
|
texofs = ofs - floor(ofs);
|
|
} else {
|
|
qreal ofs = py + v * (mapx + 1 - px) / u;
|
|
texofs = ceil(ofs) - ofs;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// get the texture, note that the texture image
|
|
// has two textures horizontally, "normal" vs "dark"
|
|
int col = static_cast<int>(texofs * TEXTURE_SIZE);
|
|
col = qBound(0, col, TEXTURE_SIZE - 1);
|
|
texture = (texture - 1) % textureCount;
|
|
const QRgb *tex = texsrc + TEXTURE_BLOCK * texture * 2 +
|
|
(TEXTURE_SIZE * 2 * col);
|
|
if (dark)
|
|
tex += TEXTURE_SIZE;
|
|
|
|
// start from the texture center (horizontally)
|
|
int h = static_cast<int>(bufw / hitdist / 2);
|
|
int dy = (TEXTURE_SIZE << 12) / h;
|
|
int p1 = ((TEXTURE_SIZE / 2) << 12) - dy;
|
|
int p2 = p1 + dy;
|
|
|
|
// start from the screen center (vertically)
|
|
// y1 will go up (decrease), y2 will go down (increase)
|
|
int y1 = bufh / 2;
|
|
int y2 = y1 + 1;
|
|
QRgb *pixel1 = start + y1 * stride + ray;
|
|
QRgb *pixel2 = pixel1 + stride;
|
|
|
|
// map the texture to the sliver
|
|
while (y1 >= 0 && y2 < bufh && p1 >= 0) {
|
|
*pixel1 = tex[p1 >> 12];
|
|
*pixel2 = tex[p2 >> 12];
|
|
p1 -= dy;
|
|
p2 += dy;
|
|
--y1;
|
|
++y2;
|
|
pixel1 -= stride;
|
|
pixel2 += stride;
|
|
}
|
|
|
|
// ceiling and floor
|
|
for (; pixel1 > start; pixel1 -= stride)
|
|
*pixel1 = qRgb(0, 0, 0);
|
|
for (; pixel2 < finish; pixel2 += stride)
|
|
*pixel2 = qRgb(96, 96, 96);
|
|
}
|
|
|
|
update(QRect(QPoint(0, 0), bufferSize));
|
|
}
|
|
|
|
protected:
|
|
|
|
void resizeEvent(QResizeEvent*) {
|
|
touchDevice = false;
|
|
if (touchDevice) {
|
|
if (width() < height()) {
|
|
trackPad = QRect(0, height() / 2, width(), height() / 2);
|
|
centerPad = QPoint(width() / 2, height() * 3 / 4);
|
|
bufferSize = QSize(width(), height() / 2);
|
|
} else {
|
|
trackPad = QRect(width() / 2, 0, width() / 2, height());
|
|
centerPad = QPoint(width() * 3 / 4, height() / 2);
|
|
bufferSize = QSize(width() / 2, height());
|
|
}
|
|
} else {
|
|
trackPad = QRect();
|
|
bufferSize = size();
|
|
}
|
|
update();
|
|
}
|
|
|
|
void timerEvent(QTimerEvent*) {
|
|
updatePlayer();
|
|
render();
|
|
showFps();
|
|
}
|
|
|
|
void paintEvent(QPaintEvent *event) {
|
|
QPainter p(this);
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
|
|
p.drawImage(event->rect(), buffer, event->rect());
|
|
|
|
if (touchDevice && event->rect().intersects(trackPad)) {
|
|
p.fillRect(trackPad, Qt::white);
|
|
p.setPen(QPen(QColor(224, 224, 224), 6));
|
|
int rad = qMin(trackPad.width(), trackPad.height()) * 0.3;
|
|
p.drawEllipse(centerPad, rad, rad);
|
|
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(Qt::gray);
|
|
|
|
QPolygon poly;
|
|
poly << QPoint(-30, 0);
|
|
poly << QPoint(0, -40);
|
|
poly << QPoint(30, 0);
|
|
|
|
p.translate(centerPad);
|
|
for (int i = 0; i < 4; ++i) {
|
|
p.rotate(90);
|
|
p.translate(0, 20 - rad);
|
|
p.drawPolygon(poly);
|
|
p.translate(0, rad - 20);
|
|
}
|
|
}
|
|
|
|
p.end();
|
|
}
|
|
|
|
void keyPressEvent(QKeyEvent *event) {
|
|
event->accept();
|
|
if (event->key() == Qt::Key_Left)
|
|
angleDelta = 1.3 * M_PI;
|
|
if (event->key() == Qt::Key_Right)
|
|
angleDelta = -1.3 * M_PI;
|
|
if (event->key() == Qt::Key_Up)
|
|
moveDelta = 2.5;
|
|
if (event->key() == Qt::Key_Down)
|
|
moveDelta = -2.5;
|
|
}
|
|
|
|
void keyReleaseEvent(QKeyEvent *event) {
|
|
event->accept();
|
|
if (event->key() == Qt::Key_Left)
|
|
angleDelta = (angleDelta > 0) ? 0 : angleDelta;
|
|
if (event->key() == Qt::Key_Right)
|
|
angleDelta = (angleDelta < 0) ? 0 : angleDelta;
|
|
if (event->key() == Qt::Key_Up)
|
|
moveDelta = (moveDelta > 0) ? 0 : moveDelta;
|
|
if (event->key() == Qt::Key_Down)
|
|
moveDelta = (moveDelta < 0) ? 0 : moveDelta;
|
|
}
|
|
|
|
void mousePressEvent(QMouseEvent *event) {
|
|
qreal dx = centerPad.x() - event->position().toPoint().x();
|
|
qreal dy = centerPad.y() - event->position().toPoint().y();
|
|
angleDelta = dx * 2 * M_PI / width();
|
|
moveDelta = dy * 10 / height();
|
|
}
|
|
|
|
void mouseMoveEvent(QMouseEvent *event) {
|
|
qreal dx = centerPad.x() - event->position().toPoint().x();
|
|
qreal dy = centerPad.y() - event->position().toPoint().y();
|
|
angleDelta = dx * 2 * M_PI / width();
|
|
moveDelta = dy * 10 / height();
|
|
}
|
|
|
|
void mouseReleaseEvent(QMouseEvent*) {
|
|
angleDelta = 0;
|
|
moveDelta = 0;
|
|
}
|
|
|
|
private:
|
|
QElapsedTimer watch;
|
|
QBasicTimer ticker;
|
|
QImage buffer;
|
|
qreal angle;
|
|
QPointF playerPos;
|
|
qreal angleDelta;
|
|
qreal moveDelta;
|
|
QImage textureImg;
|
|
int textureCount;
|
|
bool touchDevice;
|
|
QRect trackPad;
|
|
QPoint centerPad;
|
|
QSize bufferSize;
|
|
};
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
QApplication app(argc, argv);
|
|
|
|
Raycasting w;
|
|
w.setWindowTitle("Raycasting");
|
|
w.resize(640, 480);
|
|
w.show();
|
|
|
|
return app.exec();
|
|
}
|