Files
voxelforge/render/main.cpp
T

562 lines
19 KiB
C++

#include <emscripten.h>
#include <emscripten/html5.h>
#include <GLES3/gl3.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <deque>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#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<ColumnCoord> genQueue;
std::deque<Vec3i> meshQueue;
std::unordered_map<Vec3i, GpuChunk, Vec3iHash> 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<GLsizei>(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<float>(coord.x * chunkSize);
float oy = static_cast<float>(coord.y * chunkSize);
float oz = static_cast<float>(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<int>(std::floor(player.position.x)), chunkSize),
floorDiv(static_cast<int>(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<uint8_t> 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<uint8_t> 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<float>(canvasWidth) / static_cast<float>(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<float>((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<const GpuChunk*> 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<int>(w);
canvasHeight = static_cast<int>(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<float>(spawnX) + 0.5f, static_cast<float>(spawnY),
static_cast<float>(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;
}