better lighting

This commit is contained in:
asrael 2026-01-31 09:31:17 -06:00
parent 805a2713a3
commit 6ed4e17065
75 changed files with 6417 additions and 3667 deletions

View file

@ -7,11 +7,11 @@
#define MINIZ_NO_TIME
#define MINIZ_NO_ARCHIVE_APIS
#define MINIZ_NO_ARCHIVE_WRITING_APIS
#define MINIZ_NO_DEFLATE_APIS
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
#include <miniz.h>
#include "pxl8_color.h"
#include "pxl8_io.h"
#include "pxl8_log.h"
#include "pxl8_mem.h"
@ -635,3 +635,302 @@ void pxl8_ase_destroy(pxl8_ase_file* ase_file) {
memset(ase_file, 0, sizeof(pxl8_ase_file));
}
pxl8_result pxl8_ase_load_palette(const char* filepath, u32* colors, u32* count) {
if (!filepath || !colors || !count) {
return PXL8_ERROR_NULL_POINTER;
}
pxl8_ase_file ase;
pxl8_result result = pxl8_ase_load(filepath, &ase);
if (result != PXL8_OK) {
return result;
}
u32 n = ase.palette.entry_count;
if (n > 256) n = 256;
for (u32 i = 0; i < n; i++) {
colors[i] = ase.palette.colors[i];
}
*count = n;
pxl8_ase_destroy(&ase);
return PXL8_OK;
}
pxl8_result pxl8_ase_remap(const char* input_path, const char* output_path, const pxl8_ase_remap_config* config) {
if (!input_path || !output_path || !config) {
return PXL8_ERROR_NULL_POINTER;
}
if (!config->palette || config->palette_count == 0) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
f32 hue_tol = config->hue_tolerance > 0.0f ? config->hue_tolerance : 0.08f;
u8* file_data;
usize file_size;
pxl8_result result = pxl8_io_read_binary_file(input_path, &file_data, &file_size);
if (result != PXL8_OK) {
return result;
}
if (file_size < 128) {
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_ASE_TRUNCATED_FILE;
}
u8* output_data = (u8*)pxl8_malloc(file_size + 65536);
if (!output_data) {
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_OUT_OF_MEMORY;
}
memcpy(output_data, file_data, file_size);
pxl8_stream stream = pxl8_stream_create(file_data, (u32)file_size);
pxl8_stream_seek(&stream, 128);
u32 frame_start = 128;
u32 frame_size = pxl8_read_u32(&stream);
pxl8_skip_bytes(&stream, 2);
u16 old_chunks = pxl8_read_u16(&stream);
pxl8_skip_bytes(&stream, 2);
u16 num_chunks = old_chunks;
if (old_chunks == 0xFFFF || old_chunks == 0xFFFE) {
num_chunks = (u16)pxl8_read_u32(&stream);
} else {
pxl8_skip_bytes(&stream, 4);
}
u32 palette_chunk_offset = 0;
u32 palette_entry_start = 0;
u32 orig_colors[256] = {0};
u32 chunk_offset = frame_start + 16;
for (u16 c = 0; c < num_chunks; c++) {
pxl8_stream_seek(&stream, chunk_offset);
u32 chunk_size = pxl8_read_u32(&stream);
u16 chunk_type = pxl8_read_u16(&stream);
if (chunk_type == PXL8_ASE_CHUNK_PALETTE) {
palette_chunk_offset = chunk_offset;
pxl8_skip_bytes(&stream, 4);
u32 first_color = pxl8_read_u32(&stream);
u32 last_color = pxl8_read_u32(&stream);
pxl8_skip_bytes(&stream, 8);
palette_entry_start = pxl8_stream_position(&stream);
for (u32 i = first_color; i <= last_color && i < 256; i++) {
u16 flags = pxl8_read_u16(&stream);
u8 r = pxl8_read_u8(&stream);
u8 g = pxl8_read_u8(&stream);
u8 b = pxl8_read_u8(&stream);
pxl8_skip_bytes(&stream, 1);
orig_colors[i] = r | (g << 8) | (b << 16);
if (flags & 1) {
u16 name_len = pxl8_read_u16(&stream);
pxl8_skip_bytes(&stream, name_len);
}
}
}
chunk_offset += chunk_size;
}
if (palette_entry_start == 0) {
pxl8_free(output_data);
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
}
u8 remap[256];
bool used[256] = {0};
for (u32 i = 0; i < 256; i++) {
u32 src = orig_colors[i];
f32 src_hue = pxl8_color_hue(src);
f32 src_sat = pxl8_color_saturation(src);
f32 src_lum = pxl8_color_luminance(src);
u32 best_idx = 0;
f32 best_score = 999999.0f;
for (u32 j = 0; j < config->palette_count; j++) {
u32 tgt = config->palette[j];
f32 tgt_hue = pxl8_color_hue(tgt);
f32 tgt_sat = pxl8_color_saturation(tgt);
f32 tgt_lum = pxl8_color_luminance(tgt);
f32 hue_diff = pxl8_color_hue_diff(src_hue, tgt_hue);
f32 lum_diff = src_lum > tgt_lum ? src_lum - tgt_lum : tgt_lum - src_lum;
f32 sat_diff = src_sat > tgt_sat ? src_sat - tgt_sat : tgt_sat - src_sat;
f32 score;
if (src_sat < 0.1f) {
score = lum_diff + sat_diff * 100.0f;
} else if (hue_diff <= hue_tol) {
score = lum_diff + sat_diff * 50.0f;
} else {
score = hue_diff * 1000.0f + lum_diff;
}
if (score < best_score) {
best_score = score;
best_idx = j;
}
}
remap[i] = (u8)best_idx;
used[best_idx] = true;
}
u8 compact[256];
u32 compact_colors[256];
u32 compact_count = 0;
for (u32 i = 0; i < config->palette_count; i++) {
if (used[i]) {
compact[i] = (u8)compact_count;
compact_colors[compact_count] = config->palette[i];
compact_count++;
}
}
for (u32 i = 0; i < 256; i++) {
remap[i] = compact[remap[i]];
}
for (u32 i = 0; i < compact_count; i++) {
u32 offset = palette_entry_start + i * 6;
u32 color = compact_colors[i];
output_data[offset + 0] = 0;
output_data[offset + 1] = 0;
output_data[offset + 2] = color & 0xFF;
output_data[offset + 3] = (color >> 8) & 0xFF;
output_data[offset + 4] = (color >> 16) & 0xFF;
output_data[offset + 5] = 0xFF;
}
u32 new_last_color = compact_count > 0 ? compact_count - 1 : 0;
output_data[palette_chunk_offset + 6] = compact_count & 0xFF;
output_data[palette_chunk_offset + 7] = (compact_count >> 8) & 0xFF;
output_data[palette_chunk_offset + 8] = (compact_count >> 16) & 0xFF;
output_data[palette_chunk_offset + 9] = (compact_count >> 24) & 0xFF;
output_data[palette_chunk_offset + 14] = new_last_color & 0xFF;
output_data[palette_chunk_offset + 15] = (new_last_color >> 8) & 0xFF;
output_data[palette_chunk_offset + 16] = (new_last_color >> 16) & 0xFF;
output_data[palette_chunk_offset + 17] = (new_last_color >> 24) & 0xFF;
chunk_offset = frame_start + 16;
usize output_size = file_size;
for (u16 c = 0; c < num_chunks; c++) {
pxl8_stream_seek(&stream, chunk_offset);
u32 chunk_size = pxl8_read_u32(&stream);
u16 chunk_type = pxl8_read_u16(&stream);
if (chunk_type == PXL8_ASE_CHUNK_CEL) {
pxl8_skip_bytes(&stream, 7);
u16 cel_type = pxl8_read_u16(&stream);
if (cel_type == 2) {
pxl8_skip_bytes(&stream, 7);
u16 width = pxl8_read_u16(&stream);
u16 height = pxl8_read_u16(&stream);
u32 pixels_size = width * height;
u32 compressed_start = pxl8_stream_position(&stream);
u32 compressed_size = chunk_size - (compressed_start - chunk_offset);
u8* pixels = (u8*)pxl8_malloc(pixels_size);
if (!pixels) {
pxl8_free(output_data);
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_OUT_OF_MEMORY;
}
const u8* compressed_data = file_data + compressed_start;
mz_ulong dest_len = pixels_size;
i32 mz_result = mz_uncompress(pixels, &dest_len, compressed_data, compressed_size);
if (mz_result != MZ_OK) {
pxl8_free(pixels);
pxl8_free(output_data);
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
}
for (u32 i = 0; i < pixels_size; i++) {
pixels[i] = remap[pixels[i]];
}
mz_ulong new_compressed_size = mz_compressBound(pixels_size);
u8* new_compressed = (u8*)pxl8_malloc(new_compressed_size);
if (!new_compressed) {
pxl8_free(pixels);
pxl8_free(output_data);
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_OUT_OF_MEMORY;
}
mz_result = mz_compress2(new_compressed, &new_compressed_size, pixels, pixels_size, 6);
pxl8_free(pixels);
if (mz_result != MZ_OK) {
pxl8_free(new_compressed);
pxl8_free(output_data);
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
}
i32 size_diff = (i32)new_compressed_size - (i32)compressed_size;
u8* new_output = (u8*)pxl8_malloc(output_size + size_diff + 65536);
if (!new_output) {
pxl8_free(new_compressed);
pxl8_free(output_data);
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_OUT_OF_MEMORY;
}
memcpy(new_output, output_data, compressed_start);
memcpy(new_output + compressed_start, new_compressed, new_compressed_size);
memcpy(new_output + compressed_start + new_compressed_size,
output_data + compressed_start + compressed_size,
output_size - compressed_start - compressed_size);
u32 new_chunk_size = chunk_size + size_diff;
new_output[chunk_offset + 0] = new_chunk_size & 0xFF;
new_output[chunk_offset + 1] = (new_chunk_size >> 8) & 0xFF;
new_output[chunk_offset + 2] = (new_chunk_size >> 16) & 0xFF;
new_output[chunk_offset + 3] = (new_chunk_size >> 24) & 0xFF;
u32 new_frame_size = frame_size + size_diff;
new_output[frame_start + 0] = new_frame_size & 0xFF;
new_output[frame_start + 1] = (new_frame_size >> 8) & 0xFF;
new_output[frame_start + 2] = (new_frame_size >> 16) & 0xFF;
new_output[frame_start + 3] = (new_frame_size >> 24) & 0xFF;
output_size += size_diff;
new_output[0] = output_size & 0xFF;
new_output[1] = (output_size >> 8) & 0xFF;
new_output[2] = (output_size >> 16) & 0xFF;
new_output[3] = (output_size >> 24) & 0xFF;
pxl8_free(new_compressed);
pxl8_free(output_data);
output_data = new_output;
break;
}
}
chunk_offset += chunk_size;
}
pxl8_io_free_binary_data(file_data);
result = pxl8_io_write_binary_file(output_path, output_data, output_size);
pxl8_free(output_data);
if (result == PXL8_OK) {
pxl8_info("Remapped %s -> %s", input_path, output_path);
}
return result;
}

View file

@ -141,12 +141,20 @@ typedef struct pxl8_ase_file {
pxl8_ase_tileset* tilesets;
} pxl8_ase_file;
typedef struct pxl8_ase_remap_config {
const u32* palette;
u32 palette_count;
f32 hue_tolerance;
} pxl8_ase_remap_config;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file);
pxl8_result pxl8_ase_load_palette(const char* filepath, u32* colors, u32* count);
void pxl8_ase_destroy(pxl8_ase_file* ase_file);
pxl8_result pxl8_ase_remap(const char* input_path, const char* output_path, const pxl8_ase_remap_config* config);
#ifdef __cplusplus
}

View file

@ -1,11 +1,8 @@
#include "pxl8_bsp.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pxl8_color.h"
#include "pxl8_gfx.h"
#include "pxl8_io.h"
#include "pxl8_log.h"
#include "pxl8_mem.h"
@ -41,15 +38,6 @@ typedef struct {
pxl8_bsp_chunk chunks[CHUNK_COUNT];
} pxl8_bsp_header;
typedef struct {
f32 x0, y0, x1, y1;
} screen_rect;
typedef struct {
u32 leaf;
screen_rect window;
} portal_queue_entry;
static inline pxl8_vec3 read_vec3(pxl8_stream* stream) {
f32 x = pxl8_read_f32(stream);
f32 y = pxl8_read_f32(stream);
@ -167,25 +155,6 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
}
}
chunk = &header.chunks[CHUNK_TEXINFO];
if (!validate_chunk(chunk, 40, file_size)) goto error_cleanup;
bsp->num_materials = chunk->size / 40;
if (bsp->num_materials > 0) {
bsp->materials = pxl8_calloc(bsp->num_materials, sizeof(pxl8_gfx_material));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_materials; i++) {
bsp->materials[i].u_axis = read_vec3(&stream);
bsp->materials[i].u_offset = pxl8_read_f32(&stream);
bsp->materials[i].v_axis = read_vec3(&stream);
bsp->materials[i].v_offset = pxl8_read_f32(&stream);
bsp->materials[i].texture_id = pxl8_read_u32(&stream);
bsp->materials[i].alpha = 255;
bsp->materials[i].dither = true;
bsp->materials[i].dynamic_lighting = true;
bsp->materials[i].double_sided = true;
}
}
chunk = &header.chunks[CHUNK_FACES];
if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup;
bsp->num_faces = chunk->size / 20;
@ -364,12 +333,11 @@ void pxl8_bsp_destroy(pxl8_bsp* bsp) {
pxl8_free(bsp->faces);
pxl8_free(bsp->leafs);
pxl8_free(bsp->lightdata);
pxl8_free(bsp->lightmaps);
pxl8_free(bsp->marksurfaces);
pxl8_free(bsp->materials);
pxl8_free(bsp->models);
pxl8_free(bsp->nodes);
pxl8_free(bsp->planes);
pxl8_free(bsp->render_face_flags);
pxl8_free(bsp->surfedges);
pxl8_free(bsp->vertex_lights);
pxl8_free(bsp->vertices);
@ -394,47 +362,6 @@ i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos) {
return -(node_id + 1);
}
bool pxl8_bsp_point_solid(const pxl8_bsp* bsp, pxl8_vec3 pos) {
i32 leaf = pxl8_bsp_find_leaf(bsp, pos);
if (leaf < 0 || (u32)leaf >= bsp->num_leafs) return true;
return bsp->leafs[leaf].contents == -1;
}
static bool point_clear(const pxl8_bsp* bsp, f32 x, f32 y, f32 z, f32 radius) {
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x, y, z})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x + radius, y, z})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x - radius, y, z})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x, y, z + radius})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x, y, z - radius})) return false;
return true;
}
pxl8_vec3 pxl8_bsp_trace(const pxl8_bsp* bsp, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
if (!bsp || bsp->num_nodes == 0) return to;
if (point_clear(bsp, to.x, to.y, to.z, radius)) {
return to;
}
bool x_ok = point_clear(bsp, to.x, from.y, from.z, radius);
bool z_ok = point_clear(bsp, from.x, from.y, to.z, radius);
if (x_ok && z_ok) {
f32 dx = to.x - from.x;
f32 dz = to.z - from.z;
if (dx * dx > dz * dz) {
return (pxl8_vec3){to.x, from.y, from.z};
} else {
return (pxl8_vec3){from.x, from.y, to.z};
}
} else if (x_ok) {
return (pxl8_vec3){to.x, from.y, from.z};
} else if (z_ok) {
return (pxl8_vec3){from.x, from.y, to.z};
}
return from;
}
bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to) {
if (!bsp || !bsp->visdata || bsp->visdata_size == 0) return true;
@ -613,320 +540,6 @@ pxl8_bsp_lightmap_sample pxl8_bsp_sample_lightmap(const pxl8_bsp* bsp, u32 face_
return (pxl8_bsp_lightmap_sample){b, g, r};
}
static inline bool face_in_frustum(const pxl8_bsp* bsp, u32 face_id, const pxl8_frustum* frustum) {
const pxl8_bsp_face* face = &bsp->faces[face_id];
return pxl8_frustum_test_aabb(frustum, face->aabb_min, face->aabb_max);
}
static inline bool screen_rect_valid(screen_rect r) {
return r.x0 < r.x1 && r.y0 < r.y1;
}
static inline screen_rect screen_rect_intersect(screen_rect a, screen_rect b) {
return (screen_rect){
.x0 = (a.x0 > b.x0) ? a.x0 : b.x0,
.y0 = (a.y0 > b.y0) ? a.y0 : b.y0,
.x1 = (a.x1 < b.x1) ? a.x1 : b.x1,
.y1 = (a.y1 < b.y1) ? a.y1 : b.y1,
};
}
static inline void expand_rect_with_ndc(screen_rect* r, f32 nx, f32 ny) {
if (nx < r->x0) r->x0 = nx;
if (nx > r->x1) r->x1 = nx;
if (ny < r->y0) r->y0 = ny;
if (ny > r->y1) r->y1 = ny;
}
static screen_rect project_portal_to_screen(const pxl8_bsp_portal* portal, const pxl8_mat4* vp, f32 wall_height) {
pxl8_vec3 world_corners[4] = {
{portal->x0, 0, portal->z0},
{portal->x1, 0, portal->z1},
{portal->x1, wall_height, portal->z1},
{portal->x0, wall_height, portal->z0},
};
pxl8_vec4 clip[4];
bool in_front[4];
i32 front_count = 0;
const f32 NEAR_W = 0.001f;
for (i32 i = 0; i < 4; i++) {
clip[i] = pxl8_mat4_multiply_vec4(*vp, (pxl8_vec4){world_corners[i].x, world_corners[i].y, world_corners[i].z, 1.0f});
in_front[i] = clip[i].w > NEAR_W;
if (in_front[i]) front_count++;
}
if (front_count == 0) return (screen_rect){0, 0, 0, 0};
screen_rect result = {1e30f, 1e30f, -1e30f, -1e30f};
for (i32 i = 0; i < 4; i++) {
i32 j = (i + 1) % 4;
pxl8_vec4 a = clip[i];
pxl8_vec4 b = clip[j];
bool a_in = in_front[i];
bool b_in = in_front[j];
if (a_in) {
f32 inv_w = 1.0f / a.w;
expand_rect_with_ndc(&result, a.x * inv_w, a.y * inv_w);
}
if (a_in != b_in) {
f32 t = (NEAR_W - a.w) / (b.w - a.w);
pxl8_vec4 intersection = {
a.x + t * (b.x - a.x),
a.y + t * (b.y - a.y),
a.z + t * (b.z - a.z),
NEAR_W
};
f32 inv_w = 1.0f / intersection.w;
expand_rect_with_ndc(&result, intersection.x * inv_w, intersection.y * inv_w);
}
}
if (result.x0 < -1.0f) result.x0 = -1.0f;
if (result.y0 < -1.0f) result.y0 = -1.0f;
if (result.x1 > 1.0f) result.x1 = 1.0f;
if (result.y1 > 1.0f) result.y1 = 1.0f;
return result;
}
static void collect_face_to_mesh(
const pxl8_bsp* bsp,
u32 face_id,
pxl8_mesh* mesh
) {
const pxl8_bsp_face* face = &bsp->faces[face_id];
if (face->num_edges < 3) return;
pxl8_vec3 normal = {0, 1, 0};
if (face->plane_id < bsp->num_planes) {
normal = bsp->planes[face->plane_id].normal;
if (face->side) {
normal.x = -normal.x;
normal.y = -normal.y;
normal.z = -normal.z;
}
}
const pxl8_gfx_material* material = NULL;
f32 tex_scale = 64.0f;
if (face->material_id < bsp->num_materials) {
material = &bsp->materials[face->material_id];
}
u16 base_idx = (u16)mesh->vertex_count;
u32 num_verts = 0;
for (u32 i = 0; i < face->num_edges && num_verts < 64; i++) {
u32 edge_i = face->side ? (face->num_edges - 1 - i) : i;
i32 surfedge_idx = face->first_edge + edge_i;
u32 vert_idx;
if (!pxl8_bsp_get_edge_vertex(bsp, surfedge_idx, &vert_idx)) {
continue;
}
pxl8_vec3 pos = bsp->vertices[vert_idx].position;
f32 u = 0.0f, v = 0.0f;
if (material) {
u = (pxl8_vec3_dot(pos, material->u_axis) + material->u_offset) / tex_scale;
v = (pxl8_vec3_dot(pos, material->v_axis) + material->v_offset) / tex_scale;
}
u8 light = 255;
if (bsp->vertex_lights && vert_idx < bsp->num_vertex_lights) {
light = (bsp->vertex_lights[vert_idx] >> 24) & 0xFF;
}
pxl8_vertex vtx = {
.position = pos,
.normal = normal,
.u = u,
.v = v,
.color = 15,
.light = light,
};
pxl8_mesh_push_vertex(mesh, vtx);
num_verts++;
}
if (num_verts < 3) return;
for (u32 i = 1; i < num_verts - 1; i++) {
pxl8_mesh_push_triangle(mesh, base_idx, base_idx + i, base_idx + i + 1);
}
}
void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, const pxl8_gfx_material* material) {
if (!gfx || !bsp || face_id >= bsp->num_faces || !material) return;
pxl8_mesh* mesh = pxl8_mesh_create(64, 192);
if (!mesh) return;
collect_face_to_mesh(bsp, face_id, mesh);
if (mesh->index_count > 0) {
pxl8_mat4 identity = pxl8_mat4_identity();
pxl8_3d_draw_mesh(gfx, mesh, &identity, material);
}
pxl8_mesh_destroy(mesh);
}
void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos) {
if (!gfx || !bsp || bsp->num_faces == 0) {
return;
}
if (!bsp->cell_portals || bsp->num_cell_portals == 0) {
pxl8_debug("[BSP] render: no cell_portals (ptr=%p count=%u)", (void*)bsp->cell_portals, bsp->num_cell_portals);
return;
}
if (!bsp->materials || bsp->num_materials == 0) {
pxl8_debug("[BSP] render: no materials (ptr=%p count=%u)", (void*)bsp->materials, bsp->num_materials);
return;
}
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
const pxl8_mat4* vp = pxl8_3d_get_view_proj(gfx);
if (!frustum || !vp) return;
i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
if (camera_leaf < 0 || (u32)camera_leaf >= bsp->num_leafs) return;
pxl8_bsp* bsp_mut = (pxl8_bsp*)bsp;
if (!bsp_mut->render_face_flags) {
bsp_mut->render_face_flags = pxl8_calloc(bsp->num_faces, 1);
if (!bsp_mut->render_face_flags) return;
}
memset(bsp_mut->render_face_flags, 0, bsp->num_faces);
pxl8_bsp_pvs pvs = pxl8_bsp_decompress_pvs(bsp, camera_leaf);
u32 visited_bytes = (bsp->num_leafs + 7) / 8;
u8* visited = pxl8_calloc(visited_bytes, 1);
screen_rect* cell_windows = pxl8_calloc(bsp->num_leafs, sizeof(screen_rect));
portal_queue_entry* queue = pxl8_malloc(bsp->num_leafs * 4 * sizeof(portal_queue_entry));
if (!visited || !cell_windows || !queue) {
pxl8_free(visited);
pxl8_free(cell_windows);
pxl8_free(queue);
pxl8_bsp_pvs_destroy(&pvs);
return;
}
u32 head = 0, tail = 0;
screen_rect full_screen = {-1.0f, -1.0f, 1.0f, 1.0f};
visited[camera_leaf >> 3] |= (1 << (camera_leaf & 7));
cell_windows[camera_leaf] = full_screen;
queue[tail++] = (portal_queue_entry){camera_leaf, full_screen};
f32 wall_height = 128.0f;
while (head < tail) {
portal_queue_entry entry = queue[head++];
u32 leaf_id = entry.leaf;
screen_rect window = entry.window;
if (leaf_id >= bsp->num_cell_portals) continue;
const pxl8_bsp_cell_portals* cp = &bsp->cell_portals[leaf_id];
for (u8 i = 0; i < cp->num_portals; i++) {
const pxl8_bsp_portal* portal = &cp->portals[i];
u32 target = portal->target_leaf;
if (target >= bsp->num_leafs) continue;
if (bsp->leafs[target].contents == -1) continue;
if (!pxl8_bsp_pvs_is_visible(&pvs, target)) continue;
screen_rect portal_rect = project_portal_to_screen(portal, vp, wall_height);
if (!screen_rect_valid(portal_rect)) continue;
screen_rect new_window = screen_rect_intersect(window, portal_rect);
if (!screen_rect_valid(new_window)) continue;
u32 byte = target >> 3;
u32 bit = target & 7;
if (visited[byte] & (1 << bit)) {
screen_rect existing = cell_windows[target];
bool expanded = false;
if (new_window.x0 < existing.x0) { cell_windows[target].x0 = new_window.x0; expanded = true; }
if (new_window.y0 < existing.y0) { cell_windows[target].y0 = new_window.y0; expanded = true; }
if (new_window.x1 > existing.x1) { cell_windows[target].x1 = new_window.x1; expanded = true; }
if (new_window.y1 > existing.y1) { cell_windows[target].y1 = new_window.y1; expanded = true; }
if (expanded && tail < bsp->num_leafs * 4) {
queue[tail++] = (portal_queue_entry){target, cell_windows[target]};
}
continue;
}
visited[byte] |= (1 << bit);
cell_windows[target] = new_window;
queue[tail++] = (portal_queue_entry){target, new_window};
}
}
pxl8_mesh* mesh = pxl8_mesh_create(8192, 16384);
if (!mesh) {
pxl8_free(visited);
pxl8_free(cell_windows);
pxl8_free(queue);
return;
}
u32 current_material = 0xFFFFFFFF;
for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) {
if (!(visited[leaf_id >> 3] & (1 << (leaf_id & 7)))) continue;
if (bsp->leafs[leaf_id].contents == -1) continue;
const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id];
for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
u32 surf_idx = leaf->first_marksurface + i;
if (surf_idx >= bsp->num_marksurfaces) continue;
u32 face_id = bsp->marksurfaces[surf_idx];
if (face_id >= bsp->num_faces) continue;
if (bsp_mut->render_face_flags[face_id]) continue;
bsp_mut->render_face_flags[face_id] = 1;
if (!face_in_frustum(bsp, face_id, frustum)) continue;
const pxl8_bsp_face* face = &bsp->faces[face_id];
u32 material_id = face->material_id;
if (material_id >= bsp->num_materials) continue;
if (material_id != current_material && mesh->index_count > 0 && current_material < bsp->num_materials) {
pxl8_mat4 identity = pxl8_mat4_identity();
pxl8_3d_draw_mesh(gfx, mesh, &identity, &bsp->materials[current_material]);
pxl8_mesh_clear(mesh);
}
current_material = material_id;
collect_face_to_mesh(bsp, face_id, mesh);
}
}
if (mesh->index_count > 0 && current_material < bsp->num_materials) {
pxl8_mat4 identity = pxl8_mat4_identity();
pxl8_3d_draw_mesh(gfx, mesh, &identity, &bsp->materials[current_material]);
}
pxl8_bsp_pvs_destroy(&pvs);
pxl8_free(visited);
pxl8_free(cell_windows);
pxl8_free(queue);
pxl8_mesh_destroy(mesh);
}
u32 pxl8_bsp_face_count(const pxl8_bsp* bsp) {
if (!bsp) return 0;
return bsp->num_faces;
@ -952,29 +565,3 @@ void pxl8_bsp_face_set_material(pxl8_bsp* bsp, u32 face_id, u16 material_id) {
if (!bsp || face_id >= bsp->num_faces) return;
bsp->faces[face_id].material_id = material_id;
}
void pxl8_bsp_set_material(pxl8_bsp* bsp, u16 material_id, const pxl8_gfx_material* material) {
if (!bsp || !material) return;
if (material_id >= bsp->num_materials) {
u32 new_count = material_id + 1;
pxl8_gfx_material* new_materials = pxl8_realloc(bsp->materials, new_count * sizeof(pxl8_gfx_material));
if (!new_materials) return;
for (u32 i = bsp->num_materials; i < new_count; i++) {
memset(&new_materials[i], 0, sizeof(pxl8_gfx_material));
}
bsp->materials = new_materials;
bsp->num_materials = new_count;
}
bsp->materials[material_id] = *material;
}
void pxl8_bsp_set_wireframe(pxl8_bsp* bsp, bool wireframe) {
if (!bsp || !bsp->materials) return;
for (u32 i = 0; i < bsp->num_materials; i++) {
bsp->materials[i].wireframe = wireframe;
}
}

View file

@ -1,8 +1,6 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_math.h"
#include "pxl8_mesh.h"
#include "pxl8_types.h"
typedef struct pxl8_bsp_edge {
@ -105,11 +103,9 @@ typedef struct pxl8_bsp {
u8* lightdata;
pxl8_bsp_lightmap* lightmaps;
u16* marksurfaces;
pxl8_gfx_material* materials;
pxl8_bsp_model* models;
pxl8_bsp_node* nodes;
pxl8_bsp_plane* planes;
u8* render_face_flags;
i32* surfedges;
u32* vertex_lights;
pxl8_bsp_vertex* vertices;
@ -122,7 +118,6 @@ typedef struct pxl8_bsp {
u32 num_leafs;
u32 num_lightmaps;
u32 num_marksurfaces;
u32 num_materials;
u32 num_models;
u32 num_nodes;
u32 num_planes;
@ -143,18 +138,12 @@ pxl8_vec3 pxl8_bsp_face_normal(const pxl8_bsp* bsp, u32 face_id);
void pxl8_bsp_face_set_material(pxl8_bsp* bsp, u32 face_id, u16 material_id);
i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos);
bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to);
bool pxl8_bsp_point_solid(const pxl8_bsp* bsp, pxl8_vec3 pos);
pxl8_vec3 pxl8_bsp_trace(const pxl8_bsp* bsp, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
pxl8_bsp_lightmap pxl8_bsp_lightmap_mapped(u8 width, u8 height, u32 offset);
pxl8_bsp_lightmap pxl8_bsp_lightmap_uniform(u8 r, u8 g, u8 b);
pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp);
void pxl8_bsp_pvs_destroy(pxl8_bsp_pvs* pvs);
bool pxl8_bsp_pvs_is_visible(const pxl8_bsp_pvs* pvs, i32 leaf);
void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos);
void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, const pxl8_gfx_material* material);
pxl8_bsp_lightmap_sample pxl8_bsp_sample_lightmap(const pxl8_bsp* bsp, u32 face_idx, f32 u, f32 v);
void pxl8_bsp_set_material(pxl8_bsp* bsp, u16 material_id, const pxl8_gfx_material* material);
void pxl8_bsp_set_wireframe(pxl8_bsp* bsp, bool wireframe);
#ifdef __cplusplus
}

429
src/bsp/pxl8_bsp_render.c Normal file
View file

