Initial commit.

This commit is contained in:
Signal 2025-12-09 16:32:47 -05:00
commit d99b70eaa1
33 changed files with 2291 additions and 0 deletions

BIN
assets/fonts/Arial.ttf Normal file

Binary file not shown.

BIN
assets/test_stone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

View file

@ -0,0 +1,42 @@
#include <Engine.h>
#include <ChunkRenderer.h>
namespace Artifact {
void ChunkRenderer::render(glm::ivec3 pos, World::Chunk* chunk, 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;
WGPURenderPassDepthStencilAttachment depthAttachment = {};
depthAttachment.view = engine->depthTexture->view;
depthAttachment.depthLoadOp = WGPULoadOp_Clear;
depthAttachment.depthStoreOp = WGPUStoreOp_Store;
depthAttachment.depthClearValue = 1.0f;
depthAttachment.depthReadOnly = false;
depthAttachment.stencilLoadOp = WGPULoadOp_Undefined;
depthAttachment.stencilStoreOp = WGPUStoreOp_Undefined;
WGPURenderPassDescriptor renderPassDesc = {};
renderPassDesc.colorAttachmentCount = 1;
renderPassDesc.colorAttachments = &colorAttachment;
renderPassDesc.depthStencilAttachment = &depthAttachment;
WGPURenderPassEncoder cubeRenderPass = wgpuCommandEncoderBeginRenderPass(encoder, &renderPassDesc);
wgpuRenderPassEncoderSetPipeline(cubeRenderPass, engine->worldPipeline);
wgpuRenderPassEncoderSetViewport(cubeRenderPass, 0, 0, (float)engine->viewportWidth, (float)engine->viewportHeight, 0.0f, 1.0f);
wgpuRenderPassEncoderSetBindGroup(cubeRenderPass, 0, engine->worldBindGroup, 0, nullptr);
for (const auto &pair : chunkBuffers) {
const ChunkRenderData &data = pair.second;
wgpuRenderPassEncoderSetVertexBuffer(cubeRenderPass, 0, data.vertices, 0, data.vertexCount * sizeof(Vertex));
wgpuRenderPassEncoderSetIndexBuffer(cubeRenderPass, data.indices, WGPUIndexFormat_Uint32, 0, data.indexCount * sizeof(uint32_t));
wgpuRenderPassEncoderDrawIndexed(cubeRenderPass, data.indexCount, 1, 0, 0, 0);
}
wgpuRenderPassEncoderEnd(cubeRenderPass);
}
}

View file

@ -0,0 +1,37 @@
#pragma once
#include <webgpu/webgpu.h>
#include <Chunk.h>
#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;
int vertexCount;
int indexCount;
};
Engine* engine = nullptr;
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);
};
}

108
src/client/ClientWorld.cpp Normal file
View file

@ -0,0 +1,108 @@
#include <vector>
#include <webgpu/webgpu.h>
#include <ClientWorld.h>
#include <Chunk.h>
#include <Mesh.h>
namespace Artifact {
namespace World {
void ClientWorld::generateChunkMesh(WGPUDevice device, WGPUQueue queue, Chunk* chunk, ChunkMesh &mesh) {
std::vector<Vertex> verts;
std::vector<uint32_t> inds;
verts.reserve(24 * 1024); // rough upper bound
inds.reserve (36 * 1024);
auto emitQuad = [&](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 < CHUNK_SIZE; ++x) {
for (int y = 0; y < CHUNK_SIZE; ++y) {
for (int z = 0; z < CHUNK_SIZE; ++z) {
uint16_t id = chunk->getNodeRaw(x, y, z);
if (id == 0) continue; // air
float fx0 = static_cast<float>(x);
float fy0 = static_cast<float>(y);
float fz0 = static_cast<float>(z);
float fx1 = fx0 + 1.0f;
float fy1 = fy0 + 1.0f;
float fz1 = fz0 + 1.0f;
// +X
if (chunk->getNodeRaw(x+1, y, z) == 0) {
emitQuad(fx1,fy0,fz0, fx1,fy1,fz0, fx1,fy1,fz1, fx1,fy0,fz1, 0,0, 1,1);
}
// -X
if (chunk->getNodeRaw(x-1, y, z) == 0) {
emitQuad(fx0,fy0,fz1, fx0,fy1,fz1, fx0,fy1,fz0, fx0,fy0,fz0, 0,0, 1,1);
}
// +Y (top)
if (chunk->getNodeRaw(x, y+1, z) == 0) {
emitQuad(fx0,fy1,fz0, fx1,fy1,fz0, fx1,fy1,fz1, fx0,fy1,fz1, 0,0, 1,1);
}
// -Y (bottom)
if (chunk->getNodeRaw(x, y-1, z) == 0) {
emitQuad(fx0,fy0,fz1, fx1,fy0,fz1, fx1,fy0,fz0, fx0,fy0,fz0, 0,0, 1,1);
}
// +Z
if (chunk->getNodeRaw(x, y, z+1) == 0) {
emitQuad(fx0,fy0,fz1, fx0,fy1,fz1, fx1,fy1,fz1, fx1,fy0,fz1, 0,0, 1,1);
}
// -Z
if (chunk->getNodeRaw(x, y, z-1) == 0) {
emitQuad(fx1,fy0,fz0, fx1,fy1,fz0, fx0,fy1,fz0, fx0,fy0,fz0, 0,0, 1,1);
}
}
}
}
auto isEmpty = inds.empty();
// chunk.indexCount = static_cast<uint32_t>(inds.size());
if (isEmpty) {
// Clean up old buffers if any
if (mesh.vertices) wgpuBufferDestroy(mesh.vertices);
if (mesh.indices) wgpuBufferDestroy(mesh.indices);
mesh.vertices = nullptr;
mesh.indices = nullptr;
return;
}
// ---------------------------------------------------------------------
// (Re)create GPU buffers with exact size and copy data in one go
// ---------------------------------------------------------------------
WGPUBufferDescriptor vdesc{};
vdesc.size = verts.size() * sizeof(Vertex);
vdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
mesh.vertices = wgpuDeviceCreateBuffer(device, &vdesc);
wgpuQueueWriteBuffer(queue, mesh.vertices, 0, verts.data(), vdesc.size);
WGPUBufferDescriptor idesc{};
idesc.size = inds.size() * sizeof(uint32_t);
idesc.usage = WGPUBufferUsage_Index | WGPUBufferUsage_CopyDst;
mesh.indices = wgpuDeviceCreateBuffer(device, &idesc);
wgpuQueueWriteBuffer(queue, mesh.indices, 0, inds.data(), idesc.size);
}
void ClientWorld::render(WGPUDevice device, WGPUQueue queue, WGPUCommandEncoder encoder) {
}
}
}

20
src/client/ClientWorld.h Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <World.h>
namespace Artifact {
namespace World {
struct ChunkMesh {
WGPUBuffer vertices = nullptr;
WGPUBuffer indices = nullptr;
};
class ClientWorld: public World::World {
public:
void generateChunkMesh(WGPUDevice device, WGPUQueue queue, Chunk* chunk, ChunkMesh &mesh);
void render(WGPUDevice device, WGPUQueue queue, WGPUCommandEncoder encoder);
};
}
}

805
src/core/Engine.cpp Normal file
View file

