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 {
void ChunkRenderer::render(glm::ivec3 pos, World::Chunk* chunk, WGPUCommandEncoder encoder, WGPUTextureView texture) {
void ChunkRenderer::render(WGPUCommandEncoder encoder, WGPUTextureView texture) {
WGPURenderPassColorAttachment colorAttachment = {};
colorAttachment.view = texture;
colorAttachment.loadOp = WGPULoadOp_Clear;
colorAttachment.storeOp = WGPUStoreOp_Store;
colorAttachment.clearValue = {0, 0, 0, 1};
colorAttachment.depthSlice = -1;
colorAttachment.clearValue = {0.53, 0.81, 0.92, 1};
colorAttachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
WGPURenderPassDepthStencilAttachment depthAttachment = {};
depthAttachment.view = engine->depthTexture->view;
@ -20,6 +20,7 @@ void ChunkRenderer::render(glm::ivec3 pos, World::Chunk* chunk, WGPUCommandEncod
depthAttachment.depthReadOnly = false;
depthAttachment.stencilLoadOp = WGPULoadOp_Undefined;
depthAttachment.stencilStoreOp = WGPUStoreOp_Undefined;
depthAttachment.stencilReadOnly = true;
WGPURenderPassDescriptor renderPassDesc = {};
renderPassDesc.colorAttachmentCount = 1;
@ -39,4 +40,131 @@ void ChunkRenderer::render(glm::ivec3 pos, World::Chunk* chunk, WGPUCommandEncod
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>
namespace Artifact {
namespace World {
class ClientChunk: public Chunk {
WGPUBuffer vertices;
WGPUBuffer indices;
void generateMesh() {
}
};
}
class Engine;
class ChunkRenderer {
struct ChunkRenderData {
WGPUBuffer vertices;
WGPUBuffer indices;
WGPUBuffer vertices = nullptr;
WGPUBuffer indices = nullptr;
int vertexCount;
int indexCount;
};
@ -31,7 +19,10 @@ class ChunkRenderer {
std::unordered_map<glm::vec3, ChunkRenderData, Util::Vec3Hash, Util::Vec3Equal> chunkBuffers;
public:
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);
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{
.key = key,
.scancode = scancode,
@ -22,7 +22,7 @@ Window::Window() {
});
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{
.x = x,
.y = y
@ -30,7 +30,7 @@ Window::Window() {
});
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;
switch(button) {
case GLFW_MOUSE_BUTTON_LEFT:
@ -55,6 +55,14 @@ Window::Window() {
.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() {
@ -73,4 +81,14 @@ void Window::close() const {
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;
};
struct Scroll {
double dx;
double dy;
};
}
class Window: public EventTarget {
class Window: public EventTarget {public:
GLFWwindow* window;
public:
int width = 1200;
int height = 800;
bool pointerLocked = false;
Window();
~Window();
WGPUSurface createWGPUSurface(WGPUInstance &instance);
bool shouldClose();
void close() const;
// TODO: Allow changing the window cursor.
void setCursor();
void setPointerLock();
void releasePointerLock();
};
}

View file

@ -1,5 +1,6 @@
#include <iostream>
#include <memory>
#include <chrono>
#include <glfw3webgpu.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 {
_glfwInit() {
if(!glfwInit()) printf("Failed to initialize GLFW.");
@ -134,7 +135,7 @@ void Engine::init() {
depthDesc.size.width = viewportWidth;
depthDesc.size.height = viewportHeight;
depthDesc.size.depthOrArrayLayers = 1;
depthDesc.format = WGPUTextureFormat_Depth24Plus;
depthDesc.format = WGPUTextureFormat_Depth32Float;
depthDesc.usage = WGPUTextureUsage_RenderAttachment;
depthDesc.mipLevelCount = 1;
depthDesc.sampleCount = 1;
@ -255,7 +256,15 @@ void Engine::init() {
});
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);
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) {
@ -274,6 +283,11 @@ void Engine::init() {
return;
}
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;
@ -447,7 +461,7 @@ void Engine::makeWorldPipelines() {
cubeFragmentState.targets = &cubeColorTarget;
WGPUDepthStencilState depthStencilState = {};
depthStencilState.format = WGPUTextureFormat_Depth24Plus;
depthStencilState.format = WGPUTextureFormat_Depth32Float;
depthStencilState.depthWriteEnabled = WGPUOptionalBool_True;
depthStencilState.depthCompare = WGPUCompareFunction_Less;
@ -629,20 +643,22 @@ void Engine::makeUIPipeline() {
void Engine::run() {
init();
time_t last_time;
time(&last_time);
sendMessage(Events::PlayerJoin {
.name = "singleplayer",
.pos = {0, 0, 0}
});
auto lastTime = std::chrono::steady_clock::now().time_since_epoch().count();
while(!window.shouldClose()) {
nk_input_begin(ui.get());
glfwPollEvents();
nk_input_end(ui.get());
render();
time_t now;
time(&now);
if(now - last_time > STEP_SIZE) {
auto now = std::chrono::steady_clock::now().time_since_epoch().count();
if(now - lastTime > STEP_SIZE) {
tick();
dispatch(Events::Tick{now - last_time});
last_time = now;
dispatch(Events::Tick{now - lastTime});
lastTime = now;
}
}
}
@ -691,13 +707,11 @@ void Engine::render() {
WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr);
for(const auto &pair : world->chunks) {
chunkRenderer->render(pair.first, pair.second.get(), encoder, nextTexture);
}
chunkRenderer->render(encoder, nextTexture);
WGPURenderPassColorAttachment colorAttachment = {};
colorAttachment.view = nextTexture;
colorAttachment.loadOp = WGPULoadOp_Clear;
colorAttachment.loadOp = WGPULoadOp_Load;
colorAttachment.storeOp = WGPUStoreOp_Store;
colorAttachment.clearValue = {0, 0.2, 0.4, 1};
colorAttachment.depthSlice = -1;
@ -799,6 +813,45 @@ void Engine::render() {
void Engine::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";
glm::mat4 ortho;
Server* server;
Server* server = nullptr;
Engine();
~Engine();
@ -150,8 +150,8 @@ public:
std::cout << "Message received" << std::endl;
} else if constexpr (std::is_same_v<T, Events::ChunkChanged>) {
auto ev = (Events::ChunkChanged) msg;
world->chunks.at(ev.pos)->data = ev.data;
// chunkRenderer->updateChunk(ev.pos, ev.data);
world->chunks.emplace(ev.pos, std::make_unique<World::Chunk>(ev.data));
chunkRenderer->addChunk(ev.pos, world->chunks.at(ev.pos).get());
}
}
};
@ -166,7 +166,7 @@ void Artifact::Engine::sendMessage(T msg){
if(server) {
server->receiveMessage(msg);
} else {
auto buffer = cista::serialize(msg);
//auto buffer = cista::serialize(msg);
}
}

View file

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

View file

@ -42,8 +42,13 @@ int main() {
});
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);
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();

View file

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

View file

@ -5,6 +5,8 @@
#include <cista.h>
#include <Events.h>
#include <ServerPlayer.h>
#include <ServerWorld.h>
#define STEP_SIZE 200
@ -15,6 +17,9 @@ class Engine;
class Server: public EventTarget {
Artifact::Engine* localClient;
public:
std::vector<std::unique_ptr<ServerPlayer>> players;
std::unique_ptr<World::ServerWorld> world = std::make_unique<World::ServerWorld>(this);
Server(Engine* localClient);
void run();
@ -27,6 +32,21 @@ public:
void receiveMessage(T msg) {
if constexpr (std::is_same_v<T, Events::Initialized>) {
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) {
localClient->receiveMessage(msg);
} 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>
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) {
auto chunk = chunks.at(pos).get();
if(chunk) {
return chunk;
if(chunks.count(pos)) {
return chunks.at(pos).get();
}
// TODO: Load from disk
std::array<uint16_t, Chunk::SIZE_CUBED> data;
for(int x = 0; x < CHUNK_SIZE; ++x)
for(int z = 0; z < CHUNK_SIZE; ++z) {
data.fill(0);
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
if (height < 0) height = 0;
if (height >= Chunk::SIZE) height = Chunk::SIZE - 1;
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) {
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();
}

View file

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

View file

@ -16,6 +16,9 @@ Chunk::~Chunk() {
}
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));
}

View file

@ -12,7 +12,10 @@ int posToIndex(float x, float y, float z) {
}
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) {
@ -27,8 +30,10 @@ bool NodeRegistry::generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::
std::vector<std::vector<unsigned char>> textureData;
std::vector<int> widths, heights;
int maxWidth = 0, maxHeight = 0;
for (size_t i = 1; i < registeredNodes.size(); ++i) { // Skip ID 0 (air)
if(registeredNodes.at(i).visual == NodeVisual::NONE) continue;
for (size_t i = 0; i < registeredNodes.size(); ++i) { // Skip ID 0 (air)
if(registeredNodes.at(i).visual == NodeVisual::NONE) {
continue;
}
drawables.push_back(i);
int width, height, channels;
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;
}
textureData.push_back({});
textureData.at(i).resize(width * height * 4);
std::memcpy(textureData[i].data(), data, width * height * 4);
textureData.at(drawables.size() - 1).resize(width * height * 4);
std::memcpy(textureData[drawables.size() - 1].data(), data, width * height * 4);
widths.push_back(width);
heights.push_back(height);
maxWidth = std::max(maxWidth, width);
@ -48,12 +53,12 @@ bool NodeRegistry::generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::
// Simple atlas layout: stack textures horizontally
// (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
atlasHeight = maxHeight + 1;
atlasWidth = maxWidth * (drawables.size()); // -1 because ID 0 (air) has no texture
atlasHeight = maxHeight;
atlasData.resize(atlasWidth * atlasHeight * 4, 0); // RGBA
for (size_t i = 1; i < drawables.size(); ++i) {
int xOffset = (i - 1) * maxWidth;
for (uint16_t i = 0; i < drawables.size(); ++i) {
int xOffset = i * maxWidth;
for (int y = 0; y < heights[i]; ++y) {
for (int x = 0; x < widths[i]; ++x) {
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 vMin = 0.0f;
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;

View file

@ -28,7 +28,7 @@ struct NodeDef {
std::string name;
std::string texture;
AlphaBlendMode alphaBlendMode = AlphaBlendMode::NONE;
NodeVisual visual = NodeVisual::NONE;
NodeVisual visual = NodeVisual::CUBE;
};
struct NodeTextureCoords {
@ -41,7 +41,7 @@ class NodeRegistry {
uint16_t next = 0;
public:
std::unordered_map<uint16_t, NodeDef> registeredNodes;
std::vector<NodeTextureCoords> textureCoords;
std::unordered_map<uint16_t, NodeTextureCoords> textureCoords;
NodeRegistry();
@ -51,6 +51,16 @@ public:
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);
};