@ -0,0 +1,429 @@
#include "pxl8_bsp_render.h"
#include <string.h>
#include "pxl8_gfx3d.h"
#include "pxl8_log.h"
#include "pxl8_mem.h"
#include "pxl8_mesh.h"
typedef struct {
f32 x0, y0, x1, y1;
} screen_rect;
typedef struct {
u32 leaf;
screen_rect window;
} portal_queue_entry;
static inline bool pxl8_bsp_get_edge_vertex(const pxl8_bsp* bsp, i32 surfedge_idx, u32* out_vert_idx) {
if (surfedge_idx >= (i32)bsp->num_surfedges) return false;
i32 edge_idx = bsp->surfedges[surfedge_idx];
u32 vertex_index;
if (edge_idx >= 0) {
if ((u32)edge_idx >= bsp->num_edges) return false;
vertex_index = 0;
} else {
edge_idx = -edge_idx;
if ((u32)edge_idx >= bsp->num_edges) return false;
vertex_index = 1;
}
*out_vert_idx = bsp->edges[edge_idx].vertex[vertex_index];
return *out_vert_idx < bsp->num_vertices;
}
static inline bool face_in_frustum(const pxl8_bsp* bsp, u32 face_id, const pxl8_frustum* frustum) {
const pxl8_bsp_face* face = &bsp->faces[face_id];
return pxl8_frustum_test_aabb(frustum, face->aabb_min, face->aabb_max);
}
static inline bool screen_rect_valid(screen_rect r) {
return r.x0 < r.x1 && r.y0 < r.y1;
}
static inline screen_rect screen_rect_intersect(screen_rect a, screen_rect b) {
return (screen_rect){
.x0 = (a.x0 > b.x0) ? a.x0 : b.x0,
.y0 = (a.y0 > b.y0) ? a.y0 : b.y0,
.x1 = (a.x1 < b.x1) ? a.x1 : b.x1,
.y1 = (a.y1 < b.y1) ? a.y1 : b.y1,
};
}
static inline void expand_rect_with_ndc(screen_rect* r, f32 nx, f32 ny) {
if (nx < r->x0) r->x0 = nx;
if (nx > r->x1) r->x1 = nx;
if (ny < r->y0) r->y0 = ny;
if (ny > r->y1) r->y1 = ny;
}
static screen_rect project_portal_to_screen(const pxl8_bsp_portal* portal, const pxl8_mat4* vp, f32 wall_height) {
pxl8_vec3 world_corners[4] = {
{portal->x0, 0, portal->z0},
{portal->x1, 0, portal->z1},
{portal->x1, wall_height, portal->z1},
{portal->x0, wall_height, portal->z0},
};
pxl8_vec4 clip[4];
bool in_front[4];
i32 front_count = 0;
const f32 NEAR_W = 0.001f;
for (i32 i = 0; i < 4; i++) {
clip[i] = pxl8_mat4_multiply_vec4(*vp, (pxl8_vec4){world_corners[i].x, world_corners[i].y, world_corners[i].z, 1.0f});
in_front[i] = clip[i].w > NEAR_W;
if (in_front[i]) front_count++;
}
if (front_count == 0) return (screen_rect){0, 0, 0, 0};
screen_rect result = {1e30f, 1e30f, -1e30f, -1e30f};
for (i32 i = 0; i < 4; i++) {
i32 j = (i + 1) % 4;
pxl8_vec4 a = clip[i];
pxl8_vec4 b = clip[j];
bool a_in = in_front[i];
bool b_in = in_front[j];
if (a_in) {
f32 inv_w = 1.0f / a.w;
expand_rect_with_ndc(&result, a.x * inv_w, a.y * inv_w);
}
if (a_in != b_in) {
f32 t = (NEAR_W - a.w) / (b.w - a.w);
pxl8_vec4 intersection = {
a.x + t * (b.x - a.x),
a.y + t * (b.y - a.y),
a.z + t * (b.z - a.z),
NEAR_W
};
f32 inv_w = 1.0f / intersection.w;
expand_rect_with_ndc(&result, intersection.x * inv_w, intersection.y * inv_w);
}
}
if (result.x0 < -1.0f) result.x0 = -1.0f;
if (result.y0 < -1.0f) result.y0 = -1.0f;
if (result.x1 > 1.0f) result.x1 = 1.0f;
if (result.y1 > 1.0f) result.y1 = 1.0f;
return result;
}
static void collect_face_to_mesh(const pxl8_bsp* bsp, const pxl8_bsp_render_state* state,
u32 face_id, pxl8_mesh* mesh) {
const pxl8_bsp_face* face = &bsp->faces[face_id];
if (face->num_edges < 3) return;
pxl8_vec3 normal = {0, 1, 0};
if (face->plane_id < bsp->num_planes) {
normal = bsp->planes[face->plane_id].normal;
if (face->side) {
normal.x = -normal.x;
normal.y = -normal.y;
normal.z = -normal.z;
}
}
const pxl8_gfx_material* material = NULL;
f32 tex_scale = 64.0f;
if (state && face->material_id < state->num_materials) {
material = &state->materials[face->material_id];
}
pxl8_vec3 u_axis = {1.0f, 0.0f, 0.0f};
pxl8_vec3 v_axis = {0.0f, 0.0f, 1.0f};
f32 u_offset = 0.0f, v_offset = 0.0f;
if (material) {
u_offset = material->u_offset;
v_offset = material->v_offset;
}
f32 abs_ny = normal.y < 0 ? -normal.y : normal.y;
if (abs_ny > 0.7f) {
u_axis = (pxl8_vec3){1.0f, 0.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 0.0f, 1.0f};
} else {
f32 abs_nx = normal.x < 0 ? -normal.x : normal.x;
f32 abs_nz = normal.z < 0 ? -normal.z : normal.z;
if (abs_nx > abs_nz) {
u_axis = (pxl8_vec3){0.0f, 0.0f, 1.0f};
} else {
u_axis = (pxl8_vec3){1.0f, 0.0f, 0.0f};
}
v_axis = (pxl8_vec3){0.0f, 1.0f, 0.0f};
}
u16 base_idx = (u16)mesh->vertex_count;
u32 num_verts = 0;
for (u32 i = 0; i < face->num_edges && num_verts < 64; i++) {
u32 edge_i = face->side ? (face->num_edges - 1 - i) : i;
i32 surfedge_idx = face->first_edge + edge_i;
u32 vert_idx;
if (!pxl8_bsp_get_edge_vertex(bsp, surfedge_idx, &vert_idx)) {
continue;
}
pxl8_vec3 pos = bsp->vertices[vert_idx].position;
f32 u = (pxl8_vec3_dot(pos, u_axis) + u_offset) / tex_scale;
f32 v = (pxl8_vec3_dot(pos, v_axis) + v_offset) / tex_scale;
u8 light = 255;
if (bsp->vertex_lights && vert_idx < bsp->num_vertex_lights) {
light = (bsp->vertex_lights[vert_idx] >> 24) & 0xFF;
}
pxl8_vertex vtx = {
.position = pos,
.normal = normal,
.u = u,
.v = v,
.color = 15,
.light = light,
};
pxl8_mesh_push_vertex(mesh, vtx);
num_verts++;
}
if (num_verts < 3) return;
for (u32 i = 1; i < num_verts - 1; i++) {
pxl8_mesh_push_triangle(mesh, base_idx, base_idx + i, base_idx + i + 1);
}
}
pxl8_bsp_render_state* pxl8_bsp_render_state_create(u32 num_faces) {
pxl8_bsp_render_state* state = pxl8_calloc(1, sizeof(pxl8_bsp_render_state));
if (!state) return NULL;
state->num_faces = num_faces;
if (num_faces > 0) {
state->render_face_flags = pxl8_calloc(num_faces, 1);
if (!state->render_face_flags) {
pxl8_free(state);
return NULL;
}
}
return state;
}
void pxl8_bsp_render_state_destroy(pxl8_bsp_render_state* state) {
if (!state) return;
pxl8_free(state->materials);
pxl8_free(state->render_face_flags);
pxl8_free(state);
}
void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, const pxl8_gfx_material* material) {
if (!gfx || !bsp || face_id >= bsp->num_faces || !material) return;
pxl8_mesh* mesh = pxl8_mesh_create(64, 192);
if (!mesh) return;
collect_face_to_mesh(bsp, NULL, face_id, mesh);
if (mesh->index_count > 0) {
pxl8_mat4 identity = pxl8_mat4_identity();
pxl8_3d_draw_mesh(gfx, mesh, &identity, material);
}
pxl8_mesh_destroy(mesh);
}
void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp,
pxl8_bsp_render_state* state, pxl8_vec3 camera_pos) {
if (!gfx || !bsp || !state || bsp->num_faces == 0) {
return;
}
if (!bsp->cell_portals || bsp->num_cell_portals == 0) {
return;
}
if (!state->materials || state->num_materials == 0) {
return;
}
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
const pxl8_mat4* vp = pxl8_3d_get_view_proj(gfx);
if (!frustum || !vp) return;
i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
if (camera_leaf < 0 || (u32)camera_leaf >= bsp->num_leafs) return;
if (!state->render_face_flags && state->num_faces > 0) {
state->render_face_flags = pxl8_calloc(state->num_faces, 1);
if (!state->render_face_flags) return;
}
memset(state->render_face_flags, 0, state->num_faces);
pxl8_bsp_pvs pvs = pxl8_bsp_decompress_pvs(bsp, camera_leaf);
u32 visited_bytes = (bsp->num_leafs + 7) / 8;
u8* visited = pxl8_calloc(visited_bytes, 1);
screen_rect* cell_windows = pxl8_calloc(bsp->num_leafs, sizeof(screen_rect));
portal_queue_entry* queue = pxl8_malloc(bsp->num_leafs * 4 * sizeof(portal_queue_entry));
if (!visited || !cell_windows || !queue) {
pxl8_free(visited);
pxl8_free(cell_windows);
pxl8_free(queue);
pxl8_bsp_pvs_destroy(&pvs);
return;
}
u32 head = 0, tail = 0;
screen_rect full_screen = {-1.0f, -1.0f, 1.0f, 1.0f};
visited[camera_leaf >> 3] |= (1 << (camera_leaf & 7));
cell_windows[camera_leaf] = full_screen;
queue[tail++] = (portal_queue_entry){camera_leaf, full_screen};
f32 wall_height = 128.0f;
while (head < tail) {
portal_queue_entry entry = queue[head++];
u32 leaf_id = entry.leaf;
screen_rect window = entry.window;
if (leaf_id >= bsp->num_cell_portals) continue;
const pxl8_bsp_cell_portals* cp = &bsp->cell_portals[leaf_id];
for (u8 i = 0; i < cp->num_portals; i++) {
const pxl8_bsp_portal* portal = &cp->portals[i];
u32 target = portal->target_leaf;
if (target >= bsp->num_leafs) continue;
if (bsp->leafs[target].contents == -1) continue;
if (!pxl8_bsp_pvs_is_visible(&pvs, target)) continue;
screen_rect portal_rect = project_portal_to_screen(portal, vp, wall_height);
if (!screen_rect_valid(portal_rect)) continue;
screen_rect new_window = screen_rect_intersect(window, portal_rect);
if (!screen_rect_valid(new_window)) continue;
u32 byte = target >> 3;
u32 bit = target & 7;
if (visited[byte] & (1 << bit)) {
screen_rect existing = cell_windows[target];
bool expanded = false;
if (new_window.x0 < existing.x0) { cell_windows[target].x0 = new_window.x0; expanded = true; }
if (new_window.y0 < existing.y0) { cell_windows[target].y0 = new_window.y0; expanded = true; }
if (new_window.x1 > existing.x1) { cell_windows[target].x1 = new_window.x1; expanded = true; }
if (new_window.y1 > existing.y1) { cell_windows[target].y1 = new_window.y1; expanded = true; }
if (expanded && tail < bsp->num_leafs * 4) {
queue[tail++] = (portal_queue_entry){target, cell_windows[target]};
}
continue;
}
visited[byte] |= (1 << bit);
cell_windows[target] = new_window;
queue[tail++] = (portal_queue_entry){target, new_window};
}
}
pxl8_mesh* mesh = pxl8_mesh_create(8192, 16384);
if (!mesh) {
pxl8_free(visited);
pxl8_free(cell_windows);
pxl8_free(queue);
return;
}
u32 current_material = 0xFFFFFFFF;
for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) {
if (!(visited[leaf_id >> 3] & (1 << (leaf_id & 7)))) continue;
if (bsp->leafs[leaf_id].contents == -1) continue;
const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id];
for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
u32 surf_idx = leaf->first_marksurface + i;
if (surf_idx >= bsp->num_marksurfaces) continue;
u32 face_id = bsp->marksurfaces[surf_idx];
if (face_id >= bsp->num_faces) continue;
if (state->render_face_flags[face_id]) continue;
state->render_face_flags[face_id] = 1;
if (!face_in_frustum(bsp, face_id, frustum)) continue;
const pxl8_bsp_face* face = &bsp->faces[face_id];
u32 material_id = face->material_id;
if (material_id >= state->num_materials) continue;
if (material_id != current_material && mesh->index_count > 0 && current_material < state->num_materials) {
pxl8_mat4 identity = pxl8_mat4_identity();
pxl8_3d_draw_mesh(gfx, mesh, &identity, &state->materials[current_material]);
pxl8_mesh_clear(mesh);
}
current_material = material_id;
collect_face_to_mesh(bsp, state, face_id, mesh);
}
}
if (mesh->index_count > 0 && current_material < state->num_materials) {
pxl8_mat4 identity = pxl8_mat4_identity();
pxl8_3d_draw_mesh(gfx, mesh, &identity, &state->materials[current_material]);
}
pxl8_bsp_pvs_destroy(&pvs);
pxl8_free(visited);
pxl8_free(cell_windows);
pxl8_free(queue);
pxl8_mesh_destroy(mesh);
}
void pxl8_bsp_set_material(pxl8_bsp_render_state* state, u16 material_id, const pxl8_gfx_material* material) {
if (!state || !material) return;
if (material_id >= state->num_materials) {
u32 new_count = material_id + 1;
pxl8_gfx_material* new_materials = pxl8_realloc(state->materials, new_count * sizeof(pxl8_gfx_material));
if (!new_materials) return;
for (u32 i = state->num_materials; i < new_count; i++) {
memset(&new_materials[i], 0, sizeof(pxl8_gfx_material));
new_materials[i].u_axis = (pxl8_vec3){1.0f, 0.0f, 0.0f};
if (i == 0 || i == 2) {
new_materials[i].v_axis = (pxl8_vec3){0.0f, 0.0f, 1.0f};
} else {
new_materials[i].v_axis = (pxl8_vec3){0.0f, 1.0f, 0.0f};
}
}
state->materials = new_materials;
state->num_materials = new_count;
}
pxl8_vec3 u_axis = state->materials[material_id].u_axis;
pxl8_vec3 v_axis = state->materials[material_id].v_axis;
f32 u_offset = state->materials[material_id].u_offset;
f32 v_offset = state->materials[material_id].v_offset;
state->materials[material_id] = *material;
state->materials[material_id].u_axis = u_axis;
state->materials[material_id].v_axis = v_axis;
state->materials[material_id].u_offset = u_offset;
state->materials[material_id].v_offset = v_offset;
}
void pxl8_bsp_set_wireframe(pxl8_bsp_render_state* state, bool wireframe) {
if (!state || !state->materials) return;
for (u32 i = 0; i < state->num_materials; i++) {
state->materials[i].wireframe = wireframe;
}
}

30
src/bsp/pxl8_bsp_render.h Normal file
View file

@ -0,0 +1,30 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_gfx.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct pxl8_bsp_render_state {
pxl8_gfx_material* materials;
u8* render_face_flags;
u32 num_materials;
u32 num_faces;
} pxl8_bsp_render_state;
pxl8_bsp_render_state* pxl8_bsp_render_state_create(u32 num_faces);
void pxl8_bsp_render_state_destroy(pxl8_bsp_render_state* state);
void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp,
pxl8_bsp_render_state* state, pxl8_vec3 camera_pos);
void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id,
const pxl8_gfx_material* material);
void pxl8_bsp_set_material(pxl8_bsp_render_state* state, u16 material_id,
const pxl8_gfx_material* material);
void pxl8_bsp_set_wireframe(pxl8_bsp_render_state* state, bool wireframe);
#ifdef __cplusplus
}
#endif

View file

@ -9,6 +9,7 @@
#include <sys/stat.h>
#include <unistd.h>
#include "pxl8_ase.h"
#include "pxl8_game.h"
#include "pxl8_hal.h"
#include "pxl8_log.h"
@ -19,6 +20,7 @@
#include "pxl8_script.h"
#include "pxl8_sfx.h"
#include "pxl8_sys.h"
#include "pxl8_world.h"
struct pxl8 {
pxl8_cart* cart;
@ -83,6 +85,7 @@ static void pxl8_print_help(void) {
printf("Other commands:\n");
printf(" pxl8 pack <folder> <out.pxc> Pack folder into cart file\n");
printf(" pxl8 bundle <in> <out> Bundle cart into executable\n");
printf(" pxl8 remap-ase <in> <out> <palette> Remap ASE to palette by hue/lum\n");
printf(" pxl8 help Show this help\n");
}
@ -93,9 +96,11 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
const char* script_arg = NULL;
bool bundle_mode = false;
bool pack_mode = false;
bool remap_palette_mode = false;
bool run_mode = false;
const char* pack_input = NULL;
const char* pack_output = NULL;
const char* remap_palette = NULL;
bool has_embedded = pxl8_cart_has_embedded(argv[0]);
@ -125,12 +130,22 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
pxl8_error("pack requires <folder> <output.pxc>");
return PXL8_ERROR_INVALID_ARGUMENT;
}
} else if (strcmp(argv[i], "remap-ase") == 0) {
remap_palette_mode = true;
if (i + 3 < argc) {
pack_input = argv[++i];
pack_output = argv[++i];
remap_palette = argv[++i];
} else {
pxl8_error("remap-ase requires <input.ase> <output.ase> <palette.ase>");
return PXL8_ERROR_INVALID_ARGUMENT;
}
} else if (!script_arg) {
script_arg = argv[i];
}
}
if (!run_mode && !bundle_mode && !pack_mode) {
if (!run_mode && !bundle_mode && !pack_mode && !remap_palette_mode) {
run_mode = true;
}
@ -151,6 +166,22 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
return result;
}
if (remap_palette_mode) {
u32 palette[256];
u32 palette_count = 0;
pxl8_result result = pxl8_ase_load_palette(remap_palette, palette, &palette_count);
if (result != PXL8_OK) {
pxl8_error("failed to load palette: %s", remap_palette);
return result;
}
pxl8_ase_remap_config config = {
.palette = palette,
.palette_count = palette_count,
.hue_tolerance = 0.08f
};
return pxl8_ase_remap(pack_input, pack_output, &config);
}
pxl8_info("Starting up");
game->script = pxl8_script_create(game->repl_mode);
@ -246,7 +277,8 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
pxl8_net_config net_cfg = { .address = "127.0.0.1", .port = 7777 };
game->net = pxl8_net_create(&net_cfg);
if (game->net) {
pxl8_net_set_chunk_cache(game->net, pxl8_world_chunk_cache(game->world));
pxl8_net_set_chunk_cache(game->net, pxl8_world_get_chunk_cache(game->world));
pxl8_net_set_world(game->net, game->world);
pxl8_net_connect(game->net);
}
@ -280,6 +312,15 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
game->last_time = sys->hal->get_ticks();
game->running = true;
#ifdef PXL8_ASYNC_THREADS
if (game->net) {
pxl8_net_start_thread(game->net);
}
if (game->world) {
pxl8_world_start_sim_thread(game->world, game->net);
}
#endif
return PXL8_OK;
}
@ -351,13 +392,27 @@ pxl8_result pxl8_update(pxl8* sys) {
}
}
#ifdef PXL8_ASYNC_THREADS
if (game->world && (pxl8_world_local_player(game->world))) {
pxl8_input_msg msg = {0};
msg.move_x = (pxl8_key_down(&game->input, "d") ? 1.0f : 0.0f) - (pxl8_key_down(&game->input, "a") ? 1.0f : 0.0f);
msg.move_y = (pxl8_key_down(&game->input, "w") ? 1.0f : 0.0f) - (pxl8_key_down(&game->input, "s") ? 1.0f : 0.0f);
msg.look_dx = (f32)pxl8_mouse_dx(&game->input);
msg.look_dy = (f32)pxl8_mouse_dy(&game->input);
msg.buttons = pxl8_key_down(&game->input, "space") ? 1 : 0;
pxl8_world_push_input(game->world, &msg);
}
pxl8_net_update(game->net, dt);
#else
if (game->net) {
while (pxl8_net_poll(game->net)) {}
pxl8_net_update(game->net, dt);
pxl8_world_sync(game->world, game->net);
}
pxl8_world_update(game->world, dt);
pxl8_world_update(game->world, &game->input, dt);
#endif
pxl8_gfx_update(game->gfx, dt);
pxl8_sfx_mixer_process(game->mixer);
@ -430,6 +485,15 @@ void pxl8_quit(pxl8* sys) {
pxl8_info("Shutting down");
#ifdef PXL8_ASYNC_THREADS
if (game->world) {
pxl8_world_stop_sim_thread(game->world);
}
if (game->net) {
pxl8_net_stop_thread(game->net);
}
#endif
if (sys->cart) {
pxl8_cart_unmount(sys->cart);
}
@ -499,6 +563,9 @@ void pxl8_set_relative_mouse_mode(pxl8* sys, bool enabled) {
sys->hal->set_relative_mouse_mode(sys->platform_data, enabled);
if (sys->game) {
sys->game->input.mouse_relative_mode = enabled;
#ifdef PXL8_ASYNC_THREADS
pxl8_world_pause_sim(sys->game->world, !enabled);
#endif
}
}

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

@ -0,0 +1,53 @@
#pragma once
#include <stdatomic.h>
#include "pxl8_types.h"
#define PXL8_QUEUE_CAPACITY 256
typedef struct pxl8_queue {
void* items[PXL8_QUEUE_CAPACITY];
atomic_uint write_idx;
atomic_uint read_idx;
} pxl8_queue;
static inline void pxl8_queue_init(pxl8_queue* q) {
atomic_store(&q->write_idx, 0);
atomic_store(&q->read_idx, 0);
for (u32 i = 0; i < PXL8_QUEUE_CAPACITY; i++) {
q->items[i] = NULL;
}
}
static inline bool pxl8_queue_push(pxl8_queue* q, void* item) {
u32 w = atomic_load_explicit(&q->write_idx, memory_order_relaxed);
u32 next = (w + 1) % PXL8_QUEUE_CAPACITY;
if (next == atomic_load_explicit(&q->read_idx, memory_order_acquire)) {
return false;
}
q->items[w] = item;
atomic_store_explicit(&q->write_idx, next, memory_order_release);
return true;
}
static inline void* pxl8_queue_pop(pxl8_queue* q) {
u32 r = atomic_load_explicit(&q->read_idx, memory_order_relaxed);
if (r == atomic_load_explicit(&q->write_idx, memory_order_acquire)) {
return NULL;
}
void* item = q->items[r];
atomic_store_explicit(&q->read_idx, (r + 1) % PXL8_QUEUE_CAPACITY, memory_order_release);
return item;
}
static inline bool pxl8_queue_empty(const pxl8_queue* q) {
return atomic_load_explicit(&((pxl8_queue*)q)->read_idx, memory_order_acquire) ==
atomic_load_explicit(&((pxl8_queue*)q)->write_idx, memory_order_acquire);
}
static inline u32 pxl8_queue_count(const pxl8_queue* q) {
u32 w = atomic_load_explicit(&((pxl8_queue*)q)->write_idx, memory_order_acquire);
u32 r = atomic_load_explicit(&((pxl8_queue*)q)->read_idx, memory_order_acquire);
return (w >= r) ? (w - r) : (PXL8_QUEUE_CAPACITY - r + w);
}

View file

@ -64,6 +64,7 @@ typedef enum pxl8_result {
PXL8_ERROR_INVALID_COORDINATE,
PXL8_ERROR_INVALID_FORMAT,
PXL8_ERROR_INVALID_SIZE,
PXL8_ERROR_NOT_CONNECTED,
PXL8_ERROR_NOT_INITIALIZED,
PXL8_ERROR_NULL_POINTER,
PXL8_ERROR_OUT_OF_MEMORY,

View file

@ -6,45 +6,6 @@ static inline i32 pxl8_bytes_per_pixel(pxl8_pixel_mode mode) {
return (i32)mode;
}
static inline u16 pxl8_rgb565_pack(u8 r, u8 g, u8 b) {
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}
static inline void pxl8_rgb565_unpack(u16 color, u8* r, u8* g, u8* b) {
*r = (color >> 11) << 3;
*g = ((color >> 5) & 0x3F) << 2;
*b = (color & 0x1F) << 3;
}
static inline u16 pxl8_rgba32_to_rgb565(u32 rgba) {
return pxl8_rgb565_pack(rgba & 0xFF, (rgba >> 8) & 0xFF, (rgba >> 16) & 0xFF);
}
static inline u32 pxl8_rgb565_to_rgba32(u16 color) {
u8 r, g, b;
pxl8_rgb565_unpack(color, &r, &g, &b);
return r | ((u32)g << 8) | ((u32)b << 16) | 0xFF000000;
}
static inline void pxl8_rgba32_unpack(u32 color, u8* r, u8* g, u8* b, u8* a) {
*r = color & 0xFF;
*g = (color >> 8) & 0xFF;
*b = (color >> 16) & 0xFF;
*a = (color >> 24) & 0xFF;
}
static inline u32 pxl8_rgba32_pack(u8 r, u8 g, u8 b, u8 a) {
return r | ((u32)g << 8) | ((u32)b << 16) | ((u32)a << 24);
}
static inline u32 pxl8_color_to_rgba(u32 abgr) {
u8 r = abgr & 0xFF;
u8 g = (abgr >> 8) & 0xFF;
u8 b = (abgr >> 16) & 0xFF;
u8 a = (abgr >> 24) & 0xFF;
return ((u32)r << 24) | ((u32)g << 16) | ((u32)b << 8) | a;
}
static inline u32 pxl8_color_from_rgba(u32 rgba) {
u8 r = (rgba >> 24) & 0xFF;
u8 g = (rgba >> 16) & 0xFF;
@ -57,6 +18,51 @@ static inline u8 pxl8_color_lerp_channel(u8 c1, u8 c2, f32 t) {
return c1 + (i32)((c2 - c1) * t);
}
static inline f32 pxl8_color_hue(u32 color) {
u8 r = color & 0xFF;
u8 g = (color >> 8) & 0xFF;
u8 b = (color >> 16) & 0xFF;
u8 max = r > g ? (r > b ? r : b) : (g > b ? g : b);
u8 min = r < g ? (r < b ? r : b) : (g < b ? g : b);
if (max == min) return 0.0f;
f32 d = (f32)(max - min);
f32 h;
if (max == r) h = (f32)(g - b) / d + (g < b ? 6.0f : 0.0f);
else if (max == g) h = (f32)(b - r) / d + 2.0f;
else h = (f32)(r - g) / d + 4.0f;
return h / 6.0f;
}
static inline f32 pxl8_color_hue_diff(f32 h1, f32 h2) {
f32 d = h1 > h2 ? h1 - h2 : h2 - h1;
return d > 0.5f ? 1.0f - d : d;
}
static inline f32 pxl8_color_luminance(u32 color) {
u8 r = color & 0xFF;
u8 g = (color >> 8) & 0xFF;
u8 b = (color >> 16) & 0xFF;
return 0.299f * r + 0.587f * g + 0.114f * b;
}
static inline f32 pxl8_color_saturation(u32 color) {
u8 r = color & 0xFF;
u8 g = (color >> 8) & 0xFF;
u8 b = (color >> 16) & 0xFF;
u8 max = r > g ? (r > b ? r : b) : (g > b ? g : b);
u8 min = r < g ? (r < b ? r : b) : (g < b ? g : b);
if (max == 0) return 0.0f;
return (f32)(max - min) / (f32)max;
}
static inline u32 pxl8_color_to_rgba(u32 abgr) {
u8 r = abgr & 0xFF;
u8 g = (abgr >> 8) & 0xFF;
u8 b = (abgr >> 16) & 0xFF;
u8 a = (abgr >> 24) & 0xFF;
return ((u32)r << 24) | ((u32)g << 16) | ((u32)b << 8) | a;
}
static inline u8 pxl8_rgb332_pack(u8 r, u8 g, u8 b) {
return ((r >> 5) << 5) | ((g >> 5) << 2) | (b >> 6);
}
@ -69,3 +75,34 @@ static inline void pxl8_rgb332_unpack(u8 c, u8* r, u8* g, u8* b) {
*g = (gi << 5) | (gi << 2) | (gi >> 1);
*b = (bi << 6) | (bi << 4) | (bi << 2) | bi;
}
static inline u16 pxl8_rgb565_pack(u8 r, u8 g, u8 b) {
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}
static inline void pxl8_rgb565_unpack(u16 color, u8* r, u8* g, u8* b) {
*r = (color >> 11) << 3;
*g = ((color >> 5) & 0x3F) << 2;
*b = (color & 0x1F) << 3;
}
static inline u32 pxl8_rgb565_to_rgba32(u16 color) {
u8 r, g, b;
pxl8_rgb565_unpack(color, &r, &g, &b);
return r | ((u32)g << 8) | ((u32)b << 16) | 0xFF000000;
}
static inline u32 pxl8_rgba32_pack(u8 r, u8 g, u8 b, u8 a) {
return r | ((u32)g << 8) | ((u32)b << 16) | ((u32)a << 24);
}
static inline u16 pxl8_rgba32_to_rgb565(u32 rgba) {
return pxl8_rgb565_pack(rgba & 0xFF, (rgba >> 8) & 0xFF, (rgba >> 16) & 0xFF);
}
static inline void pxl8_rgba32_unpack(u32 color, u8* r, u8* g, u8* b, u8* a) {
*r = color & 0xFF;
*g = (color >> 8) & 0xFF;
*b = (color >> 16) & 0xFF;
*a = (color >> 24) & 0xFF;
}

File diff suppressed because it is too large Load diff

View file

@ -58,7 +58,9 @@ void pxl8_cpu_draw_mesh(
);
u8* pxl8_cpu_get_framebuffer(pxl8_cpu_backend* cpu);
u32* pxl8_cpu_get_light_accum(pxl8_cpu_backend* cpu);
u32* pxl8_cpu_get_output(pxl8_cpu_backend* cpu);
u16* pxl8_cpu_get_zbuffer(pxl8_cpu_backend* cpu);
u32 pxl8_cpu_get_height(const pxl8_cpu_backend* cpu);
u32 pxl8_cpu_get_width(const pxl8_cpu_backend* cpu);

View file

@ -27,6 +27,7 @@ typedef struct pxl8_sprite_cache_entry {
struct pxl8_gfx {
pxl8_atlas* atlas;
pxl8_gfx_backend backend;
const pxl8_bsp* bsp;
pxl8_colormap* colormap;
u8* framebuffer;
i32 framebuffer_height;
@ -38,6 +39,7 @@ struct pxl8_gfx {
pxl8_palette_cube* palette_cube;
pxl8_pixel_mode pixel_mode;
void* platform_data;
const pxl8_sdf* sdf;
pxl8_sprite_cache_entry* sprite_cache;
u32 sprite_cache_capacity;
u32 sprite_cache_count;
@ -61,6 +63,9 @@ pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx) {
u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx) {
if (!gfx || gfx->pixel_mode == PXL8_PIXEL_HICOLOR) return NULL;
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU) {
return pxl8_cpu_get_framebuffer(gfx->backend.cpu);
}
return gfx->framebuffer;
}
@ -73,6 +78,27 @@ i32 pxl8_gfx_get_height(const pxl8_gfx* gfx) {
return gfx ? gfx->framebuffer_height : 0;
}
u32* pxl8_gfx_get_light_accum(pxl8_gfx* gfx) {
if (!gfx) return NULL;
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU) {
return pxl8_cpu_get_light_accum(gfx->backend.cpu);
}
return NULL;
}
const u32* pxl8_gfx_palette_colors(pxl8_gfx* gfx) {
if (!gfx || !gfx->palette) return NULL;
return pxl8_palette_colors(gfx->palette);
}
u16* pxl8_gfx_get_zbuffer(pxl8_gfx* gfx) {
if (!gfx) return NULL;
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU) {
return pxl8_cpu_get_zbuffer(gfx->backend.cpu);
}
return NULL;
}
i32 pxl8_gfx_get_width(const pxl8_gfx* gfx) {
return gfx ? gfx->framebuffer_width : 0;
}
@ -608,12 +634,24 @@ pxl8_3d_frame pxl8_3d_frame_from_camera(const pxl8_3d_camera* camera, const pxl8
return frame;
}
void pxl8_3d_set_bsp(pxl8_gfx* gfx, const pxl8_bsp* bsp) {
if (!gfx) return;
gfx->bsp = bsp;
}
void pxl8_3d_set_sdf(pxl8_gfx* gfx, const pxl8_sdf* sdf) {
if (!gfx) return;
gfx->sdf = sdf;
}
void pxl8_3d_begin_frame(pxl8_gfx* gfx, const pxl8_3d_camera* camera, const pxl8_lights* lights, const pxl8_3d_uniforms* uniforms) {
if (!gfx || !camera) return;
pxl8_3d_frame frame = pxl8_3d_frame_from_camera(camera, uniforms);
frame.bsp = gfx->bsp;
frame.lights = lights ? pxl8_lights_data(lights) : NULL;
frame.lights_count = lights ? pxl8_lights_count(lights) : 0;
frame.sdf = gfx->sdf;
pxl8_mat4 vp = pxl8_mat4_multiply(frame.projection, frame.view);

View file

@ -32,6 +32,9 @@ pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx);
u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx);
u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx);
i32 pxl8_gfx_get_height(const pxl8_gfx* gfx);
u32* pxl8_gfx_get_light_accum(pxl8_gfx* gfx);
const u32* pxl8_gfx_palette_colors(pxl8_gfx* gfx);
u16* pxl8_gfx_get_zbuffer(pxl8_gfx* gfx);
pxl8_palette* pxl8_gfx_palette(pxl8_gfx* gfx);
pxl8_colormap* pxl8_gfx_colormap(pxl8_gfx* gfx);
pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx);

View file