@ -0,0 +1,805 @@
#include <iostream>
#include <memory>
#include <glfw3webgpu.h>
#include <webgpu/webgpu.h>
#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#include <nuklear.h>
#include <gtc/matrix_transform.hpp>
#include <Engine.h>
#include <Events.h>
#include <Util.h>
#include <UI.h>
#include <ClientWorld.h>
#include <Server.h>
#define STEP_SIZE 1000
/*
namespace Artifact {
void Engine::init() {
instance = wgpuCreateInstance(nullptr);
surface = glfwCreateWindowWGPUSurface(instance, window);
}
void Engine::run() {
init();
while(!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
}
Engine::Engine() {
world = std::make_unique<World::World>();
}
Engine::~Engine() {
std::cout << "Destructing engine..." << std::endl;
}
}
/*/
static struct _glfwInit {
_glfwInit() {
if(!glfwInit()) printf("Failed to initialize GLFW.");
}
~_glfwInit() {
glfwTerminate();
}
} __glfwinit;
namespace Artifact {
Engine::Engine() {
world = std::make_unique<World::ClientWorld>();
}
Engine::~Engine() {
std::cout << "Destructing engine..." << std::endl;
}
void Engine::init() {
instance = wgpuCreateInstance(nullptr);
surface = window.createWGPUSurface(instance);
if (!surface) {
std::cerr << "Failed to create surface" << std::endl;
return;
}
WGPURequestAdapterCallbackInfo adapterCallbackOpts = {};
adapterCallbackOpts.callback = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, WGPUStringView error, void* engine, void*) {
if(status == WGPURequestAdapterStatus_Success) {
static_cast<Engine*>(engine)->adapter = adapter;
}
};
adapterCallbackOpts.mode = WGPUCallbackMode_AllowProcessEvents;
adapterCallbackOpts.userdata1 = this;
WGPURequestAdapterOptions adapterOpts = {};
adapterOpts.compatibleSurface = surface;
wgpuInstanceRequestAdapter(instance, &adapterOpts, adapterCallbackOpts);
while(!adapter) wgpuInstanceProcessEvents(instance);
WGPURequestDeviceCallbackInfo deviceCallbackOpts = {};
deviceCallbackOpts.callback = [](WGPURequestDeviceStatus status, WGPUDevice device, WGPUStringView error, void* engine, void*) {
if(status == WGPURequestDeviceStatus_Success) {
static_cast<Engine*>(engine)->device = device;
} else {
std::cerr << error.data << std::endl;
}
};
deviceCallbackOpts.mode = WGPUCallbackMode_AllowProcessEvents;
deviceCallbackOpts.userdata1 = this;
WGPUUncapturedErrorCallbackInfo errorCallbackInfo = {};
errorCallbackInfo.callback = [](auto device, WGPUErrorType type, WGPUStringView message, void*, void*) {
const char* msg = (message.data ? reinterpret_cast<const char*>(message.data) : "No message");
fprintf(stderr, "Uncaptured WebGPU Error (type %d): %s\n", type, msg);
if (type == WGPUErrorType_Validation) {
// Optional: Break into debugger or abort for inspection
__builtin_trap();
}
};
errorCallbackInfo.userdata1 = nullptr; // Optional user data
WGPUDeviceDescriptor deviceDesc = {};
deviceDesc.uncapturedErrorCallbackInfo = errorCallbackInfo;
wgpuAdapterRequestDevice(adapter, nullptr, deviceCallbackOpts);
while(!device) wgpuInstanceProcessEvents(instance);
queue = wgpuDeviceGetQueue(device);
WGPUSurfaceConfiguration surfaceConfig = {};
surfaceConfig.device = device;
surfaceConfig.format = surfaceFormat;
surfaceConfig.usage = WGPUTextureUsage_RenderAttachment;
surfaceConfig.width = viewportWidth;
surfaceConfig.height = viewportHeight;
surfaceConfig.presentMode = WGPUPresentMode_Fifo;
wgpuSurfaceConfigure(surface, &surfaceConfig);
WGPUTextureDescriptor depthDesc = {};
depthDesc.dimension = WGPUTextureDimension_2D;
depthDesc.size.width = viewportWidth;
depthDesc.size.height = viewportHeight;
depthDesc.size.depthOrArrayLayers = 1;
depthDesc.format = WGPUTextureFormat_Depth24Plus;
depthDesc.usage = WGPUTextureUsage_RenderAttachment;
depthDesc.mipLevelCount = 1;
depthDesc.sampleCount = 1;
depthTexture = std::make_unique<Util::Texture>();
depthTexture->texture = wgpuDeviceCreateTexture(device, &depthDesc);
depthTexture->view = wgpuTextureCreateView(depthTexture->texture, nullptr);
std::cout << "Initializing Nuklear..." << std::endl;
// Initialize Nuklear
ui = std::make_unique<nk_context>();
nk_init_default(ui.get(), nullptr);
nk_font_atlas* atlas = (nk_font_atlas*)malloc(sizeof(nk_font_atlas));
nk_font_atlas_init_default(atlas);
nk_font_atlas_begin(atlas);
std::cout << "Loading font..." << std::endl;
nk_font* font;
void* fontData = nullptr;
FILE* fontFile = fopen((assetPath + "/fonts/Arial.ttf").c_str(), "rb");
if (fontFile) {
fseek(fontFile, 0, SEEK_END);
long fontSize = ftell(fontFile);
fseek(fontFile, 0, SEEK_SET);
fontData = malloc(fontSize);
fread(fontData, 1, fontSize, fontFile);
fclose(fontFile);
font = nk_font_atlas_add_from_memory(atlas, fontData, fontSize, 13.0f, nullptr);
} else {
std::cerr << "Failed to load font: " << (assetPath + "/fonts/Arial.ttf") << std::endl;
font = nk_font_atlas_add_default(atlas, 13.0f, nullptr);
}
int fontWidth, fontHeight;
const void* fontImage = nk_font_atlas_bake(atlas, &fontWidth, &fontHeight, NK_FONT_ATLAS_RGBA32);
uiFontTexture = Util::createTextureFromData(device, queue, fontImage, fontWidth, fontHeight);
nk_font_atlas_end(atlas, nk_handle_id(0), nullptr);
nk_style_set_font(ui.get(), &font->handle);
if (fontData) free(fontData);
free(atlas);
unsigned char whitePixel[4] = {255, 255, 255, 255};
uiDummyTexture = Util::createTextureFromData(device, queue, whitePixel, 1, 1);
nk_buffer_init_default(&uiVertexBufferNK);
nk_buffer_init_default(&uiIndexBufferNK);
nk_buffer_init_default(&uiCommandBufferNK);
auto window_ = &window;
auto ui_ = ui.get();
window.listen<Events::Key>([window_, ui_](auto ev) {
bool press = ev.action == GLFW_PRESS;
switch (ev.key) {
case GLFW_KEY_ESCAPE:
window_->close();
break;
case GLFW_KEY_DELETE:
nk_input_key(ui_, NK_KEY_DEL, press);
break;
case GLFW_KEY_ENTER:
nk_input_key(ui_, NK_KEY_ENTER, press);
break;
case GLFW_KEY_TAB:
nk_input_key(ui_, NK_KEY_TAB, press);
break;
case GLFW_KEY_BACKSPACE:
nk_input_key(ui_, NK_KEY_BACKSPACE, press);
break;
case GLFW_KEY_UP:
nk_input_key(ui_, NK_KEY_UP, press);
break;
case GLFW_KEY_DOWN:
nk_input_key(ui_, NK_KEY_DOWN, press);
break;
case GLFW_KEY_HOME:
nk_input_key(ui_, NK_KEY_TEXT_START, press);
nk_input_key(ui_, NK_KEY_SCROLL_START, press);
break;
case GLFW_KEY_END:
nk_input_key(ui_, NK_KEY_TEXT_END, press);
nk_input_key(ui_, NK_KEY_SCROLL_END, press);
break;
case GLFW_KEY_PAGE_DOWN:
nk_input_key(ui_, NK_KEY_SCROLL_DOWN, press);
break;
case GLFW_KEY_PAGE_UP:
nk_input_key(ui_, NK_KEY_SCROLL_UP, press);
break;
case GLFW_KEY_LEFT_SHIFT:
case GLFW_KEY_RIGHT_SHIFT:
nk_input_key(ui_, NK_KEY_SHIFT, press);
break;
// case GLFW_KEY_LEFT_CONTROL:
// case GLFW_KEY_RIGHT_CONTROL:
// if (press) {
// nk_input_key(ui.get(), NK_KEY_COPY, glfwGetKey(window, GLFW_KEY_C) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_PASTE, glfwGetKey(window, GLFW_KEY_P) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_CUT, glfwGetKey(window, GLFW_KEY_X) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_TEXT_UNDO, glfwGetKey(window, GLFW_KEY_Z) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_TEXT_REDO, glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_TEXT_WORD_LEFT, glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_TEXT_WORD_RIGHT, glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_TEXT_LINE_START, glfwGetKey(window, GLFW_KEY_B) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_TEXT_LINE_END, glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS);
// } else {
// nk_input_key(ui.get(), NK_KEY_LEFT, glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_RIGHT, glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS);
// nk_input_key(ui.get(), NK_KEY_COPY, false);
// nk_input_key(ui.get(), NK_KEY_PASTE, false);
// nk_input_key(ui.get(), NK_KEY_CUT, false);
// nk_input_key(ui.get(), NK_KEY_SHIFT, false);
// }
// break;
}
});
window.listen<Events::CursorPos>([this](auto ev) {
nk_input_motion(ui.get(), ev.x, ev.y);
});
window.listen<Events::Action>([this](auto ev) {
nk_buttons type;
switch(ev.type) {
case Events::ActionType::PRIMARY:
type = NK_BUTTON_LEFT;
break;
case Events::ActionType::SECONDARY:
type = NK_BUTTON_RIGHT;
break;
case Events::ActionType::TERTIARY:
type = NK_BUTTON_MIDDLE;
break;
default:
return;
}
nk_input_button(ui.get(), type, (int)ev.x, (int)ev.y, ev.state);
});
int atlasWidth, atlasHeight;
std::vector<unsigned char> atlasData;
if (!world->nodeRegistry.generateTextureAtlas(atlasWidth, atlasHeight, atlasData)) {
std::cerr << "Failed to generate texture atlas\n";
return;
}
std::cout << "Creating texture atlas..." << std::endl;
nodeTextureAtlas = Util::createTextureFromData(device, queue, atlasData.data(), atlasWidth, atlasHeight);
if (!nodeTextureAtlas->texture || !nodeTextureAtlas->view) {
std::cerr << "Failed to load texture atlas\n";
return;
}
std::cout << "Texture atlas created." << std::endl;
// MARK: Setup pipelines
reloadShaders();
dispatch(Events::Initialized{});
}
void Engine::reloadShaders() {
makeWorldPipelines();
makeObjectPipeline();
makeUIPipeline();
dispatch(Events::PipelineReload{});
}
void Engine::makeWorldPipelines() {
WGPUBufferDescriptor uniformBufferDesc = {};
uniformBufferDesc.size = sizeof(glm::mat4);
uniformBufferDesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
worldUniformBuffer = wgpuDeviceCreateBuffer(device, &uniformBufferDesc);
if (!worldUniformBuffer) {
std::cerr << "Failed to create uniform buffer" << std::endl;
return;
}
WGPUSamplerDescriptor samplerDesc = {};
samplerDesc.addressModeU = WGPUAddressMode_Repeat;
samplerDesc.addressModeV = WGPUAddressMode_Repeat;
samplerDesc.addressModeW = WGPUAddressMode_Repeat;
samplerDesc.magFilter = WGPUFilterMode_Nearest;
samplerDesc.minFilter = WGPUFilterMode_Nearest;
samplerDesc.mipmapFilter = WGPUMipmapFilterMode_Linear;
samplerDesc.maxAnisotropy = 1;
worldSampler = wgpuDeviceCreateSampler(device, &samplerDesc);
if (!worldSampler) {
std::cerr << "Failed to create sampler" << std::endl;
return;
}
WGPUBindGroupLayoutEntry bglEntries[3] = {};
bglEntries[0].binding = 0;
bglEntries[0].visibility = WGPUShaderStage_Vertex;
bglEntries[0].buffer.type = WGPUBufferBindingType_Uniform;
bglEntries[1].binding = 1;
bglEntries[1].visibility = WGPUShaderStage_Fragment;
bglEntries[1].texture.sampleType = WGPUTextureSampleType_Float;
bglEntries[1].texture.viewDimension = WGPUTextureViewDimension_2D;
bglEntries[2].binding = 2;
bglEntries[2].visibility = WGPUShaderStage_Fragment;
bglEntries[2].sampler.type = WGPUSamplerBindingType_Filtering;
WGPUBindGroupLayoutDescriptor bglDesc = {};
bglDesc.entryCount = 3;
bglDesc.entries = bglEntries;
worldBgl = wgpuDeviceCreateBindGroupLayout(device, &bglDesc);
if (!worldBgl) {
std::cerr << "Failed to create bind group layout" << std::endl;
return;
}
WGPUBindGroupEntry bgEntries[3] = {};
bgEntries[0].binding = 0;
bgEntries[0].buffer = worldUniformBuffer;
bgEntries[0].offset = 0;
bgEntries[0].size = sizeof(glm::mat4);
bgEntries[1].binding = 1;
bgEntries[1].textureView = nodeTextureAtlas->view;
bgEntries[2].binding = 2;
bgEntries[2].sampler = worldSampler;
WGPUBindGroupDescriptor bgDesc = {};
bgDesc.layout = worldBgl;
bgDesc.entryCount = 3;
bgDesc.entries = bgEntries;
worldBindGroup = wgpuDeviceCreateBindGroup(device, &bgDesc);
if (!worldBindGroup) {
std::cerr << "Failed to create cube bind group" << std::endl;
return;
}
const char* cubeVSCode = R"(
struct Uniforms {
mvp: mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct VertexOutput {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>
}
@vertex
fn vs_main(
@location(0) pos: vec3<f32>,
@location(1) uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.pos = uniforms.mvp * vec4<f32>(pos, 1.0);
out.uv = uv;
return out;
}
)";
const char* cubeFSCode = R"(
@group(0) @binding(1) var texture: texture_2d<f32>;
@group(0) @binding(2) var sampler_: sampler;
@fragment
fn fs_main(
@location(0) uv: vec2<f32>
) -> @location(0) vec4<f32> {
var color = textureSample(texture, sampler_, uv);
return color;
}
)";
WGPUShaderModuleDescriptor cubeVSDesc = {};
WGPUShaderSourceWGSL src = {.code = WGPUStringView{cubeVSCode, WGPU_STRLEN}};
src.chain.sType = WGPUSType_ShaderSourceWGSL;
cubeVSDesc.nextInChain = &src.chain;
WGPUShaderModule cubeVSModule = wgpuDeviceCreateShaderModule(device, &cubeVSDesc);
WGPUShaderModuleDescriptor cubeFSDesc = {};
src = {.code = WGPUStringView{cubeFSCode, WGPU_STRLEN}};
src.chain.sType = WGPUSType_ShaderSourceWGSL;
cubeFSDesc.nextInChain = &src.chain;
WGPUShaderModule cubeFSModule = wgpuDeviceCreateShaderModule(device, &cubeFSDesc);
WGPUVertexAttribute cubeAttributes[2] = {};
cubeAttributes[0].format = WGPUVertexFormat_Float32x3;
cubeAttributes[0].offset = offsetof(Vertex, pos);
cubeAttributes[0].shaderLocation = 0;
cubeAttributes[1].format = WGPUVertexFormat_Float32x2;
cubeAttributes[1].offset = offsetof(Vertex, uv);
cubeAttributes[1].shaderLocation = 1;
WGPUVertexBufferLayout cubeVBLayout = {};
cubeVBLayout.arrayStride = sizeof(Vertex);
cubeVBLayout.stepMode = WGPUVertexStepMode_Vertex;
cubeVBLayout.attributeCount = 2;
cubeVBLayout.attributes = cubeAttributes;
WGPUPipelineLayoutDescriptor plDesc = {};
plDesc.bindGroupLayoutCount = 1;
plDesc.bindGroupLayouts = &worldBgl;
WGPUPipelineLayout pipelineLayout = wgpuDeviceCreatePipelineLayout(device, &plDesc);
WGPUColorTargetState cubeColorTarget = {};
cubeColorTarget.format = surfaceFormat;
cubeColorTarget.writeMask = WGPUColorWriteMask_All;
WGPUFragmentState cubeFragmentState = {};
cubeFragmentState.module = cubeFSModule;
cubeFragmentState.entryPoint = WGPUStringView{"fs_main", WGPU_STRLEN};
cubeFragmentState.targetCount = 1;
cubeFragmentState.targets = &cubeColorTarget;
WGPUDepthStencilState depthStencilState = {};
depthStencilState.format = WGPUTextureFormat_Depth24Plus;
depthStencilState.depthWriteEnabled = WGPUOptionalBool_True;
depthStencilState.depthCompare = WGPUCompareFunction_Less;
WGPURenderPipelineDescriptor cubePipelineDesc = {};
cubePipelineDesc.vertex.module = cubeVSModule;
cubePipelineDesc.vertex.entryPoint = WGPUStringView{"vs_main", WGPU_STRLEN};
cubePipelineDesc.vertex.bufferCount = 1;
cubePipelineDesc.vertex.buffers = &cubeVBLayout;
cubePipelineDesc.fragment = &cubeFragmentState;
cubePipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
cubePipelineDesc.primitive.frontFace = WGPUFrontFace_CCW;
cubePipelineDesc.primitive.cullMode = WGPUCullMode_None;
cubePipelineDesc.depthStencil = &depthStencilState;
cubePipelineDesc.layout = pipelineLayout;
cubePipelineDesc.multisample.count = 1;
cubePipelineDesc.multisample.mask = ~0u;
worldPipeline = wgpuDeviceCreateRenderPipeline(device, &cubePipelineDesc);
std::cout << "Created world pipeline." << std::endl;
}
void Engine::makeObjectPipeline() {
}
void Engine::makeUIPipeline() {
WGPUBufferDescriptor uiVertexBufferDesc = {};
uiVertexBufferDesc.size = 1024 * 1024 * sizeof(NKVertex);
uiVertexBufferDesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
uiVertexBufferDesc.label = WGPUStringView{"UI Vertex Buffer", WGPU_STRLEN};
uiVertexBuffer = wgpuDeviceCreateBuffer(device, &uiVertexBufferDesc);
WGPUBufferDescriptor uiIndexBufferDesc = {};
uiIndexBufferDesc.size = 1024 * 1024 * sizeof(uint16_t);
uiIndexBufferDesc.usage = WGPUBufferUsage_Index | WGPUBufferUsage_CopyDst;
uiIndexBufferDesc.label = WGPUStringView{"UI Index Buffer", WGPU_STRLEN};
uiIndexBuffer = wgpuDeviceCreateBuffer(device, &uiIndexBufferDesc);
WGPUBufferDescriptor uiUniformBufferDesc = {};
uiUniformBufferDesc.size = sizeof(glm::mat4);
uiUniformBufferDesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
uiUniformBufferDesc.label = WGPUStringView{"UI Uniform Buffer", WGPU_STRLEN};
uiUniformBuffer = wgpuDeviceCreateBuffer(device, &uiUniformBufferDesc);
std::string uiVSCode = R"(
struct Uniforms {
ortho: mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct VertexOutput {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) color: vec4<f32>,
}
@vertex
fn vs_main(@location(0) pos: vec2<f32>, @location(1) uv: vec2<f32>, @location(2) color: vec4<f32>) -> VertexOutput {
var out: VertexOutput;
out.pos = uniforms.ortho * vec4<f32>(pos, 0.0, 1.0);
out.uv = uv;
out.color = color;
return out;
}
)";
std::string uiFSCode = R"(
@group(0) @binding(1) var texture: texture_2d<f32>;
@group(0) @binding(2) var sampler_: sampler;
@fragment
fn fs_main(@location(0) uv: vec2<f32>, @location(1) color: vec4<f32>) -> @location(0) vec4<f32> {
return textureSample(texture, sampler_, uv) * color;
}
)";
WGPUShaderModuleDescriptor uiVSDesc = {};
WGPUShaderSourceWGSL src = {.code = WGPUStringView{uiVSCode.c_str(), uiVSCode.size()}};
src.chain.sType = WGPUSType_ShaderSourceWGSL;
uiVSDesc.nextInChain = &src.chain;
WGPUShaderModule uiVSModule = wgpuDeviceCreateShaderModule(device, &uiVSDesc);
WGPUShaderModuleDescriptor uiFSDesc = {};
src = {.code = WGPUStringView{uiFSCode.c_str(), uiFSCode.size()}};
src.chain.sType = WGPUSType_ShaderSourceWGSL;
uiFSDesc.nextInChain = &src.chain;
WGPUShaderModule uiFSModule = wgpuDeviceCreateShaderModule(device, &uiFSDesc);
WGPUSamplerDescriptor uiSamplerDesc = {};
uiSamplerDesc.addressModeU = WGPUAddressMode_ClampToEdge;
uiSamplerDesc.addressModeV = WGPUAddressMode_ClampToEdge;
uiSamplerDesc.addressModeW = WGPUAddressMode_ClampToEdge;
uiSamplerDesc.magFilter = WGPUFilterMode_Linear;
uiSamplerDesc.minFilter = WGPUFilterMode_Linear;
uiSamplerDesc.mipmapFilter = WGPUMipmapFilterMode_Linear;
uiSamplerDesc.maxAnisotropy = 1;
uiSampler = wgpuDeviceCreateSampler(device, &uiSamplerDesc);
if (!uiSampler) {
std::cerr << "Failed to create UI sampler" << std::endl;
return;
}
WGPUBindGroupLayoutEntry uiBglEntries[3] = {};
uiBglEntries[0].binding = 0;
uiBglEntries[0].visibility = WGPUShaderStage_Vertex;
uiBglEntries[0].buffer.type = WGPUBufferBindingType_Uniform;
uiBglEntries[1].binding = 1;
uiBglEntries[1].visibility = WGPUShaderStage_Fragment;
uiBglEntries[1].texture.sampleType = WGPUTextureSampleType_Float;
uiBglEntries[1].texture.viewDimension = WGPUTextureViewDimension_2D;
uiBglEntries[2].binding = 2;
uiBglEntries[2].visibility = WGPUShaderStage_Fragment;
uiBglEntries[2].sampler.type = WGPUSamplerBindingType_Filtering;
WGPUBindGroupLayoutDescriptor uiBglDesc = {};
uiBglDesc.entryCount = 3;
uiBglDesc.entries = uiBglEntries;
uiBgl = wgpuDeviceCreateBindGroupLayout(device, &uiBglDesc);
if (!uiBgl) {
std::cerr << "Failed to create bind group layout" << std::endl;
return;
}
WGPUVertexAttribute uiAttributes[3] = {};
uiAttributes[0].format = WGPUVertexFormat_Float32x2;
uiAttributes[0].offset = offsetof(NKVertex, pos);
uiAttributes[0].shaderLocation = 0;
uiAttributes[1].format = WGPUVertexFormat_Float32x2;
uiAttributes[1].offset = offsetof(NKVertex, uv);
uiAttributes[1].shaderLocation = 1;
uiAttributes[2].format = WGPUVertexFormat_Unorm8x4;
uiAttributes[2].offset = offsetof(NKVertex, color);
uiAttributes[2].shaderLocation = 2;
WGPUVertexBufferLayout uiVBLayout = {};
uiVBLayout.arrayStride = sizeof(NKVertex);
uiVBLayout.stepMode = WGPUVertexStepMode_Vertex;
uiVBLayout.attributeCount = 3;
uiVBLayout.attributes = uiAttributes;
WGPUBlendState uiBlendState = {};
uiBlendState.color.srcFactor = WGPUBlendFactor_SrcAlpha;
uiBlendState.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
uiBlendState.color.operation = WGPUBlendOperation_Add;
uiBlendState.alpha.srcFactor = WGPUBlendFactor_One;
uiBlendState.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
uiBlendState.alpha.operation = WGPUBlendOperation_Add;
WGPUColorTargetState uiColorTarget = {};
uiColorTarget.format = surfaceFormat;
uiColorTarget.blend = &uiBlendState;
uiColorTarget.writeMask = WGPUColorWriteMask_All;
WGPUFragmentState uiFragmentState = {};
uiFragmentState.module = uiFSModule;
uiFragmentState.entryPoint = WGPUStringView{"fs_main", WGPU_STRLEN};
uiFragmentState.targetCount = 1;
uiFragmentState.targets = &uiColorTarget;
WGPUPipelineLayoutDescriptor uiPlDesc = {};
uiPlDesc.bindGroupLayoutCount = 1;
uiPlDesc.bindGroupLayouts = &uiBgl;
WGPUPipelineLayout uiPipelineLayout = wgpuDeviceCreatePipelineLayout(device, &uiPlDesc);
WGPURenderPipelineDescriptor uiPipelineDesc = {};
uiPipelineDesc.vertex.module = uiVSModule;
uiPipelineDesc.vertex.entryPoint = WGPUStringView{"vs_main", WGPU_STRLEN};
uiPipelineDesc.vertex.bufferCount = 1;
uiPipelineDesc.vertex.buffers = &uiVBLayout;
uiPipelineDesc.fragment = &uiFragmentState;
uiPipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
uiPipelineDesc.primitive.frontFace = WGPUFrontFace_CCW;
uiPipelineDesc.primitive.cullMode = WGPUCullMode_None;
uiPipelineDesc.layout = uiPipelineLayout;
uiPipelineDesc.multisample.count = 1;
uiPipelineDesc.multisample.mask = ~0u;
uiPipeline = wgpuDeviceCreateRenderPipeline(device, &uiPipelineDesc);
}
// Default main loop handler.
void Engine::run() {
init();
time_t last_time;
time(&last_time);
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) {
tick();
dispatch(Events::Tick{now - last_time});
last_time = now;
}
}
}
void Engine::render() {
dispatch(Events::DrawUI{ui.get()});
nk_buffer_clear(&uiVertexBufferNK);
nk_buffer_clear(&uiIndexBufferNK);
nk_buffer_clear(&uiCommandBufferNK);
struct nk_convert_config config = {};
static const struct nk_draw_vertex_layout_element vertex_layout[] = {
{NK_VERTEX_POSITION, NK_FORMAT_FLOAT, NK_OFFSETOF(NKVertex, pos)},
{NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, NK_OFFSETOF(NKVertex, uv)},
{NK_VERTEX_COLOR, NK_FORMAT_R8G8B8A8, NK_OFFSETOF(NKVertex, color)},
{NK_VERTEX_LAYOUT_END}
};
config.vertex_layout = vertex_layout;
config.vertex_size = sizeof(NKVertex);
config.vertex_alignment = alignof(NKVertex);
config.tex_null.texture = nk_handle_id(0);
config.tex_null.uv = {0.0f, 0.0f};
config.circle_segment_count = 22;
config.curve_segment_count = 22;
config.arc_segment_count = 22;
config.global_alpha = 1.0f;
config.shape_AA = NK_ANTI_ALIASING_ON;
config.line_AA = NK_ANTI_ALIASING_ON;
nk_convert(ui.get(), &uiCommandBufferNK, &uiVertexBufferNK, &uiIndexBufferNK, &config);
ortho = glm::ortho(0.0f, (float)viewportWidth, (float)viewportHeight, 0.0f, -1.0f, 1.0f);
wgpuQueueWriteBuffer(queue, uiUniformBuffer, 0, &ortho, sizeof(ortho));
WGPUSurfaceTexture surfaceTexture;
wgpuSurfaceGetCurrentTexture(surface, &surfaceTexture);
if (surfaceTexture.status == WGPUSurfaceGetCurrentTextureStatus_Error || !surfaceTexture.texture) {
std::cerr << "Failed to get surface texture" << std::endl;
return;
}
WGPUTextureView nextTexture = wgpuTextureCreateView(surfaceTexture.texture, nullptr);
if (!nextTexture) {
std::cerr << "Failed to create texture view" << std::endl;
return;
}
WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr);
for(const auto &pair : world->chunks) {
chunkRenderer->render(pair.first, pair.second.get(), encoder, nextTexture);
}
WGPURenderPassColorAttachment colorAttachment = {};
colorAttachment.view = nextTexture;
colorAttachment.loadOp = WGPULoadOp_Clear;
colorAttachment.storeOp = WGPUStoreOp_Store;
colorAttachment.clearValue = {0, 0.2, 0.4, 1};
colorAttachment.depthSlice = -1;
WGPURenderPassDepthStencilAttachment depthAttachment = {};
depthAttachment.view = depthTexture->view;
depthAttachment.depthLoadOp = WGPULoadOp_Clear;
depthAttachment.depthStoreOp = WGPUStoreOp_Store;
depthAttachment.depthClearValue = 1.0f;
depthAttachment.depthReadOnly = false;
depthAttachment.stencilLoadOp = WGPULoadOp_Undefined;
depthAttachment.stencilStoreOp = WGPUStoreOp_Undefined;
WGPURenderPassDescriptor renderPassDesc = {};
renderPassDesc.colorAttachmentCount = 1;
renderPassDesc.colorAttachments = &colorAttachment;
renderPassDesc.depthStencilAttachment = &depthAttachment;
colorAttachment.loadOp = WGPULoadOp_Load;
WGPURenderPassDescriptor uiRenderPassDesc = {};
uiRenderPassDesc.colorAttachmentCount = 1;
uiRenderPassDesc.colorAttachments = &colorAttachment;
WGPURenderPassEncoder uiRenderPass = wgpuCommandEncoderBeginRenderPass(encoder, &uiRenderPassDesc);
wgpuRenderPassEncoderSetPipeline(uiRenderPass, uiPipeline);
wgpuRenderPassEncoderSetViewport(uiRenderPass, 0, 0, (float)viewportWidth, (float)viewportHeight, 0.0f, 1.0f);
size_t vertexSize = nk_buffer_total(&uiVertexBufferNK);
size_t indexSize = nk_buffer_total(&uiIndexBufferNK);
if (vertexSize > 0 && indexSize > 0) {
wgpuQueueWriteBuffer(queue, uiVertexBuffer, 0, uiVertexBufferNK.memory.ptr, vertexSize);
wgpuQueueWriteBuffer(queue, uiIndexBuffer, 0, uiIndexBufferNK.memory.ptr, indexSize);
wgpuRenderPassEncoderSetVertexBuffer(uiRenderPass, 0, uiVertexBuffer, 0, vertexSize);
wgpuRenderPassEncoderSetIndexBuffer(uiRenderPass, uiIndexBuffer, WGPUIndexFormat_Uint16, 0, indexSize);
std::vector<WGPUBindGroup> bindGroups;
WGPUBindGroupEntry uiBgEntries[3] = {};
uiBgEntries[0].binding = 0;
uiBgEntries[0].buffer = uiUniformBuffer;
uiBgEntries[0].offset = 0;
uiBgEntries[0].size = sizeof(glm::mat4);
uiBgEntries[2].binding = 2;
uiBgEntries[2].sampler = uiSampler;
std::map<int, WGPUTextureView> textureMap = {{0, uiFontTexture->view}};
const struct nk_draw_command* cmd;
uint32_t offset = 0;
nk_draw_foreach(cmd, ui.get(), &uiCommandBufferNK) {
if (!cmd->elem_count) continue;
if (offset + cmd->elem_count > indexSize / sizeof(uint16_t)) break;
uint32_t scissorX = static_cast<uint32_t>(cmd->clip_rect.x < 0 ? 0 : cmd->clip_rect.x);
uint32_t scissorY = static_cast<uint32_t>(cmd->clip_rect.y < 0 ? 0 : cmd->clip_rect.y);
uint32_t scissorW = static_cast<uint32_t>(cmd->clip_rect.w);
uint32_t scissorH = static_cast<uint32_t>(cmd->clip_rect.h);
scissorX = std::min(scissorX, (uint32_t)viewportWidth - 1);
scissorY = std::min(scissorY, (uint32_t)viewportHeight - 1);
if (scissorW > viewportWidth || scissorH > viewportHeight || scissorX + scissorW > viewportWidth || scissorY + scissorH > viewportHeight) {
scissorW = std::min(scissorW, viewportWidth - scissorX);
scissorH = std::min(scissorH, viewportHeight - scissorY);
}
wgpuRenderPassEncoderSetScissorRect(uiRenderPass, scissorX, scissorY, scissorW, scissorH);
uiBgEntries[1].binding = 1;
uiBgEntries[1].textureView = cmd->texture.id >= 0 && textureMap.count(cmd->texture.id) ? textureMap[cmd->texture.id] : uiDummyTexture->view;
WGPUBindGroupDescriptor uiBgDesc = {};
uiBgDesc.layout = uiBgl;
uiBgDesc.entryCount = 3;
uiBgDesc.entries = uiBgEntries;
WGPUBindGroup uiDynamicBindGroup = wgpuDeviceCreateBindGroup(device, &uiBgDesc);
bindGroups.push_back(uiDynamicBindGroup);
wgpuRenderPassEncoderSetBindGroup(uiRenderPass, 0, uiDynamicBindGroup, 0, nullptr);
wgpuRenderPassEncoderDrawIndexed(uiRenderPass, cmd->elem_count, 1, offset, 0, 0);
offset += cmd->elem_count;
}
wgpuRenderPassEncoderEnd(uiRenderPass);
for (WGPUBindGroup bg : bindGroups) wgpuBindGroupRelease(bg);
} else {
wgpuRenderPassEncoderEnd(uiRenderPass);
}
WGPUCommandBufferDescriptor cmdBufferDesc = {};
cmdBufferDesc.label = WGPUStringView{"Command buffer", WGPU_STRLEN};
WGPUCommandBuffer cmdBuffer = wgpuCommandEncoderFinish(encoder, nullptr);
wgpuQueueSubmit(queue, 1, &cmdBuffer);
#ifndef WASM_BUILD
wgpuSurfacePresent(surface);
#endif
wgpuTextureViewRelease(nextTexture);
wgpuTextureRelease(surfaceTexture.texture);
nk_clear(ui.get());
}
void Engine::tick() {
if(server) server->tick();
}
}
//*/

