Render culling on sceneoverworld.h

This commit is contained in:
2026-07-08 12:02:41 -05:00
parent 195399635e
commit 3723921573
3 changed files with 179 additions and 38 deletions
+1
View File
@@ -7,6 +7,7 @@
#include "itemgive.h"
#include "rpg/item/backpack.h"
#include "rpg/item/item.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "util/string.h"
#include "error/error.h"
+88 -35
View File
@@ -31,15 +31,15 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData) {
errorret_t sceneOverworldUpdate(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
errorOk();
}
errorret_t sceneOverworldRender(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
sceneoverworld_t *overworld = &sceneData->overworld;
sceneOverworldCullUpdate(overworld);
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
@@ -65,7 +65,7 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
));
// Base terrain meshes, drawn with normal depth testing.
errorChain(sceneOverworldDrawChunksBase());
errorChain(sceneOverworldDrawChunksBase(overworld));
// Entities are drawn with depth testing fully disabled so sloped tiles
// (ramps) never clip them; entity-vs-entity overlap falls back to
@@ -75,34 +75,10 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
}));
// Entities
{
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
entity_t *ent = &ENTITIES[i];
if(ent->type == ENTITY_TYPE_NULL) continue;
spritebatchsprite_t sprite;
glm_vec3_copy(ent->renderPosition, sprite.min);
glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight
glm_vec3_copy(sprite.min, sprite.max);
glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max);
glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin);
glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax);
color_t color;
switch(ent->direction) {
case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break;
case ENTITY_DIR_EAST: color = COLOR_RED; break;
case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break;
case ENTITY_DIR_WEST: color = COLOR_BLUE; break;
default: color = COLOR_CYAN; break;
}
shadermaterial_t material = {
.unlit = { .color = color, .texture = NULL }
};
spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material);
spriteBatchFlush();
}
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
entity_t *ent = &ENTITIES[i];
if(ent->type == ENTITY_TYPE_NULL) continue;
errorChain(sceneOverworldDrawEntity(overworld, ent));
}
// Other chunk meshes (trees, buildings, etc), drawn last with normal
@@ -111,15 +87,91 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
errorChain(sceneOverworldDrawChunksProps());
errorChain(sceneOverworldDrawChunksProps(overworld));
errorOk();
}
errorret_t sceneOverworldDrawChunksBase() {
void sceneOverworldCullUpdate(sceneoverworld_t *overworld) {
rpgCameraGetPosition(overworld->cullTarget);
const float_t worldH =
(float_t)(SCREEN.height / SCREEN.scale3d) / TILE_SIZE_PIXELS;
const float_t worldW = worldH * SCREEN.aspect;
overworld->cullHalfRangeX =
(worldW * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_X;
overworld->cullHalfRangeY =
(worldH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y;
}
bool_t sceneOverworldChunkShouldRender(
const sceneoverworld_t *overworld,
const chunk_t *chunk
) {
worldpos_t worldPos;
chunkPosToWorldPos(&chunk->position, &worldPos);
const float_t chunkCenterX = (float_t)worldPos.x + (CHUNK_WIDTH * 0.5f);
const float_t chunkCenterY = (float_t)worldPos.y + (CHUNK_HEIGHT * 0.5f);
if(fabsf(overworld->cullTarget[0] - chunkCenterX) >
overworld->cullHalfRangeX) return false;
if(fabsf(overworld->cullTarget[1] - chunkCenterY) >
overworld->cullHalfRangeY) return false;
return true;
}
bool_t sceneOverworldEntityShouldRender(
const sceneoverworld_t *overworld,
const entity_t *ent
) {
if(fabsf(overworld->cullTarget[0] - ent->renderPosition[0]) >
overworld->cullHalfRangeX) return false;
if(fabsf(overworld->cullTarget[1] - ent->renderPosition[1]) >
overworld->cullHalfRangeY) return false;
return true;
}
errorret_t sceneOverworldDrawEntity(
const sceneoverworld_t *overworld,
entity_t *ent
) {
if(!sceneOverworldEntityShouldRender(overworld, ent)) errorOk();
spritebatchsprite_t sprite;
glm_vec3_copy(ent->renderPosition, sprite.min);
glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight
glm_vec3_copy(sprite.min, sprite.max);
glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max);
glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin);
glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax);
color_t color;
switch(ent->direction) {
case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break;
case ENTITY_DIR_EAST: color = COLOR_RED; break;
case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break;
case ENTITY_DIR_WEST: color = COLOR_BLUE; break;
default: color = COLOR_CYAN; break;
}
shadermaterial_t material = {
.unlit = { .color = color, .texture = NULL }
};
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
errorChain(spriteBatchFlush());
errorOk();
}
errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = MAP.chunkOrder[i];
if(chunk == NULL) continue;
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
if(chunk->meshCount == 0) continue;
if(chunk->modelEntries[0] == NULL) continue;
if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue;
@@ -152,10 +204,11 @@ errorret_t sceneOverworldDrawChunksBase() {
errorOk();
}
errorret_t sceneOverworldDrawChunksProps() {
errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = MAP.chunkOrder[i];
if(chunk == NULL) continue;
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
for(uint8_t m = 1; m < chunk->meshCount; m++) {
if(chunk->modelEntries[m] == NULL) continue;
+90 -3
View File
@@ -7,11 +7,35 @@
#pragma once
#include "scene/scenebase.h"
#include "rpg/overworld/chunk.h"
#include "rpg/entity/entity.h"
typedef struct {
// Cached per-frame chunk culling bounds, refreshed by
// sceneOverworldCullUpdate() so sceneOverworldChunkShouldRender()
// does not need to recompute them for every chunk.
vec3 cullTarget;
float_t cullHalfRangeX;
float_t cullHalfRangeY;
} sceneoverworld_t;
// Extra world-space margin, in tiles, added on top of the rough
// screen-size skin below - cheap insurance against the approximation
// culling a chunk that is actually still just in view. Y gets its own
// value since the camera's tilt makes the vertical check less exact.
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_X 8.0f
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y 4.0f
// The camera is tilted between the ground-depth (Y) and height (Z)
// axes, so a chunk's on-screen vertical extent also depends on its Z
// layers. Rather than projecting every corner, fold that into a
// generous skin margin on the depth check.
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_X \
((CHUNK_WIDTH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_PADDING_X)
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y \
((CHUNK_HEIGHT * 0.5f) + (CHUNK_DEPTH * WORLD_LAYER_HEIGHT) + \
SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y)
/**
* Initialises the overworld scene.
*
@@ -28,22 +52,85 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData);
*/
errorret_t sceneOverworldUpdate(scenedata_t *sceneData);
/**
* Refreshes the chunk culling bounds cached on the overworld scene data,
* from the current camera position and screen dimensions. Must be
* called once per frame before sceneOverworldChunkShouldRender().
*
* @param overworld The overworld scene data to update.
*/
void sceneOverworldCullUpdate(sceneoverworld_t *overworld);
/**
* Roughly checks whether a chunk falls within the visible screen area,
* based on chunk size and screen dimensions. Cheap approximation, not
* an exact frustum check - includes a generous skin margin to account
* for the camera's angle tilting height/depth onto the screen's
* vertical axis, so it may pass some chunks that are actually just out
* of view but will never wrongly cull one that is visible.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @param chunk The chunk to check.
* @return true if the chunk should be rendered, false otherwise.
*/
bool_t sceneOverworldChunkShouldRender(
const sceneoverworld_t *overworld,
const chunk_t *chunk
);
/**
* Draws the base (tile) mesh of every loaded chunk, with normal depth
* testing. Must be called before entities are rendered.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @return An error if drawing failed, or errorOk() on success.
*/
errorret_t sceneOverworldDrawChunksBase();
errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld);
/**
* Draws every loaded chunk's additional meshes (trees, buildings, etc),
* with normal depth testing so they correctly occlude entities standing
* beneath them. Must be called after entities are rendered.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @return An error if drawing failed, or errorOk() on success.
*/
errorret_t sceneOverworldDrawChunksProps();
errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld);
/**
* Roughly checks whether an entity falls within the visible screen
* area. Reuses the same cached chunk culling bounds, since an entity
* is just a point within that same space - deliberately cheap, a
* couple of subtractions and comparisons, since it runs per entity
* every frame and it is not worth spending more math than that to
* avoid drawing one that is slightly off-screen.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @param ent The entity to check.
* @return true if the entity should be rendered, false otherwise.
*/
bool_t sceneOverworldEntityShouldRender(
const sceneoverworld_t *overworld,
const entity_t *ent
);
/**
* Draws a single entity as a sprite, colored by its facing direction.
* Skips drawing (without error) if the entity should not render.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @param ent The entity to draw.
* @return An error if drawing failed, or errorOk() on success.
*/
errorret_t sceneOverworldDrawEntity(
const sceneoverworld_t *overworld,
entity_t *ent
);
/**
* Renders the overworld scene.