1
0
mirror of https://github.com/ssloy/tinyraycaster.git synced 2025-09-01 18:03:47 +02:00

refactoring time!

This commit is contained in:
Dmitry V. Sokolov
2019-02-09 20:02:38 +01:00
parent 8e221cbc89
commit 80e6e4a815
13 changed files with 292 additions and 158 deletions

BIN
doc/011.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
doc/012.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 MiB

BIN
doc/012.mkv Normal file

Binary file not shown.

30
framebuffer.cpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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
View 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
View 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