173
src/core/Engine.h Normal file
View file

@ -0,0 +1,173 @@
#pragma once
#include <iostream>
#include <glfw3webgpu.h>
#include <GLFW/glfw3.h>
#include <webgpu/webgpu.h>
#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#include <nuklear.h>
#include <cista.h>
#include <Window.h>
#include <Events.h>
#include <ClientWorld.h>
#include <ChunkRenderer.h>
#include <Texture.h>
namespace Artifact {
namespace Events {
struct PipelineReload {};
struct DrawUI {
nk_context* ctx;
};
}
/*
class Engine: public EventTarget {
public:
// Windowing
int viewportWidth = 1200;
int viewportHeight = 800;
GLFWwindow* window = []{
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
auto w = glfwCreateWindow(1200, 800, "Artifact Engine", nullptr, nullptr);
if (!w) throw std::runtime_error("Failed to create GLFW window");
glfwShowWindow(w);
glfwPollEvents();
return w;
}();
WGPUInstance instance = nullptr;
WGPUAdapter adapter = nullptr;
WGPUDevice device = nullptr;
WGPUSurface surface = nullptr;
WGPUTextureFormat surfaceFormat = WGPUTextureFormat_BGRA8Unorm;
WGPUQueue queue = nullptr;
WGPURenderPipeline worldPipeline = nullptr;
WGPURenderPipeline objectPipeline = nullptr;
WGPURenderPipeline uiPipeline = nullptr;
std::unique_ptr<Util::Texture> depthTexture;
std::unique_ptr<World::World> world;
std::string assetPath = std::string(getenv("HOME")) + "/eclipse-workspace/ArtifactEngine/assets";
Engine();
~Engine();
void init();
void run();
};
/*/
// Forward declaration.
class Server;
class Engine: public EventTarget {
public:
// Windowing
int viewportWidth = 1200;
int viewportHeight = 800;
Window window;
// WebGPU
WGPUInstance instance = nullptr;
WGPUAdapter adapter = nullptr;
WGPUDevice device = nullptr;
WGPUSurface surface = nullptr;
WGPUTextureFormat surfaceFormat = WGPUTextureFormat_BGRA8Unorm;
WGPUQueue queue = nullptr;
WGPURenderPipeline worldPipeline = nullptr;
WGPURenderPipeline objectPipeline = nullptr;
WGPURenderPipeline uiPipeline = nullptr;
std::unique_ptr<Util::Texture> depthTexture;
// Nuklear
std::unique_ptr<nk_context> ui;
std::unique_ptr<Util::Texture> uiFontTexture;
std::unique_ptr<Util::Texture> uiDummyTexture;
nk_buffer uiVertexBufferNK;
nk_buffer uiIndexBufferNK;
nk_buffer uiCommandBufferNK;
WGPUBuffer uiVertexBuffer = nullptr;
WGPUBuffer uiIndexBuffer = nullptr;
WGPUBuffer uiUniformBuffer = nullptr;
WGPUSampler uiSampler = nullptr;
WGPUBindGroupLayout uiBgl = nullptr;
// World
std::unique_ptr<World::ClientWorld> world;
WGPUBuffer worldUniformBuffer = nullptr;
WGPUSampler worldSampler = nullptr;
WGPUBindGroupLayout worldBgl = nullptr;
WGPUBindGroup worldBindGroup = nullptr;
std::unique_ptr<Util::Texture> nodeTextureAtlas;
std::unique_ptr<ChunkRenderer> chunkRenderer = std::make_unique<ChunkRenderer>(this);
// Misc
std::string assetPath = std::string(getenv("HOME")) + "/eclipse-workspace/ArtifactEngine/assets";
glm::mat4 ortho;
Server* server;
Engine();
~Engine();
// Initialize the engine: open the window, set up the wgpu device, etc.
void init();
// Reload shaders, processing overrides if present.
void reloadShaders();
// Create (and refresh) the pipelines used to render the world.
void makeWorldPipelines();
// Create (and refresh) the pipeline used to render entities.
void makeObjectPipeline();
// Create (and refresh) the pipeline used to render the UI.
void makeUIPipeline();
// Manages the main loop.
void run();
// Performs rendering per-frame.
void render();
// Updates game logic per-step.
void tick();
// Send a message to the server.
template<typename T>
void sendMessage(T msg);
// Receive a message from the server.
template<typename T>
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::ChunkChanged>) {
auto ev = (Events::ChunkChanged) msg;
world->chunks.at(ev.pos)->data = ev.data;
// chunkRenderer->updateChunk(ev.pos, ev.data);
}
}
};
//*/
}
#include <Server.h>
template<typename T>
void Artifact::Engine::sendMessage(T msg){
if(server) {
server->receiveMessage(msg);
} else {
auto buffer = cista::serialize(msg);
}
}