@ -6,6 +6,7 @@
#include "pxl8_mesh.h"
#include "pxl8_types.h"
typedef struct pxl8_bsp pxl8_bsp;
typedef struct pxl8_gfx pxl8_gfx;
typedef struct pxl8_3d_uniforms {
@ -17,7 +18,16 @@ typedef struct pxl8_3d_uniforms {
f32 time;
} pxl8_3d_uniforms;
typedef struct pxl8_3d_frame_desc {
const pxl8_bsp* bsp;
const pxl8_3d_camera* camera;
const pxl8_lights* lights;
const pxl8_sdf* sdf;
pxl8_3d_uniforms uniforms;
} pxl8_3d_frame_desc;
typedef struct pxl8_3d_frame {
const pxl8_bsp* bsp;
pxl8_vec3 camera_dir;
pxl8_vec3 camera_pos;
f32 far_clip;
@ -25,6 +35,7 @@ typedef struct pxl8_3d_frame {
u32 lights_count;
f32 near_clip;
pxl8_mat4 projection;
const pxl8_sdf* sdf;
pxl8_3d_uniforms uniforms;
pxl8_mat4 view;
} pxl8_3d_frame;
@ -33,6 +44,8 @@ typedef struct pxl8_3d_frame {
extern "C" {
#endif
void pxl8_3d_set_bsp(pxl8_gfx* gfx, const pxl8_bsp* bsp);
void pxl8_3d_set_sdf(pxl8_gfx* gfx, const pxl8_sdf* sdf);
void pxl8_3d_begin_frame(pxl8_gfx* gfx, const pxl8_3d_camera* camera, const pxl8_lights* lights, const pxl8_3d_uniforms* uniforms);
void pxl8_3d_clear(pxl8_gfx* gfx, u8 color);
void pxl8_3d_clear_depth(pxl8_gfx* gfx);

View file

@ -106,6 +106,63 @@ bool pxl8_gui_button(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y,
return clicked;
}
bool pxl8_gui_slider(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, f32* value, f32 min_val, f32 max_val) {
if (!state || !gfx || !value) return false;
bool cursor_over = is_cursor_over(state, x, y, w, h);
bool is_active = (state->active_id == id);
bool changed = false;
if (cursor_over) {
state->hot_id = id;
}
if (cursor_over && state->cursor_down && state->active_id == 0) {
state->active_id = id;
}
if (is_active && state->cursor_down) {
f32 t = (f32)(state->cursor_x - x) / (f32)w;
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
f32 new_val = min_val + t * (max_val - min_val);
if (new_val != *value) {
*value = new_val;
changed = true;
}
}
u8 bg_color = pxl8_gfx_ui_color(gfx, PXL8_UI_BG1);
u8 fill_color = pxl8_gfx_ui_color(gfx, is_active ? PXL8_UI_FG0 : PXL8_UI_BG3);
u8 handle_color = pxl8_gfx_ui_color(gfx, PXL8_UI_FG1);
pxl8_2d_rect_fill(gfx, x, y, w, h, bg_color);
f32 t = (*value - min_val) / (max_val - min_val);
i32 fill_w = (i32)(t * (f32)w);
if (fill_w > 0) {
pxl8_2d_rect_fill(gfx, x, y, fill_w, h, fill_color);
}
i32 handle_x = x + fill_w - 2;
if (handle_x < x) handle_x = x;
if (handle_x > x + w - 4) handle_x = x + w - 4;
pxl8_2d_rect_fill(gfx, handle_x, y, 4, h, handle_color);
return changed;
}
bool pxl8_gui_slider_int(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, i32* value, i32 min_val, i32 max_val) {
if (!state || !gfx || !value) return false;
f32 fval = (f32)*value;
bool changed = pxl8_gui_slider(state, gfx, id, x, y, w, h, &fval, (f32)min_val, (f32)max_val);
if (changed) {
*value = (i32)(fval + 0.5f);
}
return changed;
}
void pxl8_gui_window(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, const char* title) {
if (!gfx || !title) return;

View file

@ -30,6 +30,8 @@ void pxl8_gui_cursor_move(pxl8_gui_state* state, i32 x, i32 y);
void pxl8_gui_cursor_up(pxl8_gui_state* state);
bool pxl8_gui_button(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, const char* label);
bool pxl8_gui_slider(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, f32* value, f32 min_val, f32 max_val);
bool pxl8_gui_slider_int(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, i32* value, i32 min_val, i32 max_val);
void pxl8_gui_label(pxl8_gfx* gfx, i32 x, i32 y, const char* text, u8 color);
void pxl8_gui_window(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, const char* title);

View file

@ -2,6 +2,34 @@
#include "pxl8_types.h"
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
#define PXL8_ASYNC_THREADS
#endif
typedef struct pxl8_thread pxl8_thread;
typedef struct pxl8_mutex pxl8_mutex;
typedef struct pxl8_cond pxl8_cond;
typedef int (*pxl8_thread_fn)(void* data);
pxl8_thread* pxl8_thread_create(pxl8_thread_fn fn, const char* name, void* data);
void pxl8_thread_wait(pxl8_thread* thread, int* status);
void pxl8_thread_detach(pxl8_thread* thread);
pxl8_mutex* pxl8_mutex_create(void);
void pxl8_mutex_destroy(pxl8_mutex* mutex);
void pxl8_mutex_lock(pxl8_mutex* mutex);
void pxl8_mutex_unlock(pxl8_mutex* mutex);
bool pxl8_mutex_trylock(pxl8_mutex* mutex);
pxl8_cond* pxl8_cond_create(void);
void pxl8_cond_destroy(pxl8_cond* cond);
void pxl8_cond_signal(pxl8_cond* cond);
void pxl8_cond_broadcast(pxl8_cond* cond);
void pxl8_cond_wait(pxl8_cond* cond, pxl8_mutex* mutex);
u64 pxl8_get_ticks_ns(void);
void pxl8_sleep_ms(u32 ms);
typedef struct pxl8_hal {
void* (*create)(i32 render_w, i32 render_h,
const char* title, i32 win_w, i32 win_h);

View file

@ -83,6 +83,14 @@ static u64 sdl3_get_ticks(void) {
return SDL_GetTicksNS();
}
void pxl8_sleep_ms(u32 ms) {
SDL_Delay(ms);
}
u64 pxl8_get_ticks_ns(void) {
return SDL_GetTicksNS();
}
static void sdl3_present(void* platform_data) {
if (!platform_data) return;

20
src/hal/pxl8_mem.c Normal file
View file

@ -0,0 +1,20 @@
#include "pxl8_mem.h"
#include <stdlib.h>
#include <string.h>
void* pxl8_malloc(usize size) {
return malloc(size);
}
void* pxl8_calloc(usize count, usize size) {
return calloc(count, size);
}
void* pxl8_realloc(void* ptr, usize size) {
return realloc(ptr, size);
}
void pxl8_free(void* ptr) {
free(ptr);
}

110
src/hal/pxl8_thread_sdl3.c Normal file
View file

@ -0,0 +1,110 @@
#include "pxl8_hal.h"
#include <SDL3/SDL.h>
#include "pxl8_mem.h"
struct pxl8_thread {
SDL_Thread* handle;
};
struct pxl8_mutex {
SDL_Mutex* handle;
};
struct pxl8_cond {
SDL_Condition* handle;
};
pxl8_thread* pxl8_thread_create(pxl8_thread_fn fn, const char* name, void* data) {
pxl8_thread* t = pxl8_calloc(1, sizeof(pxl8_thread));
if (!t) return NULL;
t->handle = SDL_CreateThread((SDL_ThreadFunction)fn, name, data);
if (!t->handle) {
pxl8_free(t);
return NULL;
}
return t;
}
void pxl8_thread_wait(pxl8_thread* thread, int* status) {
if (!thread || !thread->handle) return;
SDL_WaitThread(thread->handle, status);
pxl8_free(thread);
}
void pxl8_thread_detach(pxl8_thread* thread) {
if (!thread || !thread->handle) return;
SDL_DetachThread(thread->handle);
pxl8_free(thread);
}
pxl8_mutex* pxl8_mutex_create(void) {
pxl8_mutex* m = pxl8_calloc(1, sizeof(pxl8_mutex));
if (!m) return NULL;
m->handle = SDL_CreateMutex();
if (!m->handle) {
pxl8_free(m);
return NULL;
}
return m;
}
void pxl8_mutex_destroy(pxl8_mutex* mutex) {
if (!mutex) return;
if (mutex->handle) SDL_DestroyMutex(mutex->handle);
pxl8_free(mutex);
}
void pxl8_mutex_lock(pxl8_mutex* mutex) {
if (!mutex || !mutex->handle) return;
SDL_LockMutex(mutex->handle);
}
void pxl8_mutex_unlock(pxl8_mutex* mutex) {
if (!mutex || !mutex->handle) return;
SDL_UnlockMutex(mutex->handle);
}
bool pxl8_mutex_trylock(pxl8_mutex* mutex) {
if (!mutex || !mutex->handle) return false;
return SDL_TryLockMutex(mutex->handle);
}
pxl8_cond* pxl8_cond_create(void) {
pxl8_cond* c = pxl8_calloc(1, sizeof(pxl8_cond));
if (!c) return NULL;
c->handle = SDL_CreateCondition();
if (!c->handle) {
pxl8_free(c);
return NULL;
}
return c;
}
void pxl8_cond_destroy(pxl8_cond* cond) {
if (!cond) return;
if (cond->handle) SDL_DestroyCondition(cond->handle);
pxl8_free(cond);
}
void pxl8_cond_signal(pxl8_cond* cond) {
if (!cond || !cond->handle) return;
SDL_SignalCondition(cond->handle);
}
void pxl8_cond_broadcast(pxl8_cond* cond) {
if (!cond || !cond->handle) return;
SDL_BroadcastCondition(cond->handle);
}
void pxl8_cond_wait(pxl8_cond* cond, pxl8_mutex* mutex) {
if (!cond || !cond->handle || !mutex || !mutex->handle) return;
SDL_WaitCondition(cond->handle, mutex->handle);
}

View file

@ -56,6 +56,14 @@ function Lights.new(capacity)
end
function Lights:add(x, y, z, r, g, b, intensity, radius)
if r and r > 255 then
local rgb = r
intensity = g
radius = b
r = bit.band(bit.rshift(rgb, 16), 0xFF)
g = bit.band(bit.rshift(rgb, 8), 0xFF)
b = bit.band(rgb, 0xFF)
end
C.pxl8_lights_add(self._ptr, x, y, z, r or 255, g or 255, b or 255, intensity or 255, radius or 10)
end

View file

@ -49,10 +49,9 @@ function graphics.load_palette(filepath)
end
function graphics.load_sprite(filepath)
local sprite_id = ffi.new("unsigned int[1]")
local result = C.pxl8_gfx_load_sprite(core.gfx, filepath, sprite_id)
if result == 0 then
return sprite_id[0]
local result = C.pxl8_gfx_load_sprite(core.gfx, filepath)
if result >= 0 and result < 100 then
return result
else
return nil, result
end

View file

@ -23,6 +23,18 @@ function Gui:button(id, x, y, w, h, label)
return C.pxl8_gui_button(self._ptr, core.gfx, id, x, y, w, h, label)
end
function Gui:slider(id, x, y, w, h, value, min_val, max_val)
local val = ffi.new("float[1]", value)
local changed = C.pxl8_gui_slider(self._ptr, core.gfx, id, x, y, w, h, val, min_val, max_val)
return changed, val[0]
end
function Gui:slider_int(id, x, y, w, h, value, min_val, max_val)
local val = ffi.new("i32[1]", value)
local changed = C.pxl8_gui_slider_int(self._ptr, core.gfx, id, x, y, w, h, val, min_val, max_val)
return changed, val[0]
end
function Gui:cursor_down()
C.pxl8_gui_cursor_down(self._ptr)
end

View file

@ -15,91 +15,20 @@ function net.get()
return setmetatable({ _ptr = ptr }, Net)
end
function Net:chunk_id()
return C.pxl8_net_chunk_id(self._ptr)
end
function Net:connected()
return C.pxl8_net_connected(self._ptr)
end
function Net:entities()
local snap = C.pxl8_net_snapshot(self._ptr)
if snap == nil then
return {}
end
local ents = C.pxl8_net_entities(self._ptr)
if ents == nil then
return {}
end
local result = {}
for i = 0, snap.entity_count - 1 do
result[i + 1] = {
entity_id = tonumber(ents[i].entity_id),
userdata = ents[i].userdata
}
end
return result
function Net:enter_chunk(chunk_id)
return C.pxl8_net_enter_chunk(self._ptr, chunk_id or 1) == 0
end
function Net:entity_prev_userdata(entity_id)
return C.pxl8_net_entity_prev_userdata(self._ptr, entity_id)
end
function Net:entity_userdata(entity_id)
return C.pxl8_net_entity_userdata(self._ptr, entity_id)
end
function Net:input_at(tick)
local input = C.pxl8_net_input_at(self._ptr, tick)
if input == nil then return nil end
return {
buttons = input.buttons,
look_dx = input.look_dx,
look_dy = input.look_dy,
move_x = input.move_x,
move_y = input.move_y,
yaw = input.yaw,
tick = tonumber(input.tick),
timestamp = tonumber(input.timestamp)
}
end
function Net:input_oldest_tick()
return tonumber(C.pxl8_net_input_oldest_tick(self._ptr))
end
function Net:input_push(input)
local msg = ffi.new("pxl8_input_msg")
msg.buttons = input.buttons or 0
msg.look_dx = input.look_dx or 0
msg.look_dy = input.look_dy or 0
msg.move_x = input.move_x or 0
msg.move_y = input.move_y or 0
msg.yaw = input.yaw or 0
msg.tick = input.tick or 0
msg.timestamp = input.timestamp or 0
C.pxl8_net_input_push(self._ptr, msg)
end
function Net:lerp_alpha()
return C.pxl8_net_lerp_alpha(self._ptr)
end
function Net:needs_correction()
return C.pxl8_net_needs_correction(self._ptr)
end
function Net:player_id()
return tonumber(C.pxl8_net_player_id(self._ptr))
end
function Net:predicted_state()
return C.pxl8_net_predicted_state(self._ptr)
end
function Net:predicted_tick_set(tick)
C.pxl8_net_predicted_tick_set(self._ptr, tick)
end
function Net:send_command(cmd)
return C.pxl8_net_send_command(self._ptr, cmd) == 0
function Net:exit_chunk(x, y, z)
return C.pxl8_net_exit_chunk(self._ptr, x or 0, y or 0, z or 0) == 0
end
function Net:send_input(input)
@ -115,35 +44,12 @@ function Net:send_input(input)
return C.pxl8_net_send_input(self._ptr, msg) == 0
end
function Net:snapshot()
local snap = C.pxl8_net_snapshot(self._ptr)
if snap == nil then
return nil
end
return {
entity_count = snap.entity_count,
event_count = snap.event_count,
player_id = tonumber(snap.player_id),
tick = tonumber(snap.tick),
time = snap.time
}
function Net:set_chunk_settings(render_distance, sim_distance)
return C.pxl8_net_send_chunk_settings(self._ptr, render_distance or 3, sim_distance or 4) == 0
end
function Net:spawn(x, y, z, yaw, pitch)
local cmd = ffi.new("pxl8_command_msg")
cmd.cmd_type = C.PXL8_CMD_SPAWN_ENTITY
C.pxl8_pack_f32_be(cmd.payload, 0, x or 0)
C.pxl8_pack_f32_be(cmd.payload, 4, y or 0)
C.pxl8_pack_f32_be(cmd.payload, 8, z or 0)
C.pxl8_pack_f32_be(cmd.payload, 12, yaw or 0)
C.pxl8_pack_f32_be(cmd.payload, 16, pitch or 0)
cmd.payload_size = 20
cmd.tick = 0
return self:send_command(cmd)
end
function Net:tick()
return tonumber(C.pxl8_net_tick(self._ptr))
return C.pxl8_net_spawn(self._ptr, x or 0, y or 0, z or 0, yaw or 0, pitch or 0) == 0
end
return net

281
src/lua/pxl8/shader.lua Normal file
View file

@ -0,0 +1,281 @@
local ffi = require("ffi")
local bit = require("bit")
local core = require("pxl8.core")
ffi.cdef[[
u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx);
u32* pxl8_gfx_get_light_accum(pxl8_gfx* gfx);
const u32* pxl8_gfx_palette_colors(pxl8_gfx* gfx);
u16* pxl8_gfx_get_zbuffer(pxl8_gfx* gfx);
]]
local C = ffi.C
local band, bor, rshift, lshift = bit.band, bit.bor, bit.rshift, bit.lshift
local floor, sqrt, max, min, abs = math.floor, math.sqrt, math.max, math.min, math.abs
local sin, cos, fmod = math.sin, math.cos, math.fmod
local shader = {}
local fb = ffi.new("u8*")
local light = ffi.new("u32*")
local pal = ffi.new("const u32*")
local zbuf = ffi.new("u16*")
local w, h, count = 0, 0, 0
function shader.begin_frame()
fb = C.pxl8_gfx_get_framebuffer_indexed(core.gfx)
light = C.pxl8_gfx_get_light_accum(core.gfx)
pal = C.pxl8_gfx_palette_colors(core.gfx)
zbuf = C.pxl8_gfx_get_zbuffer(core.gfx)
w = C.pxl8_gfx_get_width(core.gfx)
h = C.pxl8_gfx_get_height(core.gfx)
count = w * h
end
function shader.get_buffers()
return fb, light, pal, zbuf, w, h
end
local function clamp(x, lo, hi)
if x < lo then return lo end
if x > hi then return hi end
return x
end
shader.resolve_tint = function()
if fb == nil or light == nil or pal == nil then return end
local fb_l, light_l, pal_l = fb, light, pal
local count_l = count
local band_l, rshift_l, bor_l, lshift_l = band, rshift, bor, lshift
local floor_l = floor
for i = 0, count_l - 1 do
local lv = light_l[i]
if lv ~= 0 then
local a = rshift_l(lv, 24)
if a > 0 then
local base = pal_l[fb_l[i]]
local br = band_l(base, 0xFF)
local bg = band_l(rshift_l(base, 8), 0xFF)
local bb = band_l(rshift_l(base, 16), 0xFF)
local lr = band_l(lv, 0xFF)
local lg = band_l(rshift_l(lv, 8), 0xFF)
local lb = band_l(rshift_l(lv, 16), 0xFF)
local t = a * 0.00392156862
local r = floor_l(br + (lr - 128) * t * 2)
local g = floor_l(bg + (lg - 128) * t * 2)
local b = floor_l(bb + (lb - 128) * t * 2)
if r < 0 then r = 0 elseif r > 255 then r = 255 end
if g < 0 then g = 0 elseif g > 255 then g = 255 end
if b < 0 then b = 0 elseif b > 255 then b = 255 end
light_l[i] = bor_l(r, lshift_l(g, 8), lshift_l(b, 16), 0xFF000000)
end
end
end
end
shader.compile = function(source)
local header = [[
local ffi = require("ffi")
local bit = require("bit")
local band, bor, rshift, lshift = bit.band, bit.bor, bit.rshift, bit.lshift
local floor, sqrt, max, min, abs = math.floor, math.sqrt, math.max, math.min, math.abs
local sin, cos, fmod = math.sin, math.cos, math.fmod
local clamp = function(x, lo, hi) if x < lo then return lo elseif x > hi then return hi else return x end end
local mix = function(a, b, t) return a + (b - a) * t end
local smoothstep = function(e0, e1, x) local t = clamp((x - e0) / (e1 - e0), 0, 1); return t * t * (3 - 2 * t) end
local saturate = function(x) if x < 0 then return 0 elseif x > 1 then return 1 else return x end end
local length2 = function(x, y) return sqrt(x*x + y*y) end
local length3 = function(x, y, z) return sqrt(x*x + y*y + z*z) end
local dot2 = function(ax, ay, bx, by) return ax*bx + ay*by end
local dot3 = function(ax, ay, az, bx, by, bz) return ax*bx + ay*by + az*bz end
local fract = function(x) return x - floor(x) end
return function(fb, light, pal, zbuf, w, h, uniforms)
uniforms = uniforms or {}
local count = w * h
local time = uniforms.time or 0
local band_l, rshift_l, bor_l, lshift_l = band, rshift, bor, lshift
local floor_l, sqrt_l, max_l, min_l = floor, sqrt, max, min
for i = 0, count - 1 do
local x = i % w
local y = floor_l(i / w)
local uv_x = x / w
local uv_y = y / h
local idx = fb[i]
local base = pal[idx]
local br = band_l(base, 0xFF)
local bg = band_l(rshift_l(base, 8), 0xFF)
local bb = band_l(rshift_l(base, 16), 0xFF)
local lv = light[i]
local lr = band_l(lv, 0xFF)
local lg = band_l(rshift_l(lv, 8), 0xFF)
local lb = band_l(rshift_l(lv, 16), 0xFF)
local la = rshift_l(lv, 24)
local depth = zbuf and zbuf[i] or 0
local depth_n = depth / 65535.0
local r, g, b = br, bg, bb
]]
local footer = [[
r = floor_l(clamp(r, 0, 255))
g = floor_l(clamp(g, 0, 255))
b = floor_l(clamp(b, 0, 255))
light[i] = bor_l(r, lshift_l(g, 8), lshift_l(b, 16), 0xFF000000)
end
end
]]
local code = header .. source .. footer
local fn, err = loadstring(code)
if not fn then
error("Shader compile error: " .. tostring(err))
end
return fn()
end
shader.run = function(compiled_shader, uniforms)
if fb == nil or light == nil or pal == nil then return end
compiled_shader(fb, light, pal, zbuf, w, h, uniforms)
end
shader.presets = {}
shader.presets.passthrough = shader.compile([[
-- passthrough: just use base color
]])
shader.presets.light_tint = shader.compile([[
if la > 0 then
local t = la / 255.0
r = br + (lr - 128) * t * 2
g = bg + (lg - 128) * t * 2
b = bb + (lb - 128) * t * 2
end
]])
shader.presets.vignette = shader.compile([[
local cx = uv_x - 0.5
local cy = uv_y - 0.5
local dist = sqrt_l(cx*cx + cy*cy)
local vig = 1.0 - saturate(dist * 1.5)
vig = vig * vig
r = br * vig
g = bg * vig
b = bb * vig
]])
shader.presets.scanlines = shader.compile([[
local scan = 0.8 + 0.2 * (y % 2)
r = br * scan
g = bg * scan
b = bb * scan
]])
shader.presets.crt = shader.compile([[
-- Scanlines
local scan = 0.85 + 0.15 * (y % 2)
-- Vignette
local cx = uv_x - 0.5
local cy = uv_y - 0.5
local dist = sqrt_l(cx*cx + cy*cy)
local vig = 1.0 - saturate(dist * 1.2)
-- RGB shift based on x position
local shift = (uv_x - 0.5) * 0.02
local mult = scan * vig
r = br * mult * (1.0 + shift)
g = bg * mult
b = bb * mult * (1.0 - shift)
]])
shader.presets.dither_fade = shader.compile([[
local threshold = fract(sin(x * 12.9898 + y * 78.233) * 43758.5453)
local fade = uniforms.fade or 0.5
if threshold > fade then
r, g, b = 0, 0, 0
else
r, g, b = br, bg, bb
end
]])
shader.presets.fog = shader.compile([[
local fog_color_r = uniforms.fog_r or 32
local fog_color_g = uniforms.fog_g or 32
local fog_color_b = uniforms.fog_b or 48
local fog_start = uniforms.fog_start or 0.3
local fog_end = uniforms.fog_end or 0.9
local fog_t = saturate((depth_n - fog_start) / (fog_end - fog_start))
fog_t = fog_t * fog_t
r = mix(br, fog_color_r, fog_t)
g = mix(bg, fog_color_g, fog_t)
b = mix(bb, fog_color_b, fog_t)
]])
shader.presets.light_with_fog = shader.compile([[
-- Apply light tint first
if la > 0 then
local t = la / 255.0
r = br + (lr - 128) * t * 2
g = bg + (lg - 128) * t * 2
b = bb + (lb - 128) * t * 2
else
r, g, b = br, bg, bb
end
-- Then fog
local fog_r = uniforms.fog_r or 16
local fog_g = uniforms.fog_g or 16
local fog_b = uniforms.fog_b or 24
local fog_start = uniforms.fog_start or 0.2
local fog_end = uniforms.fog_end or 0.95
local fog_t = saturate((depth_n - fog_start) / (fog_end - fog_start))
fog_t = fog_t * fog_t
r = mix(r, fog_r, fog_t)
g = mix(g, fog_g, fog_t)
b = mix(b, fog_b, fog_t)
]])
shader.presets.posterize = shader.compile([[
local levels = uniforms.levels or 4
local step = 255 / levels
r = floor_l(br / step) * step
g = floor_l(bg / step) * step
b = floor_l(bb / step) * step
]])
shader.presets.chromatic = shader.compile([=[
local amount = uniforms.amount or 2
local ox = floor_l(amount * (uv_x - 0.5))
local r_i = clamp(i - ox, 0, count - 1)
local b_i = clamp(i + ox, 0, count - 1)
local r_base = pal[fb[r_i]]
local b_base = pal[fb[b_i]]
r = band_l(r_base, 0xFF)
g = bg
b = band_l(rshift_l(b_base, 16), 0xFF)
]=])
shader.clear_light = function()
if light == nil then return end
ffi.fill(light, count * 4, 0)
end
shader.fill_output = function(r, g, b)
if light == nil then return end
local color = bor(r, lshift(g, 8), lshift(b, 16), 0xFF000000)
for i = 0, count - 1 do
light[i] = color
end
end
return shader

View file

@ -6,8 +6,6 @@ local world = {}
world.CHUNK_VXL = 0
world.CHUNK_BSP = 1
world.PROCGEN_ROOMS = C.PXL8_PROCGEN_ROOMS
world.PROCGEN_TERRAIN = C.PXL8_PROCGEN_TERRAIN
local Bsp = {}
Bsp.__index = Bsp
@ -24,14 +22,6 @@ function Bsp:face_set_material(face_id, material_id)
C.pxl8_bsp_face_set_material(self._ptr, face_id, material_id)
end
function Bsp:set_material(material_id, material)
C.pxl8_bsp_set_material(self._ptr, material_id, material._ptr)
end
function Bsp:set_wireframe(wireframe)
C.pxl8_bsp_set_wireframe(self._ptr, wireframe)
end
world.Bsp = Bsp
local Chunk = {}
@ -45,21 +35,26 @@ function Chunk:bsp()
return setmetatable({ _ptr = ptr }, Bsp)
end
function Chunk:type()
if self._ptr == nil then return nil end
return self._ptr.type
end
function Chunk:ready()
if self._ptr == nil then return false end
if self._ptr.type == world.CHUNK_BSP then
return self._ptr.bsp ~= nil
elseif self._ptr.type == world.CHUNK_VXL then
return self._ptr.voxel ~= nil
return self._ptr.voxels ~= nil
end
return false
end
function Chunk:type()
if self._ptr == nil then return nil end
return self._ptr.type
end
function Chunk:version()
if self._ptr == nil then return 0 end
return self._ptr.version
end
world.Chunk = Chunk
local World = {}
@ -77,9 +72,32 @@ function World:active_chunk()
return setmetatable({ _ptr = ptr }, Chunk)
end
function World:check_collision(x, y, z, radius)
local pos = ffi.new("pxl8_vec3", {x = x, y = y, z = z})
return C.pxl8_world_check_collision(self._ptr, pos, radius)
function World:get_render_distance()
return C.pxl8_world_get_render_distance(self._ptr)
end
function World:get_sim_distance()
return C.pxl8_world_get_sim_distance(self._ptr)
end
function World:init_local_player(x, y, z)
C.pxl8_world_init_local_player(self._ptr, x, y, z)
end
function World:local_player()
local ptr = C.pxl8_world_local_player(self._ptr)
if ptr == nil then return nil end
return ptr
end
function World:point_solid(x, y, z)
return C.pxl8_world_point_solid(self._ptr, x, y, z)
end
function World:ray(from_x, from_y, from_z, to_x, to_y, to_z)
local from = ffi.new("pxl8_vec3", {x = from_x, y = from_y, z = from_z})
local to = ffi.new("pxl8_vec3", {x = to_x, y = to_y, z = to_z})
return C.pxl8_world_ray(self._ptr, from, to)
end
function World:render(camera_pos)
@ -87,11 +105,26 @@ function World:render(camera_pos)
C.pxl8_world_render(self._ptr, core.gfx, vec)
end
function World:resolve_collision(from_x, from_y, from_z, to_x, to_y, to_z, radius)
function World:set_bsp_material(material_id, material)
C.pxl8_world_set_bsp_material(self._ptr, material_id, material._ptr)
end
function World:set_render_distance(distance)
C.pxl8_world_set_render_distance(self._ptr, distance)
end
function World:set_sim_distance(distance)
C.pxl8_world_set_sim_distance(self._ptr, distance)
end
function World:set_wireframe(enabled)
C.pxl8_world_set_wireframe(self._ptr, enabled)
end
function World:sweep(from_x, from_y, from_z, to_x, to_y, to_z, radius)
local from = ffi.new("pxl8_vec3", {x = from_x, y = from_y, z = from_z})
local to = ffi.new("pxl8_vec3", {x = to_x, y = to_y, z = to_z})
local result = C.pxl8_world_resolve_collision(self._ptr, from, to, radius)
return result.x, result.y, result.z
return C.pxl8_world_sweep(self._ptr, from, to, radius)
end
world.World = World

View file

@ -9,6 +9,23 @@ u32 pxl8_hash32(u32 x) {
return x;
}
u64 pxl8_hash64(u64 x) {
x ^= x >> 33;
x *= 0xff51afd7ed558ccdULL;
x ^= x >> 33;
x *= 0xc4ceb9fe1a85ec53ULL;
x ^= x >> 33;
return x;
}
f32 pxl8_lerp_f32(f32 a, f32 b, f32 t) {
return a + (b - a) * t;
}
f32 pxl8_smoothstep(f32 t) {
return t * t * (3.0f - 2.0f * t);
}
pxl8_vec2 pxl8_vec2_add(pxl8_vec2 a, pxl8_vec2 b) {
return (pxl8_vec2){
.x = a.x + b.x,

View file

@ -81,11 +81,34 @@ typedef struct pxl8_projected_point {
bool visible;
} pxl8_projected_point;
typedef struct pxl8_ray {
pxl8_vec3 normal;
pxl8_vec3 point;
f32 fraction;
bool hit;
} pxl8_ray;
#define PXL8_SDF_X 32
#define PXL8_SDF_Y 16
#define PXL8_SDF_Z 32
#define PXL8_SDF_SIZE (PXL8_SDF_X * PXL8_SDF_Y * PXL8_SDF_Z)
#define PXL8_SDF_CELL 16.0f
typedef struct pxl8_sdf {
i8 data[PXL8_SDF_SIZE];
pxl8_vec3 origin;
f32 cell;
} pxl8_sdf;
#ifdef __cplusplus
extern "C" {
#endif
u32 pxl8_hash32(u32 x);
u64 pxl8_hash64(u64 x);
f32 pxl8_lerp_f32(f32 a, f32 b, f32 t);
f32 pxl8_smoothstep(f32 t);
pxl8_vec2 pxl8_vec2_add(pxl8_vec2 a, pxl8_vec2 b);
f32 pxl8_vec2_dot(pxl8_vec2 a, pxl8_vec2 b);

94
src/math/pxl8_noise.c Normal file
View file

@ -0,0 +1,94 @@
#include "pxl8_noise.h"
f32 pxl8_noise2d(i32 x, i32 z, u64 seed) {
u64 h = pxl8_hash64(seed ^ (u64)x ^ ((u64)z << 32));
return (f32)(h & 0xFFFF) / 65535.0f;
}
f32 pxl8_noise3d(i32 x, i32 y, i32 z, u64 seed) {
u64 h = pxl8_hash64(seed ^ (u64)x ^ ((u64)y << 21) ^ ((u64)z << 42));
return (f32)(h & 0xFFFF) / 65535.0f;
}
f32 pxl8_value_noise(f32 x, f32 z, u64 seed) {
i32 x0 = (i32)floorf(x);
i32 z0 = (i32)floorf(z);
i32 x1 = x0 + 1;
i32 z1 = z0 + 1;
f32 tx = pxl8_smoothstep(x - (f32)x0);
f32 tz = pxl8_smoothstep(z - (f32)z0);
f32 c00 = pxl8_noise2d(x0, z0, seed);
f32 c10 = pxl8_noise2d(x1, z0, seed);
f32 c01 = pxl8_noise2d(x0, z1, seed);
f32 c11 = pxl8_noise2d(x1, z1, seed);
f32 a = pxl8_lerp_f32(c00, c10, tx);
f32 b = pxl8_lerp_f32(c01, c11, tx);
return pxl8_lerp_f32(a, b, tz);
}
f32 pxl8_value_noise3d(f32 x, f32 y, f32 z, u64 seed) {
i32 x0 = (i32)floorf(x);
i32 y0 = (i32)floorf(y);
i32 z0 = (i32)floorf(z);
i32 x1 = x0 + 1;
i32 y1 = y0 + 1;
i32 z1 = z0 + 1;
f32 tx = pxl8_smoothstep(x - (f32)x0);
f32 ty = pxl8_smoothstep(y - (f32)y0);
f32 tz = pxl8_smoothstep(z - (f32)z0);
f32 c000 = pxl8_noise3d(x0, y0, z0, seed);
f32 c100 = pxl8_noise3d(x1, y0, z0, seed);
f32 c010 = pxl8_noise3d(x0, y1, z0, seed);
f32 c110 = pxl8_noise3d(x1, y1, z0, seed);
f32 c001 = pxl8_noise3d(x0, y0, z1, seed);
f32 c101 = pxl8_noise3d(x1, y0, z1, seed);
f32 c011 = pxl8_noise3d(x0, y1, z1, seed);
f32 c111 = pxl8_noise3d(x1, y1, z1, seed);
f32 a00 = pxl8_lerp_f32(c000, c100, tx);
f32 a10 = pxl8_lerp_f32(c010, c110, tx);
f32 a01 = pxl8_lerp_f32(c001, c101, tx);
f32 a11 = pxl8_lerp_f32(c011, c111, tx);
f32 b0 = pxl8_lerp_f32(a00, a10, ty);
f32 b1 = pxl8_lerp_f32(a01, a11, ty);
return pxl8_lerp_f32(b0, b1, tz);
}
f32 pxl8_fbm(f32 x, f32 z, u64 seed, u32 octaves) {
f32 value = 0.0f;
f32 amplitude = 1.0f;
f32 frequency = 1.0f;
f32 max_value = 0.0f;
for (u32 i = 0; i < octaves; i++) {
value += amplitude * pxl8_value_noise(x * frequency, z * frequency, seed + (u64)i * 1000);
max_value += amplitude;
amplitude *= 0.5f;
frequency *= 2.0f;
}
return value / max_value;
}
f32 pxl8_fbm3d(f32 x, f32 y, f32 z, u64 seed, u32 octaves) {
f32 value = 0.0f;
f32 amplitude = 1.0f;
f32 frequency = 1.0f;
f32 max_value = 0.0f;
for (u32 i = 0; i < octaves; i++) {
value += amplitude * pxl8_value_noise3d(x * frequency, y * frequency, z * frequency, seed + (u64)i * 1000);
max_value += amplitude;
amplitude *= 0.5f;
frequency *= 2.0f;
}
return value / max_value;
}

18
src/math/pxl8_noise.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#include "pxl8_math.h"
#ifdef __cplusplus
extern "C" {
#endif
f32 pxl8_noise2d(i32 x, i32 z, u64 seed);
f32 pxl8_noise3d(i32 x, i32 y, i32 z, u64 seed);
f32 pxl8_value_noise(f32 x, f32 z, u64 seed);
f32 pxl8_value_noise3d(f32 x, f32 y, f32 z, u64 seed);
f32 pxl8_fbm(f32 x, f32 z, u64 seed, u32 octaves);
f32 pxl8_fbm3d(f32 x, f32 y, f32 z, u64 seed, u32 octaves);
#ifdef __cplusplus
}
#endif

View file

@ -1,11 +1,21 @@
#include "pxl8_net.h"
#include "pxl8_chunk_cache.h"
#include "pxl8_log.h"
#include "pxl8_mem.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_hal.h"
#ifdef PXL8_ASYNC_THREADS
#include <stdatomic.h>
#include "pxl8_queue.h"
#endif
#include "pxl8_bytes.h"
#include "pxl8_log.h"
#include "pxl8_mem.h"
#include "pxl8_world.h"
#include "pxl8_world_chunk_cache.h"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
@ -30,28 +40,42 @@
struct pxl8_net {
char address[256];
bool connected;
u16 port;
struct sockaddr_in server_addr;
socket_t sock;
pxl8_world_chunk_cache* chunk_cache;
u32 chunk_id;
u8 chunk_type;
pxl8_chunk_cache* chunk_cache;
bool connected;
pxl8_world* world;
f32 dt;
u64 highest_tick;
f32 interp_time;
u32 sequence;
pxl8_entity_state entities[PXL8_MAX_SNAPSHOT_ENTITIES];
pxl8_event_msg events[PXL8_MAX_SNAPSHOT_EVENTS];
u64 highest_tick;
pxl8_input_msg input_history[PXL8_NET_INPUT_HISTORY_SIZE];
u64 input_head;
u64 input_oldest_tick;
f32 interp_time;
u16 port;
u8 predicted_state[PXL8_NET_USERDATA_SIZE];
u64 predicted_tick;
pxl8_entity_state prev_entities[PXL8_MAX_SNAPSHOT_ENTITIES];
pxl8_snapshot_header prev_snapshot;
pxl8_snapshot_header snapshot;
u64 input_head;
pxl8_input_msg input_history[PXL8_NET_INPUT_HISTORY_SIZE];
u64 input_oldest_tick;
u8 predicted_state[PXL8_NET_USERDATA_SIZE];
u64 predicted_tick;
u8 recv_buf[4096];
u8 send_buf[4096];
u32 sequence;
struct sockaddr_in server_addr;
pxl8_snapshot_header snapshot;
socket_t sock;
#ifdef PXL8_ASYNC_THREADS
pxl8_thread* recv_thread;
atomic_bool running;
pxl8_queue recv_queue;
#endif
};
static const pxl8_entity_state* find_entity(const pxl8_entity_state* entities, u16 count, u64 id) {
@ -225,7 +249,7 @@ bool pxl8_net_poll(pxl8_net* net) {
payload_len = len - offset;
}
pxl8_chunk_cache_receive(net->chunk_cache, &chunk_hdr, payload, payload_len);
pxl8_world_chunk_cache_receive(net->chunk_cache, &chunk_hdr, payload, payload_len);
return true;
}
@ -238,6 +262,12 @@ bool pxl8_net_poll(pxl8_net* net) {
return true;
}
if (hdr.type == PXL8_MSG_CHUNK_EXIT) {
net->chunk_id = 0;
net->chunk_type = 0;
return true;
}
if (hdr.type != PXL8_MSG_SNAPSHOT) return false;
pxl8_snapshot_header snap;
@ -260,6 +290,10 @@ bool pxl8_net_poll(pxl8_net* net) {
net->recv_buf + offset, len - offset, &net->entities[i]);
}
if (net->world) {
pxl8_world_reconcile(net->world, net, 1.0f / 30.0f);
}
return true;
}
@ -312,6 +346,8 @@ pxl8_result pxl8_net_send_command(pxl8_net* net, const pxl8_command_msg* cmd) {
pxl8_result pxl8_net_send_input(pxl8_net* net, const pxl8_input_msg* input) {
if (!net || !net->connected) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_net_input_push(net, input);
u8 buf[sizeof(pxl8_msg_header) + sizeof(pxl8_input_msg)];
pxl8_msg_header hdr = {
.type = PXL8_MSG_INPUT,
@ -338,19 +374,25 @@ u64 pxl8_net_tick(const pxl8_net* net) {
void pxl8_net_update(pxl8_net* net, f32 dt) {
if (!net) return;
net->dt = dt;
net->interp_time += dt;
}
void pxl8_net_set_chunk_cache(pxl8_net* net, pxl8_chunk_cache* cache) {
void pxl8_net_set_chunk_cache(pxl8_net* net, pxl8_world_chunk_cache* cache) {
if (!net) return;
net->chunk_cache = cache;
}
pxl8_chunk_cache* pxl8_net_chunk_cache(pxl8_net* net) {
pxl8_world_chunk_cache* pxl8_net_chunk_cache(pxl8_net* net) {
if (!net) return NULL;
return net->chunk_cache;
}
void pxl8_net_set_world(pxl8_net* net, pxl8_world* world) {
if (!net) return;
net->world = world;
}
u32 pxl8_net_chunk_id(const pxl8_net* net) {
if (!net) return 0;
return net->chunk_id;
@ -360,3 +402,186 @@ u8 pxl8_net_chunk_type(const pxl8_net* net) {
if (!net) return PXL8_CHUNK_TYPE_VXL;
return net->chunk_type;
}
pxl8_result pxl8_net_send_chunk_settings(pxl8_net* net, i32 render_distance, i32 sim_distance) {
if (!net) return PXL8_ERROR_NULL_POINTER;
if (!net->connected) return PXL8_ERROR_NOT_CONNECTED;
pxl8_command_msg cmd = {0};
cmd.cmd_type = PXL8_CMD_SET_CHUNK_SETTINGS;
pxl8_pack_i32_be(cmd.payload, 0, render_distance);
pxl8_pack_i32_be(cmd.payload, 4, sim_distance);
cmd.payload_size = 8;
return pxl8_net_send_command(net, &cmd);
}
pxl8_result pxl8_net_spawn(pxl8_net* net, f32 x, f32 y, f32 z, f32 yaw, f32 pitch) {
if (!net) return PXL8_ERROR_NULL_POINTER;
if (!net->connected) return PXL8_ERROR_NOT_CONNECTED;
pxl8_command_msg cmd = {0};
cmd.cmd_type = PXL8_CMD_SPAWN_ENTITY;
pxl8_pack_f32_be(cmd.payload, 0, x);
pxl8_pack_f32_be(cmd.payload, 4, y);
pxl8_pack_f32_be(cmd.payload, 8, z);
pxl8_pack_f32_be(cmd.payload, 12, yaw);
pxl8_pack_f32_be(cmd.payload, 16, pitch);
cmd.payload_size = 20;
return pxl8_net_send_command(net, &cmd);
}
pxl8_result pxl8_net_exit_chunk(pxl8_net* net, f32 x, f32 y, f32 z) {
if (!net) return PXL8_ERROR_NULL_POINTER;
if (!net->connected) return PXL8_ERROR_NOT_CONNECTED;
pxl8_command_msg cmd = {0};
cmd.cmd_type = PXL8_CMD_EXIT_CHUNK;
pxl8_pack_f32_be(cmd.payload, 0, x);
pxl8_pack_f32_be(cmd.payload, 4, y);
pxl8_pack_f32_be(cmd.payload, 8, z);
cmd.payload_size = 12;
return pxl8_net_send_command(net, &cmd);
}
pxl8_result pxl8_net_enter_chunk(pxl8_net* net, u32 chunk_id) {
if (!net) return PXL8_ERROR_NULL_POINTER;
if (!net->connected) return PXL8_ERROR_NOT_CONNECTED;
pxl8_command_msg cmd = {0};
cmd.cmd_type = PXL8_CMD_ENTER_CHUNK;
pxl8_pack_u32_be(cmd.payload, 0, chunk_id);
cmd.payload_size = 4;
return pxl8_net_send_command(net, &cmd);
}
#ifdef PXL8_ASYNC_THREADS
static int pxl8_net_recv_thread(void* data) {
pxl8_net* net = (pxl8_net*)data;
u8 buf[PXL8_NET_PACKET_MAX_SIZE];
while (atomic_load(&net->running)) {
if (!net->connected) {
pxl8_sleep_ms(10);
continue;
}
struct sockaddr_in from;
socklen_t from_len = sizeof(from);
ssize_t received = recvfrom(net->sock, (char*)buf, sizeof(buf), 0,
(struct sockaddr*)&from, &from_len);
if (received > 0) {
pxl8_packet* pkt = pxl8_malloc(sizeof(pxl8_packet));
if (pkt) {
memcpy(pkt->data, buf, (usize)received);
pkt->len = (usize)received;
if (!pxl8_queue_push(&net->recv_queue, pkt)) {
pxl8_free(pkt);
}
}
} else {
pxl8_sleep_ms(1);
}
}
return 0;
}
void pxl8_net_start_thread(pxl8_net* net) {
if (!net || net->recv_thread) return;
pxl8_queue_init(&net->recv_queue);
atomic_store(&net->running, true);
net->recv_thread = pxl8_thread_create(pxl8_net_recv_thread, "pxl8_net_recv", net);
}
void pxl8_net_stop_thread(pxl8_net* net) {
if (!net || !net->recv_thread) return;
atomic_store(&net->running, false);
pxl8_thread_wait(net->recv_thread, NULL);
net->recv_thread = NULL;
pxl8_packet* pkt;
while ((pkt = pxl8_queue_pop(&net->recv_queue))) {
pxl8_free(pkt);
}
}
pxl8_packet* pxl8_net_pop_packet(pxl8_net* net) {
if (!net) return NULL;
return (pxl8_packet*)pxl8_queue_pop(&net->recv_queue);
}
void pxl8_net_packet_free(pxl8_packet* pkt) {
pxl8_free(pkt);
}
bool pxl8_net_process_packet(pxl8_net* net, const pxl8_packet* pkt) {
if (!net || !pkt || pkt->len < sizeof(pxl8_msg_header)) return false;
pxl8_msg_header hdr;
usize offset = pxl8_protocol_deserialize_header(pkt->data, pkt->len, &hdr);
if (hdr.type == PXL8_MSG_CHUNK) {
if (!net->chunk_cache) return false;
pxl8_chunk_msg_header chunk_hdr;
offset += pxl8_protocol_deserialize_chunk_msg_header(pkt->data + offset, pkt->len - offset, &chunk_hdr);
const u8* payload = pkt->data + offset;
usize payload_len = chunk_hdr.payload_size;
if (payload_len > pkt->len - offset) {
payload_len = pkt->len - offset;
}
pxl8_world_chunk_cache_receive(net->chunk_cache, &chunk_hdr, payload, payload_len);
return true;
}
if (hdr.type == PXL8_MSG_CHUNK_ENTER) {
pxl8_chunk_enter_msg chunk_msg;
pxl8_protocol_deserialize_chunk_enter(pkt->data + offset, pkt->len - offset, &chunk_msg);
net->chunk_id = chunk_msg.chunk_id;
net->chunk_type = chunk_msg.chunk_type;
return true;
}
if (hdr.type == PXL8_MSG_CHUNK_EXIT) {
net->chunk_id = 0;
net->chunk_type = 0;
return true;
}
if (hdr.type != PXL8_MSG_SNAPSHOT) return false;
pxl8_snapshot_header snap;
offset += pxl8_protocol_deserialize_snapshot_header(pkt->data + offset, pkt->len - offset, &snap);
if (snap.tick <= net->highest_tick) return false;
memcpy(net->prev_entities, net->entities, sizeof(net->entities));
net->prev_snapshot = net->snapshot;
net->highest_tick = snap.tick;
net->snapshot = snap;
net->interp_time = 0.0f;
u16 count = snap.entity_count;
if (count > PXL8_MAX_SNAPSHOT_ENTITIES) count = PXL8_MAX_SNAPSHOT_ENTITIES;
for (u16 i = 0; i < count; i++) {
offset += pxl8_protocol_deserialize_entity_state(
pkt->data + offset, pkt->len - offset, &net->entities[i]);
}
return true;
}
#endif

View file

@ -9,9 +9,16 @@ extern "C" {
#define PXL8_NET_INPUT_HISTORY_SIZE 64
#define PXL8_NET_USERDATA_SIZE 56
#define PXL8_NET_PACKET_MAX_SIZE 2048
typedef struct pxl8_net pxl8_net;
typedef struct pxl8_chunk_cache pxl8_chunk_cache;
typedef struct pxl8_world pxl8_world;
typedef struct pxl8_world_chunk_cache pxl8_world_chunk_cache;
typedef struct pxl8_packet {
u8 data[PXL8_NET_PACKET_MAX_SIZE];
usize len;
} pxl8_packet;
typedef struct pxl8_net_config {
const char* address;
@ -43,12 +50,27 @@ pxl8_result pxl8_net_send_input(pxl8_net* net, const pxl8_input_msg* input);
const pxl8_snapshot_header* pxl8_net_snapshot(const pxl8_net* net);
u64 pxl8_net_tick(const pxl8_net* net);
void pxl8_net_update(pxl8_net* net, f32 dt);
void pxl8_net_set_chunk_cache(pxl8_net* net, pxl8_chunk_cache* cache);
pxl8_chunk_cache* pxl8_net_chunk_cache(pxl8_net* net);
void pxl8_net_set_chunk_cache(pxl8_net* net, pxl8_world_chunk_cache* cache);
pxl8_world_chunk_cache* pxl8_net_chunk_cache(pxl8_net* net);
void pxl8_net_set_world(pxl8_net* net, pxl8_world* world);
u32 pxl8_net_chunk_id(const pxl8_net* net);
u8 pxl8_net_chunk_type(const pxl8_net* net);
pxl8_result pxl8_net_send_chunk_settings(pxl8_net* net, i32 render_distance, i32 sim_distance);
pxl8_result pxl8_net_spawn(pxl8_net* net, f32 x, f32 y, f32 z, f32 yaw, f32 pitch);
pxl8_result pxl8_net_exit_chunk(pxl8_net* net, f32 x, f32 y, f32 z);
pxl8_result pxl8_net_enter_chunk(pxl8_net* net, u32 chunk_id);
#ifdef PXL8_ASYNC_THREADS
void pxl8_net_start_thread(pxl8_net* net);
void pxl8_net_stop_thread(pxl8_net* net);
pxl8_packet* pxl8_net_pop_packet(pxl8_net* net);
void pxl8_net_packet_free(pxl8_packet* pkt);
bool pxl8_net_process_packet(pxl8_net* net, const pxl8_packet* pkt);
#endif
#ifdef __cplusplus
}
#endif

View file

@ -16,6 +16,7 @@ typedef enum pxl8_msg_type {
PXL8_MSG_NONE = 0,
PXL8_MSG_CHUNK,
PXL8_MSG_CHUNK_ENTER,
PXL8_MSG_CHUNK_EXIT,
PXL8_MSG_COMMAND,
PXL8_MSG_CONNECT,
PXL8_MSG_DISCONNECT,
@ -34,6 +35,9 @@ typedef struct pxl8_msg_header {
typedef enum pxl8_cmd_type {
PXL8_CMD_NONE = 0,
PXL8_CMD_SPAWN_ENTITY,
PXL8_CMD_EXIT_CHUNK,
PXL8_CMD_ENTER_CHUNK,
PXL8_CMD_SET_CHUNK_SETTINGS,
} pxl8_cmd_type;
typedef struct pxl8_input_msg {
@ -92,17 +96,17 @@ typedef struct pxl8_chunk_msg_header {
} pxl8_chunk_msg_header;
typedef struct pxl8_bsp_wire_header {
u32 num_vertices;
u32 num_cell_portals;
u32 num_edges;
u32 num_faces;
u32 num_planes;
u32 num_nodes;
u32 num_leafs;
u32 num_surfedges;
u32 num_marksurfaces;
u32 num_cell_portals;
u32 visdata_size;
u32 num_nodes;
u32 num_planes;
u32 num_surfedges;
u32 num_vertex_lights;
u32 num_vertices;
u32 visdata_size;
} pxl8_bsp_wire_header;
usize pxl8_protocol_serialize_header(const pxl8_msg_header* msg, u8* buf, usize len);

View file

@ -260,7 +260,7 @@ pxl8_script* pxl8_script_create(bool repl_mode) {
lua_getglobal(script->L, "require");
lua_pushstring(script->L, "ffi");
if (lua_pcall(script->L, 1, 1, 0) != 0) {
pxl8_script_set_error(script, lua_tostring(script->L, -1));
pxl8_error("FFI require failed: %s", lua_tostring(script->L, -1));
pxl8_script_destroy(script);
return NULL;
}
@ -268,7 +268,7 @@ pxl8_script* pxl8_script_create(bool repl_mode) {
lua_getfield(script->L, -1, "cdef");
lua_pushstring(script->L, pxl8_ffi_cdefs);
if (lua_pcall(script->L, 1, 0, 0) != 0) {
pxl8_script_set_error(script, lua_tostring(script->L, -1));
pxl8_error("FFI cdef failed: %s", lua_tostring(script->L, -1));
pxl8_script_destroy(script);
return NULL;
}

View file

@ -27,7 +27,11 @@ static const char* pxl8_ffi_cdefs =
"f32 pxl8_get_fps(const pxl8* sys);\n"
"\n"
"u8 pxl8_gfx_find_color(pxl8_gfx* gfx, u32 color);\n"
"u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx);\n"
"i32 pxl8_gfx_get_height(pxl8_gfx* ctx);\n"
"u32* pxl8_gfx_get_light_accum(pxl8_gfx* gfx);\n"
"const u32* pxl8_gfx_palette_colors(pxl8_gfx* gfx);\n"
"u16* pxl8_gfx_get_zbuffer(pxl8_gfx* gfx);\n"
"typedef struct pxl8_palette pxl8_palette;\n"
"pxl8_palette* pxl8_gfx_palette(pxl8_gfx* gfx);\n"
"u32 pxl8_palette_color(const pxl8_palette* pal, u8 idx);\n"
@ -59,7 +63,7 @@ static const char* pxl8_ffi_cdefs =
"void pxl8_gfx_clear_palette_cycles(pxl8_gfx* ctx);\n"
"void pxl8_gfx_update(pxl8_gfx* ctx, f32 dt);\n"
"i32 pxl8_gfx_load_palette(pxl8_gfx* ctx, const char* filepath);\n"
"i32 pxl8_gfx_load_sprite(pxl8_gfx* ctx, const char* filepath, u32* sprite_id);\n"
"i32 pxl8_gfx_load_sprite(pxl8_gfx* ctx, const char* filepath);\n"
"i32 pxl8_gfx_create_texture(pxl8_gfx* ctx, const u8* pixels, u32 width, u32 height);\n"
"bool pxl8_gfx_push_target(pxl8_gfx* ctx);\n"
"void pxl8_gfx_pop_target(pxl8_gfx* ctx);\n"
@ -336,22 +340,6 @@ static const char* pxl8_ffi_cdefs =
"pxl8_mat4 pxl8_mat4_scale(float x, float y, float z);\n"
"pxl8_mat4 pxl8_mat4_translate(float x, float y, float z);\n"
"\n"
"typedef enum pxl8_procgen_type {\n"
" PXL8_PROCGEN_ROOMS = 0,\n"
" PXL8_PROCGEN_TERRAIN = 1\n"
"} pxl8_procgen_type;\n"
"\n"
"typedef struct pxl8_procgen_params {\n"
" pxl8_procgen_type type;\n"
" int width;\n"
" int height;\n"
" int depth;\n"
" unsigned int seed;\n"
" int min_room_size;\n"
" int max_room_size;\n"
" int num_rooms;\n"
"} pxl8_procgen_params;\n"
"\n"
"typedef enum pxl8_graph_op {\n"
" PXL8_OP_CONST = 0,\n"
" PXL8_OP_INPUT_AGE,\n"
@ -424,33 +412,58 @@ static const char* pxl8_ffi_cdefs =
"void pxl8_graph_eval_texture(const pxl8_graph* graph, u8* buffer, i32 width, i32 height);\n"
"\n"
"typedef struct pxl8_bsp pxl8_bsp;\n"
"typedef struct pxl8_vxl_chunk pxl8_vxl_chunk;\n"
"\n"
"u32 pxl8_bsp_face_count(const pxl8_bsp* bsp);\n"
"pxl8_vec3 pxl8_bsp_face_normal(const pxl8_bsp* bsp, u32 face_id);\n"
"void pxl8_bsp_face_set_material(pxl8_bsp* bsp, u32 face_id, u16 material_id);\n"
"void pxl8_bsp_set_material(pxl8_bsp* bsp, u16 material_id, const pxl8_gfx_material* material);\n"
"void pxl8_bsp_set_wireframe(pxl8_bsp* bsp, bool wireframe);\n"
"\n"
"typedef enum { PXL8_CHUNK_VXL = 0, PXL8_CHUNK_BSP = 1 } pxl8_chunk_type;\n"
"typedef enum { PXL8_WORLD_CHUNK_VXL = 0, PXL8_WORLD_CHUNK_BSP = 1 } pxl8_world_chunk_type;\n"
"\n"
"typedef struct pxl8_chunk {\n"
" pxl8_chunk_type type;\n"
"typedef struct pxl8_world_chunk {\n"
" pxl8_world_chunk_type type;\n"
" u32 id;\n"
" u32 version;\n"
" i32 cx, cy, cz;\n"
" union {\n"
" void* voxel;\n"
" pxl8_bsp* bsp;\n"
" pxl8_vxl_chunk* voxels;\n"
" };\n"
"} pxl8_chunk;\n"
"} pxl8_world_chunk;\n"
"\n"
"typedef struct pxl8_world pxl8_world;\n"
"\n"
"typedef struct pxl8_ray {\n"
" pxl8_vec3 normal;\n"
" pxl8_vec3 point;\n"
" float fraction;\n"
" bool hit;\n"
"} pxl8_ray;\n"
"\n"
"pxl8_world* pxl8_get_world(pxl8* sys);\n"
"pxl8_chunk* pxl8_world_active_chunk(pxl8_world* world);\n"
"bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, float radius);\n"
"pxl8_world_chunk* pxl8_world_active_chunk(pxl8_world* world);\n"
"bool pxl8_world_point_solid(const pxl8_world* world, float x, float y, float z);\n"
"pxl8_ray pxl8_world_ray(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to);\n"
"pxl8_ray pxl8_world_sweep(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, float radius);\n"
"void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos);\n"
"pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, float radius);\n"
"void pxl8_world_set_bsp_material(pxl8_world* world, u16 material_id, const pxl8_gfx_material* material);\n"
"void pxl8_world_set_wireframe(pxl8_world* world, bool enabled);\n"
"i32 pxl8_world_get_render_distance(const pxl8_world* world);\n"
"void pxl8_world_set_render_distance(pxl8_world* world, i32 distance);\n"
"i32 pxl8_world_get_sim_distance(const pxl8_world* world);\n"
"void pxl8_world_set_sim_distance(pxl8_world* world, i32 distance);\n"
"\n"
"typedef struct pxl8_sim_entity {\n"
" pxl8_vec3 pos;\n"
" pxl8_vec3 vel;\n"
" f32 yaw;\n"
" f32 pitch;\n"
" u32 flags;\n"
" u16 kind;\n"
" u16 _pad;\n"
"} pxl8_sim_entity;\n"
"\n"
"void pxl8_world_init_local_player(pxl8_world* world, f32 x, f32 y, f32 z);\n"
"pxl8_sim_entity* pxl8_world_local_player(pxl8_world* world);\n"
"\n"
"typedef struct { i32 cursor_x; i32 cursor_y; bool cursor_down; bool cursor_clicked; u32 hot_id; u32 active_id; } pxl8_gui_state;\n"
"pxl8_gui_state* pxl8_gui_state_create(void);\n"
@ -461,6 +474,8 @@ static const char* pxl8_ffi_cdefs =
"void pxl8_gui_cursor_down(pxl8_gui_state* state);\n"
"void pxl8_gui_cursor_up(pxl8_gui_state* state);\n"
"bool pxl8_gui_button(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, const char* label);\n"
"bool pxl8_gui_slider(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, float* value, float min_val, float max_val);\n"
"bool pxl8_gui_slider_int(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, i32* value, i32 min_val, i32 max_val);\n"
"void pxl8_gui_window(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, const char* title);\n"
"void pxl8_gui_label(pxl8_gfx* gfx, i32 x, i32 y, const char* text, u8 color);\n"
"bool pxl8_gui_is_hovering(const pxl8_gui_state* state);\n"
@ -524,7 +539,7 @@ static const char* pxl8_ffi_cdefs =
"\n"
"typedef struct pxl8_net pxl8_net;\n"
"typedef struct pxl8_net_config { const char* address; u16 port; } pxl8_net_config;\n"
"typedef enum pxl8_cmd_type { PXL8_CMD_NONE = 0, PXL8_CMD_SPAWN_ENTITY } pxl8_cmd_type;\n"
"typedef enum pxl8_cmd_type { PXL8_CMD_NONE = 0, PXL8_CMD_SPAWN_ENTITY, PXL8_CMD_EXIT_CHUNK, PXL8_CMD_ENTER_CHUNK } pxl8_cmd_type;\n"
"\n"
"typedef struct pxl8_command_msg {\n"
" u16 cmd_type;\n"
@ -571,11 +586,17 @@ static const char* pxl8_ffi_cdefs =
"f32 pxl8_net_lerp_alpha(const pxl8_net* net);\n"
"bool pxl8_net_needs_correction(const pxl8_net* net);\n"
"u64 pxl8_net_player_id(const pxl8_net* net);\n"
"u8 pxl8_net_chunk_type(const pxl8_net* net);\n"
"u32 pxl8_net_chunk_id(const pxl8_net* net);\n"
"bool pxl8_net_poll(pxl8_net* net);\n"
"u8* pxl8_net_predicted_state(pxl8_net* net);\n"
"void pxl8_net_predicted_tick_set(pxl8_net* net, u64 tick);\n"
"i32 pxl8_net_send_command(pxl8_net* net, const pxl8_command_msg* cmd);\n"
"i32 pxl8_net_send_input(pxl8_net* net, const pxl8_input_msg* input);\n"
"i32 pxl8_net_send_chunk_settings(pxl8_net* net, i32 render_distance, i32 sim_distance);\n"
"i32 pxl8_net_spawn(pxl8_net* net, f32 x, f32 y, f32 z, f32 yaw, f32 pitch);\n"
"i32 pxl8_net_exit_chunk(pxl8_net* net, f32 x, f32 y, f32 z);\n"
"i32 pxl8_net_enter_chunk(pxl8_net* net, u32 chunk_id);\n"
"const pxl8_snapshot_header* pxl8_net_snapshot(const pxl8_net* net);\n"
"u64 pxl8_net_tick(const pxl8_net* net);\n"
"void pxl8_net_update(pxl8_net* net, f32 dt);\n"

224
src/sim/pxl8_sim.c Normal file
View file

@ -0,0 +1,224 @@
#include "pxl8_sim.h"
#include <math.h>
static usize vxl_world_to_local(f32 x, i32 chunk_coord) {
f32 chunk_base = chunk_coord * PXL8_VXL_WORLD_CHUNK_SIZE;
f32 local_world = x - chunk_base;
i32 local_voxel = (i32)floorf(local_world / PXL8_VXL_SCALE);
if (local_voxel < 0) return 0;
if (local_voxel >= PXL8_VXL_CHUNK_SIZE) return PXL8_VXL_CHUNK_SIZE - 1;
return (usize)local_voxel;
}
static i32 bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos) {
if (!bsp || bsp->num_nodes == 0) return -1;
i32 node_id = 0;
while (node_id >= 0) {
const pxl8_bsp_node* node = &bsp->nodes[node_id];
const pxl8_bsp_plane* plane = &bsp->planes[node->plane_id];
f32 dist = pxl8_vec3_dot(pos, plane->normal) - plane->dist;
node_id = node->children[dist < 0 ? 1 : 0];
}
return -(node_id + 1);
}
bool pxl8_bsp_point_solid(const pxl8_bsp* bsp, pxl8_vec3 pos) {
i32 leaf = bsp_find_leaf(bsp, pos);
if (leaf < 0 || (u32)leaf >= bsp->num_leafs) return true;
return bsp->leafs[leaf].contents == -1;
}
static bool bsp_point_clear(const pxl8_bsp* bsp, f32 x, f32 y, f32 z, f32 radius) {
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x, y, z})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x + radius, y, z})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x - radius, y, z})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x, y, z + radius})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x, y, z - radius})) return false;
return true;
}
pxl8_vec3 pxl8_bsp_trace(const pxl8_bsp* bsp, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
if (!bsp || bsp->num_nodes == 0) return to;
if (bsp_point_clear(bsp, to.x, to.y, to.z, radius)) {
return to;
}
bool x_ok = bsp_point_clear(bsp, to.x, from.y, from.z, radius);
bool y_ok = bsp_point_clear(bsp, from.x, to.y, from.z, radius);
bool z_ok = bsp_point_clear(bsp, from.x, from.y, to.z, radius);
pxl8_vec3 result = from;
if (x_ok) result.x = to.x;
if (y_ok) result.y = to.y;
if (z_ok) result.z = to.z;
return result;
}
bool pxl8_vxl_point_solid(const pxl8_vxl_chunk* chunk, i32 cx, i32 cy, i32 cz,
f32 x, f32 y, f32 z) {
if (!chunk) return false;
usize lx = vxl_world_to_local(x, cx);
usize ly = vxl_world_to_local(y, cy);
usize lz = vxl_world_to_local(z, cz);
return pxl8_vxl_block_get(chunk, (i32)lx, (i32)ly, (i32)lz) != PXL8_VXL_BLOCK_AIR;
}
static bool vxl_point_clear(const pxl8_vxl_chunk* chunk, i32 cx, i32 cy, i32 cz,
f32 x, f32 y, f32 z, f32 radius) {
if (pxl8_vxl_point_solid(chunk, cx, cy, cz, x - radius, y, z)) return false;
if (pxl8_vxl_point_solid(chunk, cx, cy, cz, x + radius, y, z)) return false;
if (pxl8_vxl_point_solid(chunk, cx, cy, cz, x, y - radius, z)) return false;
if (pxl8_vxl_point_solid(chunk, cx, cy, cz, x, y + radius, z)) return false;
if (pxl8_vxl_point_solid(chunk, cx, cy, cz, x, y, z - radius)) return false;
if (pxl8_vxl_point_solid(chunk, cx, cy, cz, x, y, z + radius)) return false;
return true;
}
pxl8_vec3 pxl8_vxl_trace(const pxl8_vxl_chunk* chunk, i32 cx, i32 cy, i32 cz,
pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
if (!chunk) return to;
if (vxl_point_clear(chunk, cx, cy, cz, to.x, to.y, to.z, radius)) {
return to;
}
bool x_ok = vxl_point_clear(chunk, cx, cy, cz, to.x, from.y, from.z, radius);
bool y_ok = vxl_point_clear(chunk, cx, cy, cz, from.x, to.y, from.z, radius);
bool z_ok = vxl_point_clear(chunk, cx, cy, cz, from.x, from.y, to.z, radius);
pxl8_vec3 result = from;
if (x_ok) result.x = to.x;
if (y_ok) result.y = to.y;
if (z_ok) result.z = to.z;
return result;
}
pxl8_vec3 pxl8_sim_trace(const pxl8_sim_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
if (!world) return to;
if (world->bsp) {
return pxl8_bsp_trace(world->bsp, from, to, radius);
}
if (world->vxl) {
return pxl8_vxl_trace(world->vxl, world->vxl_cx, world->vxl_cy, world->vxl_cz,
from, to, radius);
}
return to;
}
bool pxl8_sim_check_ground(const pxl8_sim_world* world, pxl8_vec3 pos, f32 radius) {
if (!world) return true;
if (!world->bsp && !world->vxl) return true;
pxl8_vec3 down = {pos.x, pos.y - 2.0f, pos.z};
pxl8_vec3 result = pxl8_sim_trace(world, pos, down, radius);
return result.y > down.y;
}
void pxl8_sim_move_player(pxl8_sim_entity* ent, const pxl8_input_msg* input,
const pxl8_sim_world* world, f32 dt) {
if (!ent || !input) return;
if (!(ent->flags & PXL8_SIM_FLAG_ALIVE)) return;
ent->yaw -= input->look_dx * 0.008f;
f32 new_pitch = ent->pitch - input->look_dy * 0.008f;
if (new_pitch > PXL8_SIM_MAX_PITCH) new_pitch = PXL8_SIM_MAX_PITCH;
if (new_pitch < -PXL8_SIM_MAX_PITCH) new_pitch = -PXL8_SIM_MAX_PITCH;
ent->pitch = new_pitch;
f32 sin_yaw = sinf(ent->yaw);
f32 cos_yaw = cosf(ent->yaw);
f32 input_len = sqrtf(input->move_x * input->move_x + input->move_y * input->move_y);
pxl8_vec3 move_dir = {0, 0, 0};
f32 target_speed = 0.0f;
if (input_len > 0.0f) {
f32 nx = input->move_x / input_len;
f32 ny = input->move_y / input_len;
move_dir.x = cos_yaw * nx - sin_yaw * ny;
move_dir.z = -sin_yaw * nx - cos_yaw * ny;
target_speed = PXL8_SIM_MOVE_SPEED;
}
ent->vel.x = move_dir.x * target_speed;
ent->vel.z = move_dir.z * target_speed;
bool grounded = (ent->flags & PXL8_SIM_FLAG_GROUNDED) != 0;
if (grounded && (input->buttons & 1)) {
ent->vel.y = PXL8_SIM_JUMP_VELOCITY;
ent->flags &= ~PXL8_SIM_FLAG_GROUNDED;
grounded = false;
}
if (!grounded) {
ent->vel.y -= PXL8_SIM_GRAVITY * dt;
}
pxl8_vec3 target = {
ent->pos.x + ent->vel.x * dt,
ent->pos.y + ent->vel.y * dt,
ent->pos.z + ent->vel.z * dt
};
pxl8_vec3 new_pos = pxl8_sim_trace(world, ent->pos, target, PXL8_SIM_PLAYER_RADIUS);
ent->pos = new_pos;
if (pxl8_sim_check_ground(world, ent->pos, PXL8_SIM_PLAYER_RADIUS)) {
ent->flags |= PXL8_SIM_FLAG_GROUNDED;
if (ent->vel.y < 0) ent->vel.y = 0;
} else {
ent->flags &= ~PXL8_SIM_FLAG_GROUNDED;
}
}
void pxl8_sim_integrate(pxl8_sim_entity* ent, const pxl8_sim_world* world, f32 dt) {
if (!ent) return;
if (!(ent->flags & PXL8_SIM_FLAG_ALIVE)) return;
if (ent->flags & PXL8_SIM_FLAG_PLAYER) return;
bool grounded = (ent->flags & PXL8_SIM_FLAG_GROUNDED) != 0;
ent->vel.y -= PXL8_SIM_GRAVITY * dt;
if (grounded) {
f32 speed = sqrtf(ent->vel.x * ent->vel.x + ent->vel.z * ent->vel.z);
if (speed > 0.0f) {
f32 drop = speed * PXL8_SIM_FRICTION * dt;
f32 new_speed = speed - drop;
if (new_speed < 0.0f) new_speed = 0.0f;
f32 scale = new_speed / speed;
ent->vel.x *= scale;
ent->vel.z *= scale;
}
}
pxl8_vec3 target = {
ent->pos.x + ent->vel.x * dt,
ent->pos.y + ent->vel.y * dt,
ent->pos.z + ent->vel.z * dt
};
pxl8_vec3 new_pos = pxl8_sim_trace(world, ent->pos, target, PXL8_SIM_PLAYER_RADIUS);
ent->pos = new_pos;
if (pxl8_sim_check_ground(world, ent->pos, PXL8_SIM_PLAYER_RADIUS)) {
ent->flags |= PXL8_SIM_FLAG_GROUNDED;
if (ent->vel.y < 0.0f) ent->vel.y = 0.0f;
} else {
ent->flags &= ~PXL8_SIM_FLAG_GROUNDED;
}
}

56
src/sim/pxl8_sim.h Normal file
View file

@ -0,0 +1,56 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_math.h"
#include "pxl8_protocol.h"
#include "pxl8_types.h"
#include "pxl8_vxl.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_SIM_FLAG_ALIVE (1 << 0)
#define PXL8_SIM_FLAG_PLAYER (1 << 1)
#define PXL8_SIM_FLAG_GROUNDED (1 << 2)
#define PXL8_SIM_MOVE_SPEED 180.0f
#define PXL8_SIM_GROUND_ACCEL 10.0f
#define PXL8_SIM_AIR_ACCEL 1.0f
#define PXL8_SIM_STOP_SPEED 100.0f
#define PXL8_SIM_FRICTION 6.0f
#define PXL8_SIM_GRAVITY 800.0f
#define PXL8_SIM_JUMP_VELOCITY 200.0f
#define PXL8_SIM_PLAYER_RADIUS 16.0f
#define PXL8_SIM_MAX_PITCH 1.5f
typedef struct pxl8_sim_entity {
pxl8_vec3 pos;
pxl8_vec3 vel;
f32 yaw;
f32 pitch;
u32 flags;
u16 kind;
u16 _pad;
} pxl8_sim_entity;
typedef struct pxl8_sim_world {
const pxl8_bsp* bsp;
const pxl8_vxl_chunk* vxl;
i32 vxl_cx, vxl_cy, vxl_cz;
} pxl8_sim_world;
bool pxl8_bsp_point_solid(const pxl8_bsp* bsp, pxl8_vec3 pos);
pxl8_vec3 pxl8_bsp_trace(const pxl8_bsp* bsp, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
bool pxl8_vxl_point_solid(const pxl8_vxl_chunk* chunk, i32 cx, i32 cy, i32 cz, f32 x, f32 y, f32 z);
pxl8_vec3 pxl8_vxl_trace(const pxl8_vxl_chunk* chunk, i32 cx, i32 cy, i32 cz, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
pxl8_vec3 pxl8_sim_trace(const pxl8_sim_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
bool pxl8_sim_check_ground(const pxl8_sim_world* world, pxl8_vec3 pos, f32 radius);
void pxl8_sim_move_player(pxl8_sim_entity* ent, const pxl8_input_msg* input, const pxl8_sim_world* world, f32 dt);
void pxl8_sim_integrate(pxl8_sim_entity* ent, const pxl8_sim_world* world, f32 dt);
#ifdef __cplusplus
}
#endif

319
src/vxl/pxl8_vxl.c Normal file
View file

@ -0,0 +1,319 @@
#include "pxl8_vxl.h"
#include <string.h>
#include "pxl8_mem.h"
typedef struct pxl8_vxl_block_def {
char name[32];
u8 texture_top;
u8 texture_side;
u8 texture_bottom;
pxl8_vxl_geometry geometry;
bool registered;
} pxl8_vxl_block_def;
struct pxl8_vxl_block_registry {
pxl8_vxl_block_def blocks[PXL8_VXL_BLOCK_COUNT];
};
static inline bool vxl_in_bounds(i32 x, i32 y, i32 z) {
return x >= 0 && x < PXL8_VXL_CHUNK_SIZE &&
y >= 0 && y < PXL8_VXL_CHUNK_SIZE &&
z >= 0 && z < PXL8_VXL_CHUNK_SIZE;
}
static inline u32 vxl_index(i32 x, i32 y, i32 z) {
return (u32)(x + y * PXL8_VXL_CHUNK_SIZE + z * PXL8_VXL_CHUNK_SIZE * PXL8_VXL_CHUNK_SIZE);
}
static pxl8_vxl_octree_node* octree_alloc_children(void) {
pxl8_vxl_octree_node* children = pxl8_calloc(8, sizeof(pxl8_vxl_octree_node));
for (i32 i = 0; i < 8; i++) {
children[i].type = PXL8_VXL_OCTREE_UNIFORM;
children[i].block = PXL8_VXL_BLOCK_AIR;
}
return children;
}
static void octree_free_node(pxl8_vxl_octree_node* node) {
if (node->type == PXL8_VXL_OCTREE_BRANCH && node->children) {
for (i32 i = 0; i < 8; i++) {
octree_free_node(&node->children[i]);
}
pxl8_free(node->children);
node->children = NULL;
}
node->type = PXL8_VXL_OCTREE_UNIFORM;
node->block = PXL8_VXL_BLOCK_AIR;
}
static u8 octree_get(const pxl8_vxl_octree_node* node, i32 depth, i32 x, i32 y, i32 z) {
if (node->type == PXL8_VXL_OCTREE_UNIFORM) {
return node->block;
}
if (depth == 0 || !node->children) {
return PXL8_VXL_BLOCK_AIR;
}
i32 half = 1 << (depth - 1);
i32 idx = ((x >= half) ? 1 : 0) |
((y >= half) ? 2 : 0) |
((z >= half) ? 4 : 0);
return octree_get(&node->children[idx], depth - 1,
x & (half - 1), y & (half - 1), z & (half - 1));
}
static void octree_subdivide(pxl8_vxl_octree_node* node) {
if (node->type == PXL8_VXL_OCTREE_BRANCH) return;
u8 block = node->block;
node->type = PXL8_VXL_OCTREE_BRANCH;
node->children = octree_alloc_children();
for (i32 i = 0; i < 8; i++) {
node->children[i].type = PXL8_VXL_OCTREE_UNIFORM;
node->children[i].block = block;
}
}
static bool octree_try_simplify(pxl8_vxl_octree_node* node) {
if (node->type != PXL8_VXL_OCTREE_BRANCH || !node->children) {
return false;
}
for (i32 i = 0; i < 8; i++) {
if (node->children[i].type != PXL8_VXL_OCTREE_UNIFORM) {
return false;
}
}
u8 first_block = node->children[0].block;
for (i32 i = 1; i < 8; i++) {
if (node->children[i].block != first_block) {
return false;
}
}
pxl8_free(node->children);
node->children = NULL;
node->type = PXL8_VXL_OCTREE_UNIFORM;
node->block = first_block;
return true;
}
static void octree_set(pxl8_vxl_octree_node* node, i32 depth, i32 x, i32 y, i32 z, u8 block) {
if (depth == 0) {
if (node->type == PXL8_VXL_OCTREE_BRANCH) {
octree_free_node(node);
}
node->type = PXL8_VXL_OCTREE_UNIFORM;
node->block = block;
return;
}
if (node->type == PXL8_VXL_OCTREE_UNIFORM) {
if (node->block == block) {
return;
}
octree_subdivide(node);
}
i32 half = 1 << (depth - 1);
i32 idx = ((x >= half) ? 1 : 0) |
((y >= half) ? 2 : 0) |
((z >= half) ? 4 : 0);
octree_set(&node->children[idx], depth - 1,
x & (half - 1), y & (half - 1), z & (half - 1), block);
octree_try_simplify(node);
}
static void octree_linearize_recursive(const pxl8_vxl_octree_node* node, i32 depth,
i32 ox, i32 oy, i32 oz, u8* out) {
i32 size = 1 << depth;
if (node->type == PXL8_VXL_OCTREE_UNIFORM) {
for (i32 z = 0; z < size; z++) {
for (i32 y = 0; y < size; y++) {
for (i32 x = 0; x < size; x++) {
out[vxl_index(ox + x, oy + y, oz + z)] = node->block;
}
}
}
return;
}
if (depth == 0 || !node->children) return;
i32 half = size / 2;
for (i32 i = 0; i < 8; i++) {
i32 cx = ox + ((i & 1) ? half : 0);
i32 cy = oy + ((i & 2) ? half : 0);
i32 cz = oz + ((i & 4) ? half : 0);
octree_linearize_recursive(&node->children[i], depth - 1, cx, cy, cz, out);
}
}
static void octree_build_recursive(pxl8_vxl_octree_node* node, i32 depth,
i32 ox, i32 oy, i32 oz, const u8* data) {
i32 size = 1 << depth;
if (depth == 0) {
node->type = PXL8_VXL_OCTREE_UNIFORM;
node->block = data[vxl_index(ox, oy, oz)];
return;
}
u8 first_block = data[vxl_index(ox, oy, oz)];
bool all_same = true;
for (i32 z = 0; z < size && all_same; z++) {
for (i32 y = 0; y < size && all_same; y++) {
for (i32 x = 0; x < size && all_same; x++) {
if (data[vxl_index(ox + x, oy + y, oz + z)] != first_block) {
all_same = false;
}
}
}
}
if (all_same) {
node->type = PXL8_VXL_OCTREE_UNIFORM;
node->block = first_block;
return;
}
node->type = PXL8_VXL_OCTREE_BRANCH;
node->children = octree_alloc_children();
i32 half = size / 2;
for (i32 i = 0; i < 8; i++) {
i32 cx = ox + ((i & 1) ? half : 0);
i32 cy = oy + ((i & 2) ? half : 0);
i32 cz = oz + ((i & 4) ? half : 0);
octree_build_recursive(&node->children[i], depth - 1, cx, cy, cz, data);
}
octree_try_simplify(node);
}
pxl8_vxl_chunk* pxl8_vxl_chunk_create(void) {
pxl8_vxl_chunk* chunk = pxl8_calloc(1, sizeof(pxl8_vxl_chunk));
if (!chunk) return NULL;
chunk->root.type = PXL8_VXL_OCTREE_UNIFORM;
chunk->root.block = PXL8_VXL_BLOCK_AIR;
return chunk;
}
void pxl8_vxl_chunk_destroy(pxl8_vxl_chunk* chunk) {
if (!chunk) return;
octree_free_node(&chunk->root);
pxl8_free(chunk);
}
u8 pxl8_vxl_block_get(const pxl8_vxl_chunk* chunk, i32 x, i32 y, i32 z) {
if (!chunk || !vxl_in_bounds(x, y, z)) return PXL8_VXL_BLOCK_AIR;
return octree_get(&chunk->root, PXL8_VXL_OCTREE_DEPTH, x, y, z);
}
void pxl8_vxl_block_set(pxl8_vxl_chunk* chunk, i32 x, i32 y, i32 z, u8 block) {
if (!chunk || !vxl_in_bounds(x, y, z)) return;
octree_set(&chunk->root, PXL8_VXL_OCTREE_DEPTH, x, y, z, block);
}
void pxl8_vxl_block_fill(pxl8_vxl_chunk* chunk, u8 block) {
if (!chunk) return;
octree_free_node(&chunk->root);
chunk->root.type = PXL8_VXL_OCTREE_UNIFORM;
chunk->root.block = block;
}
void pxl8_vxl_block_clear(pxl8_vxl_chunk* chunk) {
pxl8_vxl_block_fill(chunk, PXL8_VXL_BLOCK_AIR);
}
void pxl8_vxl_chunk_linearize(const pxl8_vxl_chunk* chunk, u8* out) {
if (!chunk || !out) return;
octree_linearize_recursive(&chunk->root, PXL8_VXL_OCTREE_DEPTH, 0, 0, 0, out);
}
void pxl8_vxl_chunk_from_linear(pxl8_vxl_chunk* chunk, const u8* data) {
if (!chunk || !data) return;
octree_free_node(&chunk->root);
octree_build_recursive(&chunk->root, PXL8_VXL_OCTREE_DEPTH, 0, 0, 0, data);
}
bool pxl8_vxl_chunk_is_uniform(const pxl8_vxl_chunk* chunk) {
if (!chunk) return true;
return chunk->root.type == PXL8_VXL_OCTREE_UNIFORM;
}
u8 pxl8_vxl_chunk_uniform_block(const pxl8_vxl_chunk* chunk) {
if (!chunk) return PXL8_VXL_BLOCK_AIR;
if (chunk->root.type == PXL8_VXL_OCTREE_UNIFORM) {
return chunk->root.block;
}
return PXL8_VXL_BLOCK_AIR;
}
pxl8_vxl_block_registry* pxl8_vxl_block_registry_create(void) {
pxl8_vxl_block_registry* registry = pxl8_calloc(1, sizeof(pxl8_vxl_block_registry));
if (!registry) return NULL;
pxl8_vxl_block_registry_register(registry, 1, "stone", 8, PXL8_VXL_GEOMETRY_CUBE);
pxl8_vxl_block_registry_register(registry, 2, "dirt", 52, PXL8_VXL_GEOMETRY_CUBE);
pxl8_vxl_block_registry_register_ex(registry, 3, "grass", 200, 52, 52, PXL8_VXL_GEOMETRY_CUBE);
pxl8_vxl_block_registry_register_ex(registry, 4, "grass_slope_n", 200, 52, 52, PXL8_VXL_GEOMETRY_SLOPE_NORTH);
pxl8_vxl_block_registry_register_ex(registry, 5, "grass_slope_s", 200, 52, 52, PXL8_VXL_GEOMETRY_SLOPE_SOUTH);
pxl8_vxl_block_registry_register_ex(registry, 6, "grass_slope_e", 200, 52, 52, PXL8_VXL_GEOMETRY_SLOPE_EAST);
pxl8_vxl_block_registry_register_ex(registry, 7, "grass_slope_w", 200, 52, 52, PXL8_VXL_GEOMETRY_SLOPE_WEST);
return registry;
}
void pxl8_vxl_block_registry_destroy(pxl8_vxl_block_registry* registry) {
pxl8_free(registry);
}
void pxl8_vxl_block_registry_register(pxl8_vxl_block_registry* registry, u8 id, const char* name, u8 texture_id, pxl8_vxl_geometry geo) {
pxl8_vxl_block_registry_register_ex(registry, id, name, texture_id, texture_id, texture_id, geo);
}
void pxl8_vxl_block_registry_register_ex(pxl8_vxl_block_registry* registry, u8 id, const char* name,
u8 texture_top, u8 texture_side, u8 texture_bottom, pxl8_vxl_geometry geo) {
if (!registry || id == PXL8_VXL_BLOCK_AIR) return;
pxl8_vxl_block_def* def = &registry->blocks[id];
strncpy(def->name, name ? name : "", sizeof(def->name) - 1);
def->texture_top = texture_top;
def->texture_side = texture_side;
def->texture_bottom = texture_bottom;
def->geometry = geo;
def->registered = true;
}
const char* pxl8_vxl_block_registry_name(const pxl8_vxl_block_registry* registry, u8 id) {
if (!registry || !registry->blocks[id].registered) return NULL;
return registry->blocks[id].name;
}
pxl8_vxl_geometry pxl8_vxl_block_registry_geometry(const pxl8_vxl_block_registry* registry, u8 id) {
if (!registry || !registry->blocks[id].registered) return PXL8_VXL_GEOMETRY_CUBE;
return registry->blocks[id].geometry;
}
u8 pxl8_vxl_block_registry_texture(const pxl8_vxl_block_registry* registry, u8 id) {
if (!registry || !registry->blocks[id].registered) return 0;
return registry->blocks[id].texture_top;
}
u8 pxl8_vxl_block_registry_texture_for_face(const pxl8_vxl_block_registry* registry, u8 id, i32 face) {
if (!registry || !registry->blocks[id].registered) return 0;
if (face == 3) return registry->blocks[id].texture_top;
if (face == 2) return registry->blocks[id].texture_bottom;
return registry->blocks[id].texture_side;
}

78
src/vxl/pxl8_vxl.h Normal file
View file

@ -0,0 +1,78 @@
#pragma once
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_VXL_BLOCK_COUNT 256
#define PXL8_VXL_CHUNK_SIZE 32
#define PXL8_VXL_CHUNK_VOLUME (PXL8_VXL_CHUNK_SIZE * PXL8_VXL_CHUNK_SIZE * PXL8_VXL_CHUNK_SIZE)
#define PXL8_VXL_SCALE 16.0f
#define PXL8_VXL_WORLD_CHUNK_SIZE (PXL8_VXL_CHUNK_SIZE * PXL8_VXL_SCALE)
#define PXL8_VXL_OCTREE_DEPTH 5
#define PXL8_VXL_BLOCK_AIR 0
#define PXL8_VXL_BLOCK_UNKNOWN 255
typedef struct pxl8_vxl_block_registry pxl8_vxl_block_registry;
typedef enum pxl8_vxl_octree_type {
PXL8_VXL_OCTREE_UNIFORM = 0,
PXL8_VXL_OCTREE_BRANCH = 1
} pxl8_vxl_octree_type;
typedef struct pxl8_vxl_octree_node {
u8 type;
union {
u8 block;
struct pxl8_vxl_octree_node* children;
};
} pxl8_vxl_octree_node;
typedef struct pxl8_vxl_chunk {
pxl8_vxl_octree_node root;
} pxl8_vxl_chunk;
typedef enum pxl8_vxl_geometry {
PXL8_VXL_GEOMETRY_CUBE,
PXL8_VXL_GEOMETRY_SLAB_BOTTOM,
PXL8_VXL_GEOMETRY_SLAB_TOP,
PXL8_VXL_GEOMETRY_SLOPE_NORTH,
PXL8_VXL_GEOMETRY_SLOPE_SOUTH,
PXL8_VXL_GEOMETRY_SLOPE_EAST,
PXL8_VXL_GEOMETRY_SLOPE_WEST,
PXL8_VXL_GEOMETRY_STAIRS_NORTH,
PXL8_VXL_GEOMETRY_STAIRS_SOUTH,
PXL8_VXL_GEOMETRY_STAIRS_EAST,
PXL8_VXL_GEOMETRY_STAIRS_WEST
} pxl8_vxl_geometry;
pxl8_vxl_chunk* pxl8_vxl_chunk_create(void);
void pxl8_vxl_chunk_destroy(pxl8_vxl_chunk* chunk);
u8 pxl8_vxl_block_get(const pxl8_vxl_chunk* chunk, i32 x, i32 y, i32 z);
void pxl8_vxl_block_set(pxl8_vxl_chunk* chunk, i32 x, i32 y, i32 z, u8 block);
void pxl8_vxl_block_fill(pxl8_vxl_chunk* chunk, u8 block);
void pxl8_vxl_block_clear(pxl8_vxl_chunk* chunk);
void pxl8_vxl_chunk_linearize(const pxl8_vxl_chunk* chunk, u8* out);
void pxl8_vxl_chunk_from_linear(pxl8_vxl_chunk* chunk, const u8* data);
bool pxl8_vxl_chunk_is_uniform(const pxl8_vxl_chunk* chunk);
u8 pxl8_vxl_chunk_uniform_block(const pxl8_vxl_chunk* chunk);
pxl8_vxl_block_registry* pxl8_vxl_block_registry_create(void);
void pxl8_vxl_block_registry_destroy(pxl8_vxl_block_registry* registry);
void pxl8_vxl_block_registry_register(pxl8_vxl_block_registry* registry, u8 id, const char* name, u8 texture_id, pxl8_vxl_geometry geo);
void pxl8_vxl_block_registry_register_ex(pxl8_vxl_block_registry* registry, u8 id, const char* name,
u8 texture_top, u8 texture_side, u8 texture_bottom, pxl8_vxl_geometry geo);
const char* pxl8_vxl_block_registry_name(const pxl8_vxl_block_registry* registry, u8 id);
pxl8_vxl_geometry pxl8_vxl_block_registry_geometry(const pxl8_vxl_block_registry* registry, u8 id);
u8 pxl8_vxl_block_registry_texture(const pxl8_vxl_block_registry* registry, u8 id);
u8 pxl8_vxl_block_registry_texture_for_face(const pxl8_vxl_block_registry* registry, u8 id, i32 face);
#ifdef __cplusplus
}
#endif

576
src/vxl/pxl8_vxl_render.c Normal file
View file

@ -0,0 +1,576 @@
#include "pxl8_vxl_render.h"
#include <string.h>
#include "pxl8_mem.h"
#include "pxl8_noise.h"
#include "pxl8_vxl.h"
typedef struct pxl8_vxl_greedy_mask {
u8 block[PXL8_VXL_CHUNK_SIZE][PXL8_VXL_CHUNK_SIZE];
bool done[PXL8_VXL_CHUNK_SIZE][PXL8_VXL_CHUNK_SIZE];
} pxl8_vxl_greedy_mask;
static const pxl8_vec3 face_normals[6] = {
{-1, 0, 0}, { 1, 0, 0},
{ 0, -1, 0}, { 0, 1, 0},
{ 0, 0, -1}, { 0, 0, 1}
};
static const i32 face_dirs[6][3] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}
};
static inline bool vxl_in_bounds(i32 x, i32 y, i32 z) {
return x >= 0 && x < PXL8_VXL_CHUNK_SIZE &&
y >= 0 && y < PXL8_VXL_CHUNK_SIZE &&
z >= 0 && z < PXL8_VXL_CHUNK_SIZE;
}
static bool block_is_opaque(u8 block) {
return block != PXL8_VXL_BLOCK_AIR && block != PXL8_VXL_BLOCK_UNKNOWN;
}
static bool block_is_full_cube(u8 block, const pxl8_vxl_block_registry* registry) {
if (block == PXL8_VXL_BLOCK_AIR) return false;
pxl8_vxl_geometry geo = pxl8_vxl_block_registry_geometry(registry, block);
return geo == PXL8_VXL_GEOMETRY_CUBE;
}
static u8 get_block_or_neighbor(const pxl8_vxl_chunk* chunk, const pxl8_vxl_chunk** neighbors, i32 x, i32 y, i32 z) {
if (vxl_in_bounds(x, y, z)) {
return pxl8_vxl_block_get(chunk, x, y, z);
}
i32 nx = x, ny = y, nz = z;
i32 neighbor_idx = -1;
if (x < 0) { neighbor_idx = 0; nx = x + PXL8_VXL_CHUNK_SIZE; }
else if (x >= PXL8_VXL_CHUNK_SIZE) { neighbor_idx = 1; nx = x - PXL8_VXL_CHUNK_SIZE; }
else if (y < 0) { neighbor_idx = 2; ny = y + PXL8_VXL_CHUNK_SIZE; }
else if (y >= PXL8_VXL_CHUNK_SIZE) { neighbor_idx = 3; ny = y - PXL8_VXL_CHUNK_SIZE; }
else if (z < 0) { neighbor_idx = 4; nz = z + PXL8_VXL_CHUNK_SIZE; }
else if (z >= PXL8_VXL_CHUNK_SIZE) { neighbor_idx = 5; nz = z - PXL8_VXL_CHUNK_SIZE; }
if (neighbor_idx >= 0 && neighbors && neighbors[neighbor_idx]) {
return pxl8_vxl_block_get(neighbors[neighbor_idx], nx, ny, nz);
}
return PXL8_VXL_BLOCK_UNKNOWN;
}
static f32 compute_ao(const pxl8_vxl_chunk* chunk, const pxl8_vxl_chunk** neighbors,
i32 x, i32 y, i32 z, i32 dx1, i32 dy1, i32 dz1, i32 dx2, i32 dy2, i32 dz2) {
u8 s1 = get_block_or_neighbor(chunk, neighbors, x + dx1, y + dy1, z + dz1);
u8 s2 = get_block_or_neighbor(chunk, neighbors, x + dx2, y + dy2, z + dz2);
u8 corner = get_block_or_neighbor(chunk, neighbors, x + dx1 + dx2, y + dy1 + dy2, z + dz1 + dz2);
bool side1 = block_is_opaque(s1);
bool side2 = block_is_opaque(s2);
bool has_corner = block_is_opaque(corner);
if (side1 && side2) return 0.0f;
return (3.0f - (f32)side1 - (f32)side2 - (f32)has_corner) / 3.0f;
}
static f32 sample_corner_displacement(const pxl8_vxl_chunk* chunk, const pxl8_vxl_chunk** neighbors,
i32 cx, i32 cz, i32 base_y) {
f32 sum = 0.0f;
i32 count = 0;
for (i32 dz = -1; dz <= 0; dz++) {
for (i32 dx = -1; dx <= 0; dx++) {
i32 x = cx + dx;
i32 z = cz + dz;
u8 block = get_block_or_neighbor(chunk, neighbors, x, base_y, z);
u8 above = get_block_or_neighbor(chunk, neighbors, x, base_y + 1, z);
if (block_is_opaque(block) && !block_is_opaque(above)) {
sum += 1.0f;
} else if (!block_is_opaque(block)) {
sum -= 1.0f;
}
count++;
}
}
return count > 0 ? (sum / (f32)count) * 0.15f : 0.0f;
}
static void slice_to_world(i32 face, i32 slice, i32 u, i32 v, i32* x, i32* y, i32* z) {
switch (face) {
case 0: *x = slice; *y = v; *z = u; break;
case 1: *x = slice; *y = v; *z = u; break;
case 2: *x = u; *y = slice; *z = v; break;
case 3: *x = u; *y = slice; *z = v; break;
case 4: *x = u; *y = v; *z = slice; break;
case 5: *x = u; *y = v; *z = slice; break;
}
}
static f32 terrain_noise(f32 wx, f32 wz, u64 seed) {
f32 noise = pxl8_fbm(wx * 0.08f, wz * 0.08f, seed, 3);
return (noise - 0.5f) * 0.4f;
}
static void emit_greedy_quad(pxl8_mesh* mesh, const pxl8_vxl_chunk* chunk,
const pxl8_vxl_chunk** neighbors,
const pxl8_vxl_mesh_config* config,
i32 face, i32 slice,
i32 u, i32 v, i32 width, i32 height,
u8 texture_id, f32 ao_avg, f32 ao_strength) {
pxl8_vec3 normal = face_normals[face];
u8 light = (u8)(255.0f * (1.0f - ao_strength * (1.0f - ao_avg)));
f32 p[4][3];
switch (face) {
case 0:
p[0][0] = (f32)slice; p[0][1] = (f32)v; p[0][2] = (f32)(u + width);
p[1][0] = (f32)slice; p[1][1] = (f32)v; p[1][2] = (f32)u;
p[2][0] = (f32)slice; p[2][1] = (f32)(v + height); p[2][2] = (f32)u;
p[3][0] = (f32)slice; p[3][1] = (f32)(v + height); p[3][2] = (f32)(u + width);
break;
case 1:
p[0][0] = (f32)(slice + 1); p[0][1] = (f32)v; p[0][2] = (f32)u;
p[1][0] = (f32)(slice + 1); p[1][1] = (f32)v; p[1][2] = (f32)(u + width);
p[2][0] = (f32)(slice + 1); p[2][1] = (f32)(v + height); p[2][2] = (f32)(u + width);
p[3][0] = (f32)(slice + 1); p[3][1] = (f32)(v + height); p[3][2] = (f32)u;
break;
case 2:
p[0][0] = (f32)u; p[0][1] = (f32)slice; p[0][2] = (f32)v;
p[1][0] = (f32)(u + width); p[1][1] = (f32)slice; p[1][2] = (f32)v;
p[2][0] = (f32)(u + width); p[2][1] = (f32)slice; p[2][2] = (f32)(v + height);
p[3][0] = (f32)u; p[3][1] = (f32)slice; p[3][2] = (f32)(v + height);
break;
case 3: {
f32 base_y = (f32)(slice + 1);
f32 wx0 = (f32)(config->chunk_x * PXL8_VXL_CHUNK_SIZE + u);
f32 wz0 = (f32)(config->chunk_z * PXL8_VXL_CHUNK_SIZE + v);
f32 d0 = sample_corner_displacement(chunk, neighbors, u, v + height, slice);
f32 d1 = sample_corner_displacement(chunk, neighbors, u + width, v + height, slice);
f32 d2 = sample_corner_displacement(chunk, neighbors, u + width, v, slice);
f32 d3 = sample_corner_displacement(chunk, neighbors, u, v, slice);
f32 n0 = terrain_noise(wx0, wz0 + (f32)height, config->seed);
f32 n1 = terrain_noise(wx0 + (f32)width, wz0 + (f32)height, config->seed);
f32 n2 = terrain_noise(wx0 + (f32)width, wz0, config->seed);
f32 n3 = terrain_noise(wx0, wz0, config->seed);
p[0][0] = (f32)u; p[0][1] = base_y + d0 + n0; p[0][2] = (f32)(v + height);
p[1][0] = (f32)(u + width); p[1][1] = base_y + d1 + n1; p[1][2] = (f32)(v + height);
p[2][0] = (f32)(u + width); p[2][1] = base_y + d2 + n2; p[2][2] = (f32)v;
p[3][0] = (f32)u; p[3][1] = base_y + d3 + n3; p[3][2] = (f32)v;
pxl8_vec3 e1 = {p[1][0] - p[0][0], p[1][1] - p[0][1], p[1][2] - p[0][2]};
pxl8_vec3 e2 = {p[3][0] - p[0][0], p[3][1] - p[0][1], p[3][2] - p[0][2]};
normal = (pxl8_vec3){
e1.y * e2.z - e1.z * e2.y,
e1.z * e2.x - e1.x * e2.z,
e1.x * e2.y - e1.y * e2.x
};
f32 len_sq = normal.x * normal.x + normal.y * normal.y + normal.z * normal.z;
if (len_sq > 0.0001f) {
f32 inv_len = pxl8_fast_inv_sqrt(len_sq);
normal.x *= inv_len; normal.y *= inv_len; normal.z *= inv_len;
} else {
normal = (pxl8_vec3){0, 1, 0};
}
break;
}
case 4:
p[0][0] = (f32)u; p[0][1] = (f32)v; p[0][2] = (f32)slice;
p[1][0] = (f32)u; p[1][1] = (f32)(v + height); p[1][2] = (f32)slice;
p[2][0] = (f32)(u + width); p[2][1] = (f32)(v + height); p[2][2] = (f32)slice;
p[3][0] = (f32)(u + width); p[3][1] = (f32)v; p[3][2] = (f32)slice;
break;
case 5:
p[0][0] = (f32)(u + width); p[0][1] = (f32)v; p[0][2] = (f32)(slice + 1);
p[1][0] = (f32)(u + width); p[1][1] = (f32)(v + height); p[1][2] = (f32)(slice + 1);
p[2][0] = (f32)u; p[2][1] = (f32)(v + height); p[2][2] = (f32)(slice + 1);
p[3][0] = (f32)u; p[3][1] = (f32)v; p[3][2] = (f32)(slice + 1);
break;
}
f32 tex_u = (f32)width;
f32 tex_v = (f32)height;
pxl8_vertex verts[4];
for (i32 i = 0; i < 4; i++) {
verts[i].position.x = p[i][0];
verts[i].position.y = p[i][1];
verts[i].position.z = p[i][2];
verts[i].normal = normal;
verts[i].color = texture_id;
verts[i].light = light;
}
verts[0].u = 0; verts[0].v = tex_v;
verts[1].u = tex_u; verts[1].v = tex_v;
verts[2].u = tex_u; verts[2].v = 0;
verts[3].u = 0; verts[3].v = 0;
u16 i0 = pxl8_mesh_push_vertex(mesh, verts[0]);
u16 i1 = pxl8_mesh_push_vertex(mesh, verts[1]);
u16 i2 = pxl8_mesh_push_vertex(mesh, verts[2]);
u16 i3 = pxl8_mesh_push_vertex(mesh, verts[3]);
pxl8_mesh_push_quad(mesh, i0, i1, i2, i3);
}
static void build_greedy_slice(const pxl8_vxl_chunk* chunk,
const pxl8_vxl_chunk** neighbors,
const pxl8_vxl_block_registry* registry,
const pxl8_vxl_mesh_config* config,
i32 face, i32 slice,
pxl8_mesh* mesh) {
pxl8_vxl_greedy_mask mask;
memset(&mask, 0, sizeof(mask));
i32 dx = face_dirs[face][0];
i32 dy = face_dirs[face][1];
i32 dz = face_dirs[face][2];
for (i32 v = 0; v < PXL8_VXL_CHUNK_SIZE; v++) {
for (i32 u = 0; u < PXL8_VXL_CHUNK_SIZE; u++) {
i32 x, y, z;
slice_to_world(face, slice, u, v, &x, &y, &z);
u8 block = pxl8_vxl_block_get(chunk, x, y, z);
if (block == PXL8_VXL_BLOCK_AIR) continue;
pxl8_vxl_geometry geo = pxl8_vxl_block_registry_geometry(registry, block);
if (geo != PXL8_VXL_GEOMETRY_CUBE) continue;
i32 nx = x + dx;
i32 ny = y + dy;
i32 nz = z + dz;
u8 neighbor = get_block_or_neighbor(chunk, neighbors, nx, ny, nz);
if (neighbor == PXL8_VXL_BLOCK_UNKNOWN) continue;
if (block_is_full_cube(neighbor, registry)) continue;
mask.block[u][v] = block;
}
}
f32 ao_strength = config->ambient_occlusion ? config->ao_strength : 0.0f;
for (i32 v = 0; v < PXL8_VXL_CHUNK_SIZE; v++) {
for (i32 u = 0; u < PXL8_VXL_CHUNK_SIZE; u++) {
if (mask.done[u][v] || mask.block[u][v] == 0) continue;
u8 block = mask.block[u][v];
u8 texture_id = pxl8_vxl_block_registry_texture_for_face(registry, block, face);
i32 width = 1;
while (u + width < PXL8_VXL_CHUNK_SIZE &&
!mask.done[u + width][v] &&
mask.block[u + width][v] == block) {
width++;
}
i32 height = 1;
bool can_expand = true;
while (can_expand && v + height < PXL8_VXL_CHUNK_SIZE) {
for (i32 wu = 0; wu < width; wu++) {
if (mask.done[u + wu][v + height] ||
mask.block[u + wu][v + height] != block) {
can_expand = false;
break;
}
}
if (can_expand) height++;
}
for (i32 dv = 0; dv < height; dv++) {
for (i32 du = 0; du < width; du++) {
mask.done[u + du][v + dv] = true;
}
}
f32 ao_sum = 0.0f;
i32 ao_count = 0;
if (config->ambient_occlusion) {
i32 cx, cy, cz;
slice_to_world(face, slice, u, v, &cx, &cy, &cz);
i32 fx = cx + dx;
i32 fy = cy + dy;
i32 fz = cz + dz;
static const i32 ao_offsets[6][4][2][3] = {
[0] = {{{0,-1,0}, {0,0,1}}, {{0,-1,0}, {0,0,-1}}, {{0,1,0}, {0,0,-1}}, {{0,1,0}, {0,0,1}}},
[1] = {{{0,-1,0}, {0,0,-1}}, {{0,-1,0}, {0,0,1}}, {{0,1,0}, {0,0,1}}, {{0,1,0}, {0,0,-1}}},
[2] = {{{-1,0,0}, {0,0,-1}}, {{1,0,0}, {0,0,-1}}, {{1,0,0}, {0,0,1}}, {{-1,0,0}, {0,0,1}}},
[3] = {{{-1,0,0}, {0,0,1}}, {{1,0,0}, {0,0,1}}, {{1,0,0}, {0,0,-1}}, {{-1,0,0}, {0,0,-1}}},
[4] = {{{-1,0,0}, {0,1,0}}, {{-1,0,0}, {0,-1,0}}, {{1,0,0}, {0,-1,0}}, {{1,0,0}, {0,1,0}}},
[5] = {{{1,0,0}, {0,-1,0}}, {{1,0,0}, {0,1,0}}, {{-1,0,0}, {0,1,0}}, {{-1,0,0}, {0,-1,0}}}
};
for (i32 corner = 0; corner < 4; corner++) {
f32 ao = compute_ao(chunk, neighbors, fx, fy, fz,
ao_offsets[face][corner][0][0],
ao_offsets[face][corner][0][1],
ao_offsets[face][corner][0][2],
ao_offsets[face][corner][1][0],
ao_offsets[face][corner][1][1],
ao_offsets[face][corner][1][2]);
ao_sum += ao;
ao_count++;
}
}
f32 ao_avg = ao_count > 0 ? ao_sum / (f32)ao_count : 1.0f;
emit_greedy_quad(mesh, chunk, neighbors, config, face, slice, u, v, width, height,
texture_id, ao_avg, ao_strength);
}
}
}
static void add_face_vertices(pxl8_mesh* mesh, const pxl8_vxl_mesh_config* config,
pxl8_vec3 pos, i32 face, u8 texture_id,
f32 ao0, f32 ao1, f32 ao2, f32 ao3) {
static const i32 face_vertices[6][4][3] = {
{{0,0,1}, {0,0,0}, {0,1,0}, {0,1,1}},
{{1,0,0}, {1,0,1}, {1,1,1}, {1,1,0}},
{{0,0,0}, {1,0,0}, {1,0,1}, {0,0,1}},
{{0,1,1}, {1,1,1}, {1,1,0}, {0,1,0}},
{{0,0,0}, {0,1,0}, {1,1,0}, {1,0,0}},
{{1,0,1}, {1,1,1}, {0,1,1}, {0,0,1}}
};
static const f32 face_uvs[4][2] = {
{0, 1}, {1, 1}, {1, 0}, {0, 0}
};
f32 ao_values[4] = {ao0, ao1, ao2, ao3};
f32 ao_strength = config->ambient_occlusion ? config->ao_strength : 0.0f;
f32 tex_scale = config->texture_scale;
pxl8_vec3 normal = face_normals[face];
u16 indices[4];
for (i32 i = 0; i < 4; i++) {
pxl8_vertex v = {0};
v.position.x = pos.x + (f32)face_vertices[face][i][0];
v.position.y = pos.y + (f32)face_vertices[face][i][1];
v.position.z = pos.z + (f32)face_vertices[face][i][2];
v.normal = normal;
v.u = face_uvs[i][0] * tex_scale;
v.v = face_uvs[i][1] * tex_scale;
v.color = texture_id;
v.light = (u8)(255.0f * (1.0f - ao_strength * (1.0f - ao_values[i])));
indices[i] = pxl8_mesh_push_vertex(mesh, v);
}
if (ao0 + ao2 > ao1 + ao3) {
pxl8_mesh_push_quad(mesh, indices[0], indices[1], indices[2], indices[3]);
} else {
pxl8_mesh_push_triangle(mesh, indices[0], indices[1], indices[2]);
pxl8_mesh_push_triangle(mesh, indices[0], indices[2], indices[3]);
}
}
static void add_slab_faces(pxl8_mesh* mesh, const pxl8_vxl_chunk* chunk,
const pxl8_vxl_chunk** neighbors, const pxl8_vxl_block_registry* registry,
const pxl8_vxl_mesh_config* config,
i32 x, i32 y, i32 z, u8 block, bool top) {
u8 texture_id = pxl8_vxl_block_registry_texture(registry, block);
f32 y_offset = top ? 0.5f : 0.0f;
pxl8_vec3 pos = {(f32)x, (f32)y + y_offset, (f32)z};
static const i32 horiz_faces[4] = {0, 1, 4, 5};
for (i32 i = 0; i < 4; i++) {
i32 face = horiz_faces[i];
i32 nx = x + face_dirs[face][0];
i32 nz = z + face_dirs[face][2];
u8 neighbor = get_block_or_neighbor(chunk, neighbors, nx, y, nz);
if (neighbor == PXL8_VXL_BLOCK_UNKNOWN) continue;
if (block_is_full_cube(neighbor, registry)) continue;
add_face_vertices(mesh, config, pos, face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
i32 top_face = 3;
i32 bot_face = 2;
if (top) {
u8 above = get_block_or_neighbor(chunk, neighbors, x, y + 1, z);
if (above != PXL8_VXL_BLOCK_UNKNOWN && !block_is_full_cube(above, registry)) {
add_face_vertices(mesh, config, pos, top_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
add_face_vertices(mesh, config, (pxl8_vec3){(f32)x, (f32)y + 0.5f, (f32)z}, bot_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
} else {
add_face_vertices(mesh, config, pos, top_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
u8 below = get_block_or_neighbor(chunk, neighbors, x, y - 1, z);
if (below != PXL8_VXL_BLOCK_UNKNOWN && !block_is_full_cube(below, registry)) {
add_face_vertices(mesh, config, pos, bot_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
}
}
static void add_slope_faces(pxl8_mesh* mesh, const pxl8_vxl_chunk* chunk,
const pxl8_vxl_chunk** neighbors, const pxl8_vxl_block_registry* registry,
const pxl8_vxl_mesh_config* config,
i32 x, i32 y, i32 z, u8 block, i32 direction) {
u8 texture_top = pxl8_vxl_block_registry_texture_for_face(registry, block, 3);
u8 texture_side = pxl8_vxl_block_registry_texture_for_face(registry, block, 0);
pxl8_vec3 pos = {(f32)x, (f32)y, (f32)z};
u8 below = get_block_or_neighbor(chunk, neighbors, x, y - 1, z);
if (below != PXL8_VXL_BLOCK_UNKNOWN && !block_is_full_cube(below, registry)) {
add_face_vertices(mesh, config, pos, 2, texture_side, 1.0f, 1.0f, 1.0f, 1.0f);
}
static const i32 dir_to_back_face[4] = {5, 4, 1, 0};
i32 back_face = dir_to_back_face[direction];
i32 bx = x + face_dirs[back_face][0];
i32 bz = z + face_dirs[back_face][2];
u8 back_neighbor = get_block_or_neighbor(chunk, neighbors, bx, y, bz);
if (back_neighbor != PXL8_VXL_BLOCK_UNKNOWN && !block_is_full_cube(back_neighbor, registry)) {
add_face_vertices(mesh, config, pos, back_face, texture_side, 1.0f, 1.0f, 1.0f, 1.0f);
}
static const f32 slope_verts[4][4][3] = {
{{0,0,0}, {1,0,0}, {1,1,1}, {0,1,1}},
{{0,0,1}, {1,0,1}, {1,1,0}, {0,1,0}},
{{1,0,0}, {1,0,1}, {0,1,1}, {0,1,0}},
{{0,0,0}, {0,0,1}, {1,1,1}, {1,1,0}}
};
static const pxl8_vec3 slope_normals[4] = {
{0, 0.707f, -0.707f}, {0, 0.707f, 0.707f},
{-0.707f, 0.707f, 0}, {0.707f, 0.707f, 0}
};
pxl8_vertex verts[4];
memset(verts, 0, sizeof(verts));
for (i32 i = 0; i < 4; i++) {
verts[i].position.x = pos.x + slope_verts[direction][i][0];
verts[i].position.y = pos.y + slope_verts[direction][i][1];
verts[i].position.z = pos.z + slope_verts[direction][i][2];
verts[i].normal = slope_normals[direction];
verts[i].color = texture_top;
verts[i].light = 255;
}
u16 i0 = pxl8_mesh_push_vertex(mesh, verts[0]);
u16 i1 = pxl8_mesh_push_vertex(mesh, verts[1]);
u16 i2 = pxl8_mesh_push_vertex(mesh, verts[2]);
u16 i3 = pxl8_mesh_push_vertex(mesh, verts[3]);
pxl8_mesh_push_quad(mesh, i0, i1, i2, i3);
static const i32 side_faces[4][2] = {{0, 1}, {0, 1}, {4, 5}, {4, 5}};
static const f32 side_tris[4][2][3][3] = {
{{{0,0,0}, {0,1,1}, {0,0,1}}, {{1,0,0}, {1,0,1}, {1,1,1}}},
{{{0,0,0}, {0,0,1}, {0,1,0}}, {{1,0,0}, {1,1,0}, {1,0,1}}},
{{{0,0,0}, {0,0,1}, {0,1,1}}, {{1,0,0}, {1,1,0}, {1,0,1}}},
{{{0,0,0}, {0,1,0}, {0,0,1}}, {{1,0,0}, {1,0,1}, {1,1,1}}}
};
static const pxl8_vec3 side_normals[6] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}
};
for (i32 s = 0; s < 2; s++) {
i32 side_face = side_faces[direction][s];
i32 snx = x + face_dirs[side_face][0];
i32 snz = z + face_dirs[side_face][2];
u8 side_neighbor = get_block_or_neighbor(chunk, neighbors, snx, y, snz);
if (side_neighbor != PXL8_VXL_BLOCK_UNKNOWN && !block_is_full_cube(side_neighbor, registry)) {
pxl8_vertex tv[3];
memset(tv, 0, sizeof(tv));
for (i32 vi = 0; vi < 3; vi++) {
tv[vi].position.x = pos.x + side_tris[direction][s][vi][0];
tv[vi].position.y = pos.y + side_tris[direction][s][vi][1];
tv[vi].position.z = pos.z + side_tris[direction][s][vi][2];
tv[vi].normal = side_normals[side_face];
tv[vi].color = texture_side;
tv[vi].light = 255;
}
u16 ti0 = pxl8_mesh_push_vertex(mesh, tv[0]);
u16 ti1 = pxl8_mesh_push_vertex(mesh, tv[1]);
u16 ti2 = pxl8_mesh_push_vertex(mesh, tv[2]);
pxl8_mesh_push_triangle(mesh, ti0, ti1, ti2);
}
}
}
pxl8_mesh* pxl8_vxl_build_mesh(const pxl8_vxl_chunk* chunk,
const pxl8_vxl_chunk** neighbors,
const pxl8_vxl_block_registry* registry,
const pxl8_vxl_mesh_config* config) {
if (!chunk || !registry) return NULL;
pxl8_vxl_mesh_config cfg = config ? *config : PXL8_VXL_MESH_CONFIG_DEFAULT;
u32 max_faces = PXL8_VXL_CHUNK_VOLUME * 6;
pxl8_mesh* mesh = pxl8_mesh_create(max_faces * 4, max_faces * 6);
if (!mesh) return NULL;
for (i32 face = 0; face < 6; face++) {
for (i32 slice = 0; slice < PXL8_VXL_CHUNK_SIZE; slice++) {
build_greedy_slice(chunk, neighbors, registry, &cfg, face, slice, mesh);
}
}
for (i32 z = 0; z < PXL8_VXL_CHUNK_SIZE; z++) {
for (i32 y = 0; y < PXL8_VXL_CHUNK_SIZE; y++) {
for (i32 x = 0; x < PXL8_VXL_CHUNK_SIZE; x++) {
u8 block = pxl8_vxl_block_get(chunk, x, y, z);
if (block == PXL8_VXL_BLOCK_AIR) continue;
pxl8_vxl_geometry geo = pxl8_vxl_block_registry_geometry(registry, block);
switch (geo) {
case PXL8_VXL_GEOMETRY_CUBE:
break;
case PXL8_VXL_GEOMETRY_SLAB_BOTTOM:
add_slab_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, false);
break;
case PXL8_VXL_GEOMETRY_SLAB_TOP:
add_slab_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, true);
break;
case PXL8_VXL_GEOMETRY_SLOPE_NORTH:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 0);
break;
case PXL8_VXL_GEOMETRY_SLOPE_SOUTH:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 1);
break;
case PXL8_VXL_GEOMETRY_SLOPE_EAST:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 2);
break;
case PXL8_VXL_GEOMETRY_SLOPE_WEST:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 3);
break;
case PXL8_VXL_GEOMETRY_STAIRS_NORTH:
case PXL8_VXL_GEOMETRY_STAIRS_SOUTH:
case PXL8_VXL_GEOMETRY_STAIRS_EAST:
case PXL8_VXL_GEOMETRY_STAIRS_WEST:
break;
}
}
}
}
return mesh;
}
pxl8_vxl_render_state* pxl8_vxl_render_state_create(void) {
return pxl8_calloc(1, sizeof(pxl8_vxl_render_state));
}
void pxl8_vxl_render_state_destroy(pxl8_vxl_render_state* state) {
pxl8_free(state);
}
void pxl8_vxl_set_wireframe(pxl8_vxl_render_state* state, bool wireframe) {
if (!state) return;
state->wireframe = wireframe;
}

48
src/vxl/pxl8_vxl_render.h Normal file
View file

@ -0,0 +1,48 @@
#pragma once
#include "pxl8_mesh.h"
#include "pxl8_types.h"
#include "pxl8_vxl.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct pxl8_vxl_render_state {
bool wireframe;
} pxl8_vxl_render_state;
pxl8_vxl_render_state* pxl8_vxl_render_state_create(void);
void pxl8_vxl_render_state_destroy(pxl8_vxl_render_state* state);
void pxl8_vxl_set_wireframe(pxl8_vxl_render_state* state, bool wireframe);
typedef struct pxl8_vxl_mesh_config {
bool ambient_occlusion;
bool vertex_heights;
f32 ao_strength;
f32 texture_scale;
i32 chunk_x;
i32 chunk_y;
i32 chunk_z;
u64 seed;
} pxl8_vxl_mesh_config;
#define PXL8_VXL_MESH_CONFIG_DEFAULT ((pxl8_vxl_mesh_config){ \
.ambient_occlusion = true, \
.vertex_heights = true, \
.ao_strength = 0.2f, \
.texture_scale = 1.0f, \
.chunk_x = 0, \
.chunk_y = 0, \
.chunk_z = 0, \
.seed = 12345 \
})
pxl8_mesh* pxl8_vxl_build_mesh(const pxl8_vxl_chunk* chunk,
const pxl8_vxl_chunk** neighbors,
const pxl8_vxl_block_registry* registry,
const pxl8_vxl_mesh_config* config);
#ifdef __cplusplus
}
#endif

View file

@ -1,44 +0,0 @@
#include "pxl8_chunk.h"
#include "pxl8_mem.h"
pxl8_chunk* pxl8_chunk_create_vxl(i32 cx, i32 cy, i32 cz) {
pxl8_chunk* chunk = pxl8_calloc(1, sizeof(pxl8_chunk));
if (!chunk) return NULL;
chunk->type = PXL8_CHUNK_VXL;
chunk->cx = cx;
chunk->cy = cy;
chunk->cz = cz;
chunk->voxel = pxl8_voxel_chunk_create();
if (!chunk->voxel) {
pxl8_free(chunk);
return NULL;
}
return chunk;
}
pxl8_chunk* pxl8_chunk_create_bsp(u32 id) {
pxl8_chunk* chunk = pxl8_calloc(1, sizeof(pxl8_chunk));
if (!chunk) return NULL;
chunk->type = PXL8_CHUNK_BSP;
chunk->id = id;
chunk->bsp = NULL;
return chunk;
}
void pxl8_chunk_destroy(pxl8_chunk* chunk) {
if (!chunk) return;
if (chunk->type == PXL8_CHUNK_VXL && chunk->voxel) {
pxl8_voxel_chunk_destroy(chunk->voxel);
} else if (chunk->type == PXL8_CHUNK_BSP && chunk->bsp) {
pxl8_bsp_destroy(chunk->bsp);
}
pxl8_free(chunk);
}

View file

@ -1,33 +0,0 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_types.h"
#include "pxl8_voxel.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum pxl8_chunk_type {
PXL8_CHUNK_VXL,
PXL8_CHUNK_BSP
} pxl8_chunk_type;
typedef struct pxl8_chunk {
pxl8_chunk_type type;
u32 id;
u32 version;
i32 cx, cy, cz;
union {
pxl8_voxel_chunk* voxel;
pxl8_bsp* bsp;
};
} pxl8_chunk;
pxl8_chunk* pxl8_chunk_create_vxl(i32 cx, i32 cy, i32 cz);
pxl8_chunk* pxl8_chunk_create_bsp(u32 id);
void pxl8_chunk_destroy(pxl8_chunk* chunk);
#ifdef __cplusplus
}
#endif

View file

@ -1,67 +0,0 @@
#pragma once
#include "pxl8_chunk.h"
#include "pxl8_mesh.h"
#include "pxl8_protocol.h"
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_CHUNK_CACHE_SIZE 64
#define PXL8_CHUNK_MAX_FRAGMENTS 64
#define PXL8_CHUNK_MAX_DATA_SIZE 131072
typedef struct pxl8_chunk_cache_entry {
pxl8_chunk* chunk;
pxl8_mesh* mesh;
u64 last_used;
bool mesh_dirty;
bool valid;
} pxl8_chunk_cache_entry;
typedef struct pxl8_chunk_assembly {
pxl8_chunk_type type;
u32 id;
i32 cx, cy, cz;
u32 version;
u8 fragment_count;
u8 fragments_received;
u8* data;
u32 data_size;
u32 data_capacity;
bool active;
bool complete;
} pxl8_chunk_assembly;
typedef struct pxl8_chunk_cache {
pxl8_chunk_cache_entry entries[PXL8_CHUNK_CACHE_SIZE];
pxl8_chunk_assembly assembly;
u32 entry_count;
u64 frame_counter;
} pxl8_chunk_cache;
pxl8_chunk_cache* pxl8_chunk_cache_create(void);
void pxl8_chunk_cache_destroy(pxl8_chunk_cache* cache);
pxl8_result pxl8_chunk_cache_receive(pxl8_chunk_cache* cache,
const pxl8_chunk_msg_header* hdr,
const u8* payload, usize len);
pxl8_chunk* pxl8_chunk_cache_get_vxl(pxl8_chunk_cache* cache, i32 cx, i32 cy, i32 cz);
pxl8_chunk* pxl8_chunk_cache_get_bsp(pxl8_chunk_cache* cache, u32 id);
pxl8_mesh* pxl8_chunk_cache_get_mesh(pxl8_chunk_cache* cache,
i32 cx, i32 cy, i32 cz,
const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config);
void pxl8_chunk_cache_tick(pxl8_chunk_cache* cache);
void pxl8_chunk_cache_evict_distant(pxl8_chunk_cache* cache,
i32 cx, i32 cy, i32 cz, i32 radius);
void pxl8_chunk_cache_invalidate_meshes(pxl8_chunk_cache* cache);
#ifdef __cplusplus
}
#endif

