Make world rendering work.

This commit is contained in:
Signal 2025-12-19 01:11:03 -05:00
parent d99b70eaa1
commit 48d055e889
22 changed files with 438 additions and 82 deletions

BIN
assets/rgt_grass_top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

BIN
assets/rgt_oak_log_top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

BIN
assets/rgt_stone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

BIN
assets/rgt_stone_brick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

View file

@ -4,13 +4,13 @@
namespace Artifact { namespace Artifact {
void ChunkRenderer::render(glm::ivec3 pos, World::Chunk* chunk, WGPUCommandEncoder encoder, WGPUTextureView texture) { void ChunkRenderer::render(WGPUCommandEncoder encoder, WGPUTextureView texture) {
WGPURenderPassColorAttachment colorAttachment = {}; WGPURenderPassColorAttachment colorAttachment = {};
colorAttachment.view = texture; colorAttachment.view = texture;
colorAttachment.loadOp = WGPULoadOp_Clear; colorAttachment.loadOp = WGPULoadOp_Clear;
colorAttachment.storeOp = WGPUStoreOp_Store; colorAttachment.storeOp = WGPUStoreOp_Store;
colorAttachment.clearValue = {0, 0, 0, 1}; colorAttachment.clearValue = {0.53, 0.81, 0.92, 1};
colorAttachment.depthSlice = -1; colorAttachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
WGPURenderPassDepthStencilAttachment depthAttachment = {}; WGPURenderPassDepthStencilAttachment depthAttachment = {};
depthAttachment.view = engine->depthTexture->view; depthAttachment.view = engine->depthTexture->view;
@ -20,6 +20,7 @@ void ChunkRenderer::render(glm::ivec3 pos, World::Chunk* chunk, WGPUCommandEncod
depthAttachment.depthReadOnly = false; depthAttachment.depthReadOnly = false;
depthAttachment.stencilLoadOp = WGPULoadOp_Undefined; depthAttachment.stencilLoadOp = WGPULoadOp_Undefined;
depthAttachment.stencilStoreOp = WGPUStoreOp_Undefined; depthAttachment.stencilStoreOp = WGPUStoreOp_Undefined;
depthAttachment.stencilReadOnly = true;
WGPURenderPassDescriptor renderPassDesc = {}; WGPURenderPassDescriptor renderPassDesc = {};
renderPassDesc.colorAttachmentCount = 1; renderPassDesc.colorAttachmentCount = 1;
@ -39,4 +40,131 @@ void ChunkRenderer::render(glm::ivec3 pos, World::Chunk* chunk, WGPUCommandEncod
wgpuRenderPassEncoderEnd(cubeRenderPass); wgpuRenderPassEncoderEnd(cubeRenderPass);
} }
void ChunkRenderer::addChunk(glm::ivec3 pos, World::Chunk* chunk) {
generateChunkMesh(pos, chunk);
}
void ChunkRenderer::removeChunk(glm::ivec3 pos) {
auto buffers = chunkBuffers.at(pos);
wgpuBufferDestroy(buffers.vertices);
wgpuBufferDestroy(buffers.indices);
chunkBuffers.erase(pos);
}
void ChunkRenderer::generateChunkMesh(glm::ivec3 pos, World::Chunk* chunk) {
int vertexCount = 0;
int indexCount = 0;
constexpr int SX = World::Chunk::SIZE;
constexpr int SY = World::Chunk::SIZE;
constexpr int SZ = World::Chunk::SIZE;
// Temporary CPU-side vectors we will write them only to compute size,
// then copy once at the end.
std::vector<Vertex> verts;
std::vector<uint32_t> inds;
verts.reserve(24 * 1024); // rough upper bound
inds.reserve (36 * 1024);
// Helper to emit one quad (counter-clockwise when looking at the face)
auto emitQuad = [&verts, &inds](float x0, float y0, float z0,
float x1, float y1, float z1,
float x2, float y2, float z2,
float x3, float y3, float z3,
float u0, float v0, float u1, float v1) {
uint32_t base = static_cast<uint32_t>(verts.size());
verts.push_back({{x0,y0,z0}, {u0,v0}});
verts.push_back({{x1,y1,z1}, {u0,v1}});
verts.push_back({{x2,y2,z2}, {u1,v1}});
verts.push_back({{x3,y3,z3}, {u1,v0}});
inds.push_back(base + 0); inds.push_back(base + 1); inds.push_back(base + 2);
inds.push_back(base + 0); inds.push_back(base + 2); inds.push_back(base + 3);
};
for (int x = 0; x < SX; ++x) {
for (int y = 0; y < SY; ++y) {
for (int z = 0; z < SZ; ++z) {
uint16_t id = chunk->getNodeRaw(x, y, z);
float fx0 = static_cast<float>(x) + pos.x * 16;
float fy0 = static_cast<float>(y) + pos.y * 16;
float fz0 = static_cast<float>(z) + pos.z * 16;
float fx1 = fx0 + 1.0f;
float fy1 = fy0 + 1.0f;
float fz1 = fz0 + 1.0f;
auto def = engine->world->nodeRegistry.getDefinition(id);
World::NodeTextureCoords uv;
if(def.visual == World::NodeVisual::NONE) {
continue;
} else {
uv = engine->world->nodeRegistry.getTextureCoords(id);
}
// +X
if (chunk->getNodeRaw(x+1, y, z) == 0) {
emitQuad(fx1,fy0,fz0, fx1,fy1,fz0, fx1,fy1,fz1, fx1,fy0,fz1, uv.uvMin.x, uv.uvMin.y, uv.uvMax.x, uv.uvMax.y);
}
// -X
if (chunk->getNodeRaw(x-1, y, z) == 0) {
emitQuad(fx0,fy0,fz1, fx0,fy1,fz1, fx0,fy1,fz0, fx0,fy0,fz0, uv.uvMin.x, uv.uvMin.y, uv.uvMax.x, uv.uvMax.y);
}
// +Y (top)
if (chunk->getNodeRaw(x, y+1, z) == 0) {
emitQuad(fx0,fy1,fz0, fx1,fy1,fz0, fx1,fy1,fz1, fx0,fy1,fz1, uv.uvMin.x, uv.uvMin.y, uv.uvMax.x, uv.uvMax.y);
}
// -Y (bottom)
if (chunk->getNodeRaw(x, y-1, z) == 0) {
emitQuad(fx0,fy0,fz1, fx1,fy0,fz1, fx1,fy0,fz0, fx0,fy0,fz0, uv.uvMin.x, uv.uvMin.y, uv.uvMax.x, uv.uvMax.y);
}
// +Z
if (chunk->getNodeRaw(x, y, z+1) == 0) {
emitQuad(fx0,fy0,fz1, fx0,fy1,fz1, fx1,fy1,fz1, fx1,fy0,fz1, uv.uvMin.x, uv.uvMin.y, uv.uvMax.x, uv.uvMax.y);
}
// -Z
if (chunk->getNodeRaw(x, y, z-1) == 0) {
emitQuad(fx1,fy0,fz0, fx1,fy1,fz0, fx0,fy1,fz0, fx0,fy0,fz0, uv.uvMin.x, uv.uvMin.y, uv.uvMax.x, uv.uvMax.y);
}
}
}
}
if(chunkBuffers.count(pos)) {
removeChunk(pos);
}
auto isEmpty = inds.empty();
if (isEmpty) {
return;
}
vertexCount = verts.size();
indexCount = inds.size();
std::cout << "Vertices: " << vertexCount << "; Indices: " << indexCount << std::endl;
WGPUBufferDescriptor vertexBufferDesc = {};
vertexBufferDesc.size = vertexCount * sizeof(Vertex);
vertexBufferDesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
vertexBufferDesc.label = WGPUStringView{"Chunk vertex buffer", WGPU_STRLEN};
auto vertices = wgpuDeviceCreateBuffer(engine->device, &vertexBufferDesc);
wgpuQueueWriteBuffer(engine->queue, vertices, 0, verts.data(), vertexBufferDesc.size);
WGPUBufferDescriptor indexBufferDesc = {};
indexBufferDesc.size = indexCount * sizeof(uint32_t);
indexBufferDesc.usage = WGPUBufferUsage_Index | WGPUBufferUsage_CopyDst;
indexBufferDesc.label = WGPUStringView{"Chunk index buffer", WGPU_STRLEN};
auto indices = wgpuDeviceCreateBuffer(engine->device, &indexBufferDesc);
wgpuQueueWriteBuffer(engine->queue, indices, 0, inds.data(), indexBufferDesc.size);
chunkBuffers.emplace(pos, ChunkRenderData {
.vertices = vertices,
.indices = indices,
.vertexCount = vertexCount,
.indexCount = indexCount
});
}
} }

View file

@ -5,25 +5,13 @@
#include <Mesh.h> #include <Mesh.h>
namespace Artifact { namespace Artifact {
namespace World {
class ClientChunk: public Chunk {
WGPUBuffer vertices;
WGPUBuffer indices;
void generateMesh() {
}
};
}
class Engine; class Engine;
class ChunkRenderer { class ChunkRenderer {
struct ChunkRenderData { struct ChunkRenderData {
WGPUBuffer vertices; WGPUBuffer vertices = nullptr;
WGPUBuffer indices; WGPUBuffer indices = nullptr;
int vertexCount; int vertexCount;
int indexCount; int indexCount;
}; };
@ -31,7 +19,10 @@ class ChunkRenderer {
std::unordered_map<glm::vec3, ChunkRenderData, Util::Vec3Hash, Util::Vec3Equal> chunkBuffers; std::unordered_map<glm::vec3, ChunkRenderData, Util::Vec3Hash, Util::Vec3Equal> chunkBuffers;
public: public:
ChunkRenderer(Engine* engine) : engine(engine) {} ChunkRenderer(Engine* engine) : engine(engine) {}
void render(glm::ivec3 pos, World::Chunk* chunk, WGPUCommandEncoder encoder, WGPUTextureView texture); void render(WGPUCommandEncoder encoder, WGPUTextureView texture);
void addChunk(glm::ivec3 pos, World::Chunk* chunk);
void removeChunk(glm::ivec3 pos);
void generateChunkMesh(glm::ivec3 pos, World::Chunk* chunk);
}; };
} }

View file

@ -12,7 +12,7 @@ Window::Window() {
glfwSetWindowUserPointer(window, this); glfwSetWindowUserPointer(window, this);
glfwSetKeyCallback(window, [](GLFWwindow* window, int key, int scancode, int action, int mods){ glfwSetKeyCallback(window, [](GLFWwindow* window, int key, int scancode, int action, int mods){
auto me = reinterpret_cast<Window*>(glfwGetWindowUserPointer(window)); auto me = static_cast<Window*>(glfwGetWindowUserPointer(window));
me->dispatch(Events::Key{ me->dispatch(Events::Key{
.key = key, .key = key,
.scancode = scancode, .scancode = scancode,
@ -22,7 +22,7 @@ Window::Window() {
}); });
glfwSetCursorPosCallback(window, [](GLFWwindow* window, double x, double y) { glfwSetCursorPosCallback(window, [](GLFWwindow* window, double x, double y) {
auto me = reinterpret_cast<Window*>(glfwGetWindowUserPointer(window)); auto me = static_cast<Window*>(glfwGetWindowUserPointer(window));
me->dispatch(Events::CursorPos{ me->dispatch(Events::CursorPos{
.x = x, .x = x,
.y = y .y = y
@ -30,7 +30,7 @@ Window::Window() {
}); });
glfwSetMouseButtonCallback(window, [](GLFWwindow* window, int button, int action, int mods) { glfwSetMouseButtonCallback(window, [](GLFWwindow* window, int button, int action, int mods) {
auto me = reinterpret_cast<Window*>(glfwGetWindowUserPointer(window)); auto me = static_cast<Window*>(glfwGetWindowUserPointer(window));
Events::ActionType type; Events::ActionType type;
switch(button) { switch(button) {
case GLFW_MOUSE_BUTTON_LEFT: case GLFW_MOUSE_BUTTON_LEFT:
@ -55,6 +55,14 @@ Window::Window() {
.y = y .y = y
}); });
}); });
glfwSetScrollCallback(window, [](GLFWwindow* window, double dx, double dy) {
auto me = static_cast<Window*>(glfwGetWindowUserPointer(window));
me->dispatch(Events::Scroll{
.dx = dx,
.dy = dy
});
});
} }
Window::~Window() { Window::~Window() {
@ -73,4 +81,14 @@ void Window::close() const {
glfwSetWindowShouldClose(window, true); glfwSetWindowShouldClose(window, true);
} }
void Window::setPointerLock() {
pointerLocked = true;
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
}
void Window::releasePointerLock() {
pointerLocked = false;
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
}
} }

View file

@ -34,20 +34,30 @@ struct Action {
double y; double y;
}; };
struct Scroll {
double dx;
double dy;
};
} }
class Window: public EventTarget { class Window: public EventTarget {public:
GLFWwindow* window; GLFWwindow* window;
public:
int width = 1200; int width = 1200;
int height = 800; int height = 800;
bool pointerLocked = false;
Window(); Window();
~Window(); ~Window();
WGPUSurface createWGPUSurface(WGPUInstance &instance); WGPUSurface createWGPUSurface(WGPUInstance &instance);
bool shouldClose(); bool shouldClose();
void close() const; void close() const;
// TODO: Allow changing the window cursor. // TODO: Allow changing the window cursor.
void setCursor(); void setCursor();
void setPointerLock();
void releasePointerLock();
}; };
} }

View file

@ -1,5 +1,6 @@
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <chrono>
#include <glfw3webgpu.h> #include <glfw3webgpu.h>
#include <webgpu/webgpu.h> #include <webgpu/webgpu.h>
@ -50,7 +51,7 @@ Engine::~Engine() {
/*/ /*/
// Hack to ensure GLFW is initialized before the user does anything that might break it.
static struct _glfwInit { static struct _glfwInit {
_glfwInit() { _glfwInit() {
if(!glfwInit()) printf("Failed to initialize GLFW."); if(!glfwInit()) printf("Failed to initialize GLFW.");
@ -134,7 +135,7 @@ void Engine::init() {
depthDesc.size.width = viewportWidth; depthDesc.size.width = viewportWidth;
depthDesc.size.height = viewportHeight; depthDesc.size.height = viewportHeight;
depthDesc.size.depthOrArrayLayers = 1; depthDesc.size.depthOrArrayLayers = 1;
depthDesc.format = WGPUTextureFormat_Depth24Plus; depthDesc.format = WGPUTextureFormat_Depth32Float;
depthDesc.usage = WGPUTextureUsage_RenderAttachment; depthDesc.usage = WGPUTextureUsage_RenderAttachment;
depthDesc.mipLevelCount = 1; depthDesc.mipLevelCount = 1;
depthDesc.sampleCount = 1; depthDesc.sampleCount = 1;
@ -255,7 +256,15 @@ void Engine::init() {
}); });
window.listen<Events::CursorPos>([this](auto ev) { window.listen<Events::CursorPos>([this](auto ev) {
static double lastX = viewportWidth / 2.0;
static double lastY = viewportHeight / 2.0;
nk_input_motion(ui.get(), ev.x, ev.y); nk_input_motion(ui.get(), ev.x, ev.y);
server->players[0]->yaw += (ev.x - lastX) * 0.2f;
server->players[0]->pitch -= (ev.y - lastY) * 0.2f;
server->players[0]->pitch = std::max(-89.0f, std::min(89.0f, (float)server->players[0]->pitch));
lastX = ev.x;
lastY = ev.y;
}); });
window.listen<Events::Action>([this](auto ev) { window.listen<Events::Action>([this](auto ev) {
@ -274,6 +283,11 @@ void Engine::init() {
return; return;
} }
nk_input_button(ui.get(), type, (int)ev.x, (int)ev.y, ev.state); nk_input_button(ui.get(), type, (int)ev.x, (int)ev.y, ev.state);
window.setPointerLock();
});
window.listen<Events::Scroll>([this](auto ev) {
nk_input_scroll(ui.get(), nk_vec2(ev.dx, ev.dy));
}); });
int atlasWidth, atlasHeight; int atlasWidth, atlasHeight;
@ -447,7 +461,7 @@ void Engine::makeWorldPipelines() {
cubeFragmentState.targets = &cubeColorTarget; cubeFragmentState.targets = &cubeColorTarget;
WGPUDepthStencilState depthStencilState = {}; WGPUDepthStencilState depthStencilState = {};
depthStencilState.format = WGPUTextureFormat_Depth24Plus; depthStencilState.format = WGPUTextureFormat_Depth32Float;
depthStencilState.depthWriteEnabled = WGPUOptionalBool_True; depthStencilState.depthWriteEnabled = WGPUOptionalBool_True;
depthStencilState.depthCompare = WGPUCompareFunction_Less; depthStencilState.depthCompare = WGPUCompareFunction_Less;
@ -629,20 +643,22 @@ void Engine::makeUIPipeline() {
void Engine::run() { void Engine::run() {
init(); init();
time_t last_time; sendMessage(Events::PlayerJoin {
time(&last_time); .name = "singleplayer",
.pos = {0, 0, 0}
});
auto lastTime = std::chrono::steady_clock::now().time_since_epoch().count();
while(!window.shouldClose()) { while(!window.shouldClose()) {
nk_input_begin(ui.get()); nk_input_begin(ui.get());
glfwPollEvents(); glfwPollEvents();
nk_input_end(ui.get()); nk_input_end(ui.get());
render(); render();
time_t now; auto now = std::chrono::steady_clock::now().time_since_epoch().count();
time(&now); if(now - lastTime > STEP_SIZE) {
if(now - last_time > STEP_SIZE) {
tick(); tick();
dispatch(Events::Tick{now - last_time}); dispatch(Events::Tick{now - lastTime});
last_time = now; lastTime = now;
} }
} }
} }
@ -691,13 +707,11 @@ void Engine::render() {
WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr); WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr);
for(const auto &pair : world->chunks) { chunkRenderer->render(encoder, nextTexture);
chunkRenderer->render(pair.first, pair.second.get(), encoder, nextTexture);
}
WGPURenderPassColorAttachment colorAttachment = {}; WGPURenderPassColorAttachment colorAttachment = {};
colorAttachment.view = nextTexture; colorAttachment.view = nextTexture;
colorAttachment.loadOp = WGPULoadOp_Clear; colorAttachment.loadOp = WGPULoadOp_Load;
colorAttachment.storeOp = WGPUStoreOp_Store; colorAttachment.storeOp = WGPUStoreOp_Store;
colorAttachment.clearValue = {0, 0.2, 0.4, 1}; colorAttachment.clearValue = {0, 0.2, 0.4, 1};
colorAttachment.depthSlice = -1; colorAttachment.depthSlice = -1;
@ -799,6 +813,45 @@ void Engine::render() {
void Engine::tick() { void Engine::tick() {
if(server) server->tick(); if(server) server->tick();
glm::vec3 direction;
direction.x = cos(glm::radians(server->players[0]->yaw)) * cos(glm::radians(server->players[0]->pitch));
direction.y = sin(glm::radians(server->players[0]->pitch));
direction.z = sin(glm::radians(server->players[0]->yaw)) * cos(glm::radians(server->players[0]->pitch));
direction = glm::normalize(direction);
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 right = glm::normalize(glm::cross(direction, up));
glm::vec3 forward = glm::normalize(glm::vec3(direction.x, 0.0f, direction.z));
float speed = 0.1f;
glm::vec3 moveDir(0.0f);
if (glfwGetKey(window.window, GLFW_KEY_W) == GLFW_PRESS)
moveDir += forward * speed;
if (glfwGetKey(window.window, GLFW_KEY_S) == GLFW_PRESS)
moveDir -= forward * speed;
if (glfwGetKey(window.window, GLFW_KEY_A) == GLFW_PRESS)
moveDir -= right * speed;
if (glfwGetKey(window.window, GLFW_KEY_D) == GLFW_PRESS)
moveDir += right * speed;
if (glfwGetKey(window.window, GLFW_KEY_SPACE) == GLFW_PRESS)
moveDir += up * speed;
if (glfwGetKey(window.window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS)
moveDir -= up * speed;
glm::vec3 newPos = server->players[0]->pos + moveDir;
server->players[0]->pos = newPos;
glm::vec3 cameraPos = newPos;
auto view = glm::lookAt(cameraPos, cameraPos + direction, glm::vec3(0, 1, 0));
glm::mat4 model = glm::mat4(1.0f);
// model = glm::translate(model, glm::vec3(nkx, nky, nkz));
// app->view = glm::rotate(app->view, (float)property / 10, newPos + glm::vec3(0, 1, 0));
// model = glm::rotate(model, (float)rot2 / 10, glm::vec3(1, 0, 0));
// model = glm::scale(model, glm::vec3(scale));
glm::mat4 mvp = glm::perspective(glm::radians(45.0f), (float)viewportWidth / viewportHeight, 0.1f, 100.0f) * view * model;
wgpuQueueWriteBuffer(queue, worldUniformBuffer, 0, &mvp, sizeof(mvp));
} }
} }

View file

@ -119,7 +119,7 @@ public:
std::string assetPath = std::string(getenv("HOME")) + "/eclipse-workspace/ArtifactEngine/assets"; std::string assetPath = std::string(getenv("HOME")) + "/eclipse-workspace/ArtifactEngine/assets";
glm::mat4 ortho; glm::mat4 ortho;
Server* server; Server* server = nullptr;
Engine(); Engine();
~Engine(); ~Engine();
@ -150,8 +150,8 @@ public:
std::cout << "Message received" << std::endl; std::cout << "Message received" << std::endl;
} else if constexpr (std::is_same_v<T, Events::ChunkChanged>) { } else if constexpr (std::is_same_v<T, Events::ChunkChanged>) {
auto ev = (Events::ChunkChanged) msg; auto ev = (Events::ChunkChanged) msg;
world->chunks.at(ev.pos)->data = ev.data; world->chunks.emplace(ev.pos, std::make_unique<World::Chunk>(ev.data));
// chunkRenderer->updateChunk(ev.pos, ev.data); chunkRenderer->addChunk(ev.pos, world->chunks.at(ev.pos).get());
} }
} }
}; };
@ -166,7 +166,7 @@ void Artifact::Engine::sendMessage(T msg){
if(server) { if(server) {
server->receiveMessage(msg); server->receiveMessage(msg);
} else { } else {
auto buffer = cista::serialize(msg); //auto buffer = cista::serialize(msg);
} }
} }

View file

@ -7,15 +7,28 @@
#include <any> #include <any>
#include <algorithm> #include <algorithm>
#include <glm.hpp>
namespace Artifact { namespace Artifact {
namespace Events { namespace Events {
// Common events.
// Fired when the engine finishes initializing. // Fired when the engine finishes initializing.
struct Initialized {}; struct Initialized {};
// Fired every time the engine updates game logic. // Fired every time the engine updates game logic.
struct Tick { struct Tick {
time_t dtime; time_t dtime;
}; };
// Server-only events.
// Fired when a player joins the server.
struct PlayerJoin {
std::string name;
glm::vec3 pos;
};
} }
class EventTarget { class EventTarget {

View file

@ -42,8 +42,13 @@ int main() {
}); });
Artifact::World::NodeDef def_Stone; Artifact::World::NodeDef def_Stone;
def_Stone.texture = "test.png"; def_Stone.visual = Artifact::World::NodeVisual::CUBE;
def_Stone.texture = "rgt_stone_brick.png";
engine.world->nodeRegistry.registerNode("test", def_Stone); engine.world->nodeRegistry.registerNode("test", def_Stone);
def_Stone.texture = "rgt_stone.png";
engine.world->nodeRegistry.registerNode("test2", def_Stone);
def_Stone.texture = "rgt_grass_top.png";
engine.world->nodeRegistry.registerNode("test3", def_Stone);
engine.run(); engine.run();

View file

@ -7,7 +7,7 @@
namespace Artifact { namespace Artifact {
Server::Server(Engine* localClient) : localClient(localClient) { Server::Server(Engine* localClient) : localClient(localClient) {
players.push_back(std::make_unique<ServerPlayer>());
} }
void Server::run() { void Server::run() {
@ -60,7 +60,7 @@ void Server::start() {
} }
void Server::tick() { void Server::tick() {
world->tick();
} }
} }

View file

@ -5,6 +5,8 @@
#include <cista.h> #include <cista.h>
#include <Events.h> #include <Events.h>
#include <ServerPlayer.h>
#include <ServerWorld.h>
#define STEP_SIZE 200 #define STEP_SIZE 200
@ -15,6 +17,9 @@ class Engine;
class Server: public EventTarget { class Server: public EventTarget {
Artifact::Engine* localClient; Artifact::Engine* localClient;
public: public:
std::vector<std::unique_ptr<ServerPlayer>> players;
std::unique_ptr<World::ServerWorld> world = std::make_unique<World::ServerWorld>(this);
Server(Engine* localClient); Server(Engine* localClient);
void run(); void run();
@ -27,6 +32,21 @@ public:
void receiveMessage(T msg) { void receiveMessage(T msg) {
if constexpr (std::is_same_v<T, Events::Initialized>) { if constexpr (std::is_same_v<T, Events::Initialized>) {
std::cout << "Message received" << std::endl; std::cout << "Message received" << std::endl;
} else if constexpr (std::is_same_v<T, Events::PlayerJoin>) {
std::cout << "<" << msg.name << "> joined." << std::endl;
for(int x = -5, y = -5, z = -5; x <= 5 && y <= 5 && z <= 5;) {
auto chunk = world->loadChunk(msg.pos + glm::vec3{x, y, z});
sendMessage(Events::ChunkChanged{.pos = msg.pos + glm::vec3{x, y, z}, .data = chunk->data});
++x;
if(x > 5) {
x = -5;
++y;
if(y > 5) {
y = -5;
++z;
}
}
}
} }
} }
}; };
@ -40,6 +60,6 @@ void Artifact::Server::sendMessage(T msg) {
if(localClient) { if(localClient) {
localClient->receiveMessage(msg); localClient->receiveMessage(msg);
} else { } else {
auto buffer = cista::serialize(msg); //auto buffer = cista::serialize(msg);
} }
} }

View file

@ -0,0 +1,9 @@
#include <ServerPlayer.h>
namespace Artifact {
ServerPlayer::ServerPlayer() {
}
}

18
src/server/ServerPlayer.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#include <glm.hpp>
namespace Artifact {
class ServerPlayer {
public:
ServerPlayer();
glm::ivec3 lastChunkPos;
glm::vec3 pos;
double pitch;
double yaw;
int viewRange = 4;
};
}

View file

@ -1,4 +1,6 @@
#include <unordered_set>
#include <Server.h>
#include <ServerWorld.h> #include <ServerWorld.h>
namespace Artifact { namespace Artifact {
@ -8,36 +10,105 @@ ServerWorld::ServerWorld(Server* server) : server(server) {
} }
void ServerWorld::tick() { glm::ivec3 getChunkPos(glm::vec3 p) {
return {static_cast<int>(std::floor(p.x / 16.0)), static_cast<int>(std::floor(p.y / 16.0)), static_cast<int>(std::floor(p.z / 16.0))};
} }
void ServerWorld::updateLoadedChunks() { std::unordered_set<glm::ivec3, Util::Vec3Hash, Util::Vec3Equal> generateViewChunks(const glm::ivec3& center, int range) {
std::unordered_set<glm::ivec3, Util::Vec3Hash, Util::Vec3Equal> view;
for (int dx = -range; dx <= range; ++dx)
for (int dy = -range; dy <= range; ++dy)
for (int dz = -range; dz <= range; ++dz) {
if (std::max(std::abs(dx), std::abs(dz)) <= range) {
view.emplace(glm::ivec3{center.x + dx, center.y +dy, center.z + dz});
}
}
return view;
}
void ServerWorld::tick() {
std::vector<glm::ivec3> toLoad;
std::vector<glm::ivec3> toUnload;
// First pass: Load or generate chunks and update meshes
for(const auto &player : server->players) {
auto currentPos = getChunkPos(player->pos);
if(currentPos == player->lastChunkPos) continue;
auto oldView = generateViewChunks(player->lastChunkPos, player->viewRange);
auto newView = generateViewChunks(currentPos, player->viewRange);
// Compute removed chunks (in old but not new).
std::unordered_set<glm::ivec3, Util::Vec3Hash, Util::Vec3Equal> removed;
for (const auto& c : oldView) {
if (newView.find(c) == newView.end()) {
removed.insert(c);
}
}
// Compute added chunks (in new but not old).
std::unordered_set<glm::ivec3, Util::Vec3Hash, Util::Vec3Equal> added;
for (const auto& c : newView) {
if (oldView.find(c) == oldView.end()) {
added.insert(c);
}
}
// Process removed first (order doesn't matter, but unload before load for safety).
for (const auto& c : removed) {
auto it = refcounts.find(c);
if (it != refcounts.end() && --(it->second) == 0) {
toUnload.push_back(c);
refcounts.erase(it);
chunks.erase(c);
}
}
// Process added.
for (const auto& c : added) {
int& ref = refcounts[c]; // Default constructs to 0 if missing.
if (ref == 0) {
toLoad.push_back(c);
loadChunk(c);
server->sendMessage(Events::ChunkChanged{.pos = c, .data = chunks.at(c)->data});
}
++ref;
}
player->lastChunkPos = currentPos;
}
} }
Chunk* ServerWorld::loadChunk(glm::ivec3 pos) { Chunk* ServerWorld::loadChunk(glm::ivec3 pos) {
auto chunk = chunks.at(pos).get(); if(chunks.count(pos)) {
if(chunk) { return chunks.at(pos).get();
return chunk;
} }
// TODO: Load from disk // TODO: Load from disk
std::array<uint16_t, Chunk::SIZE_CUBED> data; std::array<uint16_t, Chunk::SIZE_CUBED> data;
for(int x = 0; x < CHUNK_SIZE; ++x) data.fill(0);
for(int z = 0; z < CHUNK_SIZE; ++z) { if (pos.y > 0) {
data[1] = 2; // Stone
data[2] = 3; // Stone
data[3] = 4; // Stone
} else {
// chunk.voxels.assign(Chunk::SIZE * Chunk::SIZE * Chunk::SIZE, 0); // Reset voxels
for (int x = 0; x < Chunk::SIZE; ++x) {
for (int z = 0; z < Chunk::SIZE; ++z) {
int height = 8 + ((pos.x * 7 + pos.z * 13 + x * 3 + z * 5) % 8); // Heights 8 to 15 int height = 8 + ((pos.x * 7 + pos.z * 13 + x * 3 + z * 5) % 8); // Heights 8 to 15
if (height < 0) height = 0; if (height < 0) height = 0;
if (height >= Chunk::SIZE) height = Chunk::SIZE - 1; if (height >= Chunk::SIZE) height = Chunk::SIZE - 1;
for (int y = 0; y <= 7; ++y) { for (int y = 0; y <= 7; ++y) {
data[x + y * Chunk::SIZE + z * Chunk::SIZE * Chunk::SIZE] = std::rand() % 3 + 1; // Stone data[x + y * Chunk::SIZE + z * Chunk::SIZE * Chunk::SIZE] = std::rand() % 3 + 2; // Stone
} }
if (x > 3 && x < Chunk::SIZE - 3 && z > 3 && z < Chunk::SIZE - 3) { if (x > 3 && x < Chunk::SIZE - 3 && z > 3 && z < Chunk::SIZE - 3) {
data[x + 8 * Chunk::SIZE + z * Chunk::SIZE * Chunk::SIZE] = 2; // Grass on top data[x + 8 * Chunk::SIZE + z * Chunk::SIZE * Chunk::SIZE] = 4; // Grass on top
} }
} }
chunks.at(pos) = std::make_unique<Chunk>(data); }
}
chunks.emplace(pos, std::make_unique<Chunk>(data));
return chunks.at(pos).get(); return chunks.at(pos).get();
} }

View file

@ -1,18 +1,20 @@
#pragma once #pragma once
#include <World.h> #include <World.h>
#include <Server.h>
namespace Artifact { namespace Artifact {
class Server;
namespace World { namespace World {
class ServerWorld: public World { class ServerWorld: public World {
Server* server = nullptr; Server* server = nullptr;
std::unordered_map<glm::vec3, int, Util::Vec3Hash, Util::Vec3Equal> refcounts;
public: public:
ServerWorld(Server* server); ServerWorld(Server* server);
void tick(); void tick();
void updateLoadedChunks();
Chunk* loadChunk(glm::ivec3 pos); Chunk* loadChunk(glm::ivec3 pos);
}; };

View file

@ -16,6 +16,9 @@ Chunk::~Chunk() {
} }
uint16_t Chunk::getNodeRaw(float x, float y, float z) { uint16_t Chunk::getNodeRaw(float x, float y, float z) {
if(x < 0 || y < 0 || z < 0 || x >= Chunk::SIZE || y >= Chunk::SIZE || z >= Chunk::SIZE) {
return 0;
}
return data.at(posToIndex(x, y, z)); return data.at(posToIndex(x, y, z));
} }

View file

@ -12,7 +12,10 @@ int posToIndex(float x, float y, float z) {
} }
NodeRegistry::NodeRegistry() { NodeRegistry::NodeRegistry() {
registerNode("air", NodeDef{}); registerNode("air", NodeDef{.visual = NodeVisual::NONE});
registerNode("unknown", NodeDef{
.texture = "rgt_oak_log_top.png"
});
} }
bool NodeRegistry::generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::vector<unsigned char>& atlasData) { bool NodeRegistry::generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::vector<unsigned char>& atlasData) {
@ -27,8 +30,10 @@ bool NodeRegistry::generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::
std::vector<std::vector<unsigned char>> textureData; std::vector<std::vector<unsigned char>> textureData;
std::vector<int> widths, heights; std::vector<int> widths, heights;
int maxWidth = 0, maxHeight = 0; int maxWidth = 0, maxHeight = 0;
for (size_t i = 1; i < registeredNodes.size(); ++i) { // Skip ID 0 (air) for (size_t i = 0; i < registeredNodes.size(); ++i) { // Skip ID 0 (air)
if(registeredNodes.at(i).visual == NodeVisual::NONE) continue; if(registeredNodes.at(i).visual == NodeVisual::NONE) {
continue;
}
drawables.push_back(i); drawables.push_back(i);
int width, height, channels; int width, height, channels;
unsigned char* data = stbi_load(registeredNodes.at(i).texture.c_str(), &width, &height, &channels, 4); unsigned char* data = stbi_load(registeredNodes.at(i).texture.c_str(), &width, &height, &channels, 4);
@ -37,8 +42,8 @@ bool NodeRegistry::generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::
return false; return false;
} }
textureData.push_back({}); textureData.push_back({});
textureData.at(i).resize(width * height * 4); textureData.at(drawables.size() - 1).resize(width * height * 4);
std::memcpy(textureData[i].data(), data, width * height * 4); std::memcpy(textureData[drawables.size() - 1].data(), data, width * height * 4);
widths.push_back(width); widths.push_back(width);
heights.push_back(height); heights.push_back(height);
maxWidth = std::max(maxWidth, width); maxWidth = std::max(maxWidth, width);
@ -48,12 +53,12 @@ bool NodeRegistry::generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::
// Simple atlas layout: stack textures horizontally // Simple atlas layout: stack textures horizontally
// (You can improve this with a more efficient packing algorithm if needed) // (You can improve this with a more efficient packing algorithm if needed)
atlasWidth = maxWidth * (drawables.size()) + 1; // -1 because ID 0 (air) has no texture atlasWidth = maxWidth * (drawables.size()); // -1 because ID 0 (air) has no texture
atlasHeight = maxHeight + 1; atlasHeight = maxHeight;
atlasData.resize(atlasWidth * atlasHeight * 4, 0); // RGBA atlasData.resize(atlasWidth * atlasHeight * 4, 0); // RGBA
for (size_t i = 1; i < drawables.size(); ++i) { for (uint16_t i = 0; i < drawables.size(); ++i) {
int xOffset = (i - 1) * maxWidth; int xOffset = i * maxWidth;
for (int y = 0; y < heights[i]; ++y) { for (int y = 0; y < heights[i]; ++y) {
for (int x = 0; x < widths[i]; ++x) { for (int x = 0; x < widths[i]; ++x) {
int srcIdx = (y * widths[i] + x) * 4; int srcIdx = (y * widths[i] + x) * 4;
@ -68,7 +73,7 @@ bool NodeRegistry::generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::
float uMax = static_cast<float>(xOffset + widths[i]) / atlasWidth; float uMax = static_cast<float>(xOffset + widths[i]) / atlasWidth;
float vMin = 0.0f; float vMin = 0.0f;
float vMax = static_cast<float>(heights[i]) / atlasHeight; float vMax = static_cast<float>(heights[i]) / atlasHeight;
textureCoords.at(drawables.at(i)) = {{uMin, vMin}, {uMax, vMax}}; textureCoords.emplace(drawables.at(i), NodeTextureCoords{{uMin, vMin}, {uMax, vMax}});
} }
return true; return true;

View file

@ -28,7 +28,7 @@ struct NodeDef {
std::string name; std::string name;
std::string texture; std::string texture;
AlphaBlendMode alphaBlendMode = AlphaBlendMode::NONE; AlphaBlendMode alphaBlendMode = AlphaBlendMode::NONE;
NodeVisual visual = NodeVisual::NONE; NodeVisual visual = NodeVisual::CUBE;
}; };
struct NodeTextureCoords { struct NodeTextureCoords {
@ -41,7 +41,7 @@ class NodeRegistry {
uint16_t next = 0; uint16_t next = 0;
public: public:
std::unordered_map<uint16_t, NodeDef> registeredNodes; std::unordered_map<uint16_t, NodeDef> registeredNodes;
std::vector<NodeTextureCoords> textureCoords; std::unordered_map<uint16_t, NodeTextureCoords> textureCoords;
NodeRegistry(); NodeRegistry();
@ -51,6 +51,16 @@ public:
registeredNodes.emplace(next++, def); registeredNodes.emplace(next++, def);
} }
const NodeDef& getDefinition(uint16_t id) {
if(!registeredNodes.count(id)) return registeredNodes.at(1);
return registeredNodes.at(id);
}
const NodeTextureCoords& getTextureCoords(uint16_t id) {
if(!textureCoords.count(id)) return textureCoords.at(1);
return textureCoords.at(id);
}
bool generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::vector<unsigned char>& atlasData); bool generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::vector<unsigned char>& atlasData);
}; };