74
src/core/Events.h Normal file
View file

@ -0,0 +1,74 @@
// EventBus.h — header-only, < 100 lines
#pragma once
#include <unordered_map>
#include <vector>
#include <functional>
#include <typeindex>
#include <any>
#include <algorithm>
namespace Artifact {
namespace Events {
// Fired when the engine finishes initializing.
struct Initialized {};
// Fired every time the engine updates game logic.
struct Tick {
time_t dtime;
};
}
class EventTarget {
public:
// Subscribe to an event type
template<typename T>
using Callback = std::function<void(const T&)>;
template<typename T>
void listen(Callback<T> callback) {
auto type = std::type_index(typeid(T));
auto& vec = callbacks[type];
vec.emplace_back([cb = std::move(callback)](const std::any& e) {
cb(std::any_cast<const T&>(e));
});
}
// Fire an event — all subscribers get called immediately
template<typename T>
void dispatch(const T& event) {
auto it = callbacks.find(std::type_index(typeid(T)));
if (it == callbacks.end()) return;
// Copy the list in case a callback unsubscribes during dispatch
auto copy = it->second;
for (const auto& cb : copy)
cb(event);
}
// Optional: queued dispatch (for thread safety or next-frame events)
void processQueue() {
for (auto& [anyEvent, copy] : queued) {
auto it = callbacks.find(anyEvent.type());
if (it != callbacks.end()) {
for (const auto& cb : copy)
cb(anyEvent);
}
}
queued.clear();
}
template<typename T>
void queue(const T& event) {
auto type = std::type_index(typeid(T));
auto it = callbacks.find(type);
if (it == callbacks.end()) return;
queued.emplace_back(std::any(event), it->second);
}
private:
using AnyCallback = std::function<void(const std::any&)>;
std::unordered_map<std::type_index, std::vector<AnyCallback>> callbacks;
std::vector<std::pair<std::any, std::vector<AnyCallback>>> queued;
};
}

