mirror of
https://github.com/ssloy/tinyraycaster.git
synced 2025-09-01 18:03:47 +02:00
refactoring time!
This commit is contained in:
BIN
doc/011.png
Normal file
BIN
doc/011.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
doc/012.gif
Normal file
BIN
doc/012.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.8 MiB |
BIN
doc/012.mkv
Normal file
BIN
doc/012.mkv
Normal file
Binary file not shown.
30
framebuffer.cpp
Normal file
30
framebuffer.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
|
||||
#include "framebuffer.h"
|
||||
#include "utils.h"
|
||||
|
||||
void FrameBuffer::set_pixel(const size_t x, const size_t y, const uint32_t color) {
|
||||
assert(img.size()==w*h && x<w && y<h);
|
||||
img[x+y*w] = color;
|
||||
}
|
||||
|
||||
void FrameBuffer::draw_rectangle(const size_t rect_x, const size_t rect_y, const size_t rect_w, const size_t rect_h, const uint32_t color) {
|
||||
assert(img.size()==w*h);
|
||||
for (size_t i=0; i<rect_w; i++) {
|
||||
for (size_t j=0; j<rect_h; j++) {
|
||||
size_t cx = rect_x+i;
|
||||
size_t cy = rect_y+j;
|
||||
if (cx<w && cy<h) // no need to check for negative values (unsigned variables)
|
||||
set_pixel(cx, cy, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FrameBuffer::clear(const uint32_t color) {
|
||||
img = std::vector<uint32_t>(w*h, color);
|
||||
}
|
||||
|
||||
|
17
framebuffer.h
Normal file
17
framebuffer.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef FRAMEBUFFER_H
|
||||
#define FRAMEBUFFER_H
|
||||
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
struct FrameBuffer {
|
||||
size_t w, h; // image dimensions
|
||||
std::vector<uint32_t> img; // storage container
|
||||
|
||||
void clear(const uint32_t color);
|
||||
void set_pixel(const size_t x, const size_t y, const uint32_t color);
|
||||
void draw_rectangle(const size_t x, const size_t y, const size_t w, const size_t h, const uint32_t color);
|
||||
};
|
||||
|
||||
#endif // FRAMEBUFFER_H
|
||||
|
36
map.cpp
Normal file
36
map.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#include <cstdlib>
|
||||
#include <cassert>
|
||||
|
||||
#include "map.h"
|
||||
|
||||
static const char map[] = "0000222222220000"\
|
||||
"1 0"\
|
||||
"1 11111 0"\
|
||||
"1 0 0"\
|
||||
"0 0 1110000"\
|
||||
"0 3 0"\
|
||||
"0 10000 0"\
|
||||
"0 3 11100 0"\
|
||||
"5 4 0 0"\
|
||||
"5 4 1 00000"\
|
||||
"0 1 0"\
|
||||
"2 1 0"\
|
||||
"0 0 0"\
|
||||
"0 0000000 0"\
|
||||
"0 0"\
|
||||
"0002222222200000";
|
||||
|
||||
Map::Map() : w(16), h(16) {
|
||||
assert(sizeof(map) == w*h+1); // +1 for the null terminated string
|
||||
}
|
||||
|
||||
int Map::get(const size_t i, const size_t j) {
|
||||
assert(i<w && j<h && sizeof(map) == w*h+1);
|
||||
return map[i+j*w] - '0';
|
||||
}
|
||||
|
||||
bool Map::is_empty(const size_t i, const size_t j) {
|
||||
assert(i<w && j<h && sizeof(map) == w*h+1);
|
||||
return map[i+j*w] == ' ';
|
||||
}
|
||||
|
14
map.h
Normal file
14
map.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef MAP_H
|
||||
#define MAP_H
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
struct Map {
|
||||
size_t w, h; // overall map dimensions
|
||||
Map();
|
||||
int get(const size_t i, const size_t j);
|
||||
bool is_empty(const size_t i, const size_t j);
|
||||
};
|
||||
|
||||
#endif // MAP_H
|
||||
|
11
player.h
Normal file
11
player.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef PLAYER_H
|
||||
#define PLAYER_H
|
||||
|
||||
struct Player {
|
||||
float x, y; // position
|
||||
float a; // view direction
|
||||
float fov; // field of view
|
||||
};
|
||||
|
||||
#endif // PLAYER_H
|
||||
|
61
textures.cpp
Normal file
61
textures.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "stb_image.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "textures.h"
|
||||
|
||||
Texture::Texture(const std::string filename) : img_w(0), img_h(0), count(0), size(0), img() {
|
||||
int nchannels = -1, w, h;
|
||||
unsigned char *pixmap = stbi_load(filename.c_str(), &w, &h, &nchannels, 0);
|
||||
if (!pixmap) {
|
||||
std::cerr << "Error: can not load the textures" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (4!=nchannels) {
|
||||
std::cerr << "Error: the texture must be a 32 bit image" << std::endl;
|
||||
stbi_image_free(pixmap);
|
||||
return;
|
||||
}
|
||||
|
||||
if (w!=h*int(w/h)) {
|
||||
std::cerr << "Error: the texture file must contain N square textures packed horizontally" << std::endl;
|
||||
stbi_image_free(pixmap);
|
||||
return;
|
||||
}
|
||||
count = w/h;
|
||||
size = w/count;
|
||||
img_w = w;
|
||||
img_h = h;
|
||||
|
||||
img = std::vector<uint32_t>(w*h);
|
||||
for (int j=0; j<h; j++) {
|
||||
for (int i=0; i<w; i++) {
|
||||
uint8_t r = pixmap[(i+j*w)*4+0];
|
||||
uint8_t g = pixmap[(i+j*w)*4+1];
|
||||
uint8_t b = pixmap[(i+j*w)*4+2];
|
||||
uint8_t a = pixmap[(i+j*w)*4+3];
|
||||
img[i+j*w] = pack_color(r, g, b, a);
|
||||
}
|
||||
}
|
||||
stbi_image_free(pixmap);
|
||||
}
|
||||
|
||||
uint32_t &Texture::get(const size_t i, const size_t j, const size_t idx) {
|
||||
assert(i<size && j<size && idx<count);
|
||||
return img[i+idx*size+j*img_w];
|
||||
}
|
||||
|
||||
std::vector<uint32_t> Texture::get_scaled_column(const size_t texture_id, const size_t tex_coord, const size_t column_height) {
|
||||
assert(tex_coord<size && texture_id<count);
|
||||
std::vector<uint32_t> column(column_height);
|
||||
for (size_t y=0; y<column_height; y++) {
|
||||
column[y] = get(tex_coord, (y*size)/column_height, texture_id);
|
||||
}
|
||||
return column;
|
||||
}
|
||||
|
15
textures.h
Normal file
15
textures.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#ifndef TEXTURES_H
|
||||
#define TEXTURES_H
|
||||
|
||||
struct Texture {
|
||||
size_t img_w, img_h; // overall image dimensions
|
||||
size_t count, size; // number of textures and size in pixels
|
||||
std::vector<uint32_t> img; // textures storage container
|
||||
|
||||
Texture(const std::string filename);
|
||||
uint32_t &get(const size_t i, const size_t j, const size_t idx); // get the pixel (i,j) from the textrue idx
|
||||
std::vector<uint32_t> get_scaled_column(const size_t texture_id, const size_t tex_coord, const size_t column_height); // retrieve one column (tex_coord) from the texture texture_id and scale it to the destination size
|
||||
};
|
||||
|
||||
#endif // TEXTURES_H
|
||||
|
@@ -1,185 +1,91 @@
|
||||
#define _USE_MATH_DEFINES
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "stb_image.h"
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
uint32_t pack_color(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a=255) {
|
||||
return (a<<24) + (b<<16) + (g<<8) + r;
|
||||
#include "map.h"
|
||||
#include "utils.h"
|
||||
#include "player.h"
|
||||
#include "framebuffer.h"
|
||||
#include "textures.h"
|
||||
|
||||
int wall_x_texcoord(const float x, const float y, Texture &tex_walls) {
|
||||
float hitx = x - floor(x+.5); // hitx and hity contain (signed) fractional parts of x and y,
|
||||
float hity = y - floor(y+.5); // they vary between -0.5 and +0.5, and one of them is supposed to be very close to 0
|
||||
int tex = hitx*tex_walls.size;
|
||||
if (std::abs(hity)>std::abs(hitx)) // we need to determine whether we hit a "vertical" or a "horizontal" wall (w.r.t the map)
|
||||
tex = hity*tex_walls.size;
|
||||
if (tex<0) // do not forget x_texcoord can be negative, fix that
|
||||
tex += tex_walls.size;
|
||||
assert(tex>=0 && tex<(int)tex_walls.size);
|
||||
return tex;
|
||||
}
|
||||
|
||||
void unpack_color(const uint32_t &color, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) {
|
||||
r = (color >> 0) & 255;
|
||||
g = (color >> 8) & 255;
|
||||
b = (color >> 16) & 255;
|
||||
a = (color >> 24) & 255;
|
||||
}
|
||||
void render(FrameBuffer &fb, Map &map, Player &player, Texture &tex_walls) {
|
||||
fb.clear(pack_color(255, 255, 255)); // clear the screen
|
||||
|
||||
void drop_ppm_image(const std::string filename, const std::vector<uint32_t> &image, const size_t w, const size_t h) {
|
||||
assert(image.size() == w*h);
|
||||
std::ofstream ofs(filename);
|
||||
ofs << "P6\n" << w << " " << h << "\n255\n";
|
||||
for (size_t i = 0; i < h*w; ++i) {
|
||||
uint8_t r, g, b, a;
|
||||
unpack_color(image[i], r, g, b, a);
|
||||
ofs << static_cast<char>(r) << static_cast<char>(g) << static_cast<char>(b);
|
||||
}
|
||||
ofs.close();
|
||||
}
|
||||
|
||||
void draw_rectangle(std::vector<uint32_t> &img, const size_t img_w, const size_t img_h, const size_t x, const size_t y, const size_t w, const size_t h, const uint32_t color) {
|
||||
assert(img.size()==img_w*img_h);
|
||||
for (size_t i=0; i<w; i++) {
|
||||
for (size_t j=0; j<h; j++) {
|
||||
size_t cx = x+i;
|
||||
size_t cy = y+j;
|
||||
if (cx>=img_w || cy>=img_h) continue; // no need to check for negative values (unsigned variables)
|
||||
img[cx + cy*img_w] = color;
|
||||
const size_t rect_w = fb.w/(map.w*2); // size of one map cell on the screen
|
||||
const size_t rect_h = fb.h/map.h;
|
||||
for (size_t j=0; j<map.h; j++) { // draw the map
|
||||
for (size_t i=0; i<map.w; i++) {
|
||||
if (map.is_empty(i, j)) continue; // skip empty spaces
|
||||
size_t rect_x = i*rect_w;
|
||||
size_t rect_y = j*rect_h;
|
||||
size_t texid = map.get(i, j);
|
||||
assert(texid<tex_walls.count);
|
||||
fb.draw_rectangle(rect_x, rect_y, rect_w, rect_h, tex_walls.get(0, 0, texid)); // the color is taken from the upper left pixel of the texture #texid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool load_texture(const std::string filename, std::vector<uint32_t> &texture, size_t &text_size, size_t &text_cnt) {
|
||||
int nchannels = -1, w, h;
|
||||
unsigned char *pixmap = stbi_load(filename.c_str(), &w, &h, &nchannels, 0);
|
||||
if (!pixmap) {
|
||||
std::cerr << "Error: can not load the textures" << std::endl;
|
||||
return false;
|
||||
}
|
||||
for (size_t i=0; i<fb.w/2; i++) { // draw the visibility cone AND the "3D" view
|
||||
float angle = player.a-player.fov/2 + player.fov*i/float(fb.w/2);
|
||||
for (float t=0; t<20; t+=.01) { // ray marching loop
|
||||
float x = player.x + t*cos(angle);
|
||||
float y = player.y + t*sin(angle);
|
||||
fb.set_pixel(x*rect_w, y*rect_h, pack_color(160, 160, 160)); // this draws the visibility cone
|
||||
|
||||
if (4!=nchannels) {
|
||||
std::cerr << "Error: the texture must be a 32 bit image" << std::endl;
|
||||
stbi_image_free(pixmap);
|
||||
return false;
|
||||
}
|
||||
if (map.is_empty(x, y)) continue;
|
||||
|
||||
text_cnt = w/h;
|
||||
text_size = w/text_cnt;
|
||||
if (w!=h*int(text_cnt)) {
|
||||
std::cerr << "Error: the texture file must contain N square textures packed horizontally" << std::endl;
|
||||
stbi_image_free(pixmap);
|
||||
return false;
|
||||
}
|
||||
|
||||
texture = std::vector<uint32_t>(w*h);
|
||||
for (int j=0; j<h; j++) {
|
||||
for (int i=0; i<w; i++) {
|
||||
uint8_t r = pixmap[(i+j*w)*4+0];
|
||||
uint8_t g = pixmap[(i+j*w)*4+1];
|
||||
uint8_t b = pixmap[(i+j*w)*4+2];
|
||||
uint8_t a = pixmap[(i+j*w)*4+3];
|
||||
texture[i+j*w] = pack_color(r, g, b, a);
|
||||
}
|
||||
}
|
||||
stbi_image_free(pixmap);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> texture_column(const std::vector<uint32_t> &img, const size_t texsize, const size_t ntextures, const size_t texid, const size_t texcoord, const size_t column_height) {
|
||||
const size_t img_w = texsize*ntextures;
|
||||
const size_t img_h = texsize;
|
||||
assert(img.size()==img_w*img_h && texcoord<texsize && texid<ntextures);
|
||||
std::vector<uint32_t> column(column_height);
|
||||
for (size_t y=0; y<column_height; y++) {
|
||||
size_t pix_x = texid*texsize + texcoord;
|
||||
size_t pix_y = (y*texsize)/column_height;
|
||||
column[y] = img[pix_x + pix_y*img_w];
|
||||
}
|
||||
return column;
|
||||
size_t texid = map.get(x, y); // our ray touches a wall, so draw the vertical column to create an illusion of 3D
|
||||
assert(texid<tex_walls.count);
|
||||
size_t column_height = fb.h/(t*cos(angle-player.a));
|
||||
int x_texcoord = wall_x_texcoord(x, y, tex_walls);
|
||||
std::vector<uint32_t> column = tex_walls.get_scaled_column(texid, x_texcoord, column_height);
|
||||
int pix_x = i + fb.w/2; // we are drawing at the right half of the screen, thus +fb.w/2
|
||||
for (size_t j=0; j<column_height; j++) { // copy the texture column to the framebuffer
|
||||
int pix_y = j + fb.h/2 - column_height/2;
|
||||
if (pix_y>=0 && pix_y<(int)fb.h) {
|
||||
fb.set_pixel(pix_x, pix_y, column[j]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
} // ray marching loop
|
||||
} // field of view ray sweeping
|
||||
}
|
||||
|
||||
int main() {
|
||||
const size_t win_w = 1024; // image width
|
||||
const size_t win_h = 512; // image height
|
||||
std::vector<uint32_t> framebuffer(win_w*win_h, pack_color(255, 255, 255)); // the image itself, initialized to white
|
||||
|
||||
const size_t map_w = 16; // map width
|
||||
const size_t map_h = 16; // map height
|
||||
const char map[] = "0000222222220000"\
|
||||
"1 0"\
|
||||
"1 11111 0"\
|
||||
"1 0 0"\
|
||||
"0 0 1110000"\
|
||||
"0 3 0"\
|
||||
"0 10000 0"\
|
||||
"0 3 11100 0"\
|
||||
"5 4 0 0"\
|
||||
"5 4 1 00000"\
|
||||
"0 1 0"\
|
||||
"2 1 0"\
|
||||
"0 0 0"\
|
||||
"0 0000000 0"\
|
||||
"0 0"\
|
||||
"0002222222200000"; // our game map
|
||||
assert(sizeof(map) == map_w*map_h+1); // +1 for the null terminated string
|
||||
float player_x = 3.456; // player x position
|
||||
float player_y = 2.345; // player y position
|
||||
float player_a = 1.523; // player view direction
|
||||
const float fov = M_PI/3.; // field of view
|
||||
|
||||
std::vector<uint32_t> walltext; // textures for the walls
|
||||
size_t walltext_size; // texture dimensions (it is a square)
|
||||
size_t walltext_cnt; // number of different textures in the image
|
||||
if (!load_texture("../walltext.png", walltext, walltext_size, walltext_cnt)) {
|
||||
FrameBuffer fb{1024, 512, std::vector<uint32_t>(1024*512, pack_color(255, 255, 255))};
|
||||
Player player{3.456, 2.345, 1.523, M_PI/3.};
|
||||
Map map;
|
||||
Texture tex_walls("../walltext.png");
|
||||
if (!tex_walls.count) {
|
||||
std::cerr << "Failed to load wall textures" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const size_t rect_w = win_w/(map_w*2);
|
||||
const size_t rect_h = win_h/map_h;
|
||||
for (size_t j=0; j<map_h; j++) { // draw the map
|
||||
for (size_t i=0; i<map_w; i++) {
|
||||
if (map[i+j*map_w]==' ') continue; // skip empty spaces
|
||||
size_t rect_x = i*rect_w;
|
||||
size_t rect_y = j*rect_h;
|
||||
size_t texid = map[i+j*map_w] - '0';
|
||||
assert(texid<walltext_cnt);
|
||||
draw_rectangle(framebuffer, win_w, win_h, rect_x, rect_y, rect_w, rect_h, walltext[texid*walltext_size]); // the color is taken from the upper left pixel of the texture #texid
|
||||
}
|
||||
for (size_t frame=0; frame<360; frame++) {
|
||||
std::stringstream ss;
|
||||
ss << std::setfill('0') << std::setw(5) << frame << ".ppm";
|
||||
player.a += 2*M_PI/360;
|
||||
|
||||
render(fb, map, player, tex_walls);
|
||||
drop_ppm_image(ss.str(), fb.img, fb.w, fb.h);
|
||||
}
|
||||
|
||||
for (size_t i=0; i<win_w/2; i++) { // draw the visibility cone AND the "3D" view
|
||||
float angle = player_a-fov/2 + fov*i/float(win_w/2);
|
||||
for (float t=0; t<20; t+=.01) {
|
||||
float cx = player_x + t*cos(angle);
|
||||
float cy = player_y + t*sin(angle);
|
||||
|
||||
int pix_x = cx*rect_w;
|
||||
int pix_y = cy*rect_h;
|
||||
framebuffer[pix_x + pix_y*win_w] = pack_color(160, 160, 160); // this draws the visibility cone
|
||||
|
||||
if (map[int(cx)+int(cy)*map_w]!=' ') { // our ray touches a wall, so draw the vertical column to create an illusion of 3D
|
||||
size_t texid = map[int(cx)+int(cy)*map_w] - '0';
|
||||
assert(texid<walltext_cnt);
|
||||
size_t column_height = win_h/(t*cos(angle-player_a));
|
||||
|
||||
float hitx = cx - floor(cx+.5); // hitx and hity contain (signed) fractional parts of cx and cy,
|
||||
float hity = cy - floor(cy+.5); // they vary between -0.5 and +0.5, and one of them is supposed to be very close to 0
|
||||
int x_texcoord = hitx*walltext_size;
|
||||
if (std::abs(hity)>std::abs(hitx)) { // we need to determine whether we hit a "vertical" or a "horizontal" wall (w.r.t the map)
|
||||
x_texcoord = hity*walltext_size;
|
||||
}
|
||||
if (x_texcoord<0) x_texcoord += walltext_size; // do not forget x_texcoord can be negative, fix that
|
||||
assert(x_texcoord>=0 && x_texcoord<(int)walltext_size);
|
||||
|
||||
std::vector<uint32_t> column = texture_column(walltext, walltext_size, walltext_cnt, texid, x_texcoord, column_height);
|
||||
pix_x = win_w/2+i;
|
||||
for (size_t j=0; j<column_height; j++) {
|
||||
pix_y = j + win_h/2-column_height/2;
|
||||
if (pix_y<0 || pix_y>=(int)win_h) continue;
|
||||
framebuffer[pix_x + pix_y*win_w] = column[j];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop_ppm_image("./out.ppm", framebuffer, win_w, win_h);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
31
utils.cpp
Normal file
31
utils.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
uint32_t pack_color(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) {
|
||||
return (a<<24) + (b<<16) + (g<<8) + r;
|
||||
}
|
||||
|
||||
void unpack_color(const uint32_t &color, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) {
|
||||
r = (color >> 0) & 255;
|
||||
g = (color >> 8) & 255;
|
||||
b = (color >> 16) & 255;
|
||||
a = (color >> 24) & 255;
|
||||
}
|
||||
|
||||
void drop_ppm_image(const std::string filename, const std::vector<uint32_t> &image, const size_t w, const size_t h) {
|
||||
assert(image.size() == w*h);
|
||||
std::ofstream ofs(filename);
|
||||
ofs << "P6\n" << w << " " << h << "\n255\n";
|
||||
for (size_t i = 0; i < h*w; ++i) {
|
||||
uint8_t r, g, b, a;
|
||||
unpack_color(image[i], r, g, b, a);
|
||||
ofs << static_cast<char>(r) << static_cast<char>(g) << static_cast<char>(b);
|
||||
}
|
||||
ofs.close();
|
||||
}
|
||||
|
13
utils.h
Normal file
13
utils.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef UTILS_H
|
||||
#define UTILS_H
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
uint32_t pack_color(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a=255);
|
||||
void unpack_color(const uint32_t &color, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a);
|
||||
void drop_ppm_image(const std::string filename, const std::vector<uint32_t> &image, const size_t w, const size_t h);
|
||||
|
||||
#endif // UTILS_H
|
||||
|
Reference in New Issue
Block a user