Compare commits

..

62 Commits

Author SHA1 Message Date
EmaMaker fa3d36ec58 shaders: fix rotated textures on x axis 2023-10-12 21:47:05 +02:00
EmaMaker ebef608000 blockpick: avoid blocks being placed diagonally 2023-10-12 21:47:00 +02:00
EmaMaker 0b294bb35b blockpick: cleanup blockpick place code 2023-10-09 21:55:31 +02:00
EmaMaker f3d8ffed54 blockpick: skip iteration if block has not changed 2023-10-04 23:37:04 +02:00
EmaMaker 0560fd43c6 print gpu in debug window 2023-10-04 20:58:06 +02:00
EmaMaker 62212a22ab main: do not terminate imgui, leads to crash 2023-10-04 14:59:31 +02:00
EmaMaker 357f67aac1 chunkmgr: debug info for chunkmgr 2023-10-04 14:59:17 +02:00
EmaMaker 1b4cef8958 Merge pull request 'Refactor Main/Update thread communication' (#14) from multithread-refactor into main
Reviewed-on: #14
2023-10-04 14:58:07 +02:00
EmaMaker e4c6eb50f3 Merge branch 'main' into multithread-refactor 2023-10-04 14:55:47 +02:00
EmaMaker 9b5939d256 renderer: do not send empty meshes to the gpu only at creation
I feel never sending empty meshes to the GPU is the cause of the bug causing floating quads near
chunk borders when a block is placed and then destroyed.
When destroying a block in a chunk, if nearby empty chunk meshes are not updated, the old mesh is
kept, which includes a quad at the border
2023-10-04 14:53:05 +02:00
EmaMaker 8401a3dca7 Merge branch 'main' into multithread-refactor 2023-10-04 14:51:56 +02:00
EmaMaker 255460892d blockpick control belongs to control, not chunkmgr 2023-10-04 14:41:44 +02:00
EmaMaker afdd622ec2 refactor message system between main and update threads
restores blockpicking with new multithread system
2023-10-04 14:35:29 +02:00
EmaMaker 93bc0e7066 renderer: do not send empty meshes to the gpu only at creation
I feel never sending empty meshes to the GPU is the cause of the bug causing floating quads near
chunk borders when a block is placed and then destroyed.
When destroying a block in a chunk, if nearby empty chunk meshes are not updated, the old mesh is
kept, which includes a quad at the border
2023-10-04 13:57:12 +02:00
EmaMaker 8544620899 Merge pull request 'Refactor Meshing/Rendering communication' (#13) from multithread-refactor into main
Reviewed-on: #13
2023-10-04 13:52:26 +02:00
EmaMaker a4f1e5fc1f Merge branch 'main' into multithread-refactor 2023-10-04 13:51:56 +02:00
EmaMaker 78b65894b7 chunkmesher: compiler flag to not show borders at world limit 2023-10-04 13:35:50 +02:00
EmaMaker 355da726f6 Merge pull request 'Refactor Secondary Threads' (#12) from multithread-refactor into main
Reviewed-on: #12
2023-10-04 13:32:23 +02:00
EmaMaker ba95db4678 renderer: also delete meshes of old chunks 2023-10-04 13:24:06 +02:00
EmaMaker 7c82a71dd0 chunk: vram buffers are managed by renderer now 2023-10-04 13:24:06 +02:00
EmaMaker 7786d4f04d renderer: use ChunkMeshData from rendering thread
This system decouples the Secondary threads from the Render thread. Once a chunk is meshed, only the mesh data is sent to the rendering thread, which does not use any direct reference to the chunk itself
2023-10-04 13:24:06 +02:00
EmaMaker 1822911845 chunkmesher: use goto for error handling 2023-10-04 13:24:06 +02:00
EmaMaker d0ddf2256f move chunkmeshdata into its own file
And let it be managed by chunkmesher instead of chunkmanager
2023-10-04 13:23:49 +02:00
EmaMaker 88abf21502 multithread: refactor update thread, communication between mesh/gen/upd threads
- Use parallel_for to iterate over all the stored chunks
- Only push a chunk to a queue if it is not already present, using chunk state bitfield (solves
	memory leak, continously adding new elements to a queue)
- Properly delete an element from chunks concurrent_hash_map using accessor, then free the memory
(solves memory leak: not being able to delete old elements and always adding new ones)

Breaks:

- Rendering (will be properly refactored in a future commit)
- Block picking (will be refactored in a future commit)
2023-10-04 13:10:05 +02:00
EmaMaker f4947d5f70 debugwindow: catch exception to avoid crash when missing parameters 2023-10-04 12:58:00 +02:00
EmaMaker 4e7fadd2b9 chunk: use chunk state to mark presence of chunk in thread communication queues 2023-10-04 12:57:57 +02:00
EmaMaker d1b151f92f chunk: typedef for chunk state 2023-10-04 12:57:57 +02:00
EmaMaker 44edf3e53a Merge pull request 'Misc fixes in preparation for multithreading refactor' (#10) from multithread-refactor into main
Reviewed-on: #10
2023-10-03 22:55:11 +02:00
EmaMaker 3adb061057 Merge branch 'main' into multithread-refactor 2023-10-03 22:53:42 +02:00
EmaMaker 1a50d1fb84 chunkmgr: span chunk indices in a cube around the player, not a sphere 2023-10-03 22:45:09 +02:00
EmaMaker 60bbc85682 chunk: store index in chunk itself 2023-10-03 22:45:09 +02:00
EmaMaker 490f207e39 chunk/mgr: calculate index belongs to chunk namespace 2023-10-03 22:45:09 +02:00
EmaMaker c6d00c4200 fix type mismatch in chunk index/coordinates
typedef an appropriate chunk_index_t and chunk_intcoord_t
2023-10-03 22:45:09 +02:00
EmaMaker 2a57796ed2 move input handling from main into dedicated file 2023-10-03 22:45:09 +02:00
EmaMaker ca043bac68 threads: allow for proper shutdown
using `if` instead of `while` avoids the need to wait for the queue to empty to shutdown the thread
2023-10-03 22:44:55 +02:00
EmaMaker f6f4057109 update player debug variables in main instead of chunkmgr 2023-10-03 22:09:03 +02:00
EmaMaker 353ef37186 renderer: perform frustum culling only if chunk has vertices 2023-10-03 22:08:40 +02:00
EmaMaker 880c634be0 camera: set atomic position at startup
this avoids the first few chunk update loops recognizing the camera as being positioned at (0,0,0), which in turns avoids wastefully generating chunks out of view at startup
2023-10-03 22:08:40 +02:00
EmaMaker 9ad0485a79 Merge pull request 'Adds HUD elements' (#9) from hud into main
Reviewed-on: #9
2023-09-20 12:43:59 +02:00
EmaMaker 50bc52a679 debug window: use wireframe from checkbox in rendering 2023-09-20 12:42:05 +02:00
EmaMaker 07068d740a debug window: use block type from slider in block picking 2023-09-20 12:42:05 +02:00
EmaMaker 52696f2dde debug window: use crosshair type from slider in shader 2023-09-20 12:42:05 +02:00
EmaMaker 9d2d0c8772 debug window: populate with data 2023-09-20 12:42:05 +02:00
EmaMaker f526e9b152 create debug window with imgui 2023-09-20 12:42:05 +02:00
EmaMaker e7c4b2d56b lib: add dearimgui lib 2023-09-20 12:42:05 +02:00
EmaMaker a6a419fdff hud: crosshair with fragment shader 2023-09-20 12:42:05 +02:00
EmaMaker 3f61a6a753 renderer: screenshot by saving render texture to file 2023-09-20 12:37:32 +02:00
EmaMaker 00a4b8e1e2 renderer: properly handle framebuffer resizing 2023-09-20 12:37:32 +02:00
EmaMaker 4b723d58fa libs: import stb_image_write 2023-09-20 12:37:32 +02:00
EmaMaker 1c0ee1315f renderer: render scene to a texture
And then render the texture onto a quad that fills the screen
The screen-filled quad upon which the texture is rendered must never be rendered in wireframe. Rendering in wireframe only makes sense when rendering the world on the texture
2023-09-20 12:37:32 +02:00
EmaMaker 6670f3b41c shader: make geometry shader not mandatory 2023-09-20 12:37:32 +02:00
EmaMaker 41275486a6 blockpicking: eventually mesh nearby chunks when destroying a block
this prevents stray floating quads when destroy a block near the chunk border
2023-08-12 08:44:46 +02:00
EmaMaker 3d0d8b7593 chunkmgr: chunk can be meshed if all neighbors are generated
(Restores seemless chunk borders)
2023-08-08 17:54:20 +02:00
EmaMaker 8d160c3239 Merge pull request 'Responsive block picking during generation and meshing' (#7) from better-generation into main
Reviewed-on: #7
2023-07-30 12:30:31 +02:00
EmaMaker ca04afcc89 Merge pull request 'Better World Generation' (#6) from better-generation into main
Reviewed-on: #6
2023-07-30 12:28:59 +02:00
EmaMaker 1bea6c835c separate mesh and generation threads, with priority queues for input
Allows blockpicking while the world is generating, without hiccups
2023-07-30 12:17:51 +02:00
EmaMaker 83f0aafba0 give leaves a sprinkle of color 2023-07-30 12:17:51 +02:00
EmaMaker b0cf413baf generator: add comments + a bit of refactor 2023-07-30 12:17:48 +02:00
EmaMaker baa8d14bb3 generator: optimize leaves generation routine with LUT 2023-07-30 12:17:00 +02:00
EmaMaker 381cd698c7 Initial tree generation
Still very slow because multiple noise evaluations are needed
2023-07-30 12:16:32 +02:00
EmaMaker 9bc5bab3b2 mountain terrain with multiple octaves of noise 2023-07-30 12:13:08 +02:00
EmaMaker 8313ee97bc Merge pull request 'Use Geometry Shader to generate the Mesh' (#5) from vram-reduce into main
Reviewed-on: #5
2023-07-19 13:19:18 +02:00
35 changed files with 3169 additions and 436 deletions

2
.gitignore vendored
View File

@ -9,3 +9,5 @@ gmon.out*
cscope*
test.cpp
a.out
*screenshot*
imgui.ini

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "lib/imgui"]
path = lib/imgui
url = https://github.com/ocornut/imgui/

View File

@ -2,8 +2,8 @@ cmake_minimum_required(VERSION 3.2)
project(cmake-project-template)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -O3")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -O3")
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g")
set(CMAKE_INSTALL_PREFIX ${PROJECT_SOURCE_DIR})

View File

@ -6,7 +6,9 @@ enum class Block{
AIR,
STONE,
DIRT,
GRASS
GRASS,
WOOD,
LEAVES
};
#endif
#endif

View File

@ -18,6 +18,10 @@ public:
// This matrix needs to be also updated in viewPortCallback whenever it is changed
projection = glm::perspective(glm::radians(90.0f), 800.0f / 600.0f, 0.1f, 1200.0f);
posX = cameraPos.x;
posY = cameraPos.y;
posZ = cameraPos.z;
}
void update(GLFWwindow *window, float deltaTime)
@ -112,7 +116,7 @@ public:
private:
glm::vec3 cameraPos = glm::vec3(256.0, 80.0f, 256.0f);
glm::vec3 cameraPos = glm::vec3(512.0, 80.0f, 512.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 direction = glm::vec3(0.0f);

View File

@ -20,16 +20,27 @@
#define CHUNK_VOLUME (CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE)
#define CHUNK_MAX_INDEX (CHUNK_VOLUME - 1)
// int32_t is fine, since i'm limiting the coordinate to only use up to ten bits (1023). There's actually two spare bits
typedef int32_t chunk_index_t;
typedef int16_t chunk_intcoord_t;
typedef uint16_t chunk_state_t;
namespace Chunk
{
constexpr uint8_t CHUNK_STATE_GENERATED = 1;
constexpr uint8_t CHUNK_STATE_MESHED = 2;
constexpr uint8_t CHUNK_STATE_MESH_LOADED = 4;
constexpr uint8_t CHUNK_STATE_LOADED = 8;
constexpr uint8_t CHUNK_STATE_OUTOFVISION = 16;
constexpr uint8_t CHUNK_STATE_UNLOADED = 32;
constexpr uint8_t CHUNK_STATE_EMPTY = 64;
chunk_index_t calculateIndex(chunk_intcoord_t i, chunk_intcoord_t j, chunk_intcoord_t k);
chunk_index_t calculateIndex(glm::vec3 pos);
constexpr chunk_state_t CHUNK_STATE_GENERATED = 1;
constexpr chunk_state_t CHUNK_STATE_MESHED = 2;
constexpr chunk_state_t CHUNK_STATE_MESH_LOADED = 4;
constexpr chunk_state_t CHUNK_STATE_LOADED = 8;
constexpr chunk_state_t CHUNK_STATE_OUTOFVISION = 16;
constexpr chunk_state_t CHUNK_STATE_UNLOADED = 32;
constexpr chunk_state_t CHUNK_STATE_EMPTY = 64;
constexpr chunk_state_t CHUNK_STATE_IN_GENERATION_QUEUE = 128;
constexpr chunk_state_t CHUNK_STATE_IN_MESHING_QUEUE = 256;
constexpr chunk_state_t CHUNK_STATE_IN_DELETING_QUEUE = 512;
int coord3DTo1D(int x, int y, int z);
@ -41,13 +52,15 @@ namespace Chunk
~Chunk();
public:
void createBuffers();
void deleteBuffers();
glm::vec3 getPosition() { return this->position; }
uint8_t getTotalState() { return this->state; }
bool getState(uint8_t n) { return (this->state & n) == n; }
void setState(uint8_t nstate, bool value);
void setState(chunk_state_t nstate, bool value);
bool getState(chunk_state_t n) { return (this->state & n) == n; }
bool isFree(){ return !(
this->getState(CHUNK_STATE_IN_GENERATION_QUEUE) ||
this->getState(CHUNK_STATE_IN_MESHING_QUEUE) ||
this->getState(CHUNK_STATE_IN_DELETING_QUEUE)
); }
chunk_state_t getTotalState() { return this->state; }
void setBlock(Block b, int x, int y, int z);
void setBlocks(int start, int end, Block b);
@ -56,14 +69,15 @@ namespace Chunk
std::unique_ptr<Block[]> getBlocksArray(int* len) { return (this->blocks.toArray(len)); }
public:
GLuint VAO{0}, VBO{0}, extentsBuffer{0}, texinfoBuffer{0}, numVertices{0};
std::atomic<float> unload_timer{0};
chunk_index_t getIndex(){ return this->index; }
private:
glm::vec3 position{};
IntervalMap<Block> blocks{};
std::atomic_uint8_t state{0};
std::atomic<chunk_state_t> state{0};
chunk_index_t index;
};
};

View File

@ -1,28 +1,41 @@
#ifndef CHUNKMANAGER_H
#define CHUNKMANAGER_H
#include <oneapi/tbb/concurrent_hash_map.h>
#include <oneapi/tbb/concurrent_queue.h>
#include <oneapi/tbb/concurrent_priority_queue.h>
#include <thread>
#include "chunk.hpp"
#include "globals.hpp"
#include "worldupdatemessage.h"
// Seconds to be passed outside of render distance for a chunk to be destroyed
#define UNLOAD_TIMEOUT 10
#include <thread>
#include <oneapi/tbb/concurrent_queue.h>
#include "chunk.hpp"
#include "globals.hpp"
#define MESHING_PRIORITY_NORMAL 0
#define MESHING_PRIORITY_PLAYER_EDIT 10
#define GENERATION_PRIORITY_NORMAL 0
namespace chunkmanager
{
std::thread init();
void blockpick(bool place);
uint32_t calculateIndex(uint16_t i, uint16_t j, uint16_t k);
typedef oneapi::tbb::concurrent_hash_map<chunk_index_t, Chunk::Chunk*> ChunkTable;
typedef std::pair<Chunk::Chunk*, uint8_t> ChunkPQEntry;
// The comparing function to use
struct compare_f {
bool operator()(const ChunkPQEntry& u, const ChunkPQEntry& v) const {
return u.second > v.second;
}
};
typedef oneapi::tbb::concurrent_priority_queue<ChunkPQEntry, compare_f> ChunkPriorityQueue;
void init();
void update();
void stop();
void destroy();
oneapi::tbb::concurrent_queue<Chunk::Chunk*>& getDeleteVector();
std::array<std::array<int, 3>, chunks_volume>& getChunksIndices();
WorldUpdateMsgQueue& getWorldUpdateQueue();
std::array<std::array<chunk_intcoord_t, 3>, chunks_volume>& getChunksIndices();
Block getBlockAtPos(int x, int y, int z);
void update();
}
#endif

36
include/chunkmeshdata.hpp Normal file
View File

@ -0,0 +1,36 @@
#ifndef CHUNK_MESH_DATA_H
#define CHUNK_MESH_DATA_H
#include <oneapi/tbb/concurrent_queue.h>
#include "chunk.hpp"
enum class ChunkMeshDataType{
MESH_UPDATE
};
typedef struct ChunkMeshData{
chunk_index_t index;
glm::vec3 position;
int num_vertices = 0;
std::vector<GLfloat> vertices;
std::vector<GLfloat> extents;
std::vector<GLfloat> texinfo;
ChunkMeshDataType message_type;
void clear(){
vertices.clear();
texinfo.clear();
extents.clear();
index = 0;
position = glm::vec3(0);
num_vertices = 0;
}
}ChunkMeshData;
typedef oneapi::tbb::concurrent_queue<ChunkMeshData*> ChunkMeshDataQueue;
#endif

View File

@ -5,10 +5,12 @@
#include <vector>
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <GLFW/glfw3.h>
#include <oneapi/tbb/concurrent_queue.h>
#include "chunk.hpp"
#include "chunkmeshdata.hpp"
#include "globals.hpp"
#include "shader.hpp"
@ -17,17 +19,16 @@ namespace chunkmesher{
Chunk::Chunk* chunk;
GLuint numVertices{0};
std::vector<GLubyte> vertices;
std::vector<GLubyte> extents;
std::vector<GLubyte> texinfo;
std::vector<GLfloat> vertices;
std::vector<GLfloat> extents;
std::vector<GLfloat> texinfo;
};
oneapi::tbb::concurrent_queue<MeshData*>& getMeshDataQueue();
ChunkMeshDataQueue& getMeshDataQueue();
void init();
void mesh(Chunk::Chunk* chunk);
void sendtogpu(MeshData* mesh_data);
void quad(MeshData* mesh_data, glm::vec3 bottomLeft, glm::vec3 topLeft, glm::vec3 topRight,
glm::vec3 bottomRight, glm::vec3 normal, Block block, int dim, bool backFace);
}
#endif

14
include/controls.hpp Normal file
View File

@ -0,0 +1,14 @@
#ifndef CONTROLS_H
#define CONTROLS_H
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#define BLOCKPICK_TIMEOUT 0.1f
namespace controls{
void init();
void update(GLFWwindow* window);
};
#endif

19
include/debugwindow.hpp Normal file
View File

@ -0,0 +1,19 @@
#ifndef DEBUG_WINDOW_H
#define DEBUG_WINDOW_H
#include <any>
#include <string>
#include <GLFW/glfw3.h>
namespace debug{
namespace window {
void init(GLFWwindow* window);
void prerender();
void render();
void destroy();
void set_parameter(std::string key, std::any value);
}
}
#endif

View File

@ -13,9 +13,13 @@
#define RENDER_DISTANCE 16
extr Camera theCamera;
constexpr int chunks_volume = static_cast<int>(1.333333333333*M_PI*(RENDER_DISTANCE*RENDER_DISTANCE*RENDER_DISTANCE));
// the cube spans in both directions, to each axis has to be multiplied by 2. 2^3=8
constexpr int chunks_volume = 8*(RENDER_DISTANCE*RENDER_DISTANCE*RENDER_DISTANCE);
extr bool wireframe;
extr float sines[360];
extr float cosines[360];
extr uint32_t MORTON_XYZ_ENCODE[CHUNK_SIZE][CHUNK_SIZE][CHUNK_SIZE];
extr uint32_t MORTON_XYZ_DECODE[CHUNK_VOLUME][3];
extr uint32_t HILBERT_XYZ_ENCODE[CHUNK_SIZE][CHUNK_SIZE][CHUNK_SIZE];

View File

@ -1,8 +1,10 @@
#ifndef MAIN_H
#define MAIN_H
#include <glad/glad.h>
#include <GLFW/glfw3.h>
void framebuffer_size_callback(GLFWwindow *, int, int);
void mouse_callback(GLFWwindow *window, double xpos, double ypos);
void processInput(GLFWwindow *);
#endif

View File

@ -6,18 +6,56 @@
#include "chunk.hpp"
#include "chunkmesher.hpp"
#include "chunkmeshdata.hpp"
#include "shader.hpp"
namespace renderer{
typedef oneapi::tbb::concurrent_unordered_set<Chunk::Chunk*> RenderSet;
typedef struct RenderInfo {
chunk_index_t index;
int num_vertices;
glm::vec3 position;
bool buffers_allocated=false;
void init();
GLuint VAO, VBO, extentsBuffer, texinfoBuffer;
void allocateBuffers(){
// Allocate buffers
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &extentsBuffer);
glGenBuffers(1, &texinfoBuffer);
buffers_allocated=true;
}
void deallocateBuffers(){
// Allocate buffers
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &extentsBuffer);
glDeleteBuffers(1, &texinfoBuffer);
glDeleteVertexArrays(1, &VAO);
buffers_allocated=false;
}
} RenderInfo;
typedef oneapi::tbb::concurrent_queue<int32_t> IndexQueue;
void init(GLFWwindow* window);
void send_chunk_to_gpu(ChunkMeshData* mesh_data, RenderInfo* render_info);
void render();
void resize_framebuffer(int width, int height);
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void destroy();
void saveScreenshot(bool forceFullHD=false);
Shader* getRenderShader();
RenderSet& getChunksToRender();
oneapi::tbb::concurrent_queue<chunkmesher::MeshData*>& getMeshDataQueue();
ChunkMeshDataQueue& getMeshDataQueue();
IndexQueue& getDeleteIndexQueue();
};
#endif

View File

@ -34,60 +34,70 @@ public:
gShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
std::stringstream vShaderStream, fShaderStream, gShaderStream;
if(geometryPath){
gShaderFile.open(geometryPath);
gShaderStream << gShaderFile.rdbuf();
gShaderFile.close();
geometryCode = gShaderStream.str();
}
// open files
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
gShaderFile.open(geometryPath);
std::stringstream vShaderStream, fShaderStream, gShaderStream;
// read file's buffer contents into streams
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
gShaderStream << gShaderFile.rdbuf();
// close file handlers
vShaderFile.close();
fShaderFile.close();
gShaderFile.close();
// convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
geometryCode = gShaderStream.str();
}
catch (std::ifstream::failure &e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ: " << e.what() << std::endl;
}
const char *vShaderCode = vertexCode.c_str();
const char *fShaderCode = fragmentCode.c_str();
const char *gShaderCode = geometryCode.c_str();
// 2. compile shaders
// shader Program
ID = glCreateProgram();
unsigned int vertex, fragment, geometry;
// geometry shader
if(geometryPath){
const char *gShaderCode = geometryCode.c_str();
geometry = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometry, 1, &gShaderCode, NULL);
glCompileShader(geometry);
checkCompileErrors(geometry, "GEOMETRY");
glAttachShader(ID, geometry);
}
// vertex shader
const char *vShaderCode = vertexCode.c_str();
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
glAttachShader(ID, vertex);
// fragment Shader
const char *fShaderCode = fragmentCode.c_str();
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
// geometry shader
geometry = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometry, 1, &gShaderCode, NULL);
glCompileShader(geometry);
checkCompileErrors(geometry, "GEOMETRY");
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glAttachShader(ID, geometry);
// Constructu the program
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);
glDeleteShader(geometry);
if(geometryPath) glDeleteShader(geometry);
}
// activate the shader
// ------------------------------------------------------------------------