46
src/core/Registry.h Normal file
View file

@ -0,0 +1,46 @@
#pragma once
#include <string>
#include <unordered_map>
template <typename T>
class Registry {
public:
using ID = uint32_t; // Or uint16_t if fewer items expected
ID add(const std::string& name, T&& value) {
if (nameToId_.contains(name)) {
throw std::runtime_error("Duplicate registration: " + name);
}
ID id = static_cast<ID>(entries.size());
entries.push_back(std::move(value));
nameToId_[name] = id;
idToName_[id] = name;
return id;
}
const T& get(ID id) const {
if (id >= entries.size()) throw std::out_of_range("Invalid ID");
return entries[id];
}
ID getID(const std::string& name) const {
auto it = nameToId_.find(name);
if (it == nameToId_.end()) throw std::runtime_error("Unknown name: " + name);
return it->second;
}
const std::string& getName(ID id) const {
auto it = idToName_.find(id);
if (it == idToName_.end()) throw std::out_of_range("Invalid ID");
return it->second;
}
// Optional: Iteration over all entries
const std::vector<T>& all() const { return entries; }
private:
std::vector<T> entries;
std::unordered_map<std::string, ID> nameToId_;
std::unordered_map<ID, std::string> idToName_; // For reverse lookup
};

76
src/core/Window.cpp Normal file
View file