View file

@ -1,898 +0,0 @@
#include "pxl8_gen.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include "pxl8_log.h"
#include "pxl8_mem.h"
#include "pxl8_rng.h"
#define CELL_SIZE 64.0f
#define PVS_MAX_DEPTH 64
#define WALL_HEIGHT 128.0f
typedef struct room_grid {
u8* cells;
i32 width;
i32 height;
} room_grid;
typedef struct bsp_build_context {
pxl8_bsp* bsp;
const room_grid* grid;
u32 node_count;
u32 plane_offset;
} bsp_build_context;
typedef struct light_source {
pxl8_vec3 position;
f32 intensity;
f32 radius;
} light_source;
static bool room_grid_init(room_grid* grid, i32 width, i32 height) {
grid->width = width;
grid->height = height;
grid->cells = pxl8_calloc(width * height, sizeof(u8));
return grid->cells != NULL;
}
static u8 room_grid_get(const room_grid* grid, i32 x, i32 y) {
if (x < 0 || x >= grid->width || y < 0 || y >= grid->height) {
return 1;
}
return grid->cells[y * grid->width + x];
}
static void room_grid_set(room_grid* grid, i32 x, i32 y, u8 value) {
if (x < 0 || x >= grid->width || y < 0 || y >= grid->height) {
return;
}
grid->cells[y * grid->width + x] = value;
}
static inline void compute_face_aabb(pxl8_bsp_face* face, const pxl8_bsp_vertex* verts, u32 vert_idx) {
face->aabb_min = (pxl8_vec3){1e30f, 1e30f, 1e30f};
face->aabb_max = (pxl8_vec3){-1e30f, -1e30f, -1e30f};
for (u32 i = 0; i < 4; i++) {
pxl8_vec3 v = verts[vert_idx + i].position;
if (v.x < face->aabb_min.x) face->aabb_min.x = v.x;
if (v.x > face->aabb_max.x) face->aabb_max.x = v.x;
if (v.y < face->aabb_min.y) face->aabb_min.y = v.y;
if (v.y > face->aabb_max.y) face->aabb_max.y = v.y;
if (v.z < face->aabb_min.z) face->aabb_min.z = v.z;
if (v.z > face->aabb_max.z) face->aabb_max.z = v.z;
}
}
static void room_grid_fill(room_grid* grid, u8 value) {
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
room_grid_set(grid, x, y, value);
}
}
}
static f32 compute_vertex_light(
pxl8_vec3 pos,
pxl8_vec3 normal,
const light_source* lights,
u32 num_lights,
f32 ambient
) {
f32 total = ambient;
for (u32 i = 0; i < num_lights; i++) {
pxl8_vec3 to_light = {
lights[i].position.x - pos.x,
lights[i].position.y - pos.y,
lights[i].position.z - pos.z
};
f32 dist_sq = to_light.x * to_light.x + to_light.y * to_light.y + to_light.z * to_light.z;
f32 dist = sqrtf(dist_sq);
if (dist > lights[i].radius) continue;
if (dist < 1.0f) dist = 1.0f;
f32 inv_dist = 1.0f / dist;
pxl8_vec3 light_dir = {
to_light.x * inv_dist,
to_light.y * inv_dist,
to_light.z * inv_dist
};
f32 ndotl = normal.x * light_dir.x + normal.y * light_dir.y + normal.z * light_dir.z;
if (ndotl < 0) ndotl = 0;
f32 attenuation = 1.0f - (dist / lights[i].radius);
if (attenuation < 0) attenuation = 0;
attenuation *= attenuation;
total += lights[i].intensity * ndotl * attenuation;
}
if (total > 1.0f) total = 1.0f;
return total;
}
static void compute_bsp_vertex_lighting(
pxl8_bsp* bsp,
const light_source* lights,
u32 num_lights,
f32 ambient
) {
if (!bsp || bsp->num_vertices == 0) return;
bsp->vertex_lights = pxl8_calloc(bsp->num_vertices, sizeof(u32));
if (!bsp->vertex_lights) return;
bsp->num_vertex_lights = bsp->num_vertices;
for (u32 f = 0; f < bsp->num_faces; f++) {
const pxl8_bsp_face* face = &bsp->faces[f];
pxl8_vec3 normal = {0, 1, 0};
if (face->plane_id < bsp->num_planes) {
normal = bsp->planes[face->plane_id].normal;
}
for (u32 e = 0; e < face->num_edges; e++) {
i32 surfedge_idx = face->first_edge + e;
if (surfedge_idx >= (i32)bsp->num_surfedges) continue;
i32 edge_idx = bsp->surfedges[surfedge_idx];
u32 vert_idx;
if (edge_idx >= 0) {
if ((u32)edge_idx >= bsp->num_edges) continue;
vert_idx = bsp->edges[edge_idx].vertex[0];
} else {
edge_idx = -edge_idx;
if ((u32)edge_idx >= bsp->num_edges) continue;
vert_idx = bsp->edges[edge_idx].vertex[1];
}
if (vert_idx >= bsp->num_vertices) continue;
pxl8_vec3 pos = bsp->vertices[vert_idx].position;
f32 light = compute_vertex_light(pos, normal, lights, num_lights, ambient);
u8 light_byte = (u8)(light * 255.0f);
bsp->vertex_lights[vert_idx] = ((u32)light_byte << 24) | 0x00FFFFFF;
}
}
pxl8_debug("Computed vertex lighting: %u vertices, %u lights", bsp->num_vertices, num_lights);
}
static pxl8_bsp_cell_portals* build_pxl8_bsp_cell_portals(const room_grid* grid, f32 cell_size) {
i32 total_cells = grid->width * grid->height;
pxl8_bsp_cell_portals* portals = pxl8_calloc(total_cells, sizeof(pxl8_bsp_cell_portals));
if (!portals) return NULL;
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) != 0) continue;
i32 c = y * grid->width + x;
f32 cx = x * cell_size;
f32 cz = y * cell_size;
if (x > 0 && room_grid_get(grid, x - 1, y) == 0) {
pxl8_bsp_portal* p = &portals[c].portals[portals[c].num_portals++];
p->x0 = cx;
p->z0 = cz;
p->x1 = cx;
p->z1 = cz + cell_size;
p->target_leaf = y * grid->width + (x - 1);
}
if (x < grid->width - 1 && room_grid_get(grid, x + 1, y) == 0) {
pxl8_bsp_portal* p = &portals[c].portals[portals[c].num_portals++];
p->x0 = cx + cell_size;
p->z0 = cz + cell_size;
p->x1 = cx + cell_size;
p->z1 = cz;
p->target_leaf = y * grid->width + (x + 1);
}
if (y > 0 && room_grid_get(grid, x, y - 1) == 0) {
pxl8_bsp_portal* p = &portals[c].portals[portals[c].num_portals++];
p->x0 = cx + cell_size;
p->z0 = cz;
p->x1 = cx;
p->z1 = cz;
p->target_leaf = (y - 1) * grid->width + x;
}
if (y < grid->height - 1 && room_grid_get(grid, x, y + 1) == 0) {
pxl8_bsp_portal* p = &portals[c].portals[portals[c].num_portals++];
p->x0 = cx;
p->z0 = cz + cell_size;
p->x1 = cx + cell_size;
p->z1 = cz + cell_size;
p->target_leaf = (y + 1) * grid->width + x;
}
}
}
return portals;
}
typedef struct flood_entry {
u32 leaf;
u32 depth;
} flood_entry;
static void portal_flood_bfs(
u32 start_leaf,
const pxl8_bsp_cell_portals* portals,
const pxl8_bsp_leaf* leafs,
u8* pvs,
u32 num_leafs,
f32 cell_size,
i32 grid_width
) {
(void)cell_size;
(void)grid_width;
u32 pvs_bytes = (num_leafs + 7) / 8;
u8* visited = pxl8_calloc(pvs_bytes, 1);
flood_entry* queue = pxl8_malloc(num_leafs * sizeof(flood_entry));
if (!visited || !queue) {
pxl8_free(visited);
pxl8_free(queue);
return;
}
u32 head = 0, tail = 0;
pvs[start_leaf >> 3] |= (1 << (start_leaf & 7));
visited[start_leaf >> 3] |= (1 << (start_leaf & 7));
queue[tail++] = (flood_entry){start_leaf, 0};
while (head < tail) {
flood_entry e = queue[head++];
if (e.depth >= PVS_MAX_DEPTH) continue;
if (leafs[e.leaf].contents == -1) continue;
const pxl8_bsp_cell_portals* cp = &portals[e.leaf];
for (u8 i = 0; i < cp->num_portals; i++) {
u32 target = cp->portals[i].target_leaf;
u32 byte = target >> 3;
u32 bit = target & 7;
if (visited[byte] & (1 << bit)) continue;
visited[byte] |= (1 << bit);
if (leafs[target].contents == -1) continue;
pvs[byte] |= (1 << bit);
queue[tail++] = (flood_entry){target, e.depth + 1};
}
}
pxl8_free(visited);
pxl8_free(queue);
}
static u8* compute_leaf_pvs(u32 start_leaf, const pxl8_bsp_cell_portals* portals,
u32 num_leafs, const pxl8_bsp_leaf* leafs,
const room_grid* grid, f32 cell_size) {
u32 pvs_bytes = (num_leafs + 7) / 8;
u8* pvs = pxl8_calloc(pvs_bytes, 1);
if (!pvs) return NULL;
portal_flood_bfs(start_leaf, portals, leafs, pvs, num_leafs, cell_size, grid->width);
return pvs;
}
static u32 rle_compress_pvs(const u8* pvs, u32 pvs_bytes, u8* out) {
u32 out_pos = 0;
u32 i = 0;
while (i < pvs_bytes) {
if (pvs[i] != 0) {
out[out_pos++] = pvs[i++];
} else {
u32 count = 0;
while (i < pvs_bytes && pvs[i] == 0 && count < 255) {
count++;
i++;
}
out[out_pos++] = 0;
out[out_pos++] = (u8)count;
}
}
return out_pos;
}
static pxl8_result build_pvs_data(pxl8_bsp* bsp, const pxl8_bsp_cell_portals* portals,
const room_grid* grid, f32 cell_size) {
u32 num_leafs = bsp->num_leafs;
u32 pvs_bytes = (num_leafs + 7) / 8;
u32 max_visdata = num_leafs * pvs_bytes * 2;
u8* visdata = pxl8_malloc(max_visdata);
if (!visdata) return PXL8_ERROR_OUT_OF_MEMORY;
u32 visdata_pos = 0;
u8* compressed = pxl8_malloc(pvs_bytes * 2);
if (!compressed) {
pxl8_free(visdata);
return PXL8_ERROR_OUT_OF_MEMORY;
}
u32 debug_count = 0;
for (u32 leaf = 0; leaf < num_leafs; leaf++) {
if (bsp->leafs[leaf].contents == -1) {
bsp->leafs[leaf].visofs = -1;
continue;
}
u8* pvs = compute_leaf_pvs(leaf, portals, num_leafs, bsp->leafs, grid, cell_size);
if (!pvs) {
pxl8_free(compressed);
pxl8_free(visdata);
return PXL8_ERROR_OUT_OF_MEMORY;
}
if (debug_count < 3) {
u32 visible = 0;
for (u32 b = 0; b < pvs_bytes; b++) {
for (u32 i = 0; i < 8; i++) {
if (pvs[b] & (1 << i)) visible++;
}
}
pxl8_debug("Leaf %u PVS: %u cells visible (portals: %u)", leaf, visible, portals[leaf].num_portals);
debug_count++;
}
u32 compressed_size = rle_compress_pvs(pvs, pvs_bytes, compressed);
bsp->leafs[leaf].visofs = visdata_pos;
memcpy(visdata + visdata_pos, compressed, compressed_size);
visdata_pos += compressed_size;
pxl8_free(pvs);
}
pxl8_free(compressed);
bsp->visdata = pxl8_realloc(visdata, visdata_pos > 0 ? visdata_pos : 1);
bsp->visdata_size = visdata_pos;
pxl8_debug("Built PVS: %u leafs, %u bytes visdata", num_leafs, visdata_pos);
return PXL8_OK;
}
static i32 build_bsp_node(bsp_build_context* ctx, i32 x0, i32 y0, i32 x1, i32 y1, i32 depth) {
if (x1 - x0 == 1 && y1 - y0 == 1) {
i32 leaf_idx = y0 * ctx->grid->width + x0;
return -(leaf_idx + 1);
}
i32 node_idx = ctx->node_count++;
pxl8_bsp_node* node = &ctx->bsp->nodes[node_idx];
i32 plane_idx = ctx->plane_offset++;
pxl8_bsp_plane* plane = &ctx->bsp->planes[plane_idx];
node->plane_id = plane_idx;
if (depth % 2 == 0) {
i32 mid_x = (x0 + x1) / 2;
f32 split_pos = mid_x * CELL_SIZE;
plane->normal = (pxl8_vec3){1, 0, 0};
plane->dist = split_pos;
node->children[0] = build_bsp_node(ctx, mid_x, y0, x1, y1, depth + 1);
node->children[1] = build_bsp_node(ctx, x0, y0, mid_x, y1, depth + 1);
} else {
i32 mid_y = (y0 + y1) / 2;
f32 split_pos = mid_y * CELL_SIZE;
plane->normal = (pxl8_vec3){0, 0, 1};
plane->dist = split_pos;
node->children[0] = build_bsp_node(ctx, x0, mid_y, x1, y1, depth + 1);
node->children[1] = build_bsp_node(ctx, x0, y0, x1, mid_y, depth + 1);
}
return node_idx;
}
static pxl8_result grid_to_bsp(pxl8_bsp* bsp, const room_grid* grid) {
i32 vertex_count = 0;
i32 face_count = 0;
i32 floor_ceiling_count = 0;
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) == 0) {
if (room_grid_get(grid, x - 1, y) == 1) face_count++;
if (room_grid_get(grid, x + 1, y) == 1) face_count++;
if (room_grid_get(grid, x, y - 1) == 1) face_count++;
if (room_grid_get(grid, x, y + 1) == 1) face_count++;
floor_ceiling_count++;
}
}
}
face_count += floor_ceiling_count;
vertex_count = face_count * 4;
pxl8_debug("Level generation: %dx%d grid -> %d faces, %d vertices",
grid->width, grid->height, face_count, vertex_count);
i32 total_cells = grid->width * grid->height;
u32 max_nodes = 2 * total_cells;
u32 total_planes = face_count + max_nodes;
bsp->vertices = pxl8_calloc(vertex_count, sizeof(pxl8_bsp_vertex));
bsp->faces = pxl8_calloc(face_count, sizeof(pxl8_bsp_face));
bsp->planes = pxl8_calloc(total_planes, sizeof(pxl8_bsp_plane));
bsp->edges = pxl8_calloc(vertex_count, sizeof(pxl8_bsp_edge));
bsp->surfedges = pxl8_calloc(vertex_count, sizeof(i32));
bsp->nodes = pxl8_calloc(max_nodes, sizeof(pxl8_bsp_node));
u32* face_cell = pxl8_calloc(face_count, sizeof(u32));
if (!bsp->vertices || !bsp->faces || !bsp->planes || !bsp->edges ||
!bsp->surfedges || !bsp->nodes || !face_cell) {
pxl8_free(face_cell);
return PXL8_ERROR_OUT_OF_MEMORY;
}
bsp->materials = NULL;
bsp->num_materials = 0;
i32 vert_idx = 0;
i32 face_idx = 0;
i32 edge_idx = 0;
const f32 cell_size = CELL_SIZE;
const f32 wall_height = WALL_HEIGHT;
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) == 0) {
f32 fx = (f32)x * cell_size;
f32 fy = (f32)y * cell_size;
i32 cell_idx = y * grid->width + x;
if (room_grid_get(grid, x - 1, y) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, wall_height, fy};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, 0, fy + cell_size};
bsp->planes[face_idx].normal = (pxl8_vec3){1, 0, 0};
bsp->planes[face_idx].dist = fx;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].material_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
if (room_grid_get(grid, x + 1, y) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx + cell_size, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, wall_height, fy};
bsp->planes[face_idx].normal = (pxl8_vec3){-1, 0, 0};
bsp->planes[face_idx].dist = -(fx + cell_size);
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].material_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
if (room_grid_get(grid, x, y - 1) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, 0, fy};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, wall_height, fy};
bsp->planes[face_idx].normal = (pxl8_vec3){0, 0, 1};
bsp->planes[face_idx].dist = fy;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].material_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
if (room_grid_get(grid, x, y + 1) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy + cell_size};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
bsp->planes[face_idx].normal = (pxl8_vec3){0, 0, -1};
bsp->planes[face_idx].dist = -(fy + cell_size);
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].material_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
}
}
}
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) == 0) {
f32 fx = (f32)x * cell_size;
f32 fy = (f32)y * cell_size;
i32 cell_idx = y * grid->width + x;
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, 0, fy + cell_size};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, 0, fy};
bsp->planes[face_idx].normal = (pxl8_vec3){0, 1, 0};
bsp->planes[face_idx].dist = 0;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].material_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
}
}
bsp->num_vertices = vertex_count;
bsp->num_faces = face_count;
bsp->num_edges = vertex_count;
bsp->num_surfedges = vertex_count;
bsp->leafs = pxl8_calloc(total_cells, sizeof(pxl8_bsp_leaf));
bsp->marksurfaces = pxl8_calloc(face_count, sizeof(u16));
if (!bsp->leafs || !bsp->marksurfaces) {
pxl8_free(face_cell);
return PXL8_ERROR_OUT_OF_MEMORY;
}
bsp->num_leafs = total_cells;
bsp->num_marksurfaces = face_count;
u32* faces_per_cell = pxl8_calloc(total_cells, sizeof(u32));
u32* cell_offset = pxl8_calloc(total_cells, sizeof(u32));
u32* cell_cursor = pxl8_calloc(total_cells, sizeof(u32));
if (!faces_per_cell || !cell_offset || !cell_cursor) {
pxl8_free(faces_per_cell);
pxl8_free(cell_offset);
pxl8_free(cell_cursor);
pxl8_free(face_cell);
return PXL8_ERROR_OUT_OF_MEMORY;
}
for (i32 i = 0; i < face_count; i++) {
faces_per_cell[face_cell[i]]++;
}
u32 offset = 0;
for (i32 c = 0; c < total_cells; c++) {
cell_offset[c] = offset;
offset += faces_per_cell[c];
}
for (i32 i = 0; i < face_count; i++) {
u32 c = face_cell[i];
bsp->marksurfaces[cell_offset[c] + cell_cursor[c]++] = i;
}
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
i32 c = y * grid->width + x;
pxl8_bsp_leaf* leaf = &bsp->leafs[c];
f32 fx = (f32)x * cell_size;
f32 fz = (f32)y * cell_size;
leaf->mins[0] = (i16)fx;
leaf->mins[1] = 0;
leaf->mins[2] = (i16)fz;
leaf->maxs[0] = (i16)(fx + cell_size);
leaf->maxs[1] = (i16)wall_height;
leaf->maxs[2] = (i16)(fz + cell_size);
if (room_grid_get(grid, x, y) == 0) {
leaf->contents = -2;
leaf->first_marksurface = cell_offset[c];
leaf->num_marksurfaces = faces_per_cell[c];
} else {
leaf->contents = -1;
leaf->first_marksurface = 0;
leaf->num_marksurfaces = 0;
}
leaf->visofs = -1;
}
}
pxl8_free(faces_per_cell);
pxl8_free(cell_offset);
pxl8_free(cell_cursor);
pxl8_free(face_cell);
bsp_build_context ctx = {
.bsp = bsp,
.grid = grid,
.node_count = 0,
.plane_offset = face_count,
};
build_bsp_node(&ctx, 0, 0, grid->width, grid->height, 0);
bsp->num_nodes = ctx.node_count;
bsp->num_planes = ctx.plane_offset;
pxl8_debug("Built BSP tree: %u nodes, %u leafs, %u planes",
bsp->num_nodes, bsp->num_leafs, bsp->num_planes);
pxl8_bsp_cell_portals* portals = build_pxl8_bsp_cell_portals(grid, cell_size);
if (!portals) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
u32 walkable_cells = 0;
u32 total_portals = 0;
i32 first_walkable = -1;
for (i32 c = 0; c < total_cells; c++) {
if (bsp->leafs[c].contents == -2) {
walkable_cells++;
if (first_walkable < 0) first_walkable = c;
}
total_portals += portals[c].num_portals;
}
pxl8_debug("Portal stats: %u walkable cells, %u total portals (avg %.1f per cell)",
walkable_cells, total_portals, (f32)total_portals / walkable_cells);
if (first_walkable >= 0) {
u32 pvs_bytes = (total_cells + 7) / 8;
u8* visited = pxl8_calloc(pvs_bytes, 1);
u8* queue = pxl8_malloc(total_cells * sizeof(u32));
u32 head = 0, tail = 0;
((u32*)queue)[tail++] = first_walkable;
visited[first_walkable >> 3] |= (1 << (first_walkable & 7));
u32 reachable = 0;
while (head < tail) {
u32 c = ((u32*)queue)[head++];
reachable++;
for (u8 i = 0; i < portals[c].num_portals; i++) {
u32 n = portals[c].portals[i].target_leaf;
u32 nb = n >> 3, ni = n & 7;
if (!(visited[nb] & (1 << ni))) {
visited[nb] |= (1 << ni);
((u32*)queue)[tail++] = n;
}
}
}
pxl8_debug("Connectivity: %u/%u walkable cells reachable from leaf %d",
reachable, walkable_cells, first_walkable);
pxl8_free(visited);
pxl8_free(queue);
}
pxl8_result pvs_result = build_pvs_data(bsp, portals, grid, cell_size);
if (pvs_result != PXL8_OK) {
pxl8_free(portals);
return pvs_result;
}
bsp->cell_portals = portals;
bsp->num_cell_portals = total_cells;
return PXL8_OK;
}
static bool bounds_intersects(const pxl8_bounds* a, const pxl8_bounds* b) {
return !(a->x + a->w <= b->x || b->x + b->w <= a->x ||
a->y + a->h <= b->y || b->y + b->h <= a->y);
}
static void carve_corridor_h(room_grid* grid, i32 x1, i32 x2, i32 y) {
i32 start = (x1 < x2) ? x1 : x2;
i32 end = (x1 > x2) ? x1 : x2;
for (i32 x = start; x <= end; x++) {
room_grid_set(grid, x, y, 0);
room_grid_set(grid, x, y - 1, 0);
room_grid_set(grid, x, y + 1, 0);
}
}
static void carve_corridor_v(room_grid* grid, i32 y1, i32 y2, i32 x) {
i32 start = (y1 < y2) ? y1 : y2;
i32 end = (y1 > y2) ? y1 : y2;
for (i32 y = start; y <= end; y++) {
room_grid_set(grid, x, y, 0);
room_grid_set(grid, x - 1, y, 0);
room_grid_set(grid, x + 1, y, 0);
}
}
static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* params) {
pxl8_debug("procgen_rooms called: %dx%d, seed=%u, min=%d, max=%d, num=%d",
params->width, params->height, params->seed,
params->min_room_size, params->max_room_size, params->num_rooms);
pxl8_rng rng;
pxl8_rng_seed(&rng, params->seed);
room_grid grid;
if (!room_grid_init(&grid, params->width, params->height)) {
pxl8_error("Failed to allocate room grid");
return PXL8_ERROR_OUT_OF_MEMORY;
}
room_grid_fill(&grid, 1);
pxl8_bounds rooms[256];
i32 room_count = 0;
i32 max_attempts = params->num_rooms * 10;
const f32 cell_size = CELL_SIZE;
const f32 light_height = 80.0f;
for (i32 attempt = 0; attempt < max_attempts && room_count < params->num_rooms && room_count < 256; attempt++) {
i32 w = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1));
i32 h = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1));
i32 x = 1 + (pxl8_rng_next(&rng) % (params->width - w - 2));
i32 y = 1 + (pxl8_rng_next(&rng) % (params->height - h - 2));
pxl8_bounds new_room = {x, y, w, h};
bool overlaps = false;
for (i32 i = 0; i < room_count; i++) {
if (bounds_intersects(&new_room, &rooms[i])) {
overlaps = true;
break;
}
}
if (!overlaps) {
for (i32 ry = y; ry < y + h; ry++) {
for (i32 rx = x; rx < x + w; rx++) {
room_grid_set(&grid, rx, ry, 0);
}
}
if (room_count > 0) {
i32 new_cx = x + w / 2;
i32 new_cy = y + h / 2;
i32 prev_cx = rooms[room_count - 1].x + rooms[room_count - 1].w / 2;
i32 prev_cy = rooms[room_count - 1].y + rooms[room_count - 1].h / 2;
if (pxl8_rng_next(&rng) % 2 == 0) {
carve_corridor_h(&grid, prev_cx, new_cx, prev_cy);
carve_corridor_v(&grid, prev_cy, new_cy, new_cx);
} else {
carve_corridor_v(&grid, prev_cy, new_cy, prev_cx);
carve_corridor_h(&grid, prev_cx, new_cx, new_cy);
}
}
rooms[room_count++] = new_room;
}
}
pxl8_debug("Room generation: %dx%d grid -> %d rooms created",
params->width, params->height, room_count);
pxl8_result result = grid_to_bsp(bsp, &grid);
pxl8_free(grid.cells);
if (result != PXL8_OK) {
return result;
}
light_source lights[256];
u32 num_lights = 0;
for (i32 i = 0; i < room_count && num_lights < 256; i++) {
f32 cx = (rooms[i].x + rooms[i].w / 2.0f) * cell_size;
f32 cy = (rooms[i].y + rooms[i].h / 2.0f) * cell_size;
lights[num_lights++] = (light_source){
.position = {cx, light_height, cy},
.intensity = 0.8f,
.radius = 300.0f,
};
}
compute_bsp_vertex_lighting(bsp, lights, num_lights, 0.1f);
return result;
}
pxl8_result pxl8_procgen(pxl8_bsp* bsp, const pxl8_procgen_params* params) {
if (!bsp || !params) {
return PXL8_ERROR_NULL_POINTER;
}
switch (params->type) {
case PXL8_PROCGEN_ROOMS:
return procgen_rooms(bsp, params);
case PXL8_PROCGEN_TERRAIN:
pxl8_error("Terrain generation not yet implemented");
return PXL8_ERROR_NOT_INITIALIZED;
default:
pxl8_error("Unknown procgen type: %d", params->type);
return PXL8_ERROR_INVALID_ARGUMENT;
}
}