1724
include/stb_image_write.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
#ifndef WORLD_UPDATE_MSG_H
#define WORLD_UPDATE_MSG_H
#include <glm/glm.hpp>
#include <oneapi/tbb/concurrent_queue.h>
#include "block.hpp"
enum class WorldUpdateMsgType{
BLOCKPICK_PLACE,
BLOCKPICK_BREAK
};
typedef struct WorldUpdateMsg{
WorldUpdateMsgType msg_type;
glm::vec3 cameraPos;
glm::vec3 cameraFront;
float time;
Block block;
} WorldUpdateMsg;
typedef oneapi::tbb::concurrent_queue<WorldUpdateMsg> WorldUpdateMsgQueue;
#endif

View File

@ -1,2 +1,3 @@
add_subdirectory(glad)
add_subdirectory(glm)
add_subdirectory(glm)
add_subdirectory(imgui)

1
lib/imgui Submodule

@ -0,0 +1 @@
Subproject commit 6addf28c4b5d8fd109a6db73bed6436952b230b2

28
shaders/shader-quad.fs Normal file
View File

@ -0,0 +1,28 @@
#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D renderTex;
uniform int screenWidth;
uniform int screenHeight;
uniform int crosshairType;
void main(){
float crosshair_alpha = 0.8;
float dist = length(gl_FragCoord.xy-vec2(screenWidth/2, screenHeight/2));
FragColor = texture(renderTex, TexCoord);
/*float crosshair_color = (FragColor.x + FragColor.y + FragColor.z) / 3;
/*if(crosshair_color <= 0.5) crosshair_color = 1.0;
/*else crosshair_color = 0.0;*/
float crosshair_color = 1.0;
if(dist <= 7){
if( (crosshairType == 0 && dist >= 5) ||
(crosshairType == 1 && ( int(gl_FragCoord.x) == int(screenWidth / 2) ||
int(gl_FragCoord.y) == int(screenHeight / 2)) )
) FragColor = vec4(vec3(crosshair_color), crosshair_alpha);
}
}