@ -0,0 +1,76 @@
#include <stdexcept>
#include <glfw3webgpu.h>
#include <Window.h>
namespace Artifact {
Window::Window() {
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
window = glfwCreateWindow(1200, 800, "Artifact Engine", nullptr, nullptr);
if (!window) throw std::runtime_error("Failed to create GLFW window");
glfwSetWindowUserPointer(window, this);
glfwSetKeyCallback(window, [](GLFWwindow* window, int key, int scancode, int action, int mods){
auto me = reinterpret_cast<Window*>(glfwGetWindowUserPointer(window));
me->dispatch(Events::Key{
.key = key,
.scancode = scancode,
.action = action,
.mods = mods
});
});
glfwSetCursorPosCallback(window, [](GLFWwindow* window, double x, double y) {
auto me = reinterpret_cast<Window*>(glfwGetWindowUserPointer(window));
me->dispatch(Events::CursorPos{
.x = x,
.y = y
});
});
glfwSetMouseButtonCallback(window, [](GLFWwindow* window, int button, int action, int mods) {
auto me = reinterpret_cast<Window*>(glfwGetWindowUserPointer(window));
Events::ActionType type;
switch(button) {
case GLFW_MOUSE_BUTTON_LEFT:
type = Events::ActionType::PRIMARY;
break;
case GLFW_MOUSE_BUTTON_RIGHT:
type = Events::ActionType::SECONDARY;
break;
case GLFW_MOUSE_BUTTON_MIDDLE:
type = Events::ActionType::TERTIARY;
break;
default:
type = Events::ActionType::UNKNOWN;
break;
}
double x, y;
glfwGetCursorPos(window, &x, &y);
me->dispatch(Events::Action{
.type = type,
.state = action == GLFW_PRESS,
.x = x,
.y = y
});
});
}
Window::~Window() {
glfwDestroyWindow(window);
}
WGPUSurface Window::createWGPUSurface(WGPUInstance &instance) {
return glfwCreateWindowWGPUSurface(instance, window);
}
bool Window::shouldClose() {
return glfwWindowShouldClose(window);
}
void Window::close() const {
glfwSetWindowShouldClose(window, true);
}
}

53
src/core/Window.h Normal file
View file

@ -0,0 +1,53 @@
#include <GLFW/glfw3.h>
#include <webgpu/webgpu.h>
#include <Events.h>
namespace Artifact {
namespace Events {
struct Key {
int key;
int scancode;
int action;
int mods;
};
struct CursorPos {
double x;
double y;
};
enum ActionType {
PRIMARY,
SECONDARY,
TERTIARY,
UNKNOWN
};
struct Action {
ActionType type;
bool state;
double x;
double y;
};
}
class Window: public EventTarget {
GLFWwindow* window;
public:
int width = 1200;
int height = 800;
Window();
~Window();
WGPUSurface createWGPUSurface(WGPUInstance &instance);
bool shouldClose();
void close() const;
// TODO: Allow changing the window cursor.
void setCursor();
};
}

52
src/main.cpp Normal file
View file

@ -0,0 +1,52 @@
#include <iostream>
#include <Events.h>
#include <Engine.h>
#include <World.h>
#include <Server.h>
int main() {
// printf("About to call glfwInit...\n");
// fflush(stdout);
// if (!glfwInit()) {
// fprintf(stderr, "glfwInit failed\n");
// return -1;
// }
// printf("glfwInit succeeded!\n");
//
// std::cout << "Starting..." << std::endl;
// glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
// auto window = glfwCreateWindow(1200, 800, "Test", nullptr, nullptr);
//
// std::cout << "GLFW window created" << std::endl;
Artifact::Engine engine;
Artifact::Server server(&engine);
engine.server = &server;
engine.listen<Artifact::Events::Initialized>([](const Artifact::Events::Initialized &ev) {
std::cout << "Initialization complete..." << std::endl;
});
engine.listen<Artifact::Events::DrawUI>([&engine](Artifact::Events::DrawUI ev) {
if(nk_begin(ev.ctx, "Test", nk_rect(0, 0, 100, 100), NK_WINDOW_MOVABLE|NK_WINDOW_SCALABLE|NK_WINDOW_TITLE|NK_WINDOW_BORDER)) {
nk_layout_row_dynamic(ev.ctx, 50, 2);
nk_label(ev.ctx, "Test", NK_TEXT_ALIGN_CENTERED);
if(nk_button_label(ev.ctx, "Test")) {
engine.server->sendMessage(Artifact::Events::Initialized{});
}
}
nk_end(ev.ctx);
});
Artifact::World::NodeDef def_Stone;
def_Stone.texture = "test.png";
engine.world->nodeRegistry.registerNode("test", def_Stone);
engine.run();
return 0;
}

35
src/network/Network.h Normal file
View file

@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <glm.hpp>
namespace Artifact {
namespace Network {
enum PacketType {
UPDATE_PLAYER_POS = 0,
UPDATE_PLAYER_ROTATION,
CHAT_MESSAGE,
};
namespace Packet {
struct PlayerPos {
std::string name;
glm::vec3 pos;
};
struct PlayerRotation {
std::string name;
glm::quat4 rot;
};
struct ChatMessage {
std::string name;
std::string msg;
};
}
}
}

12
src/player/Player.h Normal file
View file

@ -0,0 +1,12 @@
#include <glm.hpp>
namespace Artifact {
namespace Player {
class Player {
glm::vec3 pos;
double fov = 72.0;
};
}
}

66
src/server/Server.cpp Normal file
View file

@ -0,0 +1,66 @@
#include <ngtcp2/ngtcp2_crypto.h>
#include <gnutls/gnutls.h>
#include <Engine.h>
#include <Server.h>
namespace Artifact {
Server::Server(Engine* localClient) : localClient(localClient) {
}
void Server::run() {
start();
time_t lastTime;
time(&lastTime);
while(true) {
time_t now;
time(&now);
if(now - lastTime > STEP_SIZE) {
tick();
dispatch(Events::Tick{now - lastTime});
lastTime = now;
}
}
}
void log_printf(void *user_data, const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fputc('\n', stderr);
}
void Server::start() {
// ngtcp2_settings settings;
// ngtcp2_settings_default(&settings);
// settings.log_printf = log_printf;
// settings.initial_ts = get_time_ns();
//
// ngtcp2_transport_params params;
// ngtcp2_transport_params_default(&params);
//
// ngtcp2_callbacks callbacks = {0};
// // Set required callbacks using ngtcp2_crypto_* helpers
// callbacks.client_initial = ngtcp2_crypto_client_initial_cb;
// callbacks.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb;
// callbacks.encrypt = ngtcp2_crypto_encrypt_cb;
// callbacks.decrypt = ngtcp2_crypto_decrypt_cb;
// callbacks.hp_mask = ngtcp2_crypto_hp_mask_cb;
// callbacks.recv_retry = ngtcp2_crypto_recv_retry_cb;
// callbacks.update_key = ngtcp2_crypto_update_key_cb;
// callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb;
// callbacks.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb;
// callbacks.get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb;
// callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb;
//
// ngtcp2_path path = {};
}
void Server::tick() {
}
}

