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

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