367 lines
10 KiB
C
367 lines
10 KiB
C
#include <assert.h>
|
|
#include <asprintf.h>
|
|
#include <linmath.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include "client/client_config.h"
|
|
#include "client/client_node.h"
|
|
#include "client/client_terrain.h"
|
|
#include "client/cube.h"
|
|
#include "client/frustum.h"
|
|
#include "client/opengl.h"
|
|
#include "client/light.h"
|
|
#include "client/shader.h"
|
|
#include "client/terrain_gfx.h"
|
|
#include "common/facedir.h"
|
|
|
|
typedef struct {
|
|
TerrainChunk *chunk; // input: chunk pointer
|
|
TerrainChunkMeta *meta; // input: coersed chunk metadata pointer
|
|
v3s32 chunkp; // input: position of chunk
|
|
bool animate; // input: disable edge culling
|
|
ModelBatch *batch; // main output: vertex data
|
|
ModelBatch *batch_blend; // main output: vertex data for transparent textures
|
|
bool abort; // output: state changes have occured that invalidate generated output
|
|
bool grabbed[6]; // output: neighbors that have been grabbed
|
|
bool visible; // output: edge culled model would be visible
|
|
bool remake_needed; // output: edge culled model would be different from non-culled
|
|
} ChunkRenderData;
|
|
|
|
static VertexLayout terrain_vertex_layout = {
|
|
.attributes = (VertexAttribute []) {
|
|
{GL_FLOAT, 3, sizeof(v3f32)}, // position
|
|
{GL_FLOAT, 3, sizeof(v3f32)}, // normal
|
|
{GL_FLOAT, 2, sizeof(v2f32)}, // textureCoordinates
|
|
{GL_FLOAT, 1, sizeof(f32 )}, // textureIndex
|
|
{GL_FLOAT, 3, sizeof(v3f32)}, // color
|
|
},
|
|
.count = 5,
|
|
.size = sizeof(TerrainVertex),
|
|
};
|
|
|
|
static v3f32 center_offset = {
|
|
CHUNK_SIZE * 0.5f + 0.5f,
|
|
CHUNK_SIZE * 0.5f + 0.5f,
|
|
CHUNK_SIZE * 0.5f + 0.5f,
|
|
};
|
|
|
|
static GLuint shader_prog;
|
|
static GLint loc_VP;
|
|
|
|
static LightShader light_shader;
|
|
static ModelShader model_shader;
|
|
|
|
static void grab_neighbor(ChunkRenderData *data, int i)
|
|
{
|
|
// return if we've already subscribed/grabbed the lock
|
|
if (data->meta->depends[i])
|
|
return;
|
|
|
|
// we are now subscribed to state changes from the neighbor
|
|
data->meta->depends[i] = true;
|
|
|
|
TerrainChunk *neighbor = data->meta->neighbors[i];
|
|
TerrainChunkMeta *neighbor_meta = neighbor->extra;
|
|
|
|
pthread_rwlock_rdlock(&neighbor_meta->lock_state);
|
|
// check neighbor in case it was already in a bad state before we subscribed
|
|
if ((data->grabbed[i] = neighbor_meta->state > CHUNK_STATE_RECV))
|
|
// if state is good, actually grab the data lock in read mode
|
|
assert(pthread_rwlock_rdlock(&neighbor->lock) == 0);
|
|
else
|
|
// if state is bad, set flag to abort
|
|
data->abort = true;
|
|
pthread_rwlock_unlock(&neighbor_meta->lock_state);
|
|
}
|
|
|
|
static inline bool show_face(ChunkRenderData *data, NodeArgsRender *args, v3s32 offset)
|
|
{
|
|
NodeVisibility visibility = client_node_def[args->node->type].visibility;
|
|
|
|
// always render clip nodes
|
|
if (visibility == VISIBILITY_CLIP)
|
|
return data->visible = true;
|
|
|
|
// calculate offset of neighbor node from current chunk
|
|
v3s32 nbr_offset = v3s32_add(offset, facedir[args->f]);
|
|
|
|
// if offset is outside bounds, we'll have to select a neighbor chunk
|
|
bool nbr_chunk =
|
|
nbr_offset.x < 0 || nbr_offset.x >= CHUNK_SIZE ||
|
|
nbr_offset.y < 0 || nbr_offset.y >= CHUNK_SIZE ||
|
|
nbr_offset.z < 0 || nbr_offset.z >= CHUNK_SIZE;
|
|
|
|
NodeType nbr_node = NODE_UNKNOWN;
|
|
|
|
if (nbr_chunk) {
|
|
// grab neighbor chunk data lock
|
|
grab_neighbor(data, args->f);
|
|
|
|
// if grabbing failed, return true so caller immediately takes notice of abort
|
|
if (data->abort)
|
|
return true;
|
|
|
|
nbr_offset = terrain_offset(nbr_offset);
|
|
|
|
// select node from neighbor chunk
|
|
nbr_node = data->meta->neighbors[args->f]->data
|
|
[nbr_offset.x][nbr_offset.y][nbr_offset.z].type;
|
|
} else {
|
|
// select node from current chunk
|
|
nbr_node = data->chunk->data[nbr_offset.x][nbr_offset.y][nbr_offset.z].type;
|
|
}
|
|
|
|
if (visibility == VISIBILITY_BLEND) {
|
|
// liquid nodes only render faces towards 'outsiders'
|
|
if (nbr_node != args->node->type)
|
|
return data->visible = true;
|
|
} else { // visibility == VISIBILITY_SOLID
|
|
// faces between two solid nodes are culled
|
|
if (client_node_def[nbr_node].visibility != VISIBILITY_SOLID)
|
|
return data->visible = true;
|
|
|
|
// if the chunk needs to be animated, dont cull faces to nodes in other chunks
|
|
// but remember to rebuild the chunk model after the animation has finished
|
|
if (nbr_chunk && data->animate)
|
|
return data->remake_needed = true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static inline void render_node(ChunkRenderData *data, v3s32 offset)
|
|
{
|
|
NodeArgsRender args;
|
|
|
|
args.node = &data->chunk->data[offset.x][offset.y][offset.z];
|
|
|
|
ClientNodeDef *def = &client_node_def[args.node->type];
|
|
|
|
if (def->visibility == VISIBILITY_NONE)
|
|
return;
|
|
|
|
v3f32 vertex_offset = v3f32_sub(v3s32_to_f32(offset), center_offset);
|
|
if (def->render)
|
|
args.pos = v3s32_add(offset, data->chunkp);
|
|
|
|
for (args.f = 0; args.f < 6; args.f++) {
|
|
if (!show_face(data, &args, offset))
|
|
continue;
|
|
|
|
if (data->abort)
|
|
return;
|
|
|
|
ModelBatch *batch = def->visibility == VISIBILITY_BLEND ? data->batch_blend : data->batch;
|
|
for (args.v = 0; args.v < 6; args.v++) {
|
|
args.vertex.cube = cube_vertices[args.f][args.v];
|
|
args.vertex.cube.position = v3f32_add(args.vertex.cube.position, vertex_offset);
|
|
args.vertex.color = (v3f32) {1.0f, 1.0f, 1.0f};
|
|
|
|
if (def->render)
|
|
def->render(&args);
|
|
|
|
model_batch_add_vertex(batch, def->tiles.textures[args.f]->txo, &args.vertex);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void animate_chunk_model(Model *model, f64 dtime)
|
|
{
|
|
bool finished = (model->root->scale.x += dtime * 2.0f) > 1.0f;
|
|
if (finished)
|
|
model->root->scale.x = 1.0f;
|
|
|
|
model->root->scale.z
|
|
= model->root->scale.y
|
|
= model->root->scale.x;
|
|
|
|
model_node_transform(model->root);
|
|
|
|
if (finished) {
|
|
model->callbacks.step = NULL;
|
|
|
|
if (model->extra)
|
|
client_terrain_meshgen_task(model->extra, false);
|
|
}
|
|
}
|
|
|
|
static Model *create_chunk_model(ChunkRenderData *data)
|
|
{
|
|
if (!data->visible || (!data->batch->textures.siz && !data->batch_blend->textures.siz))
|
|
return NULL;
|
|
|
|
Model *model = model_create();
|
|
|
|
if (data->remake_needed)
|
|
model->extra = data->chunk;
|
|
|
|
model->box = (aabb3f32) {
|
|
v3f32_sub((v3f32) {-1.0f, -1.0f, -1.0f}, center_offset),
|
|
v3f32_add((v3f32) {+1.0f, +1.0f, +1.0f}, center_offset)};
|
|
|
|
model->callbacks.step = data->animate ? &animate_chunk_model : NULL;
|
|
model->callbacks.delete = &model_free_meshes;
|
|
model->flags.frustum_culling = 1;
|
|
model->flags.transparent = data->batch_blend->textures.siz > 0;
|
|
|
|
model->root->pos = v3f32_add(v3s32_to_f32(data->chunkp), center_offset);
|
|
model->root->scale = data->animate ? (v3f32) {0.0f, 0.0f, 0.0f} : (v3f32) {1.0f, 1.0f, 1.0f};
|
|
|
|
model_node_add_batch(model->root, data->batch);
|
|
model_node_add_batch(model->root, data->batch_blend);
|
|
|
|
return model;
|
|
}
|
|
|
|
void terrain_gfx_init()
|
|
{
|
|
GLint texture_batch_units = opengl_texture_batch_units();
|
|
|
|
char *shader_def;
|
|
asprintf(&shader_def,
|
|
"#define TEXURE_BATCH_UNITS %d\n"
|
|
"#define VIEW_DISTANCE %lf\n",
|
|
texture_batch_units,
|
|
client_config.view_distance
|
|
);
|
|
|
|
shader_prog = shader_program_create(ASSET_PATH "shaders/3d/terrain", shader_def);
|
|
free(shader_def);
|
|
|
|
loc_VP = glGetUniformLocation(shader_prog, "VP"); GL_DEBUG
|
|
|
|
if (texture_batch_units > 1) {
|
|
GLint texture_indices[texture_batch_units];
|
|
for (GLint i = 0; i < texture_batch_units; i++)
|
|
texture_indices[i] = i;
|
|
|
|
glProgramUniform1iv(shader_prog, glGetUniformLocation(shader_prog, "textures"),
|
|
texture_batch_units, texture_indices); GL_DEBUG
|
|
}
|
|
|
|
model_shader.prog = shader_prog;
|
|
model_shader.loc_transform = glGetUniformLocation(shader_prog, "model"); GL_DEBUG
|
|
|
|
light_shader.prog = shader_prog;
|
|
light_shader_locate(&light_shader);
|
|
}
|
|
|
|
void terrain_gfx_deinit()
|
|
{
|
|
glDeleteProgram(shader_prog); GL_DEBUG
|
|
}
|
|
|
|
void terrain_gfx_update()
|
|
{
|
|
glProgramUniformMatrix4fv(shader_prog, loc_VP, 1, GL_FALSE, frustum[0]); GL_DEBUG
|
|
light_shader_update(&light_shader);
|
|
}
|
|
|
|
void terrain_gfx_make_chunk_model(TerrainChunk *chunk)
|
|
{
|
|
// type coersion
|
|
TerrainChunkMeta *meta = chunk->extra;
|
|
|
|
// lock model mutex
|
|
pthread_mutex_lock(&meta->mtx_model);
|
|
|
|
// giving 10 arguments to a function is slow and unmaintainable, use pointer to struct instead
|
|
ChunkRenderData data = {
|
|
.chunk = chunk,
|
|
.meta = meta,
|
|
.chunkp = v3s32_scale(chunk->pos, CHUNK_SIZE),
|
|
.animate = false,
|
|
.batch = model_batch_create(
|
|
&model_shader, &terrain_vertex_layout, offsetof(TerrainVertex, textureIndex)),
|
|
.batch_blend = model_batch_create(
|
|
&model_shader, &terrain_vertex_layout, offsetof(TerrainVertex, textureIndex)),
|
|
.abort = false,
|
|
.grabbed = {false},
|
|
.visible = false,
|
|
.remake_needed = false,
|
|
};
|
|
|
|
// animate if old animation hasn't finished (or this is the first model)
|
|
if (meta->model)
|
|
data.animate = meta->model->callbacks.step ? true : false;
|
|
else
|
|
data.animate = !meta->has_model;
|
|
|
|
// obtain own data lock
|
|
assert(pthread_rwlock_rdlock(&chunk->lock) == 0);
|
|
|
|
// clear dependencies, they are repopulated by calls to grab_neighbor
|
|
for (int i = 0; i < 6; i++)
|
|
meta->depends[i] = false;
|
|
|
|
// render nodes
|
|
CHUNK_ITERATE {
|
|
// obtain changed state
|
|
pthread_rwlock_rdlock(&meta->lock_state);
|
|
data.abort = meta->state < CHUNK_STATE_CLEAN;
|
|
pthread_rwlock_unlock(&meta->lock_state);
|
|
|
|
// abort if chunk has been changed
|
|
// just "break" won't work, the CHUNK_ITERATE macro is a nested loop
|
|
if (data.abort)
|
|
goto abort;
|
|
|
|
// put vertex data into batches
|
|
render_node(&data, (v3s32) {x, y, z});
|
|
|
|
// abort if failed to grab a neighbor
|
|
if (data.abort)
|
|
goto abort;
|
|
}
|
|
abort:
|
|
|
|
// release grabbed data locks
|
|
for (int i = 0; i < 6; i++)
|
|
if (data.grabbed[i])
|
|
pthread_rwlock_unlock(&meta->neighbors[i]->lock);
|
|
|
|
// release own data lock
|
|
pthread_rwlock_unlock(&chunk->lock);
|
|
|
|
// only create model if we didn't abort
|
|
Model *model = data.abort ? NULL : create_chunk_model(&data);
|
|
|
|
// make sure to free batch mem if it wasn't fed into model
|
|
if (!model) {
|
|
model_batch_free(data.batch);
|
|
model_batch_free(data.batch_blend);
|
|
}
|
|
|
|
// abort if chunk changed
|
|
if (data.abort) {
|
|
pthread_mutex_unlock(&meta->mtx_model);
|
|
return;
|
|
}
|
|
|
|
// replace old model
|
|
if (meta->model) {
|
|
if (model) {
|
|
// copy animation callback
|
|
model->callbacks.step = meta->model->callbacks.step;
|
|
// copy scale
|
|
model->root->scale = meta->model->root->scale;
|
|
model_node_transform(model->root);
|
|
}
|
|
|
|
// if old model wasn't drawn in this frame yet, new model will be drawn instead
|
|
meta->model->replace = model;
|
|
meta->model->flags.delete = 1;
|
|
} else if (model) {
|
|
// model will be drawn in next frame
|
|
model_scene_add(model);
|
|
model_node_transform(model->root);
|
|
}
|
|
|
|
// update pointers
|
|
meta->model = model;
|
|
meta->has_model = true;
|
|
|
|
// bye bye
|
|
pthread_mutex_unlock(&meta->mtx_model);
|
|
}
|