#include #include #include #include #include #include #include #include #include #include #include #include "Atlas.h" #include "Base64.h" #include "Shaders.h" #include "vf/Frustum.h" #include "vf/Inventory.h" #include "vf/Lighting.h" #include "vf/LitMesh.h" #include "vf/Physics.h" #include "vf/Raycast.h" #include "vf/Region.h" #include "vf/Streaming.h" #include "vf/WorldGen.h" using namespace vf; namespace { struct GpuMesh { GLuint vao = 0; GLuint vbo = 0; GLuint ibo = 0; GLsizei indexCount = 0; }; struct GpuChunk { GpuMesh opaque; GpuMesh transparent; Aabb bounds; Vec3 center; }; struct KeyState { bool forward = false, back = false, left = false, right = false; bool jump = false, sink = false; }; WorldGenParams genParams; WorldGen worldGen{genParams}; World world; StreamingConfig streamConfig{6, 8}; ChunkStreamer streamer{streamConfig}; Player player; KeyState keys; PhysicsConfig physics; Hotbar hotbar; std::deque genQueue; std::deque meshQueue; std::unordered_map gpuChunks; bool haveLightCache = false; ColumnCoord lightCacheColumn{}; LightField lightCache; constexpr int lightMargin = 2; GLuint program = 0; GLint uViewProj = -1, uAtlas = -1, uLightDir = -1, uFogColor = -1; GLuint atlasTexture = 0; ColumnCoord lastCentre{2147483647, 2147483647}; double lastTime = 0.0; int canvasWidth = 1280; int canvasHeight = 720; GLuint compileShader(GLenum type, const char* source) { GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint ok = 0; glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); if (!ok) { char log[1024]; glGetShaderInfoLog(shader, sizeof(log), nullptr, log); std::printf("shader compile error: %s\n", log); } return shader; } void buildProgram() { GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSource()); GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource()); program = glCreateProgram(); glAttachShader(program, vs); glAttachShader(program, fs); glLinkProgram(program); glDeleteShader(vs); glDeleteShader(fs); uViewProj = glGetUniformLocation(program, "uViewProj"); uAtlas = glGetUniformLocation(program, "uAtlas"); uLightDir = glGetUniformLocation(program, "uLightDir"); uFogColor = glGetUniformLocation(program, "uFogColor"); } void uploadAtlas() { Atlas atlas = buildAtlas(16); glGenTextures(1, &atlasTexture); glBindTexture(GL_TEXTURE_2D_ARRAY, atlasTexture); glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, atlas.tileSize, atlas.tileSize, atlas.layers, 0, GL_RGBA, GL_UNSIGNED_BYTE, atlas.pixels.data()); glGenerateMipmap(GL_TEXTURE_2D_ARRAY); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT); } void freeGpuMesh(GpuMesh& mesh) { if (mesh.vao) glDeleteVertexArrays(1, &mesh.vao); if (mesh.vbo) glDeleteBuffers(1, &mesh.vbo); if (mesh.ibo) glDeleteBuffers(1, &mesh.ibo); mesh = GpuMesh{}; } void uploadGpuMesh(GpuMesh& gpu, const Mesh& mesh) { if (mesh.empty()) { freeGpuMesh(gpu); return; } if (gpu.vao == 0) { glGenVertexArrays(1, &gpu.vao); glGenBuffers(1, &gpu.vbo); glGenBuffers(1, &gpu.ibo); } glBindVertexArray(gpu.vao); glBindBuffer(GL_ARRAY_BUFFER, gpu.vbo); glBufferData(GL_ARRAY_BUFFER, mesh.vertices.size() * sizeof(Vertex), mesh.vertices.data(), GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpu.ibo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, mesh.indices.size() * sizeof(uint32_t), mesh.indices.data(), GL_STATIC_DRAW); GLsizei stride = sizeof(Vertex); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float))); glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float))); glEnableVertexAttribArray(3); glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, stride, (void*)(8 * sizeof(float))); glEnableVertexAttribArray(4); glVertexAttribPointer(4, 1, GL_FLOAT, GL_FALSE, stride, (void*)(9 * sizeof(float))); glEnableVertexAttribArray(5); glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, stride, (void*)(10 * sizeof(float))); gpu.indexCount = static_cast(mesh.indices.size()); } void removeChunkGpu(const Vec3i& coord) { auto it = gpuChunks.find(coord); if (it == gpuChunks.end()) return; freeGpuMesh(it->second.opaque); freeGpuMesh(it->second.transparent); gpuChunks.erase(it); } const LightField& columnLight(ColumnCoord column) { if (haveLightCache && lightCacheColumn == column) return lightCache; int top = worldGen.stackChunkCount() * chunkSize; Vec3i origin{column.x * chunkSize - lightMargin, 0, column.z * chunkSize - lightMargin}; Vec3i dims{chunkSize + 2 * lightMargin, top, chunkSize + 2 * lightMargin}; lightCache = computeLight(world, origin, dims); lightCacheColumn = column; haveLightCache = true; return lightCache; } void meshChunk(const Vec3i& coord) { ChunkMesh mesh = meshChunkLit(world, coord, columnLight({coord.x, coord.z})); if (mesh.empty()) { removeChunkGpu(coord); return; } GpuChunk& gpu = gpuChunks[coord]; uploadGpuMesh(gpu.opaque, mesh.opaque); uploadGpuMesh(gpu.transparent, mesh.transparent); float ox = static_cast(coord.x * chunkSize); float oy = static_cast(coord.y * chunkSize); float oz = static_cast(coord.z * chunkSize); gpu.bounds = Aabb{{ox, oy, oz}, {ox + chunkSize, oy + chunkSize, oz + chunkSize}}; gpu.center = Vec3{ox + chunkSize * 0.5f, oy + chunkSize * 0.5f, oz + chunkSize * 0.5f}; } void enqueueMesh(const Vec3i& coord) { for (const Vec3i& pending : meshQueue) if (pending == coord) return; meshQueue.push_back(coord); } void enqueueColumnMeshes(ColumnCoord column) { for (int cy = 0; cy < worldGen.stackChunkCount(); ++cy) enqueueMesh({column.x, cy, column.z}); } bool columnGenerated(ColumnCoord column) { return world.chunkAt({column.x, 0, column.z}) != nullptr; } void generateColumnNow(ColumnCoord column) { if (!columnGenerated(column)) worldGen.generateColumn(world, column.x, column.z); haveLightCache = false; enqueueColumnMeshes(column); const ColumnCoord neighbours[4] = {{column.x + 1, column.z}, {column.x - 1, column.z}, {column.x, column.z + 1}, {column.x, column.z - 1}}; for (ColumnCoord n : neighbours) if (columnGenerated(n)) enqueueColumnMeshes(n); } void unloadColumn(ColumnCoord column) { for (int cy = 0; cy < worldGen.stackChunkCount(); ++cy) { Vec3i coord{column.x, cy, column.z}; removeChunkGpu(coord); world.removeChunk(coord); } haveLightCache = false; } ColumnCoord playerColumn() { return {floorDiv(static_cast(std::floor(player.position.x)), chunkSize), floorDiv(static_cast(std::floor(player.position.z)), chunkSize)}; } MoveInput sampleInput() { MoveInput input; input.forward = (keys.forward ? 1.0f : 0.0f) - (keys.back ? 1.0f : 0.0f); input.strafe = (keys.right ? 1.0f : 0.0f) - (keys.left ? 1.0f : 0.0f); if (player.flying) { input.up = keys.jump; input.down = keys.sink; } else { input.jump = keys.jump; } return input; } const char* blockName(Block block) { switch (block) { case Block::Grass: return "Grass"; case Block::Dirt: return "Dirt"; case Block::Stone: return "Stone"; case Block::Sand: return "Sand"; case Block::Wood: return "Wood"; case Block::Leaves: return "Leaves"; case Block::Water: return "Water"; case Block::Snow: return "Snow"; case Block::CoalOre: return "Coal"; case Block::IronOre: return "Iron"; case Block::GoldOre: return "Gold"; case Block::Gravel: return "Gravel"; case Block::Glowstone: return "Glow"; default: return "?"; } } EM_JS(void, pushHotbar, (int selected, const char* payload), { if (typeof window.renderHotbar === 'function') window.renderHotbar(selected, UTF8ToString(payload)); }); void refreshHud() { std::string payload; for (int i = 0; i < hotbarSize; ++i) { const ItemStack& stack = hotbar.slot(i); payload += blockName(stack.block); payload += ","; payload += std::to_string(stack.count); if (i + 1 < hotbarSize) payload += ";"; } pushHotbar(hotbar.selectedIndex(), payload.c_str()); } void editBlock(int gx, int gy, int gz, Block block) { world.setBlock(gx, gy, gz, block); haveLightCache = false; static const int offsets[7][3] = {{0, 0, 0}, {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}}; for (auto& o : offsets) { Vec3i coord{floorDiv(gx + o[0], chunkSize), floorDiv(gy + o[1], chunkSize), floorDiv(gz + o[2], chunkSize)}; world.ensureChunk(coord); enqueueMesh(coord); } } void interact(bool destroy) { Vec3 eye = eyePosition(player); Vec3 dir = directionFromYawPitch(player.yaw, player.pitch); RaycastHit hit = raycastVoxels(world, eye, dir, 8.0f); if (!hit.hit) return; if (destroy) { Block broken = world.blockAt(hit.block.x, hit.block.y, hit.block.z); editBlock(hit.block.x, hit.block.y, hit.block.z, Block::Air); hotbar.add(broken, 1); refreshHud(); } else { if (hotbar.slot(hotbar.selectedIndex()).empty()) return; Vec3i target = hit.block + hit.normal; Aabb box = playerBox(player); Aabb cell{{(float)target.x, (float)target.y, (float)target.z}, {(float)target.x + 1.0f, (float)target.y + 1.0f, (float)target.z + 1.0f}}; bool overlaps = box.min.x < cell.max.x && box.max.x > cell.min.x && box.min.y < cell.max.y && box.max.y > cell.min.y && box.min.z < cell.max.z && box.max.z > cell.min.z; if (!overlaps) { editBlock(target.x, target.y, target.z, hotbar.selectedBlock()); hotbar.consumeSelected(); refreshHud(); } } } EM_JS(void, storeWorld, (const char* text), { try { localStorage.setItem('voxelforgeRegions', UTF8ToString(text)); } catch (e) {} }); EM_JS(char*, readWorld, (), { var text = String.fromCharCode(); try { var stored = localStorage.getItem('voxelforgeRegions'); if (stored) text = stored; } catch (e) {} var size = lengthBytesUTF8(text) + 1; var ptr = _malloc(size); stringToUTF8(text, ptr, size); return ptr; }); void saveWorld() { std::vector bytes = serializeRegions(world); storeWorld(base64Encode(bytes).c_str()); std::printf("world saved (%zu region bytes)\n", bytes.size()); } void loadWorld() { char* text = readWorld(); std::string encoded(text); free(text); if (encoded.empty()) return; std::vector bytes = base64Decode(encoded); if (!deserializeRegions(bytes.data(), bytes.size(), world)) return; for (auto& entry : gpuChunks) { freeGpuMesh(entry.second.opaque); freeGpuMesh(entry.second.transparent); } gpuChunks.clear(); meshQueue.clear(); haveLightCache = false; for (const auto& entry : world.allChunks()) enqueueMesh(entry.first); std::printf("world loaded\n"); } EM_BOOL onKey(int type, const EmscriptenKeyboardEvent* e, void*) { bool down = (type == EMSCRIPTEN_EVENT_KEYDOWN); std::string code = e->code; if (code == "KeyW") keys.forward = down; else if (code == "KeyS") keys.back = down; else if (code == "KeyA") keys.left = down; else if (code == "KeyD") keys.right = down; else if (code == "Space") keys.jump = down; else if (code == "ShiftLeft") keys.sink = down; else if (down && code == "KeyF") player.flying = !player.flying; else if (down && code == "KeyK") saveWorld(); else if (down && code == "KeyL") loadWorld(); else if (down && code.rfind("Digit", 0) == 0) { int digit = code[5] - '1'; if (digit >= 0 && digit < hotbarSize) { hotbar.select(digit); refreshHud(); } } else return EM_FALSE; return EM_TRUE; } EM_BOOL onWheel(int, const EmscriptenWheelEvent* e, void*) { hotbar.scroll(e->deltaY > 0 ? 1 : -1); refreshHud(); return EM_TRUE; } EM_BOOL onMouseMove(int, const EmscriptenMouseEvent* e, void*) { EmscriptenPointerlockChangeEvent lock; if (emscripten_get_pointerlock_status(&lock) == EMSCRIPTEN_RESULT_SUCCESS && !lock.isActive) return EM_FALSE; const float sensitivity = 0.0022f; player.yaw += e->movementX * sensitivity; player.pitch -= e->movementY * sensitivity; const float limit = 1.5533f; if (player.pitch > limit) player.pitch = limit; if (player.pitch < -limit) player.pitch = -limit; return EM_TRUE; } EM_BOOL onMouseDown(int, const EmscriptenMouseEvent* e, void*) { EmscriptenPointerlockChangeEvent lock; if (emscripten_get_pointerlock_status(&lock) != EMSCRIPTEN_RESULT_SUCCESS || !lock.isActive) { emscripten_request_pointerlock("#canvas", EM_TRUE); return EM_TRUE; } if (e->button == 0) interact(true); else if (e->button == 2) interact(false); return EM_TRUE; } void updateStreaming() { ColumnCoord centre = playerColumn(); if (!(centre == lastCentre)) { lastCentre = centre; StreamDelta delta = streamer.update(centre); for (ColumnCoord column : delta.load) genQueue.push_back(column); for (ColumnCoord column : delta.unload) unloadColumn(column); } int genBudget = 2; while (genBudget-- > 0 && !genQueue.empty()) { ColumnCoord column = genQueue.front(); genQueue.pop_front(); if (streamer.isLoaded(column)) generateColumnNow(column); } int meshBudget = 6; while (meshBudget-- > 0 && !meshQueue.empty()) { Vec3i coord = meshQueue.front(); meshQueue.pop_front(); if (world.chunkAt(coord)) meshChunk(coord); } } Mat4 currentViewProj() { Vec3 eye = eyePosition(player); Vec3 forward = directionFromYawPitch(player.yaw, player.pitch); Mat4 view = lookAt(eye, eye + forward, {0, 1, 0}); float aspect = static_cast(canvasWidth) / static_cast(canvasHeight); Mat4 proj = perspective(1.2217f, aspect, 0.1f, 480.0f); return proj * view; } void frame() { double now = emscripten_get_now(); float dt = static_cast((now - lastTime) / 1000.0); lastTime = now; if (dt > 0.1f) dt = 0.1f; MoveInput input = sampleInput(); int substeps = 4; float sub = dt / substeps; for (int i = 0; i < substeps; ++i) stepPlayer(world, player, input, physics, sub); updateStreaming(); Mat4 viewProj = currentViewProj(); Frustum frustum = Frustum::fromMatrix(viewProj); Vec3 eye = eyePosition(player); glViewport(0, 0, canvasWidth, canvasHeight); glClearColor(0.55f, 0.71f, 0.92f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); glDepthMask(GL_TRUE); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); glFrontFace(GL_CCW); glDisable(GL_BLEND); glUseProgram(program); glUniformMatrix4fv(uViewProj, 1, GL_FALSE, viewProj.m); glUniform3f(uLightDir, 0.45f, 0.85f, 0.28f); glUniform3f(uFogColor, 0.55f, 0.71f, 0.92f); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D_ARRAY, atlasTexture); glUniform1i(uAtlas, 0); std::vector visibleTransparent; for (const auto& entry : gpuChunks) { const GpuChunk& gpu = entry.second; if (!frustum.intersectsAabb(gpu.bounds)) continue; if (gpu.opaque.indexCount > 0) { glBindVertexArray(gpu.opaque.vao); glDrawElements(GL_TRIANGLES, gpu.opaque.indexCount, GL_UNSIGNED_INT, nullptr); } if (gpu.transparent.indexCount > 0) visibleTransparent.push_back(&gpu); } std::sort(visibleTransparent.begin(), visibleTransparent.end(), [&](const GpuChunk* a, const GpuChunk* b) { Vec3 da = a->center - eye; Vec3 db = b->center - eye; return dot(da, da) > dot(db, db); // far first }); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDepthMask(GL_FALSE); glDisable(GL_CULL_FACE); for (const GpuChunk* gpu : visibleTransparent) { glBindVertexArray(gpu->transparent.vao); glDrawElements(GL_TRIANGLES, gpu->transparent.indexCount, GL_UNSIGNED_INT, nullptr); } glDepthMask(GL_TRUE); glDisable(GL_BLEND); } EM_BOOL onResize(int, const EmscriptenUiEvent*, void*) { double w, h; emscripten_get_element_css_size("#canvas", &w, &h); canvasWidth = static_cast(w); canvasHeight = static_cast(h); emscripten_set_canvas_element_size("#canvas", canvasWidth, canvasHeight); return EM_TRUE; } } // namespace int main() { EmscriptenWebGLContextAttributes attrs; emscripten_webgl_init_context_attributes(&attrs); attrs.majorVersion = 2; attrs.minorVersion = 0; attrs.depth = EM_TRUE; attrs.antialias = EM_TRUE; EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context("#canvas", &attrs); emscripten_webgl_make_context_current(ctx); onResize(0, nullptr, nullptr); buildProgram(); uploadAtlas(); ColumnCoord spawnColumn{0, 0}; StreamDelta initial = streamer.update(spawnColumn); for (int dx = -1; dx <= 1; ++dx) for (int dz = -1; dz <= 1; ++dz) worldGen.generateColumn(world, spawnColumn.x + dx, spawnColumn.z + dz); for (ColumnCoord column : initial.load) genQueue.push_back(column); lastCentre = spawnColumn; int spawnX = 8, spawnZ = 8; int spawnY = worldGen.surfaceHeight(spawnX, spawnZ) + 3; player.position = {static_cast(spawnX) + 0.5f, static_cast(spawnY), static_cast(spawnZ) + 0.5f}; refreshHud(); emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, onKey); emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, onKey); emscripten_set_wheel_callback("#canvas", nullptr, EM_TRUE, onWheel); emscripten_set_mousemove_callback("#canvas", nullptr, EM_TRUE, onMouseMove); emscripten_set_mousedown_callback("#canvas", nullptr, EM_TRUE, onMouseDown); emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, onResize); lastTime = emscripten_get_now(); emscripten_set_main_loop(frame, 0, EM_TRUE); return 0; }