View file

@ -1,32 +0,0 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_types.h"
typedef enum pxl8_procgen_type {
PXL8_PROCGEN_ROOMS,
PXL8_PROCGEN_TERRAIN
} pxl8_procgen_type;
typedef struct pxl8_procgen_params {
pxl8_procgen_type type;
i32 width;
i32 height;
i32 depth;
u32 seed;
i32 min_room_size;
i32 max_room_size;
i32 num_rooms;
} pxl8_procgen_params;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_procgen(pxl8_bsp* bsp, const pxl8_procgen_params* params);
#ifdef __cplusplus
}
#endif

View file

@ -1,406 +0,0 @@
#include "pxl8_voxel.h"
#include <string.h>
#include "pxl8_mem.h"
#define PXL8_VOXEL_CHUNK_VOLUME (PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE)
typedef struct pxl8_block_def {
char name[32];
u8 texture_id;
pxl8_voxel_geometry geometry;
bool registered;
} pxl8_block_def;
struct pxl8_block_registry {
pxl8_block_def blocks[PXL8_BLOCK_COUNT];
};
struct pxl8_voxel_chunk {
pxl8_block blocks[PXL8_VOXEL_CHUNK_VOLUME];
};
static inline u32 voxel_index(i32 x, i32 y, i32 z) {
return (u32)(x + y * PXL8_VOXEL_CHUNK_SIZE + z * PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE);
}
static inline bool voxel_in_bounds(i32 x, i32 y, i32 z) {
return x >= 0 && x < PXL8_VOXEL_CHUNK_SIZE &&
y >= 0 && y < PXL8_VOXEL_CHUNK_SIZE &&
z >= 0 && z < PXL8_VOXEL_CHUNK_SIZE;
}
pxl8_voxel_chunk* pxl8_voxel_chunk_create(void) {
pxl8_voxel_chunk* chunk = pxl8_calloc(1, sizeof(pxl8_voxel_chunk));
return chunk;
}
void pxl8_voxel_chunk_clear(pxl8_voxel_chunk* chunk) {
if (!chunk) return;
memset(chunk->blocks, 0, sizeof(chunk->blocks));
}
void pxl8_voxel_chunk_destroy(pxl8_voxel_chunk* chunk) {
pxl8_free(chunk);
}
pxl8_block pxl8_voxel_get(const pxl8_voxel_chunk* chunk, i32 x, i32 y, i32 z) {
if (!chunk || !voxel_in_bounds(x, y, z)) return PXL8_BLOCK_AIR;
return chunk->blocks[voxel_index(x, y, z)];
}
void pxl8_voxel_set(pxl8_voxel_chunk* chunk, i32 x, i32 y, i32 z, pxl8_block block) {
if (!chunk || !voxel_in_bounds(x, y, z)) return;
chunk->blocks[voxel_index(x, y, z)] = block;
}
void pxl8_voxel_fill(pxl8_voxel_chunk* chunk, pxl8_block block) {
if (!chunk) return;
memset(chunk->blocks, block, sizeof(chunk->blocks));
}
pxl8_block_registry* pxl8_block_registry_create(void) {
pxl8_block_registry* registry = pxl8_calloc(1, sizeof(pxl8_block_registry));
return registry;
}
void pxl8_block_registry_destroy(pxl8_block_registry* registry) {
pxl8_free(registry);
}
void pxl8_block_registry_register(pxl8_block_registry* registry, pxl8_block id, const char* name, u8 texture_id, pxl8_voxel_geometry geo) {
if (!registry || id == PXL8_BLOCK_AIR) return;
pxl8_block_def* def = &registry->blocks[id];
strncpy(def->name, name ? name : "", sizeof(def->name) - 1);
def->texture_id = texture_id;
def->geometry = geo;
def->registered = true;
}
const char* pxl8_block_registry_name(const pxl8_block_registry* registry, pxl8_block id) {
if (!registry || !registry->blocks[id].registered) return NULL;
return registry->blocks[id].name;
}
pxl8_voxel_geometry pxl8_block_registry_geometry(const pxl8_block_registry* registry, pxl8_block id) {
if (!registry || !registry->blocks[id].registered) return PXL8_VOXEL_GEOMETRY_CUBE;
return registry->blocks[id].geometry;
}
u8 pxl8_block_registry_texture(const pxl8_block_registry* registry, pxl8_block id) {
if (!registry || !registry->blocks[id].registered) return 0;
return registry->blocks[id].texture_id;
}
static bool block_is_opaque(pxl8_block block) {
return block != PXL8_BLOCK_AIR;
}
static pxl8_block get_block_or_neighbor(const pxl8_voxel_chunk* chunk, const pxl8_voxel_chunk** neighbors, i32 x, i32 y, i32 z) {
if (voxel_in_bounds(x, y, z)) {
return chunk->blocks[voxel_index(x, y, z)];
}
i32 nx = x, ny = y, nz = z;
i32 neighbor_idx = -1;
if (x < 0) { neighbor_idx = 0; nx = x + PXL8_VOXEL_CHUNK_SIZE; }
else if (x >= PXL8_VOXEL_CHUNK_SIZE) { neighbor_idx = 1; nx = x - PXL8_VOXEL_CHUNK_SIZE; }
else if (y < 0) { neighbor_idx = 2; ny = y + PXL8_VOXEL_CHUNK_SIZE; }
else if (y >= PXL8_VOXEL_CHUNK_SIZE) { neighbor_idx = 3; ny = y - PXL8_VOXEL_CHUNK_SIZE; }
else if (z < 0) { neighbor_idx = 4; nz = z + PXL8_VOXEL_CHUNK_SIZE; }
else if (z >= PXL8_VOXEL_CHUNK_SIZE) { neighbor_idx = 5; nz = z - PXL8_VOXEL_CHUNK_SIZE; }
if (neighbor_idx >= 0 && neighbors && neighbors[neighbor_idx]) {
return pxl8_voxel_get(neighbors[neighbor_idx], nx, ny, nz);
}
return PXL8_BLOCK_AIR;
}
static f32 compute_ao(const pxl8_voxel_chunk* chunk, const pxl8_voxel_chunk** neighbors,
i32 x, i32 y, i32 z, i32 dx1, i32 dy1, i32 dz1, i32 dx2, i32 dy2, i32 dz2) {
bool side1 = block_is_opaque(get_block_or_neighbor(chunk, neighbors, x + dx1, y + dy1, z + dz1));
bool side2 = block_is_opaque(get_block_or_neighbor(chunk, neighbors, x + dx2, y + dy2, z + dz2));
bool corner = block_is_opaque(get_block_or_neighbor(chunk, neighbors, x + dx1 + dx2, y + dy1 + dy2, z + dz1 + dz2));
if (side1 && side2) return 0.0f;
return (3.0f - (f32)side1 - (f32)side2 - (f32)corner) / 3.0f;
}
static void add_face_vertices(pxl8_mesh* mesh, const pxl8_voxel_mesh_config* config,
pxl8_vec3 pos, i32 face, u8 texture_id,
f32 ao0, f32 ao1, f32 ao2, f32 ao3) {
static const pxl8_vec3 face_normals[6] = {
{-1, 0, 0}, { 1, 0, 0},
{ 0, -1, 0}, { 0, 1, 0},
{ 0, 0, -1}, { 0, 0, 1}
};
static const i32 face_vertices[6][4][3] = {
{{0,0,1}, {0,0,0}, {0,1,0}, {0,1,1}},
{{1,0,0}, {1,0,1}, {1,1,1}, {1,1,0}},
{{0,0,0}, {1,0,0}, {1,0,1}, {0,0,1}},
{{0,1,1}, {1,1,1}, {1,1,0}, {0,1,0}},
{{0,0,0}, {0,1,0}, {1,1,0}, {1,0,0}},
{{1,0,1}, {1,1,1}, {0,1,1}, {0,0,1}}
};
static const f32 face_uvs[4][2] = {
{0, 1}, {1, 1}, {1, 0}, {0, 0}
};
f32 ao_values[4] = {ao0, ao1, ao2, ao3};
f32 ao_strength = config->ambient_occlusion ? config->ao_strength : 0.0f;
f32 tex_scale = config->texture_scale;
pxl8_vec3 normal = face_normals[face];
u16 indices[4];
for (i32 i = 0; i < 4; i++) {
pxl8_vertex v = {0};
v.position.x = pos.x + (f32)face_vertices[face][i][0];
v.position.y = pos.y + (f32)face_vertices[face][i][1];
v.position.z = pos.z + (f32)face_vertices[face][i][2];
v.normal = normal;
v.u = face_uvs[i][0] * tex_scale;
v.v = face_uvs[i][1] * tex_scale;
v.color = texture_id;
v.light = (u8)(255.0f * (1.0f - ao_strength * (1.0f - ao_values[i])));
indices[i] = pxl8_mesh_push_vertex(mesh, v);
}
if (ao0 + ao2 > ao1 + ao3) {
pxl8_mesh_push_quad(mesh, indices[0], indices[1], indices[2], indices[3]);
} else {
pxl8_mesh_push_triangle(mesh, indices[0], indices[1], indices[2]);
pxl8_mesh_push_triangle(mesh, indices[0], indices[2], indices[3]);
}
}
static void add_cube_faces(pxl8_mesh* mesh, const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors, const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config,
i32 x, i32 y, i32 z, pxl8_block block) {
u8 texture_id = pxl8_block_registry_texture(registry, block);
pxl8_vec3 pos = {(f32)x, (f32)y, (f32)z};
static const i32 face_dirs[6][3] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}
};
for (i32 face = 0; face < 6; face++) {
i32 nx = x + face_dirs[face][0];
i32 ny = y + face_dirs[face][1];
i32 nz = z + face_dirs[face][2];
pxl8_block neighbor = get_block_or_neighbor(chunk, neighbors, nx, ny, nz);
if (block_is_opaque(neighbor)) continue;
f32 ao[4] = {1.0f, 1.0f, 1.0f, 1.0f};
if (config->ambient_occlusion) {
switch (face) {
case 0:
ao[0] = compute_ao(chunk, neighbors, x-1, y, z, 0, -1, 0, 0, 0, 1);
ao[1] = compute_ao(chunk, neighbors, x-1, y, z, 0, -1, 0, 0, 0, -1);
ao[2] = compute_ao(chunk, neighbors, x-1, y, z, 0, 1, 0, 0, 0, -1);
ao[3] = compute_ao(chunk, neighbors, x-1, y, z, 0, 1, 0, 0, 0, 1);
break;
case 1:
ao[0] = compute_ao(chunk, neighbors, x+1, y, z, 0, -1, 0, 0, 0, -1);
ao[1] = compute_ao(chunk, neighbors, x+1, y, z, 0, -1, 0, 0, 0, 1);
ao[2] = compute_ao(chunk, neighbors, x+1, y, z, 0, 1, 0, 0, 0, 1);
ao[3] = compute_ao(chunk, neighbors, x+1, y, z, 0, 1, 0, 0, 0, -1);
break;
case 2:
ao[0] = compute_ao(chunk, neighbors, x, y-1, z, -1, 0, 0, 0, 0, -1);
ao[1] = compute_ao(chunk, neighbors, x, y-1, z, 1, 0, 0, 0, 0, -1);
ao[2] = compute_ao(chunk, neighbors, x, y-1, z, 1, 0, 0, 0, 0, 1);
ao[3] = compute_ao(chunk, neighbors, x, y-1, z, -1, 0, 0, 0, 0, 1);
break;
case 3:
ao[0] = compute_ao(chunk, neighbors, x, y+1, z, -1, 0, 0, 0, 0, 1);
ao[1] = compute_ao(chunk, neighbors, x, y+1, z, 1, 0, 0, 0, 0, 1);
ao[2] = compute_ao(chunk, neighbors, x, y+1, z, 1, 0, 0, 0, 0, -1);
ao[3] = compute_ao(chunk, neighbors, x, y+1, z, -1, 0, 0, 0, 0, -1);
break;
case 4:
ao[0] = compute_ao(chunk, neighbors, x, y, z-1, -1, 0, 0, 0, 1, 0);
ao[1] = compute_ao(chunk, neighbors, x, y, z-1, -1, 0, 0, 0, -1, 0);
ao[2] = compute_ao(chunk, neighbors, x, y, z-1, 1, 0, 0, 0, -1, 0);
ao[3] = compute_ao(chunk, neighbors, x, y, z-1, 1, 0, 0, 0, 1, 0);
break;
case 5:
ao[0] = compute_ao(chunk, neighbors, x, y, z+1, 1, 0, 0, 0, -1, 0);
ao[1] = compute_ao(chunk, neighbors, x, y, z+1, 1, 0, 0, 0, 1, 0);
ao[2] = compute_ao(chunk, neighbors, x, y, z+1, -1, 0, 0, 0, 1, 0);
ao[3] = compute_ao(chunk, neighbors, x, y, z+1, -1, 0, 0, 0, -1, 0);
break;
}
}
add_face_vertices(mesh, config, pos, face, texture_id, ao[0], ao[1], ao[2], ao[3]);
}
}
static void add_slab_faces(pxl8_mesh* mesh, const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors, const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config,
i32 x, i32 y, i32 z, pxl8_block block, bool top) {
u8 texture_id = pxl8_block_registry_texture(registry, block);
f32 y_offset = top ? 0.5f : 0.0f;
pxl8_vec3 pos = {(f32)x, (f32)y + y_offset, (f32)z};
static const i32 horiz_faces[4] = {0, 1, 4, 5};
static const i32 face_dirs[6][3] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}
};
for (i32 i = 0; i < 4; i++) {
i32 face = horiz_faces[i];
i32 nx = x + face_dirs[face][0];
i32 nz = z + face_dirs[face][2];
pxl8_block neighbor = get_block_or_neighbor(chunk, neighbors, nx, y, nz);
if (block_is_opaque(neighbor)) continue;
add_face_vertices(mesh, config, pos, face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
i32 top_face = 3;
i32 bot_face = 2;
if (top) {
pxl8_block above = get_block_or_neighbor(chunk, neighbors, x, y + 1, z);
if (!block_is_opaque(above)) {
add_face_vertices(mesh, config, pos, top_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
add_face_vertices(mesh, config, (pxl8_vec3){(f32)x, (f32)y + 0.5f, (f32)z}, bot_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
} else {
add_face_vertices(mesh, config, pos, top_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
pxl8_block below = get_block_or_neighbor(chunk, neighbors, x, y - 1, z);
if (!block_is_opaque(below)) {
add_face_vertices(mesh, config, pos, bot_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
}
}
static void add_slope_faces(pxl8_mesh* mesh, const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors, const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config,
i32 x, i32 y, i32 z, pxl8_block block, i32 direction) {
u8 texture_id = pxl8_block_registry_texture(registry, block);
pxl8_vec3 pos = {(f32)x, (f32)y, (f32)z};
pxl8_block below = get_block_or_neighbor(chunk, neighbors, x, y - 1, z);
if (!block_is_opaque(below)) {
add_face_vertices(mesh, config, pos, 2, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
static const i32 dir_to_back_face[4] = {5, 4, 1, 0};
i32 back_face = dir_to_back_face[direction];
static const i32 face_dirs[6][3] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}
};
i32 bx = x + face_dirs[back_face][0];
i32 bz = z + face_dirs[back_face][2];
pxl8_block back_neighbor = get_block_or_neighbor(chunk, neighbors, bx, y, bz);
if (!block_is_opaque(back_neighbor)) {
add_face_vertices(mesh, config, pos, back_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
pxl8_vertex verts[4];
memset(verts, 0, sizeof(verts));
static const f32 slope_verts[4][4][3] = {
{{0,0,0}, {1,0,0}, {1,1,1}, {0,1,1}},
{{0,0,1}, {1,0,1}, {1,1,0}, {0,1,0}},
{{1,0,0}, {1,0,1}, {0,1,1}, {0,1,0}},
{{0,0,0}, {0,0,1}, {1,1,1}, {1,1,0}}
};
for (i32 i = 0; i < 4; i++) {
verts[i].position.x = pos.x + slope_verts[direction][i][0];
verts[i].position.y = pos.y + slope_verts[direction][i][1];
verts[i].position.z = pos.z + slope_verts[direction][i][2];
verts[i].color = texture_id;
verts[i].light = 255;
}
static const pxl8_vec3 slope_normals[4] = {
{0, 0.707f, -0.707f}, {0, 0.707f, 0.707f},
{-0.707f, 0.707f, 0}, {0.707f, 0.707f, 0}
};
for (i32 i = 0; i < 4; i++) {
verts[i].normal = slope_normals[direction];
}
u16 i0 = pxl8_mesh_push_vertex(mesh, verts[0]);
u16 i1 = pxl8_mesh_push_vertex(mesh, verts[1]);
u16 i2 = pxl8_mesh_push_vertex(mesh, verts[2]);
u16 i3 = pxl8_mesh_push_vertex(mesh, verts[3]);
pxl8_mesh_push_quad(mesh, i0, i1, i2, i3);
}
pxl8_mesh* pxl8_voxel_build_mesh(const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors,
const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config) {
if (!chunk || !registry) return NULL;
pxl8_voxel_mesh_config cfg = config ? *config : PXL8_VOXEL_MESH_CONFIG_DEFAULT;
u32 max_faces = PXL8_VOXEL_CHUNK_VOLUME * 6;
pxl8_mesh* mesh = pxl8_mesh_create(max_faces * 4, max_faces * 6);
if (!mesh) return NULL;
for (i32 z = 0; z < PXL8_VOXEL_CHUNK_SIZE; z++) {
for (i32 y = 0; y < PXL8_VOXEL_CHUNK_SIZE; y++) {
for (i32 x = 0; x < PXL8_VOXEL_CHUNK_SIZE; x++) {
pxl8_block block = chunk->blocks[voxel_index(x, y, z)];
if (block == PXL8_BLOCK_AIR) continue;
pxl8_voxel_geometry geo = pxl8_block_registry_geometry(registry, block);
switch (geo) {
case PXL8_VOXEL_GEOMETRY_CUBE:
add_cube_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block);
break;
case PXL8_VOXEL_GEOMETRY_SLAB_BOTTOM:
add_slab_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, false);
break;
case PXL8_VOXEL_GEOMETRY_SLAB_TOP:
add_slab_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, true);
break;
case PXL8_VOXEL_GEOMETRY_SLOPE_NORTH:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 0);
break;
case PXL8_VOXEL_GEOMETRY_SLOPE_SOUTH:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 1);
break;
case PXL8_VOXEL_GEOMETRY_SLOPE_EAST:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 2);
break;
case PXL8_VOXEL_GEOMETRY_SLOPE_WEST:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 3);
break;
case PXL8_VOXEL_GEOMETRY_STAIRS_NORTH:
case PXL8_VOXEL_GEOMETRY_STAIRS_SOUTH:
case PXL8_VOXEL_GEOMETRY_STAIRS_EAST:
case PXL8_VOXEL_GEOMETRY_STAIRS_WEST:
add_cube_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block);
break;
}
}
}
}
return mesh;
}

View file

@ -1,67 +0,0 @@
#pragma once
#include "pxl8_mesh.h"
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_VOXEL_CHUNK_SIZE 32
#define PXL8_BLOCK_COUNT 256
typedef struct pxl8_voxel_chunk pxl8_voxel_chunk;
typedef struct pxl8_block_registry pxl8_block_registry;
typedef u8 pxl8_block;
#define PXL8_BLOCK_AIR 0
typedef enum pxl8_voxel_geometry {
PXL8_VOXEL_GEOMETRY_CUBE,
PXL8_VOXEL_GEOMETRY_SLAB_BOTTOM,
PXL8_VOXEL_GEOMETRY_SLAB_TOP,
PXL8_VOXEL_GEOMETRY_SLOPE_NORTH,
PXL8_VOXEL_GEOMETRY_SLOPE_SOUTH,
PXL8_VOXEL_GEOMETRY_SLOPE_EAST,
PXL8_VOXEL_GEOMETRY_SLOPE_WEST,
PXL8_VOXEL_GEOMETRY_STAIRS_NORTH,
PXL8_VOXEL_GEOMETRY_STAIRS_SOUTH,
PXL8_VOXEL_GEOMETRY_STAIRS_EAST,
PXL8_VOXEL_GEOMETRY_STAIRS_WEST
} pxl8_voxel_geometry;
typedef struct pxl8_voxel_mesh_config {
bool ambient_occlusion;
f32 ao_strength;
f32 texture_scale;
} pxl8_voxel_mesh_config;
#define PXL8_VOXEL_MESH_CONFIG_DEFAULT ((pxl8_voxel_mesh_config){ \
.ambient_occlusion = true, \
.ao_strength = 0.2f, \
.texture_scale = 1.0f \
})
pxl8_voxel_chunk* pxl8_voxel_chunk_create(void);
void pxl8_voxel_chunk_clear(pxl8_voxel_chunk* chunk);
void pxl8_voxel_chunk_destroy(pxl8_voxel_chunk* chunk);
pxl8_block pxl8_voxel_get(const pxl8_voxel_chunk* chunk, i32 x, i32 y, i32 z);
void pxl8_voxel_set(pxl8_voxel_chunk* chunk, i32 x, i32 y, i32 z, pxl8_block block);
void pxl8_voxel_fill(pxl8_voxel_chunk* chunk, pxl8_block block);
pxl8_block_registry* pxl8_block_registry_create(void);
void pxl8_block_registry_destroy(pxl8_block_registry* registry);
void pxl8_block_registry_register(pxl8_block_registry* registry, pxl8_block id, const char* name, u8 texture_id, pxl8_voxel_geometry geo);
const char* pxl8_block_registry_name(const pxl8_block_registry* registry, pxl8_block id);
pxl8_voxel_geometry pxl8_block_registry_geometry(const pxl8_block_registry* registry, pxl8_block id);
u8 pxl8_block_registry_texture(const pxl8_block_registry* registry, pxl8_block id);
pxl8_mesh* pxl8_voxel_build_mesh(const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors,
const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config);
#ifdef __cplusplus
}
#endif