12
shaders/shader-quad.vs Normal file
View File

@ -0,0 +1,12 @@
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main()
{
TexCoord = aTexCoord;
gl_Position = vec4(aPos, 1.0);
}

View File

@ -23,6 +23,7 @@ void main(){
// Load the texture
// anti-gamma-correction of the texture. Without this it would be gamma corrected twice!
vec3 vColor = pow(texture(textureArray, TexCoord).rgb, vec3(gamma));
if(TexCoord.z == 4) vColor = vColor * normalize(vec3(10, 250, 10));
vec3 normal = normalize(Normal);

View File

@ -26,29 +26,30 @@ void main(){
EmitVertex();
if(gs_in[0].Extents.x == 0){
TexCoord = vec3(0.0, gs_in[0].Extents.z, gs_in[0].BlockType);
TexCoord = vec3(gs_in[0].Extents.z, 0.0, gs_in[0].BlockType);
gl_Position = gl_in[0].gl_Position + vec4(0.0, 0.0, gs_in[0].Extents.z, 0.0);
FragPos = vec3(gl_Position);
gl_Position = projection * view * gl_Position;
EmitVertex();
TexCoord = vec3(gs_in[0].Extents.y, 0.0, gs_in[0].BlockType);
TexCoord = vec3(0.0, gs_in[0].Extents.y, gs_in[0].BlockType);
gl_Position = gl_in[0].gl_Position + vec4(0.0, gs_in[0].Extents.y, 0.0, 0.0);
FragPos = vec3(gl_Position);
gl_Position = projection * view * gl_Position;
EmitVertex();
TexCoord = vec3(gs_in[0].Extents.y, gs_in[0].Extents.z, gs_in[0].BlockType);
TexCoord = vec3(gs_in[0].Extents.z, gs_in[0].Extents.y, gs_in[0].BlockType);
}
else if(gs_in[0].Extents.y == 0){
TexCoord = vec3(0.0, gs_in[0].Extents.z, gs_in[0].BlockType);
gl_Position = gl_in[0].gl_Position + vec4(0.0, 0.0, gs_in[0].Extents.z, 0.0);
TexCoord = vec3(gs_in[0].Extents.x, 0.0, gs_in[0].BlockType);
gl_Position = gl_in[0].gl_Position + vec4(gs_in[0].Extents.x, 0.0, 0.0, 0.0);
FragPos = vec3(gl_Position);
gl_Position = projection * view * gl_Position;
EmitVertex();
TexCoord = vec3(gs_in[0].Extents.x, 0.0, gs_in[0].BlockType);
gl_Position = gl_in[0].gl_Position + vec4(gs_in[0].Extents.x, 0.0, 0.0, 0.0);
TexCoord = vec3(0.0, gs_in[0].Extents.z, gs_in[0].BlockType);
gl_Position = gl_in[0].gl_Position + vec4(0.0, 0.0, gs_in[0].Extents.z, 0.0);
FragPos = vec3(gl_Position);
gl_Position = projection * view * gl_Position;
EmitVertex();

View File

@ -1,8 +1,8 @@
#version 330 core
layout (location = 0) in uvec3 aPos;
layout (location = 1) in uvec3 aExtents;
layout (location = 2) in uvec2 aInfo;
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aExtents;
layout (location = 2) in vec2 aInfo;
uniform mat4 model;
@ -18,9 +18,9 @@ void main()
vs_out.Extents = aExtents;
vs_out.BlockType = aInfo.y;
if(aExtents.x == 0.0) vs_out.Normal = vec3(1.0 - 2.0*aInfo.x, 0.0, 0.0);
else if(aExtents.y == 0.0) vs_out.Normal = vec3(0.0, 1.0 - 2.0*aInfo.x, 0.0);
else vs_out.Normal = vec3(0.0, 0.0, 1.0 - 2.0*aInfo.x);
if(aExtents.x == 0) vs_out.Normal = vec3(1.0 - 2*aInfo.x, 0.0, 0.0);
else if(aExtents.y == 0) vs_out.Normal = vec3(0.0, 1.0 - 2*aInfo.x, 0.0);
else vs_out.Normal = vec3(0.0, 0.0, 1.0 - 2*aInfo.x);
vs_out.Normal = mat3(transpose(inverse(model))) * vs_out.Normal;
gl_Position = model * vec4(aPos, 1.0);

View File

@ -1,9 +1,10 @@
cmake_minimum_required(VERSION 3.2)
project(OpenGLTest)
set(SOURCE_FILES main.cpp chunk.cpp chunkmanager.cpp chunkmesher.cpp chunkgenerator.cpp renderer.cpp spacefilling.cpp stb_image.cpp utils.cpp OpenSimplexNoise.cpp)
set(SOURCE_FILES main.cpp controls.cpp chunk.cpp chunkmanager.cpp chunkmesher.cpp chunkgenerator.cpp
debugwindow.cpp renderer.cpp spacefilling.cpp stb_image.cpp utils.cpp OpenSimplexNoise.cpp)
add_executable(OpenGLTest ${SOURCE_FILES})
target_link_libraries(OpenGLTest glfw tbb glad glm)
target_link_libraries(OpenGLTest glfw tbb glad glm imgui)
install(TARGETS OpenGLTest DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR})

View File

@ -15,32 +15,27 @@ namespace Chunk
return utils::coord3DTo1D(x, y, z, CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE);
}
chunk_index_t calculateIndex(glm::vec3 pos){
return calculateIndex(static_cast<chunk_intcoord_t>(pos.x), static_cast<chunk_intcoord_t>(pos.y),
static_cast<chunk_intcoord_t>(pos.z));
}
chunk_index_t calculateIndex(chunk_intcoord_t i, chunk_intcoord_t j, chunk_intcoord_t k){
return i | (j << 10) | (k << 20);
}
Chunk::Chunk(glm::vec3 pos)
{
this->position = pos;
this->setState(CHUNK_STATE_EMPTY, true);
this->setBlocks(0, CHUNK_MAX_INDEX, Block::AIR);
this->index = calculateIndex(pos);
}
Chunk ::~Chunk()
{
}
void Chunk::createBuffers(){
glGenVertexArrays(1, &(this->VAO));
glGenBuffers(1, &(this->VBO));
glGenBuffers(1, &(this->extentsBuffer));
glGenBuffers(1, &(this->texinfoBuffer));
}
void Chunk::deleteBuffers(){
glDeleteBuffers(1, &(this->VBO));
glDeleteBuffers(1, &(this->extentsBuffer));
glDeleteBuffers(1, &(this->texinfoBuffer));
glDeleteVertexArrays(1, &(this->VAO));
}
Block Chunk::getBlock(int x, int y, int z)
{
if(x < 0 || y < 0 || z < 0 || x > CHUNK_SIZE -1 || y > CHUNK_SIZE -1 || z > CHUNK_SIZE-1 ||
@ -59,7 +54,7 @@ namespace Chunk
this->blocks.insert(start < 0 ? 0 : start, end >= CHUNK_VOLUME ? CHUNK_VOLUME : end, b);
}
void Chunk::setState(uint8_t nstate, bool value)
void Chunk::setState(chunk_state_t nstate, bool value)
{
if (value)
this->state.fetch_or(nstate);

View File

@ -9,62 +9,111 @@
#include "utils.hpp"
#define GRASS_OFFSET 40
#define NOISE_GRASS_MULT 20
#define NOISE_GRASS_MULT 30
#define NOISE_DIRT_MULT 3
#define NOISE_DIRT_MIN 2
#define NOISE_DIRT_MIN 3
#define NOISE_DIRT_X_MULT 0.001f
#define NOISE_DIRT_Z_MULT 0.001f
#define NOISE_GRASS_X_MULT 0.018f
#define NOISE_GRASS_Z_MULT 0.018f
#define NOISE_TREE_X_MULT 0.01f
#define NOISE_TREE_Z_MULT 0.01f
#define LEAVES_RADIUS 3
#define WOOD_CELL_SIZE 13
#define WOOD_CELL_CENTER 7
#define TREE_STANDARD_HEIGHT 7
#define TREE_HEIGHT_VARIATION 2
#define WOOD_CELL_BORDER (LEAVES_RADIUS-1)
#define WOOD_MAX_OFFSET (WOOD_CELL_SIZE-WOOD_CELL_CENTER-WOOD_CELL_BORDER)
void generatePyramid(Chunk::Chunk *chunk);
void generateNoise(Chunk::Chunk *chunk);
void generateNoise3D(Chunk::Chunk *chunk);
void generateChunk(Chunk::Chunk *chunk)
{
generateNoise(chunk);
}
Block block;
double evaluateNoise(OpenSimplexNoise::Noise noiseGen, double x, double y, double amplitude, double
frequency, double persistence, double lacunarity, int octaves);
struct TreeCellInfo evaluateTreeCell(int wcx, int wcz);
std::random_device dev;
std::mt19937 mt(dev());
OpenSimplexNoise::Noise noiseGen1(mt());
OpenSimplexNoise::Noise noiseGen2(mt());
OpenSimplexNoise::Noise noiseGenWood(mt());
// Trees are generated by virtually dividing the world into cells. Each cell can contain exactly one
// tree, with some offset in the position. Having a border in the cell ensures that no trees are generated in
// adjacent blocks
// cover CHUNK_SIZE with WOOD_CELLS + 2 cells before and after the chunk
constexpr int TREE_LUT_SIZE = std::ceil(static_cast<float>(CHUNK_SIZE)/static_cast<float>(WOOD_CELL_SIZE)) + 2;
// Info on the tree cell to generate
struct TreeCellInfo{
// Cell coordinates (in "tree cell space")
int wcx, wcz;
// trunk offset from 0,0 in the cell
int trunk_x_offset, trunk_z_offset;
// Global x,z position of the trunk
int trunk_x, trunk_z;
// Y of the center of the leaves sphere
int leaves_y_pos;
};
// Lookup tables for generation
std::array<int, CHUNK_SIZE * CHUNK_SIZE> grassNoiseLUT;
std::array<int, CHUNK_SIZE * CHUNK_SIZE> dirtNoiseLUT;
std::array<TreeCellInfo, TREE_LUT_SIZE*TREE_LUT_SIZE> treeLUT;
void generateNoise(Chunk::Chunk *chunk)
{
int cx = chunk->getPosition().x * CHUNK_SIZE;
int cy = chunk->getPosition().y * CHUNK_SIZE;
int cz = chunk->getPosition().z * CHUNK_SIZE;
// Precalculate LUTs
// Terrain LUTs
// Noise value at a given (x,z), position represents:
// Grass Noise LUT: Height of the terrain: when the grass is placed and the player will stand
// Dirt Noise LUT: How many blocks of dirt to place before there is stone
// Anything below (grass-level - dirt_height) will be stone
for (int i = 0; i < grassNoiseLUT.size(); i++)
{
grassNoiseLUT[i] = -1;
dirtNoiseLUT[i] = -1;
int bx = i / CHUNK_SIZE;
int bz = i % CHUNK_SIZE;
grassNoiseLUT[i] = GRASS_OFFSET + evaluateNoise(noiseGen1, cx+bx, cz+bz, NOISE_GRASS_MULT, 0.01, 0.35, 2.1, 5);
dirtNoiseLUT[i] = NOISE_DIRT_MIN + (int)((1 + noiseGen2.eval(cx+bx * NOISE_DIRT_X_MULT,
cz+bz * NOISE_DIRT_Z_MULT)) * NOISE_DIRT_MULT);
}
Block block_prev{Block::AIR};
int block_prev_start{0};
// Tree LUT
int tree_lut_x_offset = cx / WOOD_CELL_SIZE - 1;
int tree_lut_z_offset = cz / WOOD_CELL_SIZE - 1;
// A space filling curve is continuous, so there is no particular order
for(int i = 0; i < TREE_LUT_SIZE; i++)
for(int k = 0; k < TREE_LUT_SIZE; k++){
int wcx = (tree_lut_x_offset + i);
int wcz = (tree_lut_z_offset + k);
treeLUT[i * TREE_LUT_SIZE + k] = evaluateTreeCell(wcx, wcz);
}
// Generation of terrain
// March along the space-filling curve, calculate information about the block at every position
// A space-filling curve is continuous, so there is no particular order
// Take advantage of the interval-map structure by only inserting contigous runs of blocks
Block block_prev{Block::AIR}, block;
int block_prev_start{0};
for (int s = 0; s < CHUNK_VOLUME; s++)
{
int x = HILBERT_XYZ_DECODE[s][0] + CHUNK_SIZE * chunk->getPosition().x;
int y = HILBERT_XYZ_DECODE[s][1] + CHUNK_SIZE * chunk->getPosition().y;
int z = HILBERT_XYZ_DECODE[s][2] + CHUNK_SIZE * chunk->getPosition().z;
int d2 = HILBERT_XYZ_DECODE[s][0] * CHUNK_SIZE + HILBERT_XYZ_DECODE[s][2];
if (grassNoiseLUT[d2] == -1){
grassNoiseLUT[d2] = GRASS_OFFSET + (int)((0.5 + noiseGen1.eval(x * NOISE_GRASS_X_MULT, z * NOISE_GRASS_Z_MULT) * NOISE_GRASS_MULT));
}
if (dirtNoiseLUT[d2] == -1){
dirtNoiseLUT[d2] = NOISE_DIRT_MIN + (int)((0.5 + noiseGen2.eval(x * NOISE_DIRT_X_MULT, z * NOISE_DIRT_Z_MULT) * NOISE_DIRT_MULT));
}
int grassNoise = grassNoiseLUT[d2];
int dirtNoise = dirtNoiseLUT[d2];
int bx = HILBERT_XYZ_DECODE[s][0];
int by = HILBERT_XYZ_DECODE[s][1];
int bz = HILBERT_XYZ_DECODE[s][2];
int x = bx + cx;
int y = by + cy;
int z = bz + cz;
int lut_index = bx * CHUNK_SIZE + bz;
int grassNoise = grassNoiseLUT[lut_index];
int dirtNoise = dirtNoiseLUT[lut_index];
int stoneLevel = grassNoise - dirtNoise;
if (y < stoneLevel)
@ -73,24 +122,116 @@ void generateNoise(Chunk::Chunk *chunk)
block = Block::DIRT;
else if (y == grassNoise)
block = Block::GRASS;
else
else
block = Block::AIR;
// Divide the world into cells, each with exactly one tree, so that no two trees will be adjacent of each other
struct TreeCellInfo info;
int wcx = (int)(x / WOOD_CELL_SIZE) - tree_lut_x_offset; // wood cell x
int wcz = (int)(z / WOOD_CELL_SIZE) - tree_lut_z_offset; // wood cell z
// Retrieve info on the cell from LUT
info = treeLUT[wcx * TREE_LUT_SIZE + wcz];
// A tree is to be placed in this position if the coordinates are those of the tree of the current cell
int wood_height = TREE_STANDARD_HEIGHT;
bool wood = x == info.trunk_x && z == info.trunk_z && y > grassNoiseLUT[lut_index] && y <= info.leaves_y_pos;
bool leaf{false};
// Check placing of leaves
if(wood) leaf = y > info.leaves_y_pos && y < info.leaves_y_pos+LEAVES_RADIUS;
else{
if(!leaf) leaf = utils::withinDistance(x,y,z, info.trunk_x, info.leaves_y_pos, info.trunk_z, LEAVES_RADIUS);
// Eventually search neighboring cells
if(!leaf && wcx+1 < TREE_LUT_SIZE){
info = treeLUT[(wcx+1) * TREE_LUT_SIZE + wcz];
leaf = utils::withinDistance(x,y,z, info.trunk_x, info.leaves_y_pos, info.trunk_z, LEAVES_RADIUS);
}
if(!leaf && wcx-1 >= 0){
info = treeLUT[(wcx-1) * TREE_LUT_SIZE + wcz];
leaf = utils::withinDistance(x,y,z, info.trunk_x, info.leaves_y_pos, info.trunk_z, LEAVES_RADIUS);
}
if(!leaf && wcz-1 >= 0){
info = treeLUT[wcx * TREE_LUT_SIZE + (wcz-1)];
leaf = utils::withinDistance(x,y,z, info.trunk_x, info.leaves_y_pos, info.trunk_z, LEAVES_RADIUS);
}
if(!leaf && wcz+1 < TREE_LUT_SIZE){
info = treeLUT[wcx * TREE_LUT_SIZE + (wcz+1)];
leaf = utils::withinDistance(x,y,z, info.trunk_x, info.leaves_y_pos, info.trunk_z, LEAVES_RADIUS);
}
}
if(wood) block = Block::WOOD;
if(leaf) block = Block::LEAVES;
// Use the interval-map structure of the chunk to compress the world: insert "runs" of
// equal blocks using indices in the hilbert curve
if (block != block_prev)
{
chunk->setBlocks(block_prev_start, s, block_prev);
block_prev_start = s;
}
block_prev = block;
}
// Insert the last run of blocks
chunk->setBlocks(block_prev_start, CHUNK_VOLUME, block_prev);
// Mark the chunk as generated, is needed to trigger the next steps
chunk->setState(Chunk::CHUNK_STATE_GENERATED, true);
}
// Noise evaluation with Fractal Brownian Motion
double evaluateNoise(OpenSimplexNoise::Noise noiseGen, double x, double y, double amplitude, double
frequency, double persistence, double lacunarity, int octaves){
double sum = 0;
for(int i = 0; i < octaves; i++){
sum += amplitude * noiseGen.eval(x*frequency, y*frequency);
amplitude *= persistence;
frequency *= lacunarity;
}
return sum;
}
// Tree cell Info
const int TREE_MASTER_SEED_X = mt();
const int TREE_MASTER_SEED_Z = mt();
struct TreeCellInfo evaluateTreeCell(int wcx, int wcz){
int anglex = TREE_MASTER_SEED_X*wcx+TREE_MASTER_SEED_Z*wcz;
int anglez = TREE_MASTER_SEED_Z*wcz+TREE_MASTER_SEED_X*wcx;
// Start at the center of the cell, with a bit of random offset
int wcx_off = WOOD_CELL_CENTER + WOOD_MAX_OFFSET * sines[anglex % 360];
int wcz_off = WOOD_CELL_CENTER + WOOD_MAX_OFFSET * cosines[anglez % 360];
struct TreeCellInfo result{};
// Cell to world coordinates
result.trunk_x = wcx * WOOD_CELL_SIZE + wcx_off;
result.trunk_z = wcz * WOOD_CELL_SIZE + wcz_off;
result.trunk_x_offset = wcx_off;
result.trunk_z_offset = wcz_off;
result.leaves_y_pos = 1 + TREE_STANDARD_HEIGHT + GRASS_OFFSET + evaluateNoise(noiseGen1,
result.trunk_x, result.trunk_z, NOISE_GRASS_MULT, 0.01, 0.35, 2.1, 5);
return result;
}
void generateChunk(Chunk::Chunk *chunk)
{
generateNoise(chunk);
}
/* EXPERIMENTAL STUFF */
void generateNoise3D(Chunk::Chunk *chunk) {
Block block_prev{Block::AIR};
Block block_prev{Block::AIR}, block;
int block_prev_start{0};
// A space filling curve is continuous, so there is no particular order
@ -124,11 +265,3 @@ void generateNoise3D(Chunk::Chunk *chunk) {
chunk->setBlocks(block_prev_start, CHUNK_VOLUME, block_prev);
}
void generatePyramid(Chunk::Chunk *chunk)
{
for (int i = 0; i < CHUNK_SIZE; i++)
for (int j = 0; j < CHUNK_SIZE; j++)
for (int k = 0; k < CHUNK_SIZE; k++)
chunk->setBlock(i >= j && i < CHUNK_SIZE - j && k >= j && k < CHUNK_SIZE - j ? (j & 1) == 0 ? Block::GRASS : Block::STONE : Block::AIR, i, j, k);
}

View File

@ -6,144 +6,297 @@
#include <thread>
#include <glm/glm.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <oneapi/tbb/concurrent_hash_map.h>
#include <oneapi/tbb/parallel_for.h>
#include "block.hpp"
#include "chunk.hpp"
#include "chunkgenerator.hpp"
#include "chunkmesher.hpp"
#include "debugwindow.hpp"
#include "globals.hpp"
#include "renderer.hpp"
#include "utils.hpp"
namespace chunkmanager
{
typedef oneapi::tbb::concurrent_hash_map<uint32_t, Chunk::Chunk*> ChunkTable;
void blockpick(WorldUpdateMsg& msg); // There's no need of passing by value again (check
// controls.cpp)
void generate();
void mesh();
void send_to_chunk_meshing_thread(Chunk::Chunk* c, int priority);
/* Chunk holding data structures */
// Concurrent hash table of chunks
ChunkTable chunks;
// Chunk indices. Centered at (0,0,0), going in concentric sphere outwards
std::array<std::array<chunk_intcoord_t, 3>, chunks_volume> chunks_indices;
//std::unordered_map<std::uint32_t, Chunk::Chunk *> chunks;
std::array<std::array<int, 3>, chunks_volume> chunks_indices;
/* World Update messaging data structure */
WorldUpdateMsgQueue WorldUpdateQueue;
/* Multithreading */
std::atomic_bool should_run;
std::thread gen_thread, mesh_thread, update_thread;
int chunks_volume_real;
std::thread init(){
// Queue of chunks to be generated
ChunkPriorityQueue chunks_to_generate_queue;
// Queue of chunks to be meshed
ChunkPriorityQueue chunks_to_mesh_queue;
WorldUpdateMsgQueue& getWorldUpdateQueue(){ return WorldUpdateQueue; }
// Init chunkmanager. Chunk indices and start threads
void init(){
int index{0};
int rr{RENDER_DISTANCE * RENDER_DISTANCE};
int xp{0}, x{0};
bool b = true;
for(chunk_intcoord_t i = -RENDER_DISTANCE; i < RENDER_DISTANCE; i++)
for(chunk_intcoord_t j = -RENDER_DISTANCE; j < RENDER_DISTANCE; j++)
for(chunk_intcoord_t k = -RENDER_DISTANCE; k < RENDER_DISTANCE; k++){
// Iterate over all chunks, in concentric spheres starting fron the player and going outwards. Alternate left and right
// Eq. of the sphere (x - a)² + (y - b)² + (z - c)² = r²
while (xp <= RENDER_DISTANCE)
{
// Alternate between left and right
if (b) x = +xp;
else x = -xp;
// Step 1. At current x, get the corresponding y values (2nd degree equation, up to 2
// possible results)
int y1 = static_cast<int>(sqrt((rr) - x*x));
for (int y = -y1 + 1 ; y <= y1; y++)
{
// Step 2. At both y's, get the corresponding z values
int z1 = static_cast<int>(sqrt( rr - x*x - y*y));
for (int z = -z1 + 1; z <= z1; z++){
chunks_indices[index][0] = x;
chunks_indices[index][1] = y;
chunks_indices[index][2] = z;
chunks_indices[index][0]=i;
chunks_indices[index][1]=j;
chunks_indices[index][2]=k;
index++;
}
}
if (!b)
{
xp++;
b = true;
}
else b = false;
}
chunks_volume_real = index;
// Also init mesh data queue
for(int i = 0; i < 10; i++)
chunkmesher::getMeshDataQueue().push(new chunkmesher::MeshData());
should_run = true;
std::thread update_thread (update);
return update_thread;
update_thread = std::thread(update);
gen_thread = std::thread(generate);
mesh_thread = std::thread(mesh);
}
oneapi::tbb::concurrent_queue<Chunk::Chunk*> chunks_todelete;
int nUnloaded{0};
// Method for world generation thread(s)
void generate(){
while(should_run){
ChunkPQEntry entry;
if(chunks_to_generate_queue.try_pop(entry)){
Chunk::Chunk* chunk = entry.first;
generateChunk(chunk);
chunk->setState(Chunk::CHUNK_STATE_IN_GENERATION_QUEUE, false);
}
}
chunks_to_generate_queue.clear();
}
// Method for chunk meshing thread(s)
void mesh(){
while(should_run){
ChunkPQEntry entry;
if(chunks_to_mesh_queue.try_pop(entry)){
Chunk::Chunk* chunk = entry.first;
chunkmesher::mesh(chunk);
chunk->setState(Chunk::CHUNK_STATE_IN_MESHING_QUEUE, false);
}
}
chunks_to_mesh_queue.clear();
}
void send_to_chunk_meshing_thread(Chunk::Chunk* c, int priority){
c->setState(Chunk::CHUNK_STATE_IN_MESHING_QUEUE, true);
chunks_to_mesh_queue.push(std::make_pair(c, MESHING_PRIORITY_NORMAL));
}
oneapi::tbb::concurrent_queue<chunk_index_t> chunks_todelete;
void update(){
while(should_run) {
int chunkX=static_cast<int>(theCamera.getAtomicPosX() / CHUNK_SIZE);
int chunkY=static_cast<int>(theCamera.getAtomicPosY() / CHUNK_SIZE);
int chunkZ=static_cast<int>(theCamera.getAtomicPosZ() / CHUNK_SIZE);
/* Setup variables for the whole loop */
// Atomic is needed by parallel_for
std::atomic_int nUnloaded{0}, nMarkUnload{0}, nExplored{0}, nMeshed{0}, nGenerated{0};
std::atomic_int chunkX=static_cast<int>(theCamera.getAtomicPosX() / CHUNK_SIZE);
std::atomic_int chunkY=static_cast<int>(theCamera.getAtomicPosY() / CHUNK_SIZE);
std::atomic_int chunkZ=static_cast<int>(theCamera.getAtomicPosZ() / CHUNK_SIZE);
// Update other chunks
for(int i = 0; i < chunks_volume_real; i++) {
const uint16_t x = chunks_indices[i][0] + chunkX;
const uint16_t y = chunks_indices[i][1] + chunkY;
const uint16_t z = chunks_indices[i][2] + chunkZ;
const uint32_t index = calculateIndex(x, y, z);
/* Process update messages before anything happens */
WorldUpdateMsg msg;
while(WorldUpdateQueue.try_pop(msg)){
switch(msg.msg_type){
case WorldUpdateMsgType::BLOCKPICK_BREAK:
case WorldUpdateMsgType::BLOCKPICK_PLACE:
blockpick(msg);
break;
}
}
if(x > 1023 || y > 1023 || z > 1023) continue;
/* Delete old chunks */
// In my head it makes sense to first delete old chunks, then create new ones
// I think it's easier for memory allocator to re-use the memory that was freed just
// before, but this isn't backed be any evidence and I might be wrong. Anyway this way
// works fine so I'm gonna keep it.
chunk_index_t i;
ChunkTable::accessor a;
while(chunks_todelete.try_pop(i)){
const chunk_index_t index = i;
if(chunks.find(a, index)){
Chunk::Chunk* c = a->second;
// Use the accessor to erase the element
// Using the key doesn't work
if(chunks.erase(a)){
nUnloaded++;
renderer::getDeleteIndexQueue().push(index);
delete c;
} else {
c->setState(Chunk::CHUNK_STATE_IN_DELETING_QUEUE, false);
std::cout << "failed to delete " << index << std::endl;
}
} else std::cout << "no such element found to delete\n";
}
/* Create new chunks around the player */
for(int i = 0; i < chunks_volume; i++) {
const chunk_intcoord_t x = chunks_indices[i][0] + chunkX;
const chunk_intcoord_t y = chunks_indices[i][1] + chunkY;
const chunk_intcoord_t z = chunks_indices[i][2] + chunkZ;
if(x < 0 || y < 0 || z < 0 || x > 1023 || y > 1023 || z > 1023) continue;
nExplored++;
const chunk_index_t index = Chunk::calculateIndex(x, y, z);
ChunkTable::accessor a;
if(!chunks.find(a, index)) chunks.emplace(a, std::make_pair(index, new Chunk::Chunk(glm::vec3(x,y,z))));
if(! (a->second->getState(Chunk::CHUNK_STATE_GENERATED))) generateChunk(a->second);
if(! (a->second->getState(Chunk::CHUNK_STATE_MESHED))) chunkmesher::mesh(a->second);
renderer::getChunksToRender().insert(a->second);
a.release();
if(!chunks.find(a, index)) chunks.emplace(a, std::make_pair(index, new
Chunk::Chunk(glm::vec3(x,y,z))));
}
Chunk::Chunk* n;
nUnloaded = 0;
while(chunks_todelete.try_pop(n)){
int x = static_cast<uint16_t>(n->getPosition().x);
int y = static_cast<uint16_t>(n->getPosition().y);
int z = static_cast<uint16_t>(n->getPosition().z);
if(x > 1023 || y > 1023 || z > 1023) continue;
const uint32_t index = calculateIndex(x, y, z);
/* Update all the chunks */
oneapi::tbb::parallel_for(chunks.range(), [&](ChunkTable::range_type &r){
for(ChunkTable::iterator a = r.begin(); a != r.end(); a++){
Chunk::Chunk* c = a->second;
chunks.erase(index);
//delete n;
nUnloaded++;
}
int x = c->getPosition().x;
int y = c->getPosition().y;
int z = c->getPosition().z;
int distx = x - chunkX;
int disty = y - chunkY;
int distz = z - chunkZ;
// Local variables avoid continously having to call atomic variables
int gen{0}, mesh{0}, unload{0};
if(
distx >= -RENDER_DISTANCE && distx < RENDER_DISTANCE &&
disty >= -RENDER_DISTANCE && disty < RENDER_DISTANCE &&
distz >= -RENDER_DISTANCE && distz < RENDER_DISTANCE
){
// If within distance
// Reset out-of-view flags
c->setState(Chunk::CHUNK_STATE_OUTOFVISION, false);
c->setState(Chunk::CHUNK_STATE_UNLOADED, false);
// If not yet generated
if(!c->getState(Chunk::CHUNK_STATE_GENERATED)){
if(c->isFree()){
// Generate
// Mark as present in the queue before sending to avoid strange
// a chunk being marked as in the queue after it was already
// processed
c->setState(Chunk::CHUNK_STATE_IN_GENERATION_QUEUE, true);
chunks_to_generate_queue.push(std::make_pair(c, GENERATION_PRIORITY_NORMAL));
}
}else{
gen++;
// If generated but not yet meshed
if(!c->getState(Chunk::CHUNK_STATE_MESHED)){
ChunkTable::accessor a1;
// Checking if nearby chunks have been generated allows for seamless
// borders between chunks
if(c->isFree() &&
(distx+1 >= RENDER_DISTANCE || x + 1 > 1023 || (chunks.find(a1, Chunk::calculateIndex(x+1, y, z)) &&
a1->second->getState(Chunk::CHUNK_STATE_GENERATED))) &&
(distx-1 < -RENDER_DISTANCE || x - 1 < 0 || (chunks.find(a1, Chunk::calculateIndex(x-1, y, z)) &&
a1->second->getState(Chunk::CHUNK_STATE_GENERATED))) &&
(disty+1 >= RENDER_DISTANCE || y + 1 > 1023 || (chunks.find(a1, Chunk::calculateIndex(x, y+1, z)) &&
a1->second->getState(Chunk::CHUNK_STATE_GENERATED))) &&
(disty-1 < -RENDER_DISTANCE || y - 1 < 0|| (chunks.find(a1, Chunk::calculateIndex(x, y-1, z)) &&
a1->second->getState(Chunk::CHUNK_STATE_GENERATED))) &&
(distz+1 >= RENDER_DISTANCE || z + 1 > 1023 || (chunks.find(a1, Chunk::calculateIndex(x, y, z+1)) &&
a1->second->getState(Chunk::CHUNK_STATE_GENERATED))) &&
(distz-1 < -RENDER_DISTANCE || z - 1 < 0|| (chunks.find(a1, Chunk::calculateIndex(x, y, z-1)) &&
a1->second->getState(Chunk::CHUNK_STATE_GENERATED)))
)
{
// Mesh
// Mark as present in the queue before sending to avoid strange
// a chunk being marked as in the queue after it was already
// processed
send_to_chunk_meshing_thread(c, MESHING_PRIORITY_NORMAL);
}
}else mesh++;
}
}else{
// If not within distance
if(c->getState(Chunk::CHUNK_STATE_OUTOFVISION)){
// If enough time has passed, set to be deleted
if(c->isFree() && glfwGetTime() - c->unload_timer >= UNLOAD_TIMEOUT){
c->setState(Chunk::CHUNK_STATE_IN_DELETING_QUEUE, true);
chunks_todelete.push(c->getIndex());
unload++;
}
}else{
// Mark as out of view, and start waiting time
c->setState(Chunk::CHUNK_STATE_OUTOFVISION, true);
c->setState(Chunk::CHUNK_STATE_UNLOADED, false);
c->unload_timer = glfwGetTime();
}
}
// Update atomic variables only once at the end
nGenerated += gen;
nMeshed += mesh;
nMarkUnload += unload;
}
});
debug::window::set_parameter("update_chunks_total", (int)chunks.size());
debug::window::set_parameter("update_chunks_generated", (int) nGenerated);
debug::window::set_parameter("update_chunks_meshed", (int) nMeshed);
debug::window::set_parameter("update_chunks_freed", (int) nUnloaded);
debug::window::set_parameter("update_chunks_explored", (int) nExplored);
}
}
// uint32_t is fine, since i'm limiting the coordinate to only use up to ten bits (1024). There's actually two spare bits
uint32_t calculateIndex(uint16_t i, uint16_t j, uint16_t k){
return i | (j << 10) | (k << 20);
std::array<std::array<chunk_intcoord_t, 3>, chunks_volume>& getChunksIndices(){ return chunks_indices; }
void stop() {
should_run=false;
std::cout << "Waiting for secondary threads to shut down" << std::endl;
update_thread.join();
std::cout << "Update thread has terminated" << std::endl;
gen_thread.join();
std::cout << "Generation thread has terminated" << std::endl;
mesh_thread.join();
std::cout << "Meshing thread has terminated" << std::endl;
}
oneapi::tbb::concurrent_queue<Chunk::Chunk*>& getDeleteVector(){ return chunks_todelete; }
std::array<std::array<int, 3>, chunks_volume>& getChunksIndices(){ return chunks_indices; }
void stop() { should_run=false; }
void destroy(){
/*for(const auto& n : chunks){
for(const auto& n : chunks){
delete n.second;
}*/
}
chunks.clear();
}
void blockpick(bool place){
glm::vec3 ray_intersect(glm::vec3 startposition, glm::vec3 startdir){
int old_bx{0}, old_by{0}, old_bz{0};
int old_px{0}, old_py{0}, old_pz{0};
Chunk::Chunk* old_chunk{nullptr};
glm::vec3 old_pos;
// cast a ray from the camera in the direction pointed by the camera itself
glm::vec3 pos = glm::vec3(theCamera.getAtomicPosX(), theCamera.getAtomicPosY(),
theCamera.getAtomicPosZ());
glm::vec3 origin = startposition;
glm::vec3 pos = origin;
glm::vec3 front = startdir;
for(float t = 0.0; t <= 10.0; t += 0.5){
// traverse the ray a block at the time
pos = theCamera.getPos() + t * theCamera.getFront();
pos = origin + t*front;
// get which chunk and block the ray is at
int px = ((int)(pos.x))/CHUNK_SIZE;
@ -153,55 +306,196 @@ namespace chunkmanager
int by = pos.y - py*CHUNK_SIZE;
int bz = pos.z - pz*CHUNK_SIZE;
// exit early if the position is invalid or the chunk does not exist
if(px < 0 || py < 0 || pz < 0 || px >= 1024 || py >= 1024 || pz >= 1024) return;
if(bx == old_bx && by == old_by && bz == old_bz) continue;
ChunkTable::accessor a;
if(!chunks.find(a, calculateIndex(px, py, pz))) return;
// exit early if the position is invalid or the chunk does not exist
if(px < 0 || py < 0 || pz < 0 || px >= 1024 || py >= 1024 || pz >= 1024) continue;
ChunkTable::const_accessor a;
if(!chunks.find(a, Chunk::calculateIndex(px, py, pz))) continue;
Chunk::Chunk* c = a->second;
if(!c->getState(Chunk::CHUNK_STATE_GENERATED) || c->getState(Chunk::CHUNK_STATE_EMPTY)) continue;
if(!c->isFree() || !c->getState(Chunk::CHUNK_STATE_GENERATED)){
a.release();
continue;
}
Block b = c->getBlock(bx, by, bz);
a.release();
// if the block is non empty
if(b != Block::AIR){
if(b != Block::AIR) return pos;
// if placing a new block
if(place){
// Go half a block backwards on the ray, to check the block where the ray was
// coming from
// Doing this and not using normal adds the unexpected (and unwanted) ability to
// place blocks diagonally, without faces colliding with the block that has
// been clicked
pos -= theCamera.getFront()*0.5f;
old_chunk = c;
old_bx = bx;
old_by = by;
old_bz = bz;
old_px = px;
old_py = py;
old_pz = pz;
old_pos = pos;
int px1 = ((int)(pos.x))/CHUNK_SIZE;
int py1 = ((int)(pos.y))/CHUNK_SIZE;
int pz1 = ((int)(pos.z))/CHUNK_SIZE;
int bx1 = pos.x - px1*CHUNK_SIZE;
int by1 = pos.y - py1*CHUNK_SIZE;
int bz1 = pos.z - pz1*CHUNK_SIZE;
}
return glm::vec3(-1);
}
// exit early if the position is invalid or the chunk does not exist
if(px1 < 0 || py1 < 0 || pz1 < 0 || px1 >= 1024 || py1 >= 1024 || pz1 >= 1024) return;
ChunkTable::accessor a1;
if(!chunks.find(a1, calculateIndex(px1, py1, pz1))) return;
Chunk::Chunk* c1 = a1->second;
// place the new block (only stone for now)
c1->setBlock( Block::STONE, bx1, by1, bz1);
void blockpick(WorldUpdateMsg& msg){
//std::cout << glm::to_string(ray_intersect(msg.cameraPos, msg.cameraFront)) << std::endl;
glm::vec3 ray_pos = ray_intersect(msg.cameraPos, msg.cameraFront);
if(ray_pos == glm::vec3(-1)) return;
// mark the mesh of the chunk the be updated
c1->setState(Chunk::CHUNK_STATE_MESHED, false);
// Chunk in which the blockpick is happening
int chunkx = (int)(ray_pos.x) / CHUNK_SIZE;
int chunky = (int)(ray_pos.y) / CHUNK_SIZE;
int chunkz = (int)(ray_pos.z) / CHUNK_SIZE;
// Block (chunk coord) in which the blockpick is happening
int blockx = ray_pos.x - chunkx*CHUNK_SIZE;
int blocky = ray_pos.y - chunky*CHUNK_SIZE;
int blockz = ray_pos.z - chunkz*CHUNK_SIZE;
// The chunk must exist, otherwise ray_intersect would have returned an error
// Also, the block must be different from AIR
ChunkTable::accessor a;
if(!chunks.find(a, Chunk::calculateIndex(chunkx, chunky, chunkz))) return;
Chunk::Chunk* c = a->second;
if(!(c->isFree() && c->getState(Chunk::CHUNK_STATE_GENERATED))) return;
if(msg.msg_type == WorldUpdateMsgType::BLOCKPICK_BREAK){
c->setBlock(Block::AIR, blockx, blocky, blockz);
send_to_chunk_meshing_thread(c, MESHING_PRIORITY_PLAYER_EDIT);
}else{
// Traverse voxel using Amanatides&Woo traversal algorithm
// http://www.cse.yorku.ca/~amana/research/grid.pdf
glm::vec3 pos = msg.cameraPos;
glm::vec3 front = glm::normalize(pos - ray_pos);
// Original chunk in which the blockpick started
const int ochunkX=chunkx, ochunkY = chunky, ochunkZ = chunkz;
// The ray has equation pos + t*front
// Initialize phase
// Origin integer voxel coordinates
// Avoid floating point accuracy errors: work as close to 0 as possible, translate
// everything later
int basex = std::floor(ray_pos.x);
int basey = std::floor(ray_pos.y);
int basez = std::floor(ray_pos.z);
double x = ray_pos.x - basex;
double y = ray_pos.y - basey;
double z = ray_pos.z - basez;
auto sign = [=](double f){ return f > 0 ? 1 : f < 0 ? -1 : 0; };
auto tmax = [=](double p, double dir){
int s = sign(dir);
if(s > 0)
return (1 - p) / dir;
else if(s < 0)
return -(p) / dir;
return 0.0;
};
// Step
int stepX = sign(front.x);
int stepY = sign(front.y);
int stepZ = sign(front.z);
// tMax: the value of t at which the ray crosses the first voxel boundary
double tMaxX = tmax(x, front.x);
double tMaxY = tmax(y, front.y);
double tMaxZ = tmax(z, front.z);
// tDelta: how far along the ray along they ray (in units of t) for the _ component of such
// a movement to equal the width of a voxel
double tDeltaX = stepX / front.x;
double tDeltaY = stepY / front.y;
double tDeltaZ = stepZ / front.z;
for(int i = 0; i < 10; i++){
if(tMaxX < tMaxY){
if(tMaxX < tMaxZ) {
x += stepX;
tMaxX += tDeltaX;
}else{
z += stepZ;
tMaxZ += tDeltaZ;
}
}else{
// replace the current block with air to remove it
c->setBlock( Block::AIR, bx, by, bz);
c->setState(Chunk::CHUNK_STATE_MESHED, false);
if(tMaxY < tMaxZ){
y += stepY;
tMaxY += tDeltaY;
}else{
z += stepZ;
tMaxZ += tDeltaZ;
}
}
int realx = basex + x;
int realy = basey + y;
int realz = basez + z;
chunkx = realx / CHUNK_SIZE;
chunky = realy / CHUNK_SIZE;
chunkz = realz / CHUNK_SIZE;
if(chunkx < 0 || chunky < 0 || chunkz < 0 || chunkx > 1023 || chunky > 1023 ||
chunkz > 1023) continue;
blockx = realx - chunkx*CHUNK_SIZE;
blocky = realy - chunky*CHUNK_SIZE;
blockz = realz - chunkz*CHUNK_SIZE;
Chunk::Chunk* chunk;
ChunkTable::accessor b;
if(chunkx != ochunkX || chunky != ochunkY || chunkz != ochunkZ){
if(!chunks.find(b, Chunk::calculateIndex(chunkx, chunky, chunkz)))
continue;
chunk = b->second;
if(!(chunk->isFree() && chunk->getState(Chunk::CHUNK_STATE_GENERATED)))
continue;
}else{
chunk = c;
}
if(chunk->getBlock(blockx, blocky, blockz) != Block::AIR) continue;
chunk->setBlock(msg.block, blockx, blocky, blockz);
send_to_chunk_meshing_thread(chunk, MESHING_PRIORITY_PLAYER_EDIT);
break;
}
}
// Release the chunk in which the blockpick started to avoid locks
a.release();
// When necessary, also mesh nearby chunks
ChunkTable::accessor a1, a2, b1, b2, c1, c2;
if(blockx == 0 && chunkx - 1 >= 0 && chunks.find(a1, Chunk::calculateIndex(chunkx - 1, chunky, chunkz)))
send_to_chunk_meshing_thread(a1->second, MESHING_PRIORITY_PLAYER_EDIT);
if(blocky == 0 && chunky - 1 >= 0 && chunks.find(b1, Chunk::calculateIndex(chunkx, chunky - 1, chunkz)))
send_to_chunk_meshing_thread(b1->second, MESHING_PRIORITY_PLAYER_EDIT);
if(blockz == 0 && chunkz - 1 >= 0 && chunks.find(c1, Chunk::calculateIndex(chunkx, chunky, chunkz - 1)))
send_to_chunk_meshing_thread(c1->second, MESHING_PRIORITY_PLAYER_EDIT);
if(blockx == CHUNK_SIZE - 1 && chunkx +1 < 1024 && chunks.find(a2, Chunk::calculateIndex(chunkx +1, chunky, chunkz)))
send_to_chunk_meshing_thread(a2->second, MESHING_PRIORITY_PLAYER_EDIT);
if(blocky == CHUNK_SIZE - 1 && chunky +1 < 1024 && chunks.find(b2, Chunk::calculateIndex(chunkx, chunky +1, chunkz)))
send_to_chunk_meshing_thread(b2->second, MESHING_PRIORITY_PLAYER_EDIT);
if(blockz == CHUNK_SIZE - 1 && chunkz +1 < 1024 && chunks.find(c2, Chunk::calculateIndex(chunkx, chunky, chunkz +1)))
send_to_chunk_meshing_thread(c2->second, MESHING_PRIORITY_PLAYER_EDIT);
// Update debugging information
debug::window::set_parameter("block_last_action", msg.msg_type ==
WorldUpdateMsgType::BLOCKPICK_PLACE);
debug::window::set_parameter("block_last_action_block_type", (int)(msg.msg_type ==
WorldUpdateMsgType::BLOCKPICK_PLACE ? msg.block : Block::AIR));
debug::window::set_parameter("block_last_action_x", chunkx*CHUNK_SIZE+blockx);
debug::window::set_parameter("block_last_action_y", chunky*CHUNK_SIZE+blocky);
debug::window::set_parameter("block_last_action_z", chunkz*CHUNK_SIZE+blockz);
}
Block getBlockAtPos(int x, int y, int z){
@ -215,7 +509,7 @@ namespace chunkmanager
//std::cout << "Block at " << x << ", " << y << ", " << z << " is in chunk " << cx << "," << cy << "," << cz << "\n";
ChunkTable::accessor a;
if(!chunks.find(a, calculateIndex(cx, cy, cz))) return Block::NULLBLK;
if(!chunks.find(a, Chunk::calculateIndex(cx, cy, cz))) return Block::NULLBLK;
else {
int bx = x % CHUNK_SIZE;
int by = y % CHUNK_SIZE;
@ -227,3 +521,4 @@ namespace chunkmanager
}
}
};

View File

@ -11,15 +11,24 @@
#include "spacefilling.hpp"
#include "utils.hpp"
#define CHUNK_MESH_DATA_QUANTITY 100
#define CHUNK_MESH_WORLD_LIMIT_BORDERS 0
namespace chunkmesher{
oneapi::tbb::concurrent_queue<MeshData*> MeshDataQueue;
ChunkMeshDataQueue MeshDataQueue;
oneapi::tbb::concurrent_queue<MeshData*>& getMeshDataQueue(){ return MeshDataQueue; }
ChunkMeshDataQueue& getMeshDataQueue(){ return MeshDataQueue; }
void init()
{
for(int i = 0; i < CHUNK_MESH_DATA_QUANTITY; i++)
MeshDataQueue.push(new ChunkMeshData{});
}
void mesh(Chunk::Chunk* chunk)
{
MeshData* mesh_data;
ChunkMeshData* mesh_data;
if(!MeshDataQueue.try_pop(mesh_data)) return;
/*
@ -38,34 +47,27 @@ void mesh(Chunk::Chunk* chunk)
*/
// Cleanup previous data
mesh_data->numVertices = 0;
mesh_data->chunk = chunk;
mesh_data->vertices.clear();
mesh_data->extents.clear();
mesh_data->texinfo.clear();
// Abort if chunk is empty
if(chunk->getState(Chunk::CHUNK_STATE_EMPTY)){
chunk->setState(Chunk::CHUNK_STATE_MESHED, true);
renderer::getMeshDataQueue().push(mesh_data);
return;
}
mesh_data->clear();
mesh_data->message_type = ChunkMeshDataType::MESH_UPDATE;
mesh_data->index = chunk->getIndex();
mesh_data->position = chunk->getPosition();
// convert tree to array since it is easier to work with it
int length{0};
std::unique_ptr<Block[]> blocks = chunk->getBlocksArray(&length);
if(length == 0) {
chunk->setState(Chunk::CHUNK_STATE_MESHED, true);
renderer::getMeshDataQueue().push(mesh_data);
return;
}
std::unique_ptr<Block[]> blocks;
int k, l, u, v, w, h, n, j, i;
int x[]{0, 0, 0};
int q[]{0, 0, 0};
int du[]{0, 0, 0};
int dv[]{0, 0, 0};
// Abort if chunk is empty
if(chunk->getState(Chunk::CHUNK_STATE_EMPTY)) goto end;
blocks = chunk->getBlocksArray(&length);
if(length == 0) goto end;
std::array<Block, CHUNK_SIZE * CHUNK_SIZE> mask;
for (bool backFace = true, b = false; b != backFace; backFace = backFace && b, b = !b)
{
@ -128,9 +130,15 @@ void mesh(Chunk::Chunk* chunk)
// The else case provides face culling for adjacent solid faces
// Checking for NULLBLK avoids creating empty faces if nearby chunk was not
// yet generated
#if CHUNK_MESH_WORLD_LIMIT_BORDERS == 1
mask[n++] = b1 == b2 ? Block::NULLBLK
: backFace ? b1 == Block::NULLBLK || b1 == Block::AIR ? b2 : Block::NULLBLK
: b2 == Block::NULLBLK || b2 == Block::AIR ? b1 : Block::NULLBLK;
#else
mask[n++] = b1 == b2 ? Block::NULLBLK
: backFace ? b1 == Block::AIR ? b2 : Block::NULLBLK
: b2 == Block::AIR ? b1 : Block::NULLBLK;
#endif
}
}
@ -191,7 +199,7 @@ void mesh(Chunk::Chunk* chunk)
mesh_data->texinfo.push_back(backFace ? 0.0 : 1.0);
mesh_data->texinfo.push_back((int)(mask[n]) - 2);
mesh_data->numVertices++;
mesh_data->num_vertices++;
}
for (l = 0; l < h; ++l)
@ -220,50 +228,8 @@ void mesh(Chunk::Chunk* chunk)
}
}
end:
chunk->setState(Chunk::CHUNK_STATE_MESHED, true);
renderer::getMeshDataQueue().push(mesh_data);
return;
}
void sendtogpu(MeshData* mesh_data)
{
if (mesh_data->numVertices > 0)
{
if(mesh_data->chunk->VAO == 0) mesh_data->chunk->createBuffers();
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(mesh_data->chunk->VAO);
// position attribute
glBindBuffer(GL_ARRAY_BUFFER, mesh_data->chunk->VBO);
glBufferData(GL_ARRAY_BUFFER, mesh_data->vertices.size() * sizeof(GLubyte), &(mesh_data->vertices[0]), GL_STATIC_DRAW);
glVertexAttribIPointer(0, 3, GL_UNSIGNED_BYTE, 3 * sizeof(GLubyte), (void *)0);
glEnableVertexAttribArray(0);
// normal attribute
glBindBuffer(GL_ARRAY_BUFFER, mesh_data->chunk->extentsBuffer);
glBufferData(GL_ARRAY_BUFFER, mesh_data->extents.size() * sizeof(GLubyte), &(mesh_data->extents[0]), GL_STATIC_DRAW);
glVertexAttribIPointer(1, 3, GL_UNSIGNED_BYTE, 3 * sizeof(GLubyte), (void *)0);
glEnableVertexAttribArray(1);
// texcoords attribute
glBindBuffer(GL_ARRAY_BUFFER, mesh_data->chunk->texinfoBuffer);
glBufferData(GL_ARRAY_BUFFER, mesh_data->texinfo.size() * sizeof(GLubyte), &(mesh_data->texinfo[0]), GL_STATIC_DRAW);
glEnableVertexAttribArray(2);
glVertexAttribIPointer(2, 2, GL_UNSIGNED_BYTE, 2 * sizeof(GLubyte), (void *)0);
glBindVertexArray(0);
// save the number of indices of the mesh, it is needed later for drawing
mesh_data->chunk->numVertices = (GLuint)(mesh_data->numVertices);
// once data has been sent to the GPU, it can be cleared from system RAM
mesh_data->vertices.clear();
mesh_data->extents.clear();
mesh_data->texinfo.clear();
}
// mark the chunk mesh has loaded on GPU
mesh_data->chunk->setState(Chunk::CHUNK_STATE_MESH_LOADED, true);
}
};

66
src/controls.cpp Normal file
View File

@ -0,0 +1,66 @@
#include "controls.hpp"
#include "camera.hpp"
#include "chunkmanager.hpp"
#include "debugwindow.hpp"
#include "globals.hpp"
#include "renderer.hpp"
namespace controls{
/* Block picking */
int block_to_place{2};
float lastBlockPick=0.0;
bool blockpick = false;
/* Cursor */
bool cursor = false;
void init(){
debug::window::set_parameter("block_type_return", &block_to_place);
}
void update(GLFWwindow* window){
float current_time = glfwGetTime();
/* BlockPicking */
// Reset blockpicking if enough time has passed
if(current_time - lastBlockPick > BLOCKPICK_TIMEOUT) blockpick = false;
// Reset blockpicking if both mouse buttons are released
if(glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) == GLFW_RELEASE && glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) == GLFW_RELEASE) blockpick = false;
// Process block picking if a mouse button is pressed
if((glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) == GLFW_PRESS ||
glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2 == GLFW_PRESS)) && !blockpick){
// Start timeout for next block pick action
blockpick = true;
lastBlockPick = current_time;
// Construct the message to send to chunkmanager
// WorldUpdateMsg is allocated on the stack
// unlike ChunkMeshData, the fields of WorldUpdateMsg are few and light, so there's no
// problem in passing them by value each time.
// It also has the advantage of having less memory to manage, since I'm not allocating
// anything on the heap
WorldUpdateMsg msg{};
msg.cameraPos = theCamera.getPos();
msg.cameraFront = theCamera.getFront();
msg.time = current_time;
msg.msg_type = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) == GLFW_PRESS ?
WorldUpdateMsgType::BLOCKPICK_PLACE : WorldUpdateMsgType::BLOCKPICK_BREAK;
msg.block = (Block)(block_to_place);
// Send to chunk manager
chunkmanager::getWorldUpdateQueue().push(msg);
}
/* SCREENSHOTS */
if(glfwGetKey(window, GLFW_KEY_F2) == GLFW_PRESS) renderer::saveScreenshot();
if(glfwGetKey(window, GLFW_KEY_F3) == GLFW_PRESS) renderer::saveScreenshot(true);
if(glfwGetKey(window, GLFW_KEY_M) == GLFW_PRESS) {
cursor = !cursor;
glfwSetInputMode(window, GLFW_CURSOR, cursor ? GLFW_CURSOR_NORMAL : GLFW_CURSOR_DISABLED);
}
}
};

136
src/debugwindow.cpp Normal file
View File

@ -0,0 +1,136 @@
#include "debugwindow.hpp"
#include <imgui/imgui.h>
#include <imgui/imgui_impl_opengl3.h>
#include <imgui/imgui_impl_glfw.h>
#include <imgui_stdlib.h>
#include <iostream>
#include <string>
#include <unordered_map>
namespace debug{
namespace window{
void show_debug_window();
constexpr int frametimes_array_size = 20;
float frametimes_array[frametimes_array_size]{};
std::unordered_map<std::string, std::any> parameters;
void init(GLFWwindow* window){
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
// Setup Platform/Renderer backends
ImGui_ImplGlfw_InitForOpenGL(window, true); // Second param install_callback=true will install GLFW callbacks and chain to existing ones.
ImGui_ImplOpenGL3_Init();
}
void prerender(){
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
//ImGui::ShowDemoWindow(); // Show demo window! :)
show_debug_window();
}
void render(){
// (Your code clears your framebuffer, renders your other stuff etc.)
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
// (Your code calls glfwSwapBuffers() etc.)
}
void destroy(){
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
}
void set_parameter(std::string key, std::any value){
parameters[key] = value;
}
void show_debug_window(){
ImGui::Begin("Debug Window");
ImGui::PushItemWidth(ImGui::GetFontSize() * -12);
try{
if (ImGui::CollapsingHeader("Frametimes")){
ImGui::Text("FPS: %d", std::any_cast<int>(parameters.at("fps")));
ImGui::Text("Frametime (ms): %f",
std::any_cast<float>(parameters.at("frametime"))*1000);
ImGui::Text("GPU: (%s) %s",
std::any_cast<const GLubyte*>(parameters.at("gpu_vendor")),
std::any_cast<const GLubyte*>(parameters.at("gpu_renderer")));
//ImGui::PlotLines("Frame Times", arr, IM_ARRAYSIZE(arr);
}
if(ImGui::CollapsingHeader("Player")){
ImGui::Text("X: %f, Y: %f, Z: %f",
std::any_cast<float>(parameters.at("px")),std::any_cast<float>(parameters.at("py")),std::any_cast<float>(parameters.at("pz")) );
ImGui::Text("X: %d, Y: %d, Z: %d (chunk)", std::any_cast<int>(parameters.at("cx")),std::any_cast<int>(parameters.at("cy")),std::any_cast<int>(parameters.at("cz")) );
ImGui::Text("Pointing in direction: %f, %f, %f",
std::any_cast<float>(parameters.at("lx")),std::any_cast<float>(parameters.at("ly")),std::any_cast<float>(parameters.at("lz")) );
ImGui::SliderInt("Crosshair type",
std::any_cast<int*>(parameters.at("crosshair_type_return")), 0, 1);
ImGui::SliderInt("Block to place",
std::any_cast<int*>(parameters.at("block_type_return")), 2, 6);
if(parameters.find("block_last_action") != parameters.end()){
ImGui::Text("Last Block action: %s",
std::any_cast<bool>(parameters.at("block_last_action")) ? "place" : "destroy");
ImGui::Text("Last Block action block type: %d",
std::any_cast<int>(parameters.at("block_last_action_block_type")));
ImGui::Text("Last Block action position: X: %d, Y: %d, Z: %d",
std::any_cast<int>(parameters.at("block_last_action_x")),std::any_cast<int>(parameters.at("block_last_action_y")),std::any_cast<int>(parameters.at("block_last_action_z")) );
}
}
if(ImGui::CollapsingHeader("Mesh")){
ImGui::Text("Total chunk meshed: %d",
std::any_cast<int>(parameters.at("render_chunks_total")));
ImGui::Text("Of which renderable (not empty): %d",
std::any_cast<int>(parameters.at("render_chunks_renderable")));
ImGui::Text("Chunks rendered: %d",
std::any_cast<int>(parameters.at("render_chunks_rendered")));
ImGui::Text("Frustum culled: %d",
std::any_cast<int>(parameters.at("render_chunks_culled")));
ImGui::Text("Total vertices in the scene: %d",
std::any_cast<int>(parameters.at("render_chunks_vertices")));
ImGui::Checkbox("Wireframe",
std::any_cast<bool*>(parameters.at("wireframe_return")));
}
if(ImGui::CollapsingHeader("Chunks")){
ImGui::Text("Total chunks present: %d",
std::any_cast<int>(parameters.at("update_chunks_total")));
ImGui::Text("Chunks generated: %d",
std::any_cast<int>(parameters.at("update_chunks_generated")));
ImGui::Text("Chunks meshed: %d",
std::any_cast<int>(parameters.at("update_chunks_meshed")));
ImGui::Text("Chunks actually freed from memory: %d",
std::any_cast<int>(parameters.at("update_chunks_freed")));
ImGui::Text("Chunks explored: %d",
std::any_cast<int>(parameters.at("update_chunks_explored")));
}
}catch(const std::bad_any_cast& e){
std::cout << e.what() << std::endl;
}catch(const std::out_of_range& e){
std::cout << e.what() << std::endl;
}
ImGui::End();
}
}
}

View File

@ -1,5 +1,4 @@
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "main.hpp"
#include <iostream>
#include <thread>
@ -7,22 +6,18 @@
#define GLOBALS_DEFINER
#include "globals.hpp"
#undef GLOBALS_DEFINER
#include "chunkmanager.hpp"
#include "main.hpp"
#include "controls.hpp"
#include "debugwindow.hpp"
#include "renderer.hpp"
#include "spacefilling.hpp"
#include "shader.hpp"
#include "spacefilling.hpp"
float deltaTime = 0.0f; // Time between current frame and last frame
float lastFrame = 0.0f; // Time of last frame
float lastFPSFrame = 0.0f;
int frames = 0;
float lastBlockPick=0.0;
bool blockpick = false;
bool canChangeWireframe = true;
int main()
{
@ -57,16 +52,23 @@ int main()
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
glfwSetCursorPosCallback(window, mouse_callback);
glEnable(GL_DEPTH_TEST);
//glEnable(GL_FRAMEBUFFER_SRGB); //gamma correction done in fragment shader
//glEnable(GL_CULL_FACE); //GL_BACK GL_CCW by default
std::cout << "Using GPU: " << glGetString(GL_VENDOR) << " " << glGetString(GL_RENDERER) << "\n";
debug::window::set_parameter("gpu_vendor", glGetString(GL_VENDOR));
debug::window::set_parameter("gpu_renderer", glGetString(GL_RENDERER));
for(int i = 0; i < 360; i++){
sines[i] = sin(3.14 / 180 * i);
cosines[i] = cos(3.14 / 180 * i);
}
SpaceFilling::initLUT();
wireframe = false;
renderer::init();
std::thread chunkmanager_thread = chunkmanager::init();
controls::init();
chunkmanager::init();
chunkmesher::init();
debug::window::init(window);
renderer::init(window);
while (!glfwWindowShouldClose(window))
{
@ -75,10 +77,12 @@ int main()
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
debug::window::set_parameter("frametime", deltaTime);
// FPS Counter
frames++;
if(currentFrame - lastFPSFrame >= 1.0f){
std::cout << "FPS: " << frames << " Frametime: " << deltaTime << std::endl;
//std::cout << "FPS: " << frames << " Frametime: " << deltaTime << std::endl;
debug::window::set_parameter("fps", frames);
frames = 0;
lastFPSFrame = currentFrame;
}
@ -86,14 +90,26 @@ int main()
glClearColor(0.431f, 0.694f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Input handling
// Only close event is handles by main
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
// the rest of input processing is handled by controls.cpp
// Input processing
processInput(window);
controls::update(window);
// Camera
theCamera.update(window, deltaTime);
// Reset blockping timeout if 200ms have passed
if(glfwGetTime() - lastBlockPick > 0.1) blockpick = false;
debug::window::set_parameter("px", theCamera.getPos().x);
debug::window::set_parameter("py", theCamera.getPos().y);
debug::window::set_parameter("pz", theCamera.getPos().z);
debug::window::set_parameter("cx", (int)(theCamera.getPos().x / CHUNK_SIZE));
debug::window::set_parameter("cy", (int)(theCamera.getPos().y / CHUNK_SIZE));
debug::window::set_parameter("cz", (int)(theCamera.getPos().z / CHUNK_SIZE));
debug::window::set_parameter("lx", theCamera.getFront().x);
debug::window::set_parameter("ly", theCamera.getFront().y);
debug::window::set_parameter("lz", theCamera.getFront().z);
// Render pass
renderer::render();
@ -105,7 +121,6 @@ int main()
// Stop threads and wait for them to finish
chunkmanager::stop();
chunkmanager_thread.join();
// Cleanup allocated memory
chunkmanager::destroy();
@ -119,37 +134,10 @@ void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
glViewport(0, 0, width, height);
theCamera.viewPortCallBack(window, width, height);
renderer::framebuffer_size_callback(window, width, height);
}
void mouse_callback(GLFWwindow *window, double xpos, double ypos)
{
theCamera.mouseCallback(window, xpos, ypos);
}
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if(glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) == GLFW_PRESS && !blockpick){
chunkmanager::blockpick(false);
blockpick=true;
lastBlockPick=glfwGetTime();
}
if(glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) == GLFW_PRESS && !blockpick){
chunkmanager::blockpick(true);
blockpick=true;
lastBlockPick=glfwGetTime();
}
if (glfwGetKey(window, GLFW_KEY_F) == GLFW_PRESS && canChangeWireframe){
wireframe = !wireframe;
canChangeWireframe = false;
}
if (glfwGetKey(window, GLFW_KEY_F) == GLFW_RELEASE) canChangeWireframe = true;
// Reset blockpicking if enough time has passed
if(glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) == GLFW_RELEASE && glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) == GLFW_RELEASE) blockpick = false;
}