45
src/server/Server.h Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include <iostream>
#include <ngtcp2/ngtcp2.h>
#include <cista.h>
#include <Events.h>
#define STEP_SIZE 200
namespace Artifact {
class Engine;
class Server: public EventTarget {
Artifact::Engine* localClient;
public:
Server(Engine* localClient);
void run();
void start();
void tick();
template<typename T>
void sendMessage(T msg);
// Process a message from the client.
template<typename T>
void receiveMessage(T msg) {
if constexpr (std::is_same_v<T, Events::Initialized>) {
std::cout << "Message received" << std::endl;
}
}
};
}
#include <Engine.h>
template<typename T>
void Artifact::Server::sendMessage(T msg) {
if(localClient) {
localClient->receiveMessage(msg);
} else {
auto buffer = cista::serialize(msg);
}
}

View file

@ -0,0 +1,9 @@
#include <ServerClient.h>
namespace Artifact {
ServerClient::ServerClient(ngtcp2_conn* connection, std::string name) : connection(connection, &ngtcp2_conn_del), name(name) {
}
}

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

@ -0,0 +1,18 @@
#pragma once
#include <string>
#include <ngtcp2/ngtcp2.h>
namespace Artifact {
class ServerClient {
std::unique_ptr<ngtcp2_conn, decltype(&ngtcp2_conn_del)> connection;
ServerClient();
public:
std::string name;
ServerClient(ngtcp2_conn* connection, std::string name);
};
}

View file

@ -0,0 +1,45 @@
#include <ServerWorld.h>
namespace Artifact {
namespace World {
ServerWorld::ServerWorld(Server* server) : server(server) {
}
void ServerWorld::tick() {
}
void ServerWorld::updateLoadedChunks() {
}
Chunk* ServerWorld::loadChunk(glm::ivec3 pos) {
auto chunk = chunks.at(pos).get();
if(chunk) {
return chunk;
}
// 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) {
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
}
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
}
}
chunks.at(pos) = std::make_unique<Chunk>(data);
return chunks.at(pos).get();
}
}
}

20
src/server/ServerWorld.h Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <World.h>
#include <Server.h>
namespace Artifact {
namespace World {
class ServerWorld: public World {
Server* server = nullptr;
public:
ServerWorld(Server* server);
void tick();
void updateLoadedChunks();
Chunk* loadChunk(glm::ivec3 pos);
};
}
}

18
src/util/Mesh.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#include <vector>
#include <glm.hpp>
namespace Artifact {
struct Vertex {
float pos[3];
float uv[2];
};
struct Mesh {
std::unique_ptr<std::vector<Vertex>> vertices;
std::unique_ptr<std::vector<int>> indices;
};
}

65
src/util/Paths.cpp Normal file
View file

@ -0,0 +1,65 @@
#include <Paths.h>
namespace Artifact {
std::string getExecutablePath() {
std::string path;
char buffer[PATH_MAX]; // PATH_MAX is typically 4096 on Unix-like systems
#ifdef _WIN32
// Windows: Use GetModuleFileName
DWORD size = GetModuleFileNameA(NULL, buffer, PATH_MAX);
if (size == 0 || size == PATH_MAX) {
throw std::runtime_error("Failed to get executable path on Windows");
}
path = std::string(buffer, size);
// Remove the executable name by finding the last backslash
size_t lastSlash = path.find_last_of("\\");
if (lastSlash != std::string::npos) {
path = path.substr(0, lastSlash + 1); // Include the trailing slash
} else {
path = ""; // Fallback to current directory
}
#elif __APPLE__
// macOS: Use _NSGetExecutablePath
uint32_t size = sizeof(buffer);
if (_NSGetExecutablePath(buffer, &size) != 0) {
throw std::runtime_error("Failed to get executable path on macOS");
}
path = std::string(buffer);
// Remove the executable name by finding the last slash
size_t lastSlash = path.find_last_of("/");
if (lastSlash != std::string::npos) {
path = path.substr(0, lastSlash + 1); // Include the trailing slash
} else {
path = ""; // Fallback to current directory
}
#elif WASM_BUILD
path = "";
#else
// Linux: Use /proc/self/exe
ssize_t count = readlink("/proc/self/exe", buffer, PATH_MAX);
if (count == -1) {
throw std::runtime_error("Failed to get executable path on Linux");
}
path = std::string(buffer, count);
// Remove the executable name by finding the last slash
size_t lastSlash = path.find_last_of("/");
if (lastSlash != std::string::npos) {
path = path.substr(0, lastSlash + 1); // Include the trailing slash
} else {
path = ""; // Fallback to current directory
}
#endif
return path;
}
std::string globalAssetPath = getExecutablePath() + "../assets";
std::string getResourcePath(std::string name) {
return globalAssetPath + "/" + name;
}
}

14
src/util/Paths.h Normal file
View file

@ -0,0 +1,14 @@
#pragma once
#include <string>
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif
namespace Artifact {
std::string getExecutablePath();
std::string getResourcePath(std::string name);
}

53
src/util/Texture.cpp Normal file
View file

@ -0,0 +1,53 @@
#include <memory>
#define STB_IMAGE_IMPLEMENTATION
#include <Texture.h>
namespace Artifact {
namespace Util {
std::unique_ptr<Texture> createTextureFromData(WGPUDevice device, WGPUQueue queue, const void* data, uint32_t width, uint32_t height) {
auto tex = std::make_unique<Texture>();
WGPUTextureDescriptor textureDesc = {};
textureDesc.dimension = WGPUTextureDimension_2D;
textureDesc.size.width = width;
textureDesc.size.height = height;
textureDesc.size.depthOrArrayLayers = 1;
textureDesc.format = WGPUTextureFormat_RGBA8Unorm;
textureDesc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst;
textureDesc.mipLevelCount = 1;
textureDesc.sampleCount = 1;
tex->texture = wgpuDeviceCreateTexture(device, &textureDesc);
WGPUTextureViewDescriptor viewDesc = {};
viewDesc.format = WGPUTextureFormat_RGBA8Unorm;
viewDesc.dimension = WGPUTextureViewDimension_2D;
viewDesc.mipLevelCount = 1;
viewDesc.arrayLayerCount = 1;
tex->view = wgpuTextureCreateView(tex->texture, &viewDesc);
WGPUTexelCopyTextureInfo destination = {};
destination.texture = tex->texture;
destination.mipLevel = 0;
destination.origin = {0, 0, 0};
destination.aspect = WGPUTextureAspect_All;
WGPUTexelCopyBufferLayout layout = {};
layout.offset = 0;
layout.bytesPerRow = width * 4;
layout.rowsPerImage = height;
WGPUExtent3D writeSize = {width, height, 1};
wgpuQueueWriteTexture(queue, &destination, data, width * height * 4, &layout, &writeSize);
return tex;
}
std::unique_ptr<Texture> createTextureFromFile(WGPUDevice device, WGPUQueue queue, const char* file) {
int width, height, channels;
auto data = stbi_load(file, &width, &height, &channels, 4);
auto out = createTextureFromData(device, queue, data, width, height);
stbi_image_free(data);
return out;
}
}
}

18
src/util/Texture.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#include <webgpu/webgpu.h>
#include <stb_image.h>
namespace Artifact {
namespace Util {
struct Texture {
WGPUTexture texture;
WGPUTextureView view;
};
std::unique_ptr<Texture> createTextureFromData(WGPUDevice device, WGPUQueue queue, const void* data, uint32_t width, uint32_t height);
std::unique_ptr<Texture> createTextureFromFile(WGPUDevice device, WGPUQueue queue, const char* file);
}
}

21
src/util/UI.h Normal file
View file

@ -0,0 +1,21 @@
#pragma once
#include <glm.hpp>
#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#define NK_IMPLEMENTATION
#include <nuklear.h>
namespace Artifact {
struct NKVertex {
float pos[2];
float uv[2];
uint8_t color[4];
};
}

36
src/util/Util.h Normal file
View file

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <glm.hpp>
#include <webgpu/webgpu.h>
#include <stb_image.h>
namespace Artifact {
namespace Util {
// Custom hash for glm::vec3
struct Vec3Hash {
std::size_t operator()(const glm::vec3& v) const noexcept {
// Combine x, y, z into a single hash
// Using std::hash<float> and a good mixing function
std::size_t h1 = std::hash<float>{}(v.x);
std::size_t h2 = std::hash<float>{}(v.y);
std::size_t h3 = std::hash<float>{}(v.z);
return h1 ^ (h2 << 1) ^ (h3 << 2); // Simple but effective mixing
}
};
// Custom equality for glm::vec3
struct Vec3Equal {
bool operator()(const glm::vec3& lhs, const glm::vec3& rhs) const noexcept {
// Use exact equality or epsilon comparison
constexpr float EPSILON = 1e-6f;
return std::abs(lhs.x - rhs.x) < EPSILON &&
std::abs(lhs.y - rhs.y) < EPSILON &&
std::abs(lhs.z - rhs.z) < EPSILON;
}
};
}
}

