feat: player rendering (#27)

* refactor(player): extract player rendering into PlayerRenderer class

Move player rendering code from Renderer to new PlayerRenderer class.
Add normal and tangent vertex attributes for improved per-pixel lighting.


* feat(player-renderer): add shadow rendering and include local player data

* feat(gameplay): add player yaw/pitch synchronization and rendering

* refactor(player_renderer): split rendering into per-body-part VAOs and VBOs

Move `PlayerVertex` struct to header and replace single VAO/VBO with arrays of 6 parts. Implement head pitch rotation in `render()` and `shadow_render()` by applying a separate model matrix for the head (index 0). Remove old `vao()` and `sum()` accessors. Adjust model translation for consistency.

* feat(player): add texture mapping to player model using skin

* fix(player-renderer): correct player rendering rotation and texture coordinates

* feat(gameplay): add player gait system with limb animation

* fix(player): update leg UVs and remove yaw wrap

Update left and right leg UV coordinates to align with updated player texture. Remove yaw modulo operation to allow unrestricted yaw rotation beyond 360 degrees.

* feat(camera): add third-person perspective and walk animation

- Introduce Perspective enum (FIRST_PERSON, THIRD_PERSON_BACK, THIRD_PERSON_FRONT) and m_front member for camera direction.
- Handle F5 key to cycle perspectives.
- Add angle and walk_time to ClientPlayer for walking animation.
- Update player rendering to skip local player only in first person and fix rotation sign.
- Remove redundant player info reporting; update player data with animation angle.

* fix(camera): replace magic number with constant and fix missing break

* fix(client-player): stop sprinting on forward key release

* feat: add camera collision and sphere collision for world

* fix(skin): update player001 texture

* refactor(gameplay): improve thread safety and remove debug logging

* refactor(shaders): extract shadow and normal logic into include files and add lighting to player shaders

* chore(shader): remove unused uniforms cameraPos and specularStrength

* fix: inlcude <array> header
This commit is contained in:
zhenyan121
2026-07-05 17:18:40 +08:00
committed by GitHub
parent 0f75144b2f
commit 8a4081cf9c
31 changed files with 1194 additions and 371 deletions

View File

@@ -20,7 +20,7 @@ AABB ClientPlayer::get_aabb(const glm::vec3& pos) {
}
const glm::vec3& ClientPlayer::get_front() const { return m_front; }
const Gait& ClientPlayer::get_gait() const { return m_gait; }
Gait ClientPlayer::get_gait() const { return m_gait.load(); }
const std::optional<LookBlock>& ClientPlayer::get_look_block_pos() const {
return m_look_block;
@@ -111,10 +111,9 @@ void ClientPlayer::change_mode(GameMode mode) {
Logger::info("Change GameMode to {}", to_str(mode));
if (mode == CREATIVE) {
is_fly = false;
m_gait = Gait::WALK;
m_max_speed = m_max_walk_speed;
} else if (mode == SPECTATOR) {
is_fly = true;
m_gait = Gait::RUN;
m_max_speed = m_max_run_speed;
}
}
@@ -128,7 +127,7 @@ void ClientPlayer::set_player_pos(const glm::vec3& pos) { m_player_pos = pos; }
void ClientPlayer::set_place_block(unsigned id) { m_place_block = id; }
void ClientPlayer::update(float delta_time) {
m_gait = compute_gait();
update_move(delta_time);
update_lookup_block();
@@ -145,36 +144,42 @@ void ClientPlayer::update_player_move_state(int key, int action) {
case GLFW_KEY_W:
if (action == GLFW_PRESS) {
m_move_state.forward = true;
m_moving = true;
}
if (action == GLFW_RELEASE) {
m_move_state.forward = false;
if (m_game_mode != SPECTATOR) {
m_gait = Gait::WALK;
}
m_moving = false;
m_sprinting = false;
}
break;
case GLFW_KEY_S:
if (action == GLFW_PRESS) {
m_move_state.back = true;
m_moving = true;
}
if (action == GLFW_RELEASE) {
m_move_state.back = false;
m_moving = false;
}
break;
case GLFW_KEY_A:
if (action == GLFW_PRESS) {
m_move_state.left = true;
m_moving = true;
}
if (action == GLFW_RELEASE) {
m_move_state.left = false;
m_moving = false;
}
break;
case GLFW_KEY_D:
if (action == GLFW_PRESS) {
m_move_state.right = true;
m_moving = true;
}
if (action == GLFW_RELEASE) {
m_move_state.right = false;
m_moving = false;
}
break;
case GLFW_KEY_SPACE:
@@ -205,7 +210,7 @@ void ClientPlayer::update_player_move_state(int key, int action) {
break;
case GLFW_KEY_LEFT_CONTROL:
if (action == GLFW_PRESS) {
m_gait = Gait::RUN;
m_sprinting = true;
}
break;
case GLFW_KEY_F4:
@@ -224,13 +229,15 @@ void ClientPlayer::update_front_vec(float offset_x, float offset_y) {
m_yaw += offset_x * m_sensitivity;
m_pitch += offset_y * m_sensitivity;
m_yaw = std::fmod(m_yaw, 360.0);
// m_yaw = std::fmod(m_yaw.load(), 360.0);
m_pitch = std::clamp(m_pitch, -89.0f, 89.0f);
m_pitch = std::clamp(m_pitch.load(), -89.0f, 89.0f);
m_front.x = sin(glm::radians(m_yaw)) * cos(glm::radians(m_pitch));
m_front.y = sin(glm::radians(m_pitch));
m_front.z = -cos(glm::radians(m_yaw)) * cos(glm::radians(m_pitch));
m_front.x =
sin(glm::radians(m_yaw.load())) * cos(glm::radians(m_pitch.load()));
m_front.y = sin(glm::radians(m_pitch.load()));
m_front.z =
-cos(glm::radians(m_yaw.load())) * cos(glm::radians(m_pitch.load()));
m_front = glm::normalize(m_front);
}
@@ -298,6 +305,9 @@ void ClientPlayer::update_move(float delta_time) {
if (delta_time > 1.0f) {
return;
}
if (m_xz_speed < 0.01f) {
m_sprinting = false;
}
// ensure the thread safe
glm::vec3 player_pos;
@@ -307,12 +317,10 @@ void ClientPlayer::update_move(float delta_time) {
}
if (m_game_mode != SPECTATOR) {
if (m_gait == Gait::RUN) {
m_max_speed = m_max_run_speed;
}
if (m_gait == Gait::WALK) {
m_max_speed = m_max_walk_speed;
}
m_max_speed =
(m_gait == Gait::RUN) ? m_max_run_speed : m_max_walk_speed;
} else {
m_max_speed = m_max_run_speed;
}
if (space_on) {
@@ -405,7 +413,7 @@ void ClientPlayer::update_x_move(glm::vec3& player_pos) {
if (!m_world.can_pass_block(block_pos)) {
AABB block_box = ClientWorld::get_block_aabb(block_pos);
if (player_box.intersects(block_box)) {
m_gait = Gait::WALK;
m_sprinting = false;
player_pos.x -= move_distance.x;
return;
}
@@ -469,7 +477,7 @@ void ClientPlayer::update_z_move(glm::vec3& player_pos) {
if (!m_world.can_pass_block(block_pos)) {
AABB block_box = ClientWorld::get_block_aabb(block_pos);
if (player_box.intersects(block_box)) {
m_gait = Gait::WALK;
m_sprinting = false;
player_pos.z -= move_distance.z;
return;
}
@@ -495,6 +503,16 @@ void ClientPlayer::update_player_chunk() {
}
}
Gait ClientPlayer::compute_gait() const {
if (m_xz_speed < 0.01f)
return Gait::STOP;
if (m_sprinting)
return Gait::RUN;
return Gait::WALK;
}
void ClientPlayer::update_scroll(double yoffset) {
if (m_game_mode == SPECTATOR) {
if (yoffset > 0) {
@@ -546,12 +564,24 @@ float& ClientPlayer::deceleration() { return m_deceleration; }
float& ClientPlayer::g() { return m_g; }
float& ClientPlayer::fly_y_speed() { return m_fly_y_speed; }
unsigned ClientPlayer::place_block() const { return m_place_block; };
Gait& ClientPlayer::gait() { return m_gait; }
void ClientPlayer::set_gait(Gait gait) { m_gait = gait; }
GameMode& ClientPlayer::game_mode() { return m_game_mode; }
const ClientWorld& ClientPlayer::get_world() const { return m_world; }
void ClientPlayer::set_uuid(std::string_view uuid) { m_uuid = uuid; }
const std::string& ClientPlayer::get_uuid() const { return m_uuid; }
void ClientPlayer::set_uuid(std::string_view uuid) {
std::lock_guard lock(m_uuid_mutex);
m_uuid = uuid;
}
std::string ClientPlayer::get_uuid() const {
std::shared_lock lock(m_uuid_mutex);
return m_uuid;
}
const std::string& ClientPlayer::get_name() const { return m_name; }
void ClientPlayer::init(std::string_view name) { m_name = name; }
float ClientPlayer::yaw() const { return m_yaw; }
float ClientPlayer::pitch() const { return m_pitch; }
float& ClientPlayer::angle() { return m_angle; }
float& ClientPlayer::walk_time() { return m_walk_time; }
} // namespace Cubed

View File

@@ -51,7 +51,7 @@ const std::optional<LookBlock>& ClientWorld::get_look_block_pos() const {
}
ClientPlayer& ClientWorld::get_player() { return m_player; }
const ClientPlayer& ClientWorld::get_player() const { return m_player; }
int ClientWorld::get_block(const glm::ivec3& block_pos) const {
auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z);
chunk_cacc cacc;
@@ -262,11 +262,11 @@ void ClientWorld::push_delete_vao(GLuint vao) {
void ClientWorld::report_block_change(const glm::ivec3& pos,
unsigned id) const {
{
if (id != 0) {
AABB block_box = get_block_aabb(pos);
std::shared_lock lock(m_other_players_mutex);
std::shared_lock lock(m_player_info_mutex);
for (auto& [uuid, player] : m_other_players) {
for (auto& [uuid, player] : m_player_info) {
AABB box = ClientPlayer::get_aabb(player.target_pos);
if (box.intersects(block_box)) {
return;
@@ -296,16 +296,23 @@ void ClientWorld::receive_time(const UpdateTime& rsp) {
}
void ClientWorld::receive_remote_player(const PlayerInfoRsp& rsp) {
auto pitch = rsp.pitch();
auto yaw = rsp.yaw();
{
std::lock_guard lock(m_other_players_mutex);
std::lock_guard lock(m_player_info_mutex);
glm::vec3 pos{rsp.pos().x(), rsp.pos().y(), rsp.pos().z()};
auto it = m_other_players.find(rsp.uuid());
if (it == m_other_players.end()) {
m_other_players.emplace(
auto it = m_player_info.find(rsp.uuid());
if (it == m_player_info.end()) {
m_player_info.emplace(
std::piecewise_construct, std::forward_as_tuple(rsp.uuid()),
std::forward_as_tuple(rsp.name(), pos, pos));
std::forward_as_tuple(rsp.name(), rsp.uuid(), pos, pos, yaw,
yaw, pitch, pitch,
get_gait_from_id(rsp.gait())));
} else {
it->second.target_pos = pos;
it->second.yaw = yaw;
it->second.pitch = pitch;
it->second.gait = get_gait_from_id(rsp.gait());
}
// Logger::info("Player {} pos Update", rsp.name());
}
@@ -321,8 +328,8 @@ void ClientWorld::receive_player_logout(const LogoutRsp& rsp) {
return;
}
{
std::lock_guard lock(m_other_players_mutex);
int sum = m_other_players.erase(rsp.uuid());
std::lock_guard lock(m_player_info_mutex);
int sum = m_player_info.erase(rsp.uuid());
if (sum == 0) {
Logger::warn("Player {} not find", rsp.uuid());
} else {
@@ -336,7 +343,7 @@ void ClientWorld::init(std::string_view player_name,
m_player.init(player_name);
m_client = client;
// timer
register_timer("player_pos", 1, [this]() { report_player_pos(); });
register_timer("player_pos", 1, [this]() { report_player_info(); });
LoginReq req;
req.set_name(m_player.get_name());
while (!client->is_connected()) {
@@ -427,19 +434,23 @@ void ClientWorld::client_run(std::stop_token stoken) {
}
}
void ClientWorld::report_player_pos() {
void ClientWorld::report_player_info() {
if (!m_client) {
return;
}
Arena arena;
auto* pos = Arena::Create<PlayerPos>(&arena);
pos->set_uuid(m_player.get_uuid());
auto* info = Arena::Create<C2S_PlayerInfo>(&arena);
info->set_uuid(m_player.get_uuid());
glm::vec3 player_pos = m_player.get_player_pos();
auto* v3 = pos->mutable_pos();
auto* v3 = info->mutable_pos();
v3->set_x(player_pos.x);
v3->set_y(player_pos.y);
v3->set_z(player_pos.z);
m_client->send(make_packet(*pos), 0);
info->set_yaw(m_player.yaw());
info->set_pitch(m_player.pitch());
info->set_gait(get_gait_id(m_player.get_gait()));
m_client->send(make_packet(*info), 0);
}
void ClientWorld::update_chunk(const ChunkPosSet& old, const ChunkPosSet& now) {
@@ -690,8 +701,8 @@ void ClientWorld::update(float delta_time) {
m_render_player_data.clear();
{
std::lock_guard lock(m_other_players_mutex);
for (auto& [uuid, player] : m_other_players) {
std::lock_guard lock(m_player_info_mutex);
for (auto& [uuid, player] : m_player_info) {
player.render_pos =
glm::mix(player.render_pos, player.target_pos, 0.15f);
if (Math::distance2(player.render_pos, m_player.get_player_pos()) >
@@ -699,7 +710,49 @@ void ClientWorld::update(float delta_time) {
CHUNK_SIZE) {
continue;
}
m_render_player_data.emplace_back(player.name, player.render_pos);
player.render_yaw = glm::mix(player.render_yaw, player.yaw, 0.15);
player.render_pitch =
glm::mix(player.render_pitch, player.pitch, 0.15);
if (player.gait == Gait::WALK || player.gait == Gait::RUN) {
player.walk_time += delta_time;
float speed = player.gait == Gait::RUN ? 14.0f : 8.0f;
float amp = player.gait == Gait::RUN ? 50.0f : 35.0f;
// float amp = 90.0f;
player.angle =
glm::sin(player.walk_time * speed) * glm::radians(amp);
} else if (player.gait == Gait::STOP) {
float t = glm::clamp(delta_time * 10.0f, 0.0f, 1.0f);
player.angle = glm::mix(player.angle, 0.0f, t);
}
m_render_player_data.emplace_back(
player.name, player.uuid, player.render_pos, player.render_yaw,
player.render_pitch, player.gait, player.angle);
}
{
auto gait = m_player.get_gait();
auto& walk_time = m_player.walk_time();
auto& angle = m_player.angle();
if (gait == Gait::WALK || gait == Gait::RUN) {
walk_time += delta_time;
float speed = gait == Gait::RUN ? 14.0f : 8.0f;
float amp = gait == Gait::RUN ? 50.0f : 35.0f;
// float amp = 90.0f;
angle = glm::sin(walk_time * speed) * glm::radians(amp);
} else if (gait == Gait::STOP) {
float t = glm::clamp(delta_time * 10.0f, 0.0f, 1.0f);
angle = glm::mix(angle, 0.0f, t);
}
m_render_player_data.emplace_back(
m_player.get_name(), m_player.get_uuid(),
m_player.get_player_pos(), m_player.yaw(), m_player.pitch(),
m_player.get_gait(), m_player.angle());
}
}
}
@@ -721,6 +774,33 @@ glm::vec3 ClientWorld::sunlight_dir() const {
return glm::normalize(-dir);
}
bool ClientWorld::sphere_collide_world(glm::vec3 center, float radius) const {
glm::ivec3 min = glm::floor(center - glm::vec3(radius));
glm::ivec3 max = glm::floor(center + glm::vec3(radius));
for (int x = min.x; x <= max.x; ++x) {
for (int y = min.y; y <= max.y; ++y) {
for (int z = min.z; z <= max.z; ++z) {
if (!is_solid({x, y, z}))
continue;
glm::vec3 closest;
closest.x = glm::clamp(center.x, float(x), float(x + 1));
closest.y = glm::clamp(center.y, float(y), float(y + 1));
closest.z = glm::clamp(center.z, float(z), float(z + 1));
glm::vec3 d = center - closest;
if (glm::dot(d, d) < radius * radius)
return true;
}
}
}
return false;
}
int ClientWorld::rendering_distance() const {
return m_rendering_distance.load();
}
@@ -738,8 +818,10 @@ const std::vector<const ChunkRenderSnapshot*>&
ClientWorld::render_snapshots() const {
return m_render_snapshots;
};
const std::vector<RemotePlayerRenderData>&
ClientWorld::render_player_data() const {
const std::vector<PlayerRenderData>& ClientWorld::render_player_data() const {
return m_render_player_data;
}
std::vector<PlayerRenderData>& ClientWorld::render_player_data() {
return m_render_player_data;
}
std::vector<glm::vec4>& ClientWorld::planes() { return m_planes; }

View File

@@ -53,4 +53,10 @@ ServerPlayer::ChunkPosSet& ServerPlayer::get_chunk_pos_set() {
std::lock_guard lock(m_chunk_pos_mutex);
return m_player_chunk_pos_set;
}
void ServerPlayer::set_yaw(float yaw) { m_yaw = yaw; }
void ServerPlayer::set_pitch(float pitch) { m_pitch = pitch; }
float ServerPlayer::yaw() const { return m_yaw.load(); }
float ServerPlayer::pitch() const { return m_pitch.load(); }
Gait ServerPlayer::gait() const { return m_gait; }
void ServerPlayer::set_gait(Gait gait) { m_gait = gait; }
} // namespace Cubed

View File

@@ -82,9 +82,11 @@ void ServerWorld::send_time() {
rsp->set_day_tick(m_day_tick);
rsp->set_game_tick(m_game_ticks);
for (auto& [uuid, player] : m_players) {
player.get_session()->send(make_packet(*rsp), 3);
{
std::shared_lock lock(m_player_mutex);
for (auto& [uuid, player] : m_players) {
player.get_session()->send(make_packet(*rsp), 3);
}
}
}
@@ -537,9 +539,14 @@ void ServerWorld::update() {
}
}
void ServerWorld::sync_player_pos(const std::string& uuid, float x, float y,
float z) {
void ServerWorld::sync_player_pos(const C2S_PlayerInfo& prsp) {
std::string name;
auto x = prsp.pos().x();
auto y = prsp.pos().y();
auto z = prsp.pos().z();
auto uuid = prsp.uuid();
auto yaw = prsp.yaw();
auto pitch = prsp.pitch();
{
std::lock_guard lock(m_player_mutex);
auto it = m_players.find(uuid);
@@ -547,8 +554,13 @@ void ServerWorld::sync_player_pos(const std::string& uuid, float x, float y,
Logger::warn("Player {} is not in this Server", uuid);
return;
}
it->second.update_pos(x, y, z);
it->second.update_sync_gametick(m_game_ticks);
it->second.set_pitch(pitch);
it->second.set_yaw(yaw);
it->second.set_gait(get_gait_from_id(prsp.gait()));
name = it->second.get_name();
}
ChunkPos pos = get_chunk_pos(x, z);
@@ -578,6 +590,9 @@ void ServerWorld::sync_player_pos(const std::string& uuid, float x, float y,
pos->set_x(x);
pos->set_y(y);
pos->set_z(z);
rsp->set_yaw(yaw);
rsp->set_pitch(pitch);
rsp->set_gait(prsp.gait());
session->send(make_packet(*rsp), 0);
}
}
@@ -605,7 +620,7 @@ void ServerWorld::handle_player_login(const std::string& name,
session->send(make_packet(*rsp), 0);
return;
}
++m_player_sum;
m_uuid_to_name.emplace(uuid, name);
// Pre-insert into new_chunks to ensure correct addition to waiting_player
/*ChunkPosSet required_chunks;
@@ -647,7 +662,7 @@ void ServerWorld::handle_player_exit(const std::string& uuid) {
}
m_uuid_to_name.erase(uuid);
--m_player_sum;
update_ref_count(old_set, {});
Arena arena;

View File

@@ -64,12 +64,10 @@ asio::awaitable<void> Session::read_loop() {
shared_from_this());
}
}
if (cmd_id == std::to_underlying(PacketEnum::PLAYER_POS)) {
auto* pos = Arena::Create<PlayerPos>(&arena);
if (cmd_id == std::to_underlying(PacketEnum::C2S_PLAYER_INFO)) {
auto* pos = Arena::Create<C2S_PlayerInfo>(&arena);
if (decode_packet(*pos, body_data, header)) {
m_server_world.sync_player_pos(pos->uuid(), pos->pos().x(),
pos->pos().y(),
pos->pos().z());
m_server_world.sync_player_pos(*pos);
}
}
if (cmd_id == std::to_underlying(PacketEnum::CHUNK_DATA_REQ)) {