View file

@ -1,28 +1,66 @@
#include "pxl8_world.h"
#include <stdio.h>
#include <string.h>
#include "pxl8_hal.h"
#ifdef PXL8_ASYNC_THREADS
#include <stdatomic.h>
#include "pxl8_queue.h"
#endif
#include "pxl8_bsp_render.h"
#include "pxl8_io.h"
#include "pxl8_gfx3d.h"
#include "pxl8_log.h"
#include "pxl8_mem.h"
#include "pxl8_sim.h"
#include "pxl8_vxl.h"
#include "pxl8_vxl_render.h"
#define PXL8_WORLD_ENTITY_CAPACITY 256
#define VOXEL_SCALE 16.0f
#define VOXEL_CHUNK_SIZE 32.0f
#define WORLD_CHUNK_SIZE (VOXEL_CHUNK_SIZE * VOXEL_SCALE)
struct pxl8_world {
pxl8_chunk* active_chunk;
pxl8_block_registry* block_registry;
pxl8_chunk_cache* chunk_cache;
pxl8_world_chunk* active_chunk;
pxl8_vxl_block_registry* block_registry;
pxl8_world_chunk_cache* chunk_cache;
pxl8_entity_pool* entities;
pxl8_bsp_render_state* bsp_render_state;
pxl8_vxl_render_state* vxl_render_state;
pxl8_sdf sdf;
i32 render_distance;
i32 sim_distance;
pxl8_sim_entity local_player;
u64 client_tick;
#ifdef PXL8_ASYNC_THREADS
pxl8_sim_entity render_state[2];
atomic_uint active_buffer;
pxl8_thread* sim_thread;
atomic_bool sim_running;
atomic_bool sim_paused;
pxl8_net* net;
pxl8_queue input_queue;
f32 sim_accumulator;
#endif
};
pxl8_world* pxl8_world_create(void) {
pxl8_world* world = pxl8_calloc(1, sizeof(pxl8_world));
if (!world) return NULL;
world->block_registry = pxl8_block_registry_create();
world->chunk_cache = pxl8_chunk_cache_create();
world->block_registry = pxl8_vxl_block_registry_create();
world->chunk_cache = pxl8_world_chunk_cache_create();
world->entities = pxl8_entity_pool_create(PXL8_WORLD_ENTITY_CAPACITY);
world->vxl_render_state = pxl8_vxl_render_state_create();
world->render_distance = 3;
world->sim_distance = 4;
if (!world->block_registry || !world->chunk_cache || !world->entities) {
if (!world->block_registry || !world->chunk_cache || !world->entities || !world->vxl_render_state) {
pxl8_world_destroy(world);
return NULL;
}
@ -33,28 +71,30 @@ pxl8_world* pxl8_world_create(void) {
void pxl8_world_destroy(pxl8_world* world) {
if (!world) return;
pxl8_block_registry_destroy(world->block_registry);
pxl8_chunk_cache_destroy(world->chunk_cache);
pxl8_vxl_block_registry_destroy(world->block_registry);
pxl8_world_chunk_cache_destroy(world->chunk_cache);
pxl8_entity_pool_destroy(world->entities);
pxl8_bsp_render_state_destroy(world->bsp_render_state);
pxl8_vxl_render_state_destroy(world->vxl_render_state);
pxl8_free(world);
}
pxl8_chunk_cache* pxl8_world_chunk_cache(pxl8_world* world) {
pxl8_world_chunk_cache* pxl8_world_get_chunk_cache(pxl8_world* world) {
if (!world) return NULL;
return world->chunk_cache;
}
pxl8_chunk* pxl8_world_active_chunk(pxl8_world* world) {
pxl8_world_chunk* pxl8_world_active_chunk(pxl8_world* world) {
if (!world) return NULL;
return world->active_chunk;
}
void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_chunk* chunk) {
void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_world_chunk* chunk) {
if (!world) return;
world->active_chunk = chunk;
}
pxl8_block_registry* pxl8_world_block_registry(pxl8_world* world) {
pxl8_vxl_block_registry* pxl8_world_block_registry(pxl8_world* world) {
if (!world) return NULL;
return world->block_registry;
}
@ -69,40 +109,362 @@ pxl8_entity pxl8_world_spawn(pxl8_world* world) {
return pxl8_entity_spawn(world->entities);
}
bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius) {
(void)radius;
static bool voxel_point_solid(pxl8_world_chunk_cache* cache, f32 x, f32 y, f32 z) {
i32 cx = (i32)floorf(x / WORLD_CHUNK_SIZE);
i32 cy = (i32)floorf(y / WORLD_CHUNK_SIZE);
i32 cz = (i32)floorf(z / WORLD_CHUNK_SIZE);
if (!world || !world->active_chunk) return false;
f32 local_x = (x - cx * WORLD_CHUNK_SIZE) / VOXEL_SCALE;
f32 local_y = (y - cy * WORLD_CHUNK_SIZE) / VOXEL_SCALE;
f32 local_z = (z - cz * WORLD_CHUNK_SIZE) / VOXEL_SCALE;
if (world->active_chunk->type == PXL8_CHUNK_BSP && world->active_chunk->bsp) {
return pxl8_bsp_point_solid(world->active_chunk->bsp, pos);
i32 lx = (i32)floorf(local_x);
i32 ly = (i32)floorf(local_y);
i32 lz = (i32)floorf(local_z);
lx = lx < 0 ? 0 : (lx >= 32 ? 31 : lx);
ly = ly < 0 ? 0 : (ly >= 32 ? 31 : ly);
lz = lz < 0 ? 0 : (lz >= 32 ? 31 : lz);
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_vxl(cache, cx, cy, cz);
if (!chunk || !chunk->voxels) {
return ly < 8;
}
return pxl8_vxl_block_get(chunk->voxels, lx, ly, lz) != PXL8_VXL_BLOCK_AIR;
}
static pxl8_sim_world make_sim_world(const pxl8_world* world, pxl8_vec3 pos) {
pxl8_sim_world sim = {0};
if (world->active_chunk && world->active_chunk->type == PXL8_WORLD_CHUNK_BSP) {
sim.bsp = world->active_chunk->bsp;
return sim;
}
if (world->chunk_cache) {
i32 cx = (i32)floorf(pos.x / WORLD_CHUNK_SIZE);
i32 cy = (i32)floorf(pos.y / WORLD_CHUNK_SIZE);
i32 cz = (i32)floorf(pos.z / WORLD_CHUNK_SIZE);
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_vxl(world->chunk_cache, cx, cy, cz);
if (chunk && chunk->voxels) {
sim.vxl = chunk->voxels;
sim.vxl_cx = cx;
sim.vxl_cy = cy;
sim.vxl_cz = cz;
}
}
return sim;
}
static void entity_to_userdata(const pxl8_sim_entity* ent, u8* userdata) {
u8* p = userdata;
memcpy(p, &ent->pos.x, 4); p += 4;
memcpy(p, &ent->pos.y, 4); p += 4;
memcpy(p, &ent->pos.z, 4); p += 4;
memcpy(p, &ent->yaw, 4); p += 4;
memcpy(p, &ent->pitch, 4); p += 4;
memcpy(p, &ent->vel.x, 4); p += 4;
memcpy(p, &ent->vel.y, 4); p += 4;
memcpy(p, &ent->vel.z, 4); p += 4;
memcpy(p, &ent->flags, 4); p += 4;
memcpy(p, &ent->kind, 2);
}
static void userdata_to_entity(const u8* userdata, pxl8_sim_entity* ent) {
const u8* p = userdata;
memcpy(&ent->pos.x, p, 4); p += 4;
memcpy(&ent->pos.y, p, 4); p += 4;
memcpy(&ent->pos.z, p, 4); p += 4;
memcpy(&ent->yaw, p, 4); p += 4;
memcpy(&ent->pitch, p, 4); p += 4;
memcpy(&ent->vel.x, p, 4); p += 4;
memcpy(&ent->vel.y, p, 4); p += 4;
memcpy(&ent->vel.z, p, 4); p += 4;
memcpy(&ent->flags, p, 4); p += 4;
memcpy(&ent->kind, p, 2);
}
static void update_sdf(pxl8_world* world, pxl8_vec3 center, f32 cell_size) {
if (!world) return;
world->sdf.cell = cell_size;
world->sdf.origin.x = center.x - (PXL8_SDF_X / 2) * cell_size;
world->sdf.origin.y = center.y - (PXL8_SDF_Y / 2) * cell_size;
world->sdf.origin.z = center.z - (PXL8_SDF_Z / 2) * cell_size;
i16 seed_x[PXL8_SDF_SIZE];
i16 seed_y[PXL8_SDF_SIZE];
i16 seed_z[PXL8_SDF_SIZE];
for (i32 iy = 0; iy < PXL8_SDF_Y; iy++) {
f32 wy = world->sdf.origin.y + (iy + 0.5f) * cell_size;
for (i32 iz = 0; iz < PXL8_SDF_Z; iz++) {
f32 wz = world->sdf.origin.z + (iz + 0.5f) * cell_size;
for (i32 ix = 0; ix < PXL8_SDF_X; ix++) {
f32 wx = world->sdf.origin.x + (ix + 0.5f) * cell_size;
i32 idx = iy * PXL8_SDF_Z * PXL8_SDF_X + iz * PXL8_SDF_X + ix;
if (pxl8_world_point_solid(world, wx, wy, wz)) {
seed_x[idx] = (i16)ix;
seed_y[idx] = (i16)iy;
seed_z[idx] = (i16)iz;
} else {
seed_x[idx] = -1;
seed_y[idx] = -1;
seed_z[idx] = -1;
}
}
}
}
for (i32 step = 16; step >= 1; step /= 2) {
for (i32 iy = 0; iy < PXL8_SDF_Y; iy++) {
for (i32 iz = 0; iz < PXL8_SDF_Z; iz++) {
for (i32 ix = 0; ix < PXL8_SDF_X; ix++) {
i32 idx = iy * PXL8_SDF_Z * PXL8_SDF_X + iz * PXL8_SDF_X + ix;
i32 best_dist_sq = (seed_x[idx] >= 0)
? (ix - seed_x[idx]) * (ix - seed_x[idx]) +
(iy - seed_y[idx]) * (iy - seed_y[idx]) +
(iz - seed_z[idx]) * (iz - seed_z[idx])
: 0x7FFFFFFF;
for (i32 dy = -step; dy <= step; dy += step) {
i32 ny = iy + dy;
if (ny < 0 || ny >= PXL8_SDF_Y) continue;
for (i32 dz = -step; dz <= step; dz += step) {
i32 nz = iz + dz;
if (nz < 0 || nz >= PXL8_SDF_Z) continue;
for (i32 dx = -step; dx <= step; dx += step) {
if (dx == 0 && dy == 0 && dz == 0) continue;
i32 nx = ix + dx;
if (nx < 0 || nx >= PXL8_SDF_X) continue;
i32 nidx = ny * PXL8_SDF_Z * PXL8_SDF_X + nz * PXL8_SDF_X + nx;
if (seed_x[nidx] < 0) continue;
i32 dist_sq = (ix - seed_x[nidx]) * (ix - seed_x[nidx]) +
(iy - seed_y[nidx]) * (iy - seed_y[nidx]) +
(iz - seed_z[nidx]) * (iz - seed_z[nidx]);
if (dist_sq < best_dist_sq) {
best_dist_sq = dist_sq;
seed_x[idx] = seed_x[nidx];
seed_y[idx] = seed_y[nidx];
seed_z[idx] = seed_z[nidx];
}
}
}
}
}
}
}
}
for (i32 i = 0; i < PXL8_SDF_SIZE; i++) {
if (seed_x[i] < 0) {
world->sdf.data[i] = 127;
} else {
i32 idx_x = i % PXL8_SDF_X;
i32 idx_z = (i / PXL8_SDF_X) % PXL8_SDF_Z;
i32 idx_y = i / (PXL8_SDF_X * PXL8_SDF_Z);
i32 dx = idx_x - seed_x[i];
i32 dy = idx_y - seed_y[i];
i32 dz = idx_z - seed_z[i];
f32 dist = sqrtf((f32)(dx * dx + dy * dy + dz * dz));
i32 d = (i32)(dist * cell_size);
if (d > 127) d = 127;
world->sdf.data[i] = (i8)d;
}
}
}
bool pxl8_world_point_solid(const pxl8_world* world, f32 x, f32 y, f32 z) {
if (!world) return false;
if (world->active_chunk) {
if (world->active_chunk->type == PXL8_WORLD_CHUNK_BSP && world->active_chunk->bsp) {
return pxl8_bsp_point_solid(world->active_chunk->bsp, (pxl8_vec3){x, y, z});
}
} else if (world->chunk_cache) {
return voxel_point_solid(world->chunk_cache, x, y, z);
}
return false;
}
pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
if (!world || !world->active_chunk) return to;
pxl8_ray pxl8_world_ray(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to) {
pxl8_ray result = {0};
if (world->active_chunk->type == PXL8_CHUNK_BSP && world->active_chunk->bsp) {
return pxl8_bsp_trace(world->active_chunk->bsp, from, to, radius);
if (!world) return result;
pxl8_vec3 dir = pxl8_vec3_sub(to, from);
f32 length = pxl8_vec3_length(dir);
if (length < 0.001f) return result;
f32 step_size = 1.0f;
f32 traveled = 0.0f;
while (traveled < length) {
f32 t = traveled / length;
pxl8_vec3 pos = {
from.x + dir.x * t,
from.y + dir.y * t,
from.z + dir.z * t
};
if (pxl8_world_point_solid(world, pos.x, pos.y, pos.z)) {
result.hit = true;
result.fraction = t;
result.point = pos;
f32 eps = 0.1f;
bool sx_neg = pxl8_world_point_solid(world, pos.x - eps, pos.y, pos.z);
bool sx_pos = pxl8_world_point_solid(world, pos.x + eps, pos.y, pos.z);
bool sy_neg = pxl8_world_point_solid(world, pos.x, pos.y - eps, pos.z);
bool sy_pos = pxl8_world_point_solid(world, pos.x, pos.y + eps, pos.z);
bool sz_neg = pxl8_world_point_solid(world, pos.x, pos.y, pos.z - eps);
bool sz_pos = pxl8_world_point_solid(world, pos.x, pos.y, pos.z + eps);
result.normal = (pxl8_vec3){
(sx_neg && !sx_pos) ? 1.0f : (!sx_neg && sx_pos) ? -1.0f : 0.0f,
(sy_neg && !sy_pos) ? 1.0f : (!sy_neg && sy_pos) ? -1.0f : 0.0f,
(sz_neg && !sz_pos) ? 1.0f : (!sz_neg && sz_pos) ? -1.0f : 0.0f
};
f32 nl = pxl8_vec3_length(result.normal);
if (nl > 0.001f) {
result.normal = pxl8_vec3_scale(result.normal, 1.0f / nl);
} else {
result.normal = (pxl8_vec3){0, 1, 0};
}
return result;
}
traveled += step_size;
}
return to;
return result;
}
void pxl8_world_update(pxl8_world* world, f32 dt) {
(void)dt;
pxl8_ray pxl8_world_sweep(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
pxl8_ray result = {0};
if (!world) return result;
f32 diag = radius * 0.707f;
bool dest_blocked = pxl8_world_point_solid(world, to.x, to.y, to.z) ||
pxl8_world_point_solid(world, to.x + radius, to.y, to.z) ||
pxl8_world_point_solid(world, to.x - radius, to.y, to.z) ||
pxl8_world_point_solid(world, to.x, to.y, to.z + radius) ||
pxl8_world_point_solid(world, to.x, to.y, to.z - radius) ||
pxl8_world_point_solid(world, to.x + diag, to.y, to.z + diag) ||
pxl8_world_point_solid(world, to.x + diag, to.y, to.z - diag) ||
pxl8_world_point_solid(world, to.x - diag, to.y, to.z + diag) ||
pxl8_world_point_solid(world, to.x - diag, to.y, to.z - diag);
if (dest_blocked) {
result.hit = true;
result.fraction = 0;
result.point = from;
pxl8_vec3 dir = pxl8_vec3_sub(to, from);
f32 length = pxl8_vec3_length(dir);
if (length > 0.001f) {
result.normal = pxl8_vec3_scale(dir, -1.0f / length);
} else {
result.normal = (pxl8_vec3){0, 1, 0};
}
}
return result;
}
void pxl8_world_update(pxl8_world* world, const pxl8_input_state* input, f32 dt) {
if (!world) return;
pxl8_chunk_cache_tick(world->chunk_cache);
pxl8_world_chunk_cache_tick(world->chunk_cache);
if (input && (world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) {
pxl8_input_msg msg = {0};
msg.move_x = (pxl8_key_down(input, "d") ? 1.0f : 0.0f) - (pxl8_key_down(input, "a") ? 1.0f : 0.0f);
msg.move_y = (pxl8_key_down(input, "w") ? 1.0f : 0.0f) - (pxl8_key_down(input, "s") ? 1.0f : 0.0f);
msg.look_dx = (f32)pxl8_mouse_dx(input);
msg.look_dy = (f32)pxl8_mouse_dy(input);
msg.buttons = pxl8_key_down(input, "space") ? 1 : 0;
pxl8_sim_world sim = make_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, &msg, &sim, dt);
}
}
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) {
if (!world || !gfx || !world->active_chunk) return;
if (!world || !gfx) return;
if (world->active_chunk->type == PXL8_CHUNK_BSP && world->active_chunk->bsp) {
pxl8_bsp_render(gfx, world->active_chunk->bsp, camera_pos);
update_sdf(world, camera_pos, PXL8_SDF_CELL);
pxl8_3d_set_sdf(gfx, &world->sdf);
if (world->active_chunk) {
if (world->active_chunk->type == PXL8_WORLD_CHUNK_BSP && world->active_chunk->bsp) {
pxl8_3d_set_bsp(gfx, world->active_chunk->bsp);
pxl8_bsp_render(gfx, world->active_chunk->bsp,
world->bsp_render_state, camera_pos);
}
} else {
pxl8_3d_set_bsp(gfx, NULL);
i32 cx = (i32)floorf(camera_pos.x / WORLD_CHUNK_SIZE);
i32 cy = (i32)floorf(camera_pos.y / WORLD_CHUNK_SIZE);
i32 cz = (i32)floorf(camera_pos.z / WORLD_CHUNK_SIZE);
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
bool wireframe = world->vxl_render_state && world->vxl_render_state->wireframe;
pxl8_gfx_material mat = {
.texture_id = 0,
.dynamic_lighting = true,
.vertex_color_passthrough = true,
.alpha = 255,
.wireframe = wireframe,
};
i32 r = world->render_distance;
for (i32 dy = -r; dy <= r; dy++) {
for (i32 dz = -r; dz <= r; dz++) {
for (i32 dx = -r; dx <= r; dx++) {
i32 chunk_cx = cx + dx;
i32 chunk_cy = cy + dy;
i32 chunk_cz = cz + dz;
f32 chunk_x = chunk_cx * WORLD_CHUNK_SIZE;
f32 chunk_y = chunk_cy * WORLD_CHUNK_SIZE;
f32 chunk_z = chunk_cz * WORLD_CHUNK_SIZE;
if (frustum) {
pxl8_vec3 aabb_min = {chunk_x, chunk_y, chunk_z};
pxl8_vec3 aabb_max = {chunk_x + WORLD_CHUNK_SIZE, chunk_y + WORLD_CHUNK_SIZE, chunk_z + WORLD_CHUNK_SIZE};
if (!pxl8_frustum_test_aabb(frustum, aabb_min, aabb_max)) continue;
}
pxl8_vxl_mesh_config config = PXL8_VXL_MESH_CONFIG_DEFAULT;
config.chunk_x = chunk_cx;
config.chunk_y = chunk_cy;
config.chunk_z = chunk_cz;
pxl8_mesh* mesh = pxl8_world_chunk_cache_get_mesh(
world->chunk_cache,
chunk_cx, chunk_cy, chunk_cz,
world->block_registry, &config);
if (mesh && mesh->index_count > 0) {
pxl8_mat4 scale = pxl8_mat4_scale(VOXEL_SCALE, VOXEL_SCALE, VOXEL_SCALE);
pxl8_mat4 translate = pxl8_mat4_translate(chunk_x, chunk_y, chunk_z);
pxl8_mat4 model = pxl8_mat4_multiply(translate, scale);
pxl8_3d_draw_mesh(gfx, mesh, &model, &mat);
}
}
}
}
}
}
@ -113,7 +475,7 @@ void pxl8_world_sync(pxl8_world* world, pxl8_net* net) {
u32 chunk_id = pxl8_net_chunk_id(net);
if (chunk_type == PXL8_CHUNK_TYPE_BSP && chunk_id != 0) {
pxl8_chunk* chunk = pxl8_chunk_cache_get_bsp(world->chunk_cache, chunk_id);
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_bsp(world->chunk_cache, chunk_id);
if (chunk && chunk->bsp) {
if (world->active_chunk != chunk) {
world->active_chunk = chunk;
@ -121,5 +483,278 @@ void pxl8_world_sync(pxl8_world* world, pxl8_net* net) {
chunk_id, chunk->bsp->num_vertices, chunk->bsp->num_faces);
}
}
} else if (chunk_id == 0 && world->active_chunk != NULL) {
world->active_chunk = NULL;
}
}
static void ensure_bsp_render_state(pxl8_world* world) {
if (!world || world->bsp_render_state) return;
if (!world->active_chunk || world->active_chunk->type != PXL8_WORLD_CHUNK_BSP) return;
if (!world->active_chunk->bsp) return;
world->bsp_render_state = pxl8_bsp_render_state_create(world->active_chunk->bsp->num_faces);
}
void pxl8_world_set_bsp_material(pxl8_world* world, u16 material_id, const pxl8_gfx_material* material) {
if (!world || !material) return;
ensure_bsp_render_state(world);
if (!world->bsp_render_state) return;
pxl8_bsp_set_material(world->bsp_render_state, material_id, material);
}
void pxl8_world_set_wireframe(pxl8_world* world, bool enabled) {
if (!world) return;
ensure_bsp_render_state(world);
if (world->bsp_render_state) {
pxl8_bsp_set_wireframe(world->bsp_render_state, enabled);
}
if (world->vxl_render_state) {
pxl8_vxl_set_wireframe(world->vxl_render_state, enabled);
}
}
i32 pxl8_world_get_render_distance(const pxl8_world* world) {
if (!world) return 3;
return world->render_distance;
}
void pxl8_world_set_render_distance(pxl8_world* world, i32 distance) {
if (!world) return;
if (distance < 1) distance = 1;
if (distance > 8) distance = 8;
world->render_distance = distance;
}
i32 pxl8_world_get_sim_distance(const pxl8_world* world) {
if (!world) return 4;
return world->sim_distance;
}
void pxl8_world_set_sim_distance(pxl8_world* world, i32 distance) {
if (!world) return;
if (distance < 1) distance = 1;
if (distance > 8) distance = 8;
world->sim_distance = distance;
}
void pxl8_world_init_local_player(pxl8_world* world, f32 x, f32 y, f32 z) {
if (!world) return;
world->local_player.pos = (pxl8_vec3){x, y, z};
world->local_player.vel = (pxl8_vec3){0, 0, 0};
world->local_player.yaw = 0;
world->local_player.pitch = 0;
world->local_player.flags = PXL8_SIM_FLAG_ALIVE | PXL8_SIM_FLAG_PLAYER | PXL8_SIM_FLAG_GROUNDED;
world->local_player.kind = 0;
world->client_tick = 0;
#ifdef PXL8_ASYNC_THREADS
world->render_state[0] = world->local_player;
world->render_state[1] = world->local_player;
#endif
}
pxl8_sim_entity* pxl8_world_local_player(pxl8_world* world) {
if (!world) return NULL;
if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return NULL;
return &world->local_player;
}
void pxl8_world_predict(pxl8_world* world, pxl8_net* net, const pxl8_input_msg* input, f32 dt) {
if (!world || !net || !input) return;
if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return;
pxl8_sim_world sim = make_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, input, &sim, dt);
world->client_tick++;
entity_to_userdata(&world->local_player, pxl8_net_predicted_state(net));
pxl8_net_predicted_tick_set(net, world->client_tick);
}
void pxl8_world_reconcile(pxl8_world* world, pxl8_net* net, f32 dt) {
if (!world || !net) return;
if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return;
if (!pxl8_net_needs_correction(net)) return;
u64 player_id = pxl8_net_player_id(net);
const u8* server_state = pxl8_net_entity_userdata(net, player_id);
if (!server_state) return;
userdata_to_entity(server_state, &world->local_player);
const pxl8_snapshot_header* snap = pxl8_net_snapshot(net);
u64 server_tick = snap ? snap->tick : 0;
for (u64 tick = server_tick + 1; tick <= world->client_tick; tick++) {
const pxl8_input_msg* input = pxl8_net_input_at(net, tick);
if (!input) continue;
pxl8_sim_world sim = make_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, input, &sim, dt);
}
entity_to_userdata(&world->local_player, pxl8_net_predicted_state(net));
}
#ifdef PXL8_ASYNC_THREADS
#define SIM_TIMESTEP (1.0f / 60.0f)
static void pxl8_world_sim_tick(pxl8_world* world, f32 dt) {
if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return;
pxl8_input_msg merged = {0};
pxl8_input_msg* input = NULL;
bool has_input = false;
while ((input = pxl8_queue_pop(&world->input_queue))) {
merged.look_dx += input->look_dx;
merged.look_dy += input->look_dy;
merged.move_x = input->move_x;
merged.move_y = input->move_y;
merged.buttons |= input->buttons;
has_input = true;
pxl8_free(input);
}
const f32 MAX_LOOK_DELTA = 100.0f;
if (merged.look_dx > MAX_LOOK_DELTA) merged.look_dx = MAX_LOOK_DELTA;
if (merged.look_dx < -MAX_LOOK_DELTA) merged.look_dx = -MAX_LOOK_DELTA;
if (merged.look_dy > MAX_LOOK_DELTA) merged.look_dy = MAX_LOOK_DELTA;
if (merged.look_dy < -MAX_LOOK_DELTA) merged.look_dy = -MAX_LOOK_DELTA;
pxl8_sim_world sim = make_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, &merged, &sim, dt);
if (world->net) {
entity_to_userdata(&world->local_player, pxl8_net_predicted_state(world->net));
pxl8_net_predicted_tick_set(world->net, world->client_tick);
}
world->client_tick++;
(void)has_input;
}
static void pxl8_world_swap_buffers(pxl8_world* world) {
u32 back = atomic_load(&world->active_buffer) ^ 1;
world->render_state[back] = world->local_player;
atomic_store(&world->active_buffer, back);
}
static int pxl8_world_sim_thread(void* data) {
pxl8_world* world = (pxl8_world*)data;
u64 last_time = pxl8_get_ticks_ns();
while (atomic_load(&world->sim_running)) {
if (atomic_load(&world->sim_paused)) {
last_time = pxl8_get_ticks_ns();
pxl8_sleep_ms(1);
continue;
}
u64 now = pxl8_get_ticks_ns();
f32 dt = (f32)(now - last_time) / 1e9f;
last_time = now;
if (dt > 0.1f) dt = 0.1f;
if (dt < 0.0001f) dt = 0.0001f;
world->sim_accumulator += dt;
while (world->sim_accumulator >= SIM_TIMESTEP) {
pxl8_world_chunk_cache_tick(world->chunk_cache);
if (world->net) {
pxl8_packet* pkt;
while ((pkt = pxl8_net_pop_packet(world->net))) {
pxl8_net_process_packet(world->net, pkt);
pxl8_net_packet_free(pkt);
}
pxl8_world_sync(world, world->net);
pxl8_world_reconcile(world, world->net, SIM_TIMESTEP);
}
pxl8_world_sim_tick(world, SIM_TIMESTEP);
world->sim_accumulator -= SIM_TIMESTEP;
}
pxl8_world_swap_buffers(world);
pxl8_sleep_ms(1);
}
return 0;
}
void pxl8_world_start_sim_thread(pxl8_world* world, pxl8_net* net) {
if (!world || world->sim_thread) return;
world->net = net;
pxl8_queue_init(&world->input_queue);
atomic_store(&world->active_buffer, 0);
atomic_store(&world->sim_running, true);
world->sim_accumulator = 0.0f;
world->render_state[0] = world->local_player;
world->render_state[1] = world->local_player;
world->sim_thread = pxl8_thread_create(pxl8_world_sim_thread, "pxl8_sim", world);
}
void pxl8_world_stop_sim_thread(pxl8_world* world) {
if (!world || !world->sim_thread) return;
atomic_store(&world->sim_running, false);
pxl8_thread_wait(world->sim_thread, NULL);
world->sim_thread = NULL;
pxl8_input_msg* input;
while ((input = pxl8_queue_pop(&world->input_queue))) {
pxl8_free(input);
}
}
void pxl8_world_pause_sim(pxl8_world* world, bool paused) {
if (!world) return;
if (paused) {
atomic_store(&world->sim_paused, true);
} else {
pxl8_input_msg* input;
while ((input = pxl8_queue_pop(&world->input_queue))) {
pxl8_free(input);
}
world->sim_accumulator = 0.0f;
atomic_store(&world->sim_paused, false);
}
}
void pxl8_world_push_input(pxl8_world* world, const pxl8_input_msg* input) {
if (!world || !input) return;
pxl8_input_msg* copy = pxl8_malloc(sizeof(pxl8_input_msg));
if (copy) {
*copy = *input;
if (!pxl8_queue_push(&world->input_queue, copy)) {
pxl8_free(copy);
}
}
}
const pxl8_sim_entity* pxl8_world_get_render_state(const pxl8_world* world) {
if (!world) return NULL;
u32 front = atomic_load(&((pxl8_world*)world)->active_buffer);
return &world->render_state[front];
}
f32 pxl8_world_get_interp_alpha(const pxl8_world* world) {
if (!world) return 1.0f;
return world->sim_accumulator / SIM_TIMESTEP;
}
#endif

View file

@ -1,12 +1,13 @@
#pragma once
#include "pxl8_chunk.h"
#include "pxl8_chunk_cache.h"
#include "pxl8_entity.h"
#include "pxl8_gfx.h"
#include "pxl8_math.h"
#include "pxl8_net.h"
#include "pxl8_sim.h"
#include "pxl8_types.h"
#include "pxl8_world_chunk.h"
#include "pxl8_world_chunk_cache.h"
#ifdef __cplusplus
extern "C" {
@ -17,21 +18,44 @@ typedef struct pxl8_world pxl8_world;
pxl8_world* pxl8_world_create(void);
void pxl8_world_destroy(pxl8_world* world);
pxl8_chunk_cache* pxl8_world_chunk_cache(pxl8_world* world);
pxl8_chunk* pxl8_world_active_chunk(pxl8_world* world);
void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_chunk* chunk);
pxl8_world_chunk_cache* pxl8_world_get_chunk_cache(pxl8_world* world);
pxl8_world_chunk* pxl8_world_active_chunk(pxl8_world* world);
void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_world_chunk* chunk);
pxl8_block_registry* pxl8_world_block_registry(pxl8_world* world);
pxl8_vxl_block_registry* pxl8_world_block_registry(pxl8_world* world);
pxl8_entity_pool* pxl8_world_entities(pxl8_world* world);
pxl8_entity pxl8_world_spawn(pxl8_world* world);
bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius);
pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
bool pxl8_world_point_solid(const pxl8_world* world, f32 x, f32 y, f32 z);
pxl8_ray pxl8_world_ray(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to);
pxl8_ray pxl8_world_sweep(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
void pxl8_world_update(pxl8_world* world, f32 dt);
void pxl8_world_update(pxl8_world* world, const pxl8_input_state* input, f32 dt);
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos);
void pxl8_world_sync(pxl8_world* world, pxl8_net* net);
void pxl8_world_set_bsp_material(pxl8_world* world, u16 material_id, const pxl8_gfx_material* material);
void pxl8_world_set_wireframe(pxl8_world* world, bool enabled);
i32 pxl8_world_get_render_distance(const pxl8_world* world);
void pxl8_world_set_render_distance(pxl8_world* world, i32 distance);
i32 pxl8_world_get_sim_distance(const pxl8_world* world);
void pxl8_world_set_sim_distance(pxl8_world* world, i32 distance);
void pxl8_world_init_local_player(pxl8_world* world, f32 x, f32 y, f32 z);
pxl8_sim_entity* pxl8_world_local_player(pxl8_world* world);
void pxl8_world_predict(pxl8_world* world, pxl8_net* net, const pxl8_input_msg* input, f32 dt);
void pxl8_world_reconcile(pxl8_world* world, pxl8_net* net, f32 dt);
#ifdef PXL8_ASYNC_THREADS
void pxl8_world_start_sim_thread(pxl8_world* world, pxl8_net* net);
void pxl8_world_stop_sim_thread(pxl8_world* world);
void pxl8_world_pause_sim(pxl8_world* world, bool paused);
void pxl8_world_push_input(pxl8_world* world, const pxl8_input_msg* input);
const pxl8_sim_entity* pxl8_world_get_render_state(const pxl8_world* world);
f32 pxl8_world_get_interp_alpha(const pxl8_world* world);
#endif
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,46 @@
#include "pxl8_world_chunk.h"
#include "pxl8_bsp.h"
#include "pxl8_mem.h"
#include "pxl8_vxl.h"
pxl8_world_chunk* pxl8_world_chunk_create_vxl(i32 cx, i32 cy, i32 cz) {
pxl8_world_chunk* chunk = pxl8_calloc(1, sizeof(pxl8_world_chunk));
if (!chunk) return NULL;
chunk->type = PXL8_WORLD_CHUNK_VXL;
chunk->cx = cx;
chunk->cy = cy;
chunk->cz = cz;
chunk->voxels = pxl8_vxl_chunk_create();
if (!chunk->voxels) {
pxl8_free(chunk);
return NULL;
}
return chunk;
}
pxl8_world_chunk* pxl8_world_chunk_create_bsp(u32 id) {
pxl8_world_chunk* chunk = pxl8_calloc(1, sizeof(pxl8_world_chunk));
if (!chunk) return NULL;
chunk->type = PXL8_WORLD_CHUNK_BSP;
chunk->id = id;
chunk->bsp = NULL;
return chunk;
}
void pxl8_world_chunk_destroy(pxl8_world_chunk* chunk) {
if (!chunk) return;
if (chunk->type == PXL8_WORLD_CHUNK_VXL && chunk->voxels) {
pxl8_vxl_chunk_destroy(chunk->voxels);
} else if (chunk->type == PXL8_WORLD_CHUNK_BSP && chunk->bsp) {
pxl8_bsp_destroy(chunk->bsp);
}
pxl8_free(chunk);
}

View file

@ -0,0 +1,33 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_types.h"
#include "pxl8_vxl.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum pxl8_world_chunk_type {
PXL8_WORLD_CHUNK_VXL,
PXL8_WORLD_CHUNK_BSP
} pxl8_world_chunk_type;
typedef struct pxl8_world_chunk {
pxl8_world_chunk_type type;
u32 id;
u32 version;
i32 cx, cy, cz;
union {
pxl8_bsp* bsp;
pxl8_vxl_chunk* voxels;
};
} pxl8_world_chunk;
pxl8_world_chunk* pxl8_world_chunk_create_vxl(i32 cx, i32 cy, i32 cz);
pxl8_world_chunk* pxl8_world_chunk_create_bsp(u32 id);
void pxl8_world_chunk_destroy(pxl8_world_chunk* chunk);
#ifdef __cplusplus
}
#endif

View file

@ -1,4 +1,4 @@
#include "pxl8_chunk_cache.h"
#include "pxl8_world_chunk_cache.h"
#include <stdlib.h>
#include <string.h>
@ -7,12 +7,12 @@
#include "pxl8_log.h"
#include "pxl8_mem.h"
#define PXL8_VOXEL_CHUNK_VOLUME (PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE)
#define PXL8_VXL_CHUNK_VOLUME (PXL8_VXL_CHUNK_SIZE * PXL8_VXL_CHUNK_SIZE * PXL8_VXL_CHUNK_SIZE)
static pxl8_chunk_cache_entry* find_entry_vxl(pxl8_chunk_cache* cache, i32 cx, i32 cy, i32 cz) {
static pxl8_world_chunk_cache_entry* find_entry_vxl(pxl8_world_chunk_cache* cache, i32 cx, i32 cy, i32 cz) {
for (u32 i = 0; i < cache->entry_count; i++) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
if (e->valid && e->chunk && e->chunk->type == PXL8_CHUNK_VXL &&
pxl8_world_chunk_cache_entry* e = &cache->entries[i];
if (e->valid && e->chunk && e->chunk->type == PXL8_WORLD_CHUNK_VXL &&
e->chunk->cx == cx && e->chunk->cy == cy && e->chunk->cz == cz) {
return e;
}
@ -20,10 +20,10 @@ static pxl8_chunk_cache_entry* find_entry_vxl(pxl8_chunk_cache* cache, i32 cx, i
return NULL;
}
static pxl8_chunk_cache_entry* find_entry_bsp(pxl8_chunk_cache* cache, u32 id) {
static pxl8_world_chunk_cache_entry* find_entry_bsp(pxl8_world_chunk_cache* cache, u32 id) {
for (u32 i = 0; i < cache->entry_count; i++) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
if (e->valid && e->chunk && e->chunk->type == PXL8_CHUNK_BSP &&
pxl8_world_chunk_cache_entry* e = &cache->entries[i];
if (e->valid && e->chunk && e->chunk->type == PXL8_WORLD_CHUNK_BSP &&
e->chunk->id == id) {
return e;
}
@ -31,16 +31,16 @@ static pxl8_chunk_cache_entry* find_entry_bsp(pxl8_chunk_cache* cache, u32 id) {
return NULL;
}
static pxl8_chunk_cache_entry* alloc_entry(pxl8_chunk_cache* cache) {
if (cache->entry_count < PXL8_CHUNK_CACHE_SIZE) {
pxl8_chunk_cache_entry* e = &cache->entries[cache->entry_count++];
static pxl8_world_chunk_cache_entry* alloc_entry(pxl8_world_chunk_cache* cache) {
if (cache->entry_count < PXL8_WORLD_CHUNK_CACHE_SIZE) {
pxl8_world_chunk_cache_entry* e = &cache->entries[cache->entry_count++];
memset(e, 0, sizeof(*e));
return e;
}
for (u32 i = 0; i < PXL8_CHUNK_CACHE_SIZE; i++) {
for (u32 i = 0; i < PXL8_WORLD_CHUNK_CACHE_SIZE; i++) {
if (!cache->entries[i].valid) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
pxl8_world_chunk_cache_entry* e = &cache->entries[i];
memset(e, 0, sizeof(*e));
return e;
}
@ -48,22 +48,22 @@ static pxl8_chunk_cache_entry* alloc_entry(pxl8_chunk_cache* cache) {
u64 oldest = cache->entries[0].last_used;
u32 slot = 0;
for (u32 i = 1; i < PXL8_CHUNK_CACHE_SIZE; i++) {
for (u32 i = 1; i < PXL8_WORLD_CHUNK_CACHE_SIZE; i++) {
if (cache->entries[i].last_used < oldest) {
oldest = cache->entries[i].last_used;
slot = i;
}
}
pxl8_chunk_cache_entry* e = &cache->entries[slot];
if (e->chunk) pxl8_chunk_destroy(e->chunk);
pxl8_world_chunk_cache_entry* e = &cache->entries[slot];
if (e->chunk) pxl8_world_chunk_destroy(e->chunk);
if (e->mesh) pxl8_mesh_destroy(e->mesh);
memset(e, 0, sizeof(*e));
return e;
}
static void assembly_reset(pxl8_chunk_assembly* a) {
a->type = PXL8_CHUNK_VXL;
static void assembly_reset(pxl8_world_chunk_assembly* a) {
a->type = PXL8_WORLD_CHUNK_VXL;
a->id = 0;
a->cx = 0;
a->cy = 0;
@ -76,9 +76,9 @@ static void assembly_reset(pxl8_chunk_assembly* a) {
a->complete = false;
}
static void assembly_init(pxl8_chunk_assembly* a, const pxl8_chunk_msg_header* hdr) {
static void assembly_init(pxl8_world_chunk_assembly* a, const pxl8_chunk_msg_header* hdr) {
assembly_reset(a);
a->type = hdr->chunk_type == PXL8_CHUNK_TYPE_BSP ? PXL8_CHUNK_BSP : PXL8_CHUNK_VXL;
a->type = hdr->chunk_type == PXL8_CHUNK_TYPE_BSP ? PXL8_WORLD_CHUNK_BSP : PXL8_WORLD_CHUNK_VXL;
a->id = hdr->id;
a->cx = hdr->cx;
a->cy = hdr->cy;
@ -94,25 +94,31 @@ static void assembly_init(pxl8_chunk_assembly* a, const pxl8_chunk_msg_header* h
}
}
static bool rle_decode_voxel(const u8* src, usize src_len, pxl8_voxel_chunk* chunk) {
static bool rle_decode_voxel(const u8* src, usize src_len, pxl8_vxl_chunk* chunk) {
u8* linear = pxl8_malloc(PXL8_VXL_CHUNK_VOLUME);
if (!linear) return false;
usize src_pos = 0;
usize dst_pos = 0;
while (src_pos + 1 < src_len && dst_pos < PXL8_VOXEL_CHUNK_VOLUME) {
while (src_pos + 1 < src_len && dst_pos < PXL8_VXL_CHUNK_VOLUME) {
u8 block = src[src_pos++];
u8 run_minus_one = src[src_pos++];
usize run = (usize)run_minus_one + 1;
for (usize i = 0; i < run && dst_pos < PXL8_VOXEL_CHUNK_VOLUME; i++) {
i32 x = (i32)(dst_pos % PXL8_VOXEL_CHUNK_SIZE);
i32 y = (i32)((dst_pos / PXL8_VOXEL_CHUNK_SIZE) % PXL8_VOXEL_CHUNK_SIZE);
i32 z = (i32)(dst_pos / (PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE));
pxl8_voxel_set(chunk, x, y, z, block);
dst_pos++;
for (usize i = 0; i < run && dst_pos < PXL8_VXL_CHUNK_VOLUME; i++) {
linear[dst_pos++] = block;
}
}
return dst_pos == PXL8_VOXEL_CHUNK_VOLUME;
if (dst_pos != PXL8_VXL_CHUNK_VOLUME) {
pxl8_free(linear);
return false;
}
pxl8_vxl_chunk_from_linear(chunk, linear);
pxl8_free(linear);
return true;
}
static pxl8_result deserialize_vertex(pxl8_stream* s, pxl8_bsp_vertex* v) {
@ -204,8 +210,8 @@ static pxl8_result deserialize_cell_portals(pxl8_stream* s, pxl8_bsp_cell_portal
return PXL8_OK;
}
static pxl8_bsp* assembly_to_bsp(pxl8_chunk_assembly* a) {
if (!a->complete || a->data_size < 44) {
static pxl8_bsp* assembly_to_bsp(pxl8_world_chunk_assembly* a) {
if (!a->complete || a->data_size < 48) {
return NULL;
}
@ -315,11 +321,11 @@ static pxl8_bsp* assembly_to_bsp(pxl8_chunk_assembly* a) {
return bsp;
}
static pxl8_result assemble_vxl(pxl8_chunk_cache* cache, pxl8_chunk_assembly* a) {
pxl8_chunk_cache_entry* entry = find_entry_vxl(cache, a->cx, a->cy, a->cz);
static pxl8_result assemble_vxl(pxl8_world_chunk_cache* cache, pxl8_world_chunk_assembly* a) {
pxl8_world_chunk_cache_entry* entry = find_entry_vxl(cache, a->cx, a->cy, a->cz);
if (!entry) {
entry = alloc_entry(cache);
entry->chunk = pxl8_chunk_create_vxl(a->cx, a->cy, a->cz);
entry->chunk = pxl8_world_chunk_create_vxl(a->cx, a->cy, a->cz);
entry->valid = true;
}
@ -332,9 +338,10 @@ static pxl8_result assemble_vxl(pxl8_chunk_cache* cache, pxl8_chunk_assembly* a)
entry->mesh = NULL;
}
pxl8_voxel_chunk_clear(entry->chunk->voxel);
pxl8_vxl_block_clear(entry->chunk->voxels);
if (!rle_decode_voxel(a->data, a->data_size, entry->chunk->voxel)) {
if (!rle_decode_voxel(a->data, a->data_size, entry->chunk->voxels)) {
pxl8_error("[CLIENT] RLE decode failed for chunk (%d,%d,%d)", a->cx, a->cy, a->cz);
return PXL8_ERROR_INVALID_ARGUMENT;
}
@ -342,7 +349,7 @@ static pxl8_result assemble_vxl(pxl8_chunk_cache* cache, pxl8_chunk_assembly* a)
return PXL8_OK;
}
static pxl8_result assemble_bsp(pxl8_chunk_cache* cache, pxl8_chunk_assembly* a) {
static pxl8_result assemble_bsp(pxl8_world_chunk_cache* cache, pxl8_world_chunk_assembly* a) {
pxl8_debug("[CLIENT] assemble_bsp: id=%u data_size=%zu", a->id, a->data_size);
pxl8_bsp* bsp = assembly_to_bsp(a);
if (!bsp) {
@ -352,14 +359,14 @@ static pxl8_result assemble_bsp(pxl8_chunk_cache* cache, pxl8_chunk_assembly* a)
}
pxl8_debug("[CLIENT] assemble_bsp: BSP created with %u verts %u faces", bsp->num_vertices, bsp->num_faces);
pxl8_chunk_cache_entry* entry = find_entry_bsp(cache, a->id);
pxl8_world_chunk_cache_entry* entry = find_entry_bsp(cache, a->id);
if (entry) {
if (entry->chunk && entry->chunk->bsp) {
pxl8_bsp_destroy(entry->chunk->bsp);
}
} else {
entry = alloc_entry(cache);
entry->chunk = pxl8_chunk_create_bsp(a->id);
entry->chunk = pxl8_world_chunk_create_bsp(a->id);
entry->valid = true;
}
@ -371,19 +378,19 @@ static pxl8_result assemble_bsp(pxl8_chunk_cache* cache, pxl8_chunk_assembly* a)
return PXL8_OK;
}
pxl8_chunk_cache* pxl8_chunk_cache_create(void) {
pxl8_chunk_cache* cache = pxl8_calloc(1, sizeof(pxl8_chunk_cache));
pxl8_world_chunk_cache* pxl8_world_chunk_cache_create(void) {
pxl8_world_chunk_cache* cache = pxl8_calloc(1, sizeof(pxl8_world_chunk_cache));
if (!cache) return NULL;
assembly_reset(&cache->assembly);
return cache;
}
void pxl8_chunk_cache_destroy(pxl8_chunk_cache* cache) {
void pxl8_world_chunk_cache_destroy(pxl8_world_chunk_cache* cache) {
if (!cache) return;
for (u32 i = 0; i < cache->entry_count; i++) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
if (e->chunk) pxl8_chunk_destroy(e->chunk);
pxl8_world_chunk_cache_entry* e = &cache->entries[i];
if (e->chunk) pxl8_world_chunk_destroy(e->chunk);
if (e->mesh) pxl8_mesh_destroy(e->mesh);
}
@ -391,12 +398,12 @@ void pxl8_chunk_cache_destroy(pxl8_chunk_cache* cache) {
pxl8_free(cache);
}
pxl8_result pxl8_chunk_cache_receive(pxl8_chunk_cache* cache,
const pxl8_chunk_msg_header* hdr,
const u8* payload, usize len) {
pxl8_result pxl8_world_chunk_cache_receive(pxl8_world_chunk_cache* cache,
const pxl8_chunk_msg_header* hdr,
const u8* payload, usize len) {
if (!cache || !hdr || !payload) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_chunk_assembly* a = &cache->assembly;
pxl8_world_chunk_assembly* a = &cache->assembly;
bool new_assembly = !a->active ||
(hdr->chunk_type == PXL8_CHUNK_TYPE_BSP && a->id != hdr->id) ||
@ -409,7 +416,7 @@ pxl8_result pxl8_chunk_cache_receive(pxl8_chunk_cache* cache,
assembly_init(a, hdr);
}
if (hdr->fragment_idx >= PXL8_CHUNK_MAX_FRAGMENTS) {
if (hdr->fragment_idx >= PXL8_WORLD_CHUNK_MAX_FRAGMENTS) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
@ -430,9 +437,7 @@ pxl8_result pxl8_chunk_cache_receive(pxl8_chunk_cache* cache,
if (hdr->flags & PXL8_CHUNK_FLAG_FINAL) {
a->complete = true;
pxl8_debug("[CLIENT] Final fragment received, assembling type=%d data_size=%zu", a->type, a->data_size);
if (a->type == PXL8_CHUNK_BSP) {
if (a->type == PXL8_WORLD_CHUNK_BSP) {
return assemble_bsp(cache, a);
} else {
return assemble_vxl(cache, a);
@ -442,9 +447,9 @@ pxl8_result pxl8_chunk_cache_receive(pxl8_chunk_cache* cache,
return PXL8_OK;
}
pxl8_chunk* pxl8_chunk_cache_get_vxl(pxl8_chunk_cache* cache, i32 cx, i32 cy, i32 cz) {
pxl8_world_chunk* pxl8_world_chunk_cache_get_vxl(pxl8_world_chunk_cache* cache, i32 cx, i32 cy, i32 cz) {
if (!cache) return NULL;
pxl8_chunk_cache_entry* e = find_entry_vxl(cache, cx, cy, cz);
pxl8_world_chunk_cache_entry* e = find_entry_vxl(cache, cx, cy, cz);
if (e) {
e->last_used = cache->frame_counter;
return e->chunk;
@ -452,9 +457,9 @@ pxl8_chunk* pxl8_chunk_cache_get_vxl(pxl8_chunk_cache* cache, i32 cx, i32 cy, i3
return NULL;
}
pxl8_chunk* pxl8_chunk_cache_get_bsp(pxl8_chunk_cache* cache, u32 id) {
pxl8_world_chunk* pxl8_world_chunk_cache_get_bsp(pxl8_world_chunk_cache* cache, u32 id) {
if (!cache) return NULL;
pxl8_chunk_cache_entry* e = find_entry_bsp(cache, id);
pxl8_world_chunk_cache_entry* e = find_entry_bsp(cache, id);
if (e) {
e->last_used = cache->frame_counter;
return e->chunk;
@ -462,17 +467,28 @@ pxl8_chunk* pxl8_chunk_cache_get_bsp(pxl8_chunk_cache* cache, u32 id) {
return NULL;
}
pxl8_mesh* pxl8_chunk_cache_get_mesh(pxl8_chunk_cache* cache,
i32 cx, i32 cy, i32 cz,
const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config) {
pxl8_mesh* pxl8_world_chunk_cache_get_mesh(pxl8_world_chunk_cache* cache,
i32 cx, i32 cy, i32 cz,
const pxl8_vxl_block_registry* registry,
const pxl8_vxl_mesh_config* config) {
if (!cache || !registry) return NULL;
pxl8_chunk_cache_entry* entry = find_entry_vxl(cache, cx, cy, cz);
if (!entry || !entry->chunk || !entry->chunk->voxel) return NULL;
pxl8_world_chunk_cache_entry* entry = find_entry_vxl(cache, cx, cy, cz);
if (!entry || !entry->chunk || !entry->chunk->voxels) return NULL;
pxl8_world_chunk* nx = pxl8_world_chunk_cache_get_vxl(cache, cx - 1, cy, cz);
pxl8_world_chunk* px = pxl8_world_chunk_cache_get_vxl(cache, cx + 1, cy, cz);
pxl8_world_chunk* ny = pxl8_world_chunk_cache_get_vxl(cache, cx, cy - 1, cz);
pxl8_world_chunk* py = pxl8_world_chunk_cache_get_vxl(cache, cx, cy + 1, cz);
pxl8_world_chunk* nz = pxl8_world_chunk_cache_get_vxl(cache, cx, cy, cz - 1);
pxl8_world_chunk* pz = pxl8_world_chunk_cache_get_vxl(cache, cx, cy, cz + 1);
bool all_neighbors = nx && px && ny && py && nz && pz;
if (entry->mesh && !entry->mesh_dirty) {
return entry->mesh;
if (entry->has_all_neighbors == all_neighbors) {
return entry->mesh;
}
}
if (entry->mesh) {
@ -480,47 +496,43 @@ pxl8_mesh* pxl8_chunk_cache_get_mesh(pxl8_chunk_cache* cache,
entry->mesh = NULL;
}
pxl8_chunk* nx = pxl8_chunk_cache_get_vxl(cache, cx - 1, cy, cz);
pxl8_chunk* px = pxl8_chunk_cache_get_vxl(cache, cx + 1, cy, cz);
pxl8_chunk* ny = pxl8_chunk_cache_get_vxl(cache, cx, cy - 1, cz);
pxl8_chunk* py = pxl8_chunk_cache_get_vxl(cache, cx, cy + 1, cz);
pxl8_chunk* nz = pxl8_chunk_cache_get_vxl(cache, cx, cy, cz - 1);
pxl8_chunk* pz = pxl8_chunk_cache_get_vxl(cache, cx, cy, cz + 1);
const pxl8_voxel_chunk* neighbors[6] = {
nx ? nx->voxel : NULL,
px ? px->voxel : NULL,
ny ? ny->voxel : NULL,
py ? py->voxel : NULL,
nz ? nz->voxel : NULL,
pz ? pz->voxel : NULL
const pxl8_vxl_chunk* neighbors[6] = {
nx ? nx->voxels : NULL,
px ? px->voxels : NULL,
ny ? ny->voxels : NULL,
py ? py->voxels : NULL,
nz ? nz->voxels : NULL,
pz ? pz->voxels : NULL
};
entry->mesh = pxl8_voxel_build_mesh(entry->chunk->voxel, neighbors, registry, config);
pxl8_vxl_mesh_config local_config = config ? *config : PXL8_VXL_MESH_CONFIG_DEFAULT;
entry->mesh = pxl8_vxl_build_mesh(entry->chunk->voxels, neighbors, registry, &local_config);
entry->mesh_dirty = false;
entry->has_all_neighbors = all_neighbors;
return entry->mesh;
}
void pxl8_chunk_cache_tick(pxl8_chunk_cache* cache) {
void pxl8_world_chunk_cache_tick(pxl8_world_chunk_cache* cache) {
if (!cache) return;
cache->frame_counter++;
}
void pxl8_chunk_cache_evict_distant(pxl8_chunk_cache* cache,
i32 cx, i32 cy, i32 cz, i32 radius) {
void pxl8_world_chunk_cache_evict_distant(pxl8_world_chunk_cache* cache,
i32 cx, i32 cy, i32 cz, i32 radius) {
if (!cache) return;
for (u32 i = 0; i < cache->entry_count; i++) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
if (!e->valid || !e->chunk || e->chunk->type != PXL8_CHUNK_VXL) continue;
pxl8_world_chunk_cache_entry* e = &cache->entries[i];
if (!e->valid || !e->chunk || e->chunk->type != PXL8_WORLD_CHUNK_VXL) continue;
i32 dx = e->chunk->cx - cx;
i32 dy = e->chunk->cy - cy;
i32 dz = e->chunk->cz - cz;
if (abs(dx) > radius || abs(dy) > radius || abs(dz) > radius) {
pxl8_chunk_destroy(e->chunk);
pxl8_world_chunk_destroy(e->chunk);
if (e->mesh) pxl8_mesh_destroy(e->mesh);
e->chunk = NULL;
e->mesh = NULL;
@ -529,7 +541,7 @@ void pxl8_chunk_cache_evict_distant(pxl8_chunk_cache* cache,
}
}
void pxl8_chunk_cache_invalidate_meshes(pxl8_chunk_cache* cache) {
void pxl8_world_chunk_cache_invalidate_meshes(pxl8_world_chunk_cache* cache) {
if (!cache) return;
for (u32 i = 0; i < cache->entry_count; i++) {

View file

@ -0,0 +1,69 @@
#pragma once
#include "pxl8_mesh.h"
#include "pxl8_protocol.h"
#include "pxl8_types.h"
#include "pxl8_vxl_render.h"
#include "pxl8_world_chunk.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_WORLD_CHUNK_CACHE_SIZE 512
#define PXL8_WORLD_CHUNK_MAX_FRAGMENTS 64
#define PXL8_WORLD_CHUNK_MAX_DATA_SIZE 131072
typedef struct pxl8_world_chunk_cache_entry {
pxl8_world_chunk* chunk;
pxl8_mesh* mesh;
u64 last_used;
bool mesh_dirty;
bool valid;
bool has_all_neighbors;
} pxl8_world_chunk_cache_entry;
typedef struct pxl8_world_chunk_assembly {
pxl8_world_chunk_type type;
u32 id;
i32 cx, cy, cz;
u32 version;
u8 fragment_count;
u8 fragments_received;
u8* data;
u32 data_size;
u32 data_capacity;
bool active;
bool complete;
} pxl8_world_chunk_assembly;
typedef struct pxl8_world_chunk_cache {
pxl8_world_chunk_cache_entry entries[PXL8_WORLD_CHUNK_CACHE_SIZE];
pxl8_world_chunk_assembly assembly;
u32 entry_count;
u64 frame_counter;
} pxl8_world_chunk_cache;
pxl8_world_chunk_cache* pxl8_world_chunk_cache_create(void);
void pxl8_world_chunk_cache_destroy(pxl8_world_chunk_cache* cache);
pxl8_result pxl8_world_chunk_cache_receive(pxl8_world_chunk_cache* cache,
const pxl8_chunk_msg_header* hdr,
const u8* payload, usize len);
pxl8_world_chunk* pxl8_world_chunk_cache_get_vxl(pxl8_world_chunk_cache* cache, i32 cx, i32 cy, i32 cz);
pxl8_world_chunk* pxl8_world_chunk_cache_get_bsp(pxl8_world_chunk_cache* cache, u32 id);
pxl8_mesh* pxl8_world_chunk_cache_get_mesh(pxl8_world_chunk_cache* cache,
i32 cx, i32 cy, i32 cz,
const pxl8_vxl_block_registry* registry,
const pxl8_vxl_mesh_config* config);
void pxl8_world_chunk_cache_tick(pxl8_world_chunk_cache* cache);
void pxl8_world_chunk_cache_evict_distant(pxl8_world_chunk_cache* cache,
i32 cx, i32 cy, i32 cz, i32 radius);
void pxl8_world_chunk_cache_invalidate_meshes(pxl8_world_chunk_cache* cache);
#ifdef __cplusplus
}
#endif