feat(render): add WebGL2 renderer and game loop entry point
This commit is contained in:
1 parent
5630eaa313
commit
0356a942f5
1 file changed
+561
+561
@@ -0,0 +1,561 @@
|
||||
#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;
|
||||
}
|
||||
Reference in new issue
Block a user