23
src/world/Chunk.cpp Normal file
View file

@ -0,0 +1,23 @@
#include <vector>
#include <World.h>
#include <Chunk.h>
#include <Mesh.h>
namespace Artifact {
namespace World {
Chunk::Chunk() {
}
Chunk::~Chunk() {
}
uint16_t Chunk::getNodeRaw(float x, float y, float z) {
return data.at(posToIndex(x, y, z));
}
}
}

38
src/world/Chunk.h Normal file
View file

@ -0,0 +1,38 @@
#pragma once
#include <vector>
#ifndef CHUNK_SIZE
# define CHUNK_SIZE 16
#endif
namespace Artifact {
namespace World {
int posToIndex(float x, float y, float z);
class Chunk {
public:
static constexpr int SIZE = CHUNK_SIZE;
static constexpr int SIZE_CUBED = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE;
bool dirty = false;
std::array<uint16_t, SIZE_CUBED> data;
Chunk();
Chunk(std::array<uint16_t, SIZE_CUBED> data) : data(data) {};
~Chunk();
uint16_t getNodeRaw(float x, float y, float z);
};
}
namespace Events {
struct ChunkChanged {
glm::ivec3 pos;
std::array<uint16_t, World::Chunk::SIZE_CUBED> data;
};
}
}

83
src/world/World.cpp Normal file
View file

@ -0,0 +1,83 @@
#include <iostream>
#include <stb_image.h>
#include <World.h>
namespace Artifact {
namespace World {
int posToIndex(float x, float y, float z) {
return x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE;
}
NodeRegistry::NodeRegistry() {
registerNode("air", NodeDef{});
}
bool NodeRegistry::generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::vector<unsigned char>& atlasData) {
if (registeredNodes.size() <= 1) { // Only air (ID 0) exists
std::cerr << "No textures to create atlas\n";
return false;
}
std::vector<uint16_t> drawables;
// Load all textures and find the maximum dimensions
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;
drawables.push_back(i);
int width, height, channels;
unsigned char* data = stbi_load(registeredNodes.at(i).texture.c_str(), &width, &height, &channels, 4);
if (!data) {
std::cerr << "Failed to load texture: " << registeredNodes.at(i).texture << "\n";
return false;
}
textureData.push_back({});
textureData.at(i).resize(width * height * 4);
std::memcpy(textureData[i].data(), data, width * height * 4);
widths.push_back(width);
heights.push_back(height);
maxWidth = std::max(maxWidth, width);
maxHeight = std::max(maxHeight, height);
stbi_image_free(data);
}
// 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;
atlasData.resize(atlasWidth * atlasHeight * 4, 0); // RGBA
for (size_t i = 1; i < drawables.size(); ++i) {
int xOffset = (i - 1) * maxWidth;
for (int y = 0; y < heights[i]; ++y) {
for (int x = 0; x < widths[i]; ++x) {
int srcIdx = (y * widths[i] + x) * 4;
int dstIdx = (y * atlasWidth + (xOffset + x)) * 4;
for (int c = 0; c < 4; ++c) {
atlasData[dstIdx + c] = textureData[i][srcIdx + c];
}
}
}
// Update texture coordinates (normalized 0-1)
float uMin = static_cast<float>(xOffset) / atlasWidth;
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}};
}
return true;
}
World::World() {
}
}
}

66
src/world/World.h Normal file
View file

@ -0,0 +1,66 @@
#pragma once
#include <map>
#include <glm.hpp>
#include <Chunk.h>
#include <Util.h>
#include <Paths.h>
#include <Mesh.h>
#include <Texture.h>
namespace Artifact {
namespace World {
enum class AlphaBlendMode {
NONE,
CLIP,
BLEND
};
enum class NodeVisual {
NONE,
CUBE,
MESH
};
struct NodeDef {
std::string name;
std::string texture;
AlphaBlendMode alphaBlendMode = AlphaBlendMode::NONE;
NodeVisual visual = NodeVisual::NONE;
};
struct NodeTextureCoords {
glm::vec2 uvMin;
glm::vec2 uvMax;
};
class NodeRegistry {
uint16_t next = 0;
public:
std::unordered_map<uint16_t, NodeDef> registeredNodes;
std::vector<NodeTextureCoords> textureCoords;
NodeRegistry();
void registerNode(std::string name, NodeDef def) {
def.name = name;
def.texture = getResourcePath(def.texture);
registeredNodes.emplace(next++, def);
}
bool generateTextureAtlas(int& atlasWidth, int& atlasHeight, std::vector<unsigned char>& atlasData);
};
class World {
public:
NodeRegistry nodeRegistry;
std::unordered_map<glm::ivec3, std::unique_ptr<Chunk>, Artifact::Util::Vec3Hash, Artifact::Util::Vec3Equal> chunks;
World();
};
}
}

120
tools/update_deps.sh Normal file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env bash
set -e
CMAKE="$HOME/Downloads/cmake-3.28.3-macos-universal/CMake.app/Contents/bin/cmake"
DEPS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../deps" && pwd)"
mkdir -p "$DEPS_DIR"
cd $DEPS_DIR
# GLFW
if [ ! -d "glfw/build" ] || [ ! -f "glfw/build/src/libglfw3.a" ]; then
echo "Building GLFW..."
if [ ! -d "glfw" ]; then
echo " Cloning GLFW 3.4..."
git clone --depth 1 --branch 3.4 https://github.com/glfw/glfw.git glfw
else
echo " Updating GLFW..."
git -C glfw pull
fi
cd glfw
mkdir -p build
cd build
"$CMAKE" .. \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DBUILD_SHARED_LIBS=OFF \
-DGLFW_BUILD_EXAMPLES=OFF \
-DGLFW_BUILD_TESTS=OFF \
-DGLFW_BUILD_DOCS=OFF \
-DGLFW_INSTALL=ON \
-DGLFW_BUILD_COCOA=ON \
-DCMAKE_INSTALL_PREFIX="$DEPS_DIR/glfw/install"
make -j$(sysctl -n hw.logicalcpu)
make install # installs libglfw3.a + headers into $DEPS_DIR/glfw/install
cd ../..
echo "GLFW built and installed to $DEPS_DIR/glfw/install"
else
echo "GLFW already built skipping."
fi
# If you don't use -L, using curl to access 'modern' websites can get very annoying.
if [ ! -d "$DEPS_DIR/webgpu" ]; then
mkdir -p "$DEPS_DIR/webgpu"
curl -L https://github.com/gfx-rs/wgpu-native/releases/download/v27.0.2.0/wgpu-macos-aarch64-debug.zip -o "$DEPS_DIR/webgpu/wgpu-macos-aarch64-debug.zip"
cd webgpu
unzip wgpu-macos-aarch64-debug.zip
rm wgpu-macos-aarch64-debug.zip
cd ..
fi
# glfw3webgpu helper
if [ ! -d "$DEPS_DIR/glfw3webgpu" ]; then
git clone --depth 1 https://github.com/eliemichel/glfw3webgpu.git "$DEPS_DIR/glfw3webgpu"
else
git -C "$DEPS_DIR/glfw3webgpu" pull
fi
# Nuklear (single header)
curl -L https://raw.githubusercontent.com/Immediate-Mode-UI/Nuklear/master/nuklear.h -o "$DEPS_DIR/nuklear.h"
if [ ! -d "glm" ]; then
git clone --depth 1 https://github.com/g-truc/glm.git glm
else
git -C glm fetch origin
git -C glm reset --hard origin/master
fi
# Cista (serialization)
curl -L https://github.com/felixguendling/cista/releases/download/v0.16/cista.h -o "$DEPS_DIR/cista.h"
# ================================================================
# ngtcp2 + ngtcp2_crypto_openssl (static libraries)
# ================================================================
if [ ! -d "ngtcp2/build" ] || [ ! -f "ngtcp2/build/libngtcp2.a" ]; then
echo "Building ngtcp2 (with OpenSSL crypto backend)..."
if [ ! -d "ngtcp2" ]; then
echo " Cloning ngtcp2..."
git clone --depth 1 --branch v1.9.0 https://github.com/ngtcp2/ngtcp2.git ngtcp2
else
echo " Updating ngtcp2..."
git -C ngtcp2 pull
fi
cd ngtcp2
# Ensure we have the submodules (crypto/openssl)
git submodule update --init --depth 1
mkdir -p build
cd build
# Static-only build, disable examples/tests, enable gnutls backend
$CMAKE .. \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_STATIC_LIBS=ON \
-DENABLE_EXAMPLES=OFF \
-DENABLE_TEST=OFF \
-DENABLE_OPENSSL=OFF \
-DENABLE_GNUTLS=ON \
-DGNUTLS_LIBRARY=$HOME/homebrew/Cellar/gnutls/3.8.11/lib/libgnutls.dylib \
-DGNUTLS_INCLUDE_DIR=$HOME/homebrew/Cellar/gnutls/3.8.11/include \
-DCMAKE_INSTALL_PREFIX="$DEPS_DIR/ngtcp2/install"
make -j$(sysctl -n hw.logicalcpu)
make install # installs headers + .a files into $DEPS_DIR/ngtcp2/install
cd ../../
echo "ngtcp2 built and installed to $DEPS_DIR/ngtcp2/install"
else
echo "ngtcp2 already built skipping."
fi
echo "All dependencies updated."