View File

@ -1,32 +1,95 @@
#include "renderer.hpp"
#include <oneapi/tbb/concurrent_vector.h>
#include <oneapi/tbb/concurrent_queue.h>
#include <glm/ext.hpp>
#include <glm/gtx/string_cast.hpp>
#include <oneapi/tbb/concurrent_hash_map.h>
#include "chunkmanager.hpp"
#include "chunkmesher.hpp"
#include "debugwindow.hpp"
#include "globals.hpp"
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
namespace renderer{
RenderSet chunks_torender;
oneapi::tbb::concurrent_vector<Chunk::Chunk*> render_todelete;
oneapi::tbb::concurrent_queue<chunkmesher::MeshData*> MeshDataQueue;
typedef oneapi::tbb::concurrent_hash_map<chunk_index_t, RenderInfo*> RenderTable;
Shader* theShader;
RenderTable ChunksToRender;
ChunkMeshDataQueue MeshDataQueue;
IndexQueue MeshDataToDelete;
Shader* theShader, *quadShader;
GLuint chunkTexture;
Shader* getRenderShader() { return theShader; }
RenderSet& getChunksToRender(){ return chunks_torender; }
oneapi::tbb::concurrent_queue<chunkmesher::MeshData*>& getMeshDataQueue(){ return MeshDataQueue; }
ChunkMeshDataQueue& getMeshDataQueue(){ return MeshDataQueue; }
IndexQueue& getDeleteIndexQueue(){ return MeshDataToDelete; }
GLuint renderTexFrameBuffer, renderTex, renderTexDepthBuffer, quadVAO, quadVBO;
int screenWidth, screenHeight;
void init(){
int crosshair_type{0};
bool wireframe{false};
void init(GLFWwindow* window){
// Setup rendering
// We will render the image to a texture, then display the texture on a quad that fills the
// entire screen.
// This makes it easy to capture screenshots or apply filters to the final image (e.g.
// over-impress HUD elements like a crosshair)
glfwGetWindowSize(window, &screenWidth, &screenHeight);
glGenFramebuffers(1, &renderTexFrameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, renderTexFrameBuffer);
// Depth buffer
glGenRenderbuffers(1, &renderTexDepthBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderTexDepthBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, screenWidth, screenHeight); //Support up to
//full-hd for now
// Attach it to the frame buffer
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER,
renderTexDepthBuffer);
// Create texture to render to
// The texture we're going to render to
glGenTextures(1, &renderTex);
glBindTexture(GL_TEXTURE_2D, renderTex);
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, screenWidth, screenHeight, 0,GL_RGB, GL_UNSIGNED_BYTE, 0); // Support
// up to
// full-hd
// for now
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// Set the texture as a render attachment for the framebuffer
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, renderTex, 0);
// Create the quad to render the texture to
float vertices[] = {
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f, 1.0f, 1.0f
};
glGenBuffers(1, &quadVBO);
glGenVertexArrays(1, &quadVAO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5*sizeof(float), (void*)0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindVertexArray(0);
// Rendering of the world
// Create Shader
theShader = new Shader{"shaders/shader-texture.gs", "shaders/shader-texture.vs", "shaders/shader-texture.fs"};
quadShader = new Shader{nullptr, "shaders/shader-quad.vs", "shaders/shader-quad.fs"};
// Block textures
// Create 3d array texture
constexpr int layerCount = 3;
constexpr int layerCount = 5;
glGenTextures(1, &chunkTexture);
glBindTexture(GL_TEXTURE_2D_ARRAY, chunkTexture);
@ -38,15 +101,38 @@ namespace renderer{
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 1, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, texels1);
unsigned char *texels2 = stbi_load("textures/grass_top.png", &width, &height, &nrChannels, 0);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 2, width, height, 1, GL_RGB, GL_UNSIGNED_BYTE, texels2);
unsigned char *texels3 = stbi_load("textures/wood.png", &width, &height, &nrChannels, 0);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 3, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, texels3);
unsigned char *texels4 = stbi_load("textures/leaves.png", &width, &height, &nrChannels, 0);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 4, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, texels4);
glTexParameteri(GL_TEXTURE_2D_ARRAY,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY,GL_TEXTURE_WRAP_S,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D_ARRAY,GL_TEXTURE_WRAP_T,GL_REPEAT);
debug::window::set_parameter("crosshair_type_return", &crosshair_type);
debug::window::set_parameter("wireframe_return", &wireframe);
}
void render(){
int total{0}, toGpu{0};
// Bind the frame buffer to render to the texture
glBindFramebuffer(GL_FRAMEBUFFER, renderTexFrameBuffer);
glViewport(0, 0, screenWidth, screenHeight);
glEnable(GL_DEPTH_TEST);
if(wireframe) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
// Clear the screen
glClearColor(0.431f, 0.694f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/* UPDATE IMGUI */
debug::window::prerender();
/* RENDER THE WORLD TO TEXTURE */
int total{0}, toGpu{0}, oof{0}, vertices{0};
glm::vec4 frustumPlanes[6];
theCamera.getFrustumPlanes(frustumPlanes, true);
glm::vec3 cameraPos = theCamera.getPos();
@ -55,32 +141,69 @@ namespace renderer{
theShader->use();
theShader->setVec3("viewPos", cameraPos);
chunkmesher::MeshData* m;
/* Process incoming mesh data */
ChunkMeshData* m;
while(MeshDataQueue.try_pop(m)){
chunkmesher::sendtogpu(m);
RenderTable::accessor a;
RenderInfo* render_info;
if(ChunksToRender.find(a, m->index)){
render_info = a->second;
render_info->position = m->position;
render_info->num_vertices = m->num_vertices;
// Always updated the mesh, even if it's empty
// This should solve the problem of having floating quads when destroying a block
// near chunk borders
send_chunk_to_gpu(m, render_info);
}else{
render_info = new RenderInfo();
render_info->index = m->index;
render_info->position = m->position;
render_info->num_vertices = m->num_vertices;
ChunksToRender.emplace(a, std::make_pair(render_info->index, render_info));
// Only send the mesh to the GPU if it's not empty
if(render_info->num_vertices > 0) send_chunk_to_gpu(m, render_info);
}
chunkmesher::getMeshDataQueue().push(m);
}
if(wireframe) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
/* Process chunks to be removed */
chunk_index_t queue_index;
while(MeshDataToDelete.try_pop(queue_index)){
RenderTable::accessor a;
for(auto& c : chunks_torender){
float dist = glm::distance(c->getPosition(), cameraChunkPos);
if(dist <= static_cast<float>(RENDER_DISTANCE)){
if(!c->getState(Chunk::CHUNK_STATE_MESH_LOADED)) continue;
if(ChunksToRender.find(a, queue_index)){
RenderInfo* render_info = a->second;
render_info->deallocateBuffers();
delete render_info;
ChunksToRender.erase(a);
}
}
// reset out-of-vision and unload flags
c->setState(Chunk::CHUNK_STATE_OUTOFVISION, false);
c->setState(Chunk::CHUNK_STATE_UNLOADED, false);
/* Render the chunks */
// parallel_for cannot be used since all the rendering needs to happen in a single thread
for(RenderTable::iterator i = ChunksToRender.begin(); i != ChunksToRender.end(); i++){
RenderInfo* render_info = i->second;
if(render_info->num_vertices > 0)
{
total++;
// Increase total vertex count
vertices += render_info->num_vertices;
// Perform frustum culling and eventually render
glm::vec3 chunk = c->getPosition();
glm::vec3 chunk = render_info->position;
glm::vec4 chunkW = glm::vec4(chunk.x*static_cast<float>(CHUNK_SIZE), chunk.y*static_cast<float>(CHUNK_SIZE), chunk.z*static_cast<float>(CHUNK_SIZE),1.0);
glm::mat4 model = glm::translate(glm::mat4(1.0), ((float)CHUNK_SIZE) * chunk);
// Check if all the corners of the chunk are outside any of the planes
// TODO (?) implement frustum culling as per (Inigo Quilez)[https://iquilezles.org/articles/frustumcorrect/], and check each
// plane against each corner of the chunk
// plane against each corner of the chunk
bool out=false;
int a{0};
for(int p = 0; p < 6; p++){
@ -96,49 +219,125 @@ namespace renderer{
if (!out)
{
if(c->numVertices > 0)
{
theShader->setMat4("model", model);
theShader->setMat4("view", theCamera.getView());
theShader->setMat4("projection", theCamera.getProjection());
theShader->setMat4("model", model);
theShader->setMat4("view", theCamera.getView());
theShader->setMat4("projection", theCamera.getProjection());
glBindVertexArray(c->VAO);
glDrawArrays(GL_POINTS, 0, c->numVertices);
glBindVertexArray(0);
}
}
}else{
// When the chunk is outside render distance
glBindVertexArray(render_info->VAO);
glDrawArrays(GL_POINTS, 0, render_info->num_vertices);
glBindVertexArray(0);
if(c->getState(Chunk::CHUNK_STATE_OUTOFVISION)){
if(glfwGetTime() - c->unload_timer > UNLOAD_TIMEOUT){
// If chunk was already out and enough time has passed
// Mark the chunk to be unloaded
// And mark is to be removed from the render set
render_todelete.push_back(c);
}
} else{
// Mark has out of vision and annotate when it started
c->setState(Chunk::CHUNK_STATE_OUTOFVISION, true);
c->setState(Chunk::CHUNK_STATE_UNLOADED, false);
c->unload_timer = glfwGetTime();
toGpu++;
}
}
}
for(auto& c : render_todelete){
// we can get away with unsafe erase as access to the container is only done by this
// thread
c->deleteBuffers();
chunks_torender.unsafe_erase(c);
chunkmanager::getDeleteVector().push(c);
debug::window::set_parameter("render_chunks_total", (int)(ChunksToRender.size()));
debug::window::set_parameter("render_chunks_rendered", toGpu);
debug::window::set_parameter("render_chunks_renderable", total);
debug::window::set_parameter("render_chunks_culled", total-toGpu);
debug::window::set_parameter("render_chunks_vertices", vertices);
/* DISPLAY TEXTURE ON A QUAD THAT FILLS THE SCREEN */
// Now to render the quad, with the texture on top
// Switch to the default frame buffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(0.431f, 0.694f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, renderTex);
quadShader->use();
quadShader->setInt("screenWidth", screenWidth);
quadShader->setInt("screenHeight", screenHeight);
quadShader->setInt("crosshairType", crosshair_type);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
debug::window::render();
}
void send_chunk_to_gpu(ChunkMeshData* mesh_data, RenderInfo* render_info)
{
if(!render_info->buffers_allocated) render_info->allocateBuffers();
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(render_info->VAO);
// TODO: change GL_STATIC_DRAW to the one that means "few redraws and further in between"
// position attribute
glBindBuffer(GL_ARRAY_BUFFER, render_info->VBO);
glBufferData(GL_ARRAY_BUFFER, mesh_data->vertices.size() * sizeof(GLfloat), &(mesh_data->vertices[0]), GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// normal attribute
glBindBuffer(GL_ARRAY_BUFFER, render_info->extentsBuffer);
glBufferData(GL_ARRAY_BUFFER, mesh_data->extents.size() * sizeof(GLfloat), &(mesh_data->extents[0]), GL_STATIC_DRAW);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)(0));
glEnableVertexAttribArray(1);
// texcoords attribute
glBindBuffer(GL_ARRAY_BUFFER, render_info->texinfoBuffer);
glBufferData(GL_ARRAY_BUFFER, mesh_data->texinfo.size() * sizeof(GLfloat), &(mesh_data->texinfo[0]), GL_STATIC_DRAW);
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0);
glBindVertexArray(0);
}
void framebuffer_size_callback(GLFWwindow *window, int width, int height){
resize_framebuffer(width, height);
}
void resize_framebuffer(int width, int height){
screenWidth = width;
screenHeight = height;
theCamera.viewPortCallBack(nullptr, width, height);
glBindFramebuffer(GL_FRAMEBUFFER, renderTexFrameBuffer);
glBindTexture(GL_TEXTURE_2D, renderTex);
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, width, height, 0,GL_RGB, GL_UNSIGNED_BYTE, 0); // Support
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, renderTex, 0);
glBindRenderbuffer(GL_RENDERBUFFER, renderTexDepthBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height); //Support up to
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER,
renderTexDepthBuffer);
}
void saveScreenshot(bool forceFullHD){
int old_screenWidth = screenWidth;
int old_screenHeight = screenHeight;
if(forceFullHD){
resize_framebuffer(1920, 1080);
// Do a render pass
render();
}
render_todelete.clear();
// Bind the render frame buffer
glBindFramebuffer(GL_FRAMEBUFFER, renderTexFrameBuffer);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
// Save the framebuffer in a byte array
GLubyte data[screenWidth*screenHeight*3];
glReadPixels(0, 0, screenWidth, screenHeight, GL_RGB, GL_UNSIGNED_BYTE, data);
// Save the byte array onto a texture
stbi_flip_vertically_on_write(1);
stbi_write_png(forceFullHD ? "screenshot_fullhd.png" : "screenshot.png", screenWidth,
screenHeight, 3, data, screenWidth*3);
if(forceFullHD) resize_framebuffer(old_screenWidth, old_screenHeight);
}
void destroy(){
delete theShader;
delete quadShader;
}

BIN
textures/leaves.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

View File

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 263 B