commit d99b70eaa15e6f915a404765f3eecf66d0b10dba Author: Signal Date: Tue Dec 9 16:32:47 2025 -0500 Initial commit. diff --git a/assets/fonts/Arial.ttf b/assets/fonts/Arial.ttf new file mode 100644 index 0000000..ab68fb1 Binary files /dev/null and b/assets/fonts/Arial.ttf differ diff --git a/assets/test_stone.png b/assets/test_stone.png new file mode 100644 index 0000000..fccc21f Binary files /dev/null and b/assets/test_stone.png differ diff --git a/src/client/ChunkRenderer.cpp b/src/client/ChunkRenderer.cpp new file mode 100644 index 0000000..bd3afbf --- /dev/null +++ b/src/client/ChunkRenderer.cpp @@ -0,0 +1,42 @@ + +#include +#include + +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); +} + +} diff --git a/src/client/ChunkRenderer.h b/src/client/ChunkRenderer.h new file mode 100644 index 0000000..a432b78 --- /dev/null +++ b/src/client/ChunkRenderer.h @@ -0,0 +1,37 @@ +#pragma once +#include + +#include +#include + +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 chunkBuffers; +public: + ChunkRenderer(Engine* engine) : engine(engine) {} + void render(glm::ivec3 pos, World::Chunk* chunk, WGPUCommandEncoder encoder, WGPUTextureView texture); +}; + +} diff --git a/src/client/ClientWorld.cpp b/src/client/ClientWorld.cpp new file mode 100644 index 0000000..65c830e --- /dev/null +++ b/src/client/ClientWorld.cpp @@ -0,0 +1,108 @@ +#include + +#include + +#include +#include +#include + +namespace Artifact { +namespace World { + +void ClientWorld::generateChunkMesh(WGPUDevice device, WGPUQueue queue, Chunk* chunk, ChunkMesh &mesh) { + std::vector verts; + std::vector 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(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(x); + float fy0 = static_cast(y); + float fz0 = static_cast(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(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) { + +} + +} +} diff --git a/src/client/ClientWorld.h b/src/client/ClientWorld.h new file mode 100644 index 0000000..bcd41e8 --- /dev/null +++ b/src/client/ClientWorld.h @@ -0,0 +1,20 @@ +#pragma once +#include + +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); +}; + +} +} diff --git a/src/core/Engine.cpp b/src/core/Engine.cpp new file mode 100644 index 0000000..bb7f4d5 --- /dev/null +++ b/src/core/Engine.cpp @@ -0,0 +1,805 @@ +#include +#include + +#include +#include +#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 +#include + +#include +#include +#include +#include +#include +#include + +#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(); +} + +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(); +} + +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)->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)->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(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(); + depthTexture->texture = wgpuDeviceCreateTexture(device, &depthDesc); + depthTexture->view = wgpuTextureCreateView(depthTexture->texture, nullptr); + + std::cout << "Initializing Nuklear..." << std::endl; + // Initialize Nuklear + ui = std::make_unique(); + 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([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([this](auto ev) { + nk_input_motion(ui.get(), ev.x, ev.y); + }); + + window.listen([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 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, + } + @group(0) @binding(0) var uniforms: Uniforms; + struct VertexOutput { + @builtin(position) pos: vec4, + @location(0) uv: vec2 + } + @vertex + fn vs_main( + @location(0) pos: vec3, + @location(1) uv: vec2 + ) -> VertexOutput { + var out: VertexOutput; + out.pos = uniforms.mvp * vec4(pos, 1.0); + out.uv = uv; + return out; + } + )"; + + const char* cubeFSCode = R"( + @group(0) @binding(1) var texture: texture_2d; + @group(0) @binding(2) var sampler_: sampler; + @fragment + fn fs_main( + @location(0) uv: vec2 + ) -> @location(0) vec4 { + 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, + } + @group(0) @binding(0) var uniforms: Uniforms; + struct VertexOutput { + @builtin(position) pos: vec4, + @location(0) uv: vec2, + @location(1) color: vec4, + } + @vertex + fn vs_main(@location(0) pos: vec2, @location(1) uv: vec2, @location(2) color: vec4) -> VertexOutput { + var out: VertexOutput; + out.pos = uniforms.ortho * vec4(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; + @group(0) @binding(2) var sampler_: sampler; + @fragment + fn fs_main(@location(0) uv: vec2, @location(1) color: vec4) -> @location(0) vec4 { + 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 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 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(cmd->clip_rect.x < 0 ? 0 : cmd->clip_rect.x); + uint32_t scissorY = static_cast(cmd->clip_rect.y < 0 ? 0 : cmd->clip_rect.y); + uint32_t scissorW = static_cast(cmd->clip_rect.w); + uint32_t scissorH = static_cast(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(); +} + +} +//*/ diff --git a/src/core/Engine.h b/src/core/Engine.h new file mode 100644 index 0000000..b4b2bbf --- /dev/null +++ b/src/core/Engine.h @@ -0,0 +1,173 @@ +#pragma once +#include + +#include +#include +#include +#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 +#include + +#include +#include +#include +#include +#include + +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 depthTexture; + + std::unique_ptr 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 depthTexture; + + // Nuklear + std::unique_ptr ui; + std::unique_ptr uiFontTexture; + std::unique_ptr 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; + WGPUBuffer worldUniformBuffer = nullptr; + WGPUSampler worldSampler = nullptr; + WGPUBindGroupLayout worldBgl = nullptr; + WGPUBindGroup worldBindGroup = nullptr; + std::unique_ptr nodeTextureAtlas; + std::unique_ptr chunkRenderer = std::make_unique(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 + void sendMessage(T msg); + // Receive a message from the server. + template + void receiveMessage(T msg) { + if constexpr (std::is_same_v) { + std::cout << "Message received" << std::endl; + } else if constexpr (std::is_same_v) { + auto ev = (Events::ChunkChanged) msg; + world->chunks.at(ev.pos)->data = ev.data; +// chunkRenderer->updateChunk(ev.pos, ev.data); + } + } +}; +//*/ + +} + +#include + +template +void Artifact::Engine::sendMessage(T msg){ + if(server) { + server->receiveMessage(msg); + } else { + auto buffer = cista::serialize(msg); + } +} + + diff --git a/src/core/Events.h b/src/core/Events.h new file mode 100644 index 0000000..9730276 --- /dev/null +++ b/src/core/Events.h @@ -0,0 +1,74 @@ +// EventBus.h — header-only, < 100 lines +#pragma once +#include +#include +#include +#include +#include +#include + +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 + using Callback = std::function; + + template + void listen(Callback 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(e)); + }); + } + + // Fire an event — all subscribers get called immediately + template + 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 + 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; + std::unordered_map> callbacks; + std::vector>> queued; +}; + +} diff --git a/src/core/Registry.h b/src/core/Registry.h new file mode 100644 index 0000000..d2766a9 --- /dev/null +++ b/src/core/Registry.h @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +template +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(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& all() const { return entries; } + +private: + std::vector entries; + std::unordered_map nameToId_; + std::unordered_map idToName_; // For reverse lookup +}; + diff --git a/src/core/Window.cpp b/src/core/Window.cpp new file mode 100644 index 0000000..d154332 --- /dev/null +++ b/src/core/Window.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include + +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(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(glfwGetWindowUserPointer(window)); + me->dispatch(Events::CursorPos{ + .x = x, + .y = y + }); + }); + + glfwSetMouseButtonCallback(window, [](GLFWwindow* window, int button, int action, int mods) { + auto me = reinterpret_cast(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); +} + +} diff --git a/src/core/Window.h b/src/core/Window.h new file mode 100644 index 0000000..b312576 --- /dev/null +++ b/src/core/Window.h @@ -0,0 +1,53 @@ +#include +#include + +#include + +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(); +}; + +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..873516c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,52 @@ +#include + +#include +#include +#include +#include + +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([](const Artifact::Events::Initialized &ev) { + std::cout << "Initialization complete..." << std::endl; + }); + + engine.listen([&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; +} diff --git a/src/network/Network.h b/src/network/Network.h new file mode 100644 index 0000000..7a82618 --- /dev/null +++ b/src/network/Network.h @@ -0,0 +1,35 @@ +#pragma once +#include + +#include + +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; +}; + +} + +} +} diff --git a/src/player/Player.h b/src/player/Player.h new file mode 100644 index 0000000..e0f1b51 --- /dev/null +++ b/src/player/Player.h @@ -0,0 +1,12 @@ +#include + +namespace Artifact { +namespace Player { + +class Player { + glm::vec3 pos; + double fov = 72.0; +}; + +} +} diff --git a/src/server/Server.cpp b/src/server/Server.cpp new file mode 100644 index 0000000..2414a20 --- /dev/null +++ b/src/server/Server.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include +#include + +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(¶ms); +// +// 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() { + +} + +} diff --git a/src/server/Server.h b/src/server/Server.h new file mode 100644 index 0000000..cad7476 --- /dev/null +++ b/src/server/Server.h @@ -0,0 +1,45 @@ +#pragma once +#include + +#include +#include + +#include + +#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 + void sendMessage(T msg); + // Process a message from the client. + template + void receiveMessage(T msg) { + if constexpr (std::is_same_v) { + std::cout << "Message received" << std::endl; + } + } +}; + +} + +#include + +template +void Artifact::Server::sendMessage(T msg) { + if(localClient) { + localClient->receiveMessage(msg); + } else { + auto buffer = cista::serialize(msg); + } +} diff --git a/src/server/ServerClient.cpp b/src/server/ServerClient.cpp new file mode 100644 index 0000000..90cada9 --- /dev/null +++ b/src/server/ServerClient.cpp @@ -0,0 +1,9 @@ +#include + +namespace Artifact { + +ServerClient::ServerClient(ngtcp2_conn* connection, std::string name) : connection(connection, &ngtcp2_conn_del), name(name) { + +} + +} diff --git a/src/server/ServerClient.h b/src/server/ServerClient.h new file mode 100644 index 0000000..fd7c946 --- /dev/null +++ b/src/server/ServerClient.h @@ -0,0 +1,18 @@ +#pragma once +#include + +#include + +namespace Artifact { + +class ServerClient { + std::unique_ptr connection; + + ServerClient(); +public: + std::string name; + + ServerClient(ngtcp2_conn* connection, std::string name); +}; + +} diff --git a/src/server/ServerWorld.cpp b/src/server/ServerWorld.cpp new file mode 100644 index 0000000..78ffb6c --- /dev/null +++ b/src/server/ServerWorld.cpp @@ -0,0 +1,45 @@ + +#include + +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 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(data); + return chunks.at(pos).get(); +} + +} +} diff --git a/src/server/ServerWorld.h b/src/server/ServerWorld.h new file mode 100644 index 0000000..501418c --- /dev/null +++ b/src/server/ServerWorld.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace Artifact { +namespace World { + +class ServerWorld: public World { + Server* server = nullptr; +public: + ServerWorld(Server* server); + + void tick(); + void updateLoadedChunks(); + Chunk* loadChunk(glm::ivec3 pos); +}; + +} +} diff --git a/src/util/Mesh.h b/src/util/Mesh.h new file mode 100644 index 0000000..f836841 --- /dev/null +++ b/src/util/Mesh.h @@ -0,0 +1,18 @@ +#pragma once +#include + +#include + +namespace Artifact { + +struct Vertex { + float pos[3]; + float uv[2]; +}; + +struct Mesh { + std::unique_ptr> vertices; + std::unique_ptr> indices; +}; + +} diff --git a/src/util/Paths.cpp b/src/util/Paths.cpp new file mode 100644 index 0000000..55ce8be --- /dev/null +++ b/src/util/Paths.cpp @@ -0,0 +1,65 @@ +#include + +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; +} + +} diff --git a/src/util/Paths.h b/src/util/Paths.h new file mode 100644 index 0000000..a38fd60 --- /dev/null +++ b/src/util/Paths.h @@ -0,0 +1,14 @@ +#pragma once +#include + +#ifdef __APPLE__ +#include +#endif + +namespace Artifact { + +std::string getExecutablePath(); + +std::string getResourcePath(std::string name); + +} diff --git a/src/util/Texture.cpp b/src/util/Texture.cpp new file mode 100644 index 0000000..de35fd8 --- /dev/null +++ b/src/util/Texture.cpp @@ -0,0 +1,53 @@ +#include + +#define STB_IMAGE_IMPLEMENTATION +#include + +namespace Artifact { +namespace Util { + +std::unique_ptr createTextureFromData(WGPUDevice device, WGPUQueue queue, const void* data, uint32_t width, uint32_t height) { + auto tex = std::make_unique(); + 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 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; +} + +} +} diff --git a/src/util/Texture.h b/src/util/Texture.h new file mode 100644 index 0000000..b1d72b6 --- /dev/null +++ b/src/util/Texture.h @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +namespace Artifact { +namespace Util { + +struct Texture { + WGPUTexture texture; + WGPUTextureView view; +}; + +std::unique_ptr createTextureFromData(WGPUDevice device, WGPUQueue queue, const void* data, uint32_t width, uint32_t height); + +std::unique_ptr createTextureFromFile(WGPUDevice device, WGPUQueue queue, const char* file); + +} +} diff --git a/src/util/UI.h b/src/util/UI.h new file mode 100644 index 0000000..ff52ac6 --- /dev/null +++ b/src/util/UI.h @@ -0,0 +1,21 @@ +#pragma once +#include +#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 + +namespace Artifact { + +struct NKVertex { + float pos[2]; + float uv[2]; + uint8_t color[4]; +}; + +} diff --git a/src/util/Util.h b/src/util/Util.h new file mode 100644 index 0000000..8167873 --- /dev/null +++ b/src/util/Util.h @@ -0,0 +1,36 @@ +#pragma once +#include + +#include +#include +#include + +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 and a good mixing function + std::size_t h1 = std::hash{}(v.x); + std::size_t h2 = std::hash{}(v.y); + std::size_t h3 = std::hash{}(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; + } +}; + +} +} diff --git a/src/world/Chunk.cpp b/src/world/Chunk.cpp new file mode 100644 index 0000000..d94f5fb --- /dev/null +++ b/src/world/Chunk.cpp @@ -0,0 +1,23 @@ +#include + +#include +#include +#include + +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)); +} + +} +} diff --git a/src/world/Chunk.h b/src/world/Chunk.h new file mode 100644 index 0000000..58af527 --- /dev/null +++ b/src/world/Chunk.h @@ -0,0 +1,38 @@ +#pragma once +#include + +#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 data; + + Chunk(); + Chunk(std::array data) : data(data) {}; + ~Chunk(); + + uint16_t getNodeRaw(float x, float y, float z); +}; + +} + +namespace Events { + +struct ChunkChanged { + glm::ivec3 pos; + std::array data; +}; + +} +} diff --git a/src/world/World.cpp b/src/world/World.cpp new file mode 100644 index 0000000..eca8888 --- /dev/null +++ b/src/world/World.cpp @@ -0,0 +1,83 @@ +#include + +#include + +#include + +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& atlasData) { + if (registeredNodes.size() <= 1) { // Only air (ID 0) exists + std::cerr << "No textures to create atlas\n"; + return false; + } + + std::vector drawables; + + // Load all textures and find the maximum dimensions + std::vector> textureData; + std::vector 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(xOffset) / atlasWidth; + float uMax = static_cast(xOffset + widths[i]) / atlasWidth; + float vMin = 0.0f; + float vMax = static_cast(heights[i]) / atlasHeight; + textureCoords.at(drawables.at(i)) = {{uMin, vMin}, {uMax, vMax}}; + } + + return true; +} + +World::World() { + +} + + +} +} diff --git a/src/world/World.h b/src/world/World.h new file mode 100644 index 0000000..07f2075 --- /dev/null +++ b/src/world/World.h @@ -0,0 +1,66 @@ +#pragma once +#include + +#include + +#include +#include +#include +#include +#include + +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 registeredNodes; + std::vector 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& atlasData); +}; + +class World { +public: + NodeRegistry nodeRegistry; + std::unordered_map, Artifact::Util::Vec3Hash, Artifact::Util::Vec3Equal> chunks; + + World(); +}; + +} +} diff --git a/tools/update_deps.sh b/tools/update_deps.sh new file mode 100644 index 0000000..12e4107 --- /dev/null +++ b/tools/update_deps.sh @@ -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."