Compare commits

...

6 Commits

Author SHA1 Message Date
c00f05aafd feat(renderer): smooth shadow sun direction transitions using quantized directions and slerp 2026-06-17 22:09:50 +08:00
dc1ef70231 fix(texture): set texture wrap mode to clamp to edge 2026-06-17 21:13:23 +08:00
e224110452 feat(renderer): add runtime shader and shadow mode controls
Introduce user-controllable shader on/off, shadow mode (rotated Poisson disk, 3x3 grid, or off), light cull face, and discard transparent in depth pass. Expose all settings in new dev panel "shader" tab. Move ambient strength slider to the new tab.
2026-06-17 20:16:20 +08:00
74b0aebc9f chore(world): add missing <numbers> include 2026-06-17 19:16:02 +08:00
be425a705c feat(dev-panel): add tick freeze toggle and fix TickType sign
Add checkbox to freeze tick advancement in dev panel.
Change TickType from unsigned long long to signed long long to prevent underflow.
2026-06-17 19:15:16 +08:00
31bf337f6f perf(shadow): increase depth map resolution and refine PCF sampling 2026-06-17 15:23:38 +08:00
12 changed files with 310 additions and 97 deletions

View File

@@ -12,6 +12,8 @@ layout (binding = 1) uniform sampler2DArray samp;
uniform float ambientStrength;
uniform vec3 sunlightColor;
uniform vec3 sunlightDir;
uniform bool shader_on;
uniform int shadowMode;
const vec2 poissonDisk[8] = vec2[](
vec2( 0.1440, 0.7659), vec2(-0.5761, 0.4479),
@@ -37,16 +39,22 @@ float ShadowCalculation(vec4 fragPosLightSpace, vec3 norm, vec3 lightDir)
}
float currentDepth = projCoords.z;
vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));
float bias = 0.0002;
bias += max(0.005 * texelSize.x * (1.0 - dot(norm, lightDir)), 0.0);
float shadow = 0.0;
float bias =
max(
0.0003,
0.001 * (1.0 - dot(norm, lightDir))
);
float angle = random(gl_FragCoord.xyy) * 6.2831853; // 2*PI
if (shadowMode == 0) {
vec3 seed = vert_pos * 37.0 + sin(vert_pos * 91.7) * 13.0;
float angle = random(seed) * 6.2831853;; // 2*PI
float s = sin(angle), c = cos(angle);
mat2 rot = mat2(c, -s, s, c);
float radius = 1.5;
float radius = 0.7;
float shadow = 0.0;
const int samples = 8;
for (int i = 0; i < samples; ++i) {
vec2 offset = rot * poissonDisk[i] * radius * texelSize;
@@ -54,6 +62,34 @@ float ShadowCalculation(vec4 fragPosLightSpace, vec3 norm, vec3 lightDir)
shadow += (currentDepth - bias > pcfDepth ? 1.0 : 0.0);
}
shadow /= float(samples);
} else if (shadowMode == 1) {
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
vec2 offset = vec2(x, y) * texelSize;
float pcfDepth = texture(shadowMap, projCoords.xy + offset).r;
shadow += (currentDepth - bias > pcfDepth ? 1.0 : 0.0);
}
}
shadow /= 9.0;
} else if (shadowMode == 2) {
// pcf off
float pcfDepth =
texture(shadowMap, projCoords.xy).r;
shadow =
currentDepth - bias > pcfDepth
? 1.0
: 0.0;
} else {
float pcfDepth =
texture(shadowMap, projCoords.xy).r;
shadow =
currentDepth - bias > pcfDepth
? 1.0
: 0.0;
}
return shadow;
}
@@ -65,6 +101,11 @@ void main(void) {
if (objectColor.a < 0.8) {
discard;
}
if (!shader_on) {
color = objectColor;
return;
}
vec3 lightDir = normalize(-sunlightDir);

View File

@@ -4,9 +4,13 @@ in vec2 tc;
flat in int tex_layer;
layout (binding = 1) uniform sampler2DArray samp;
uniform bool is_discard_tranparent;
void main() {
if (is_discard_tranparent) {
vec4 texColor = texture(samp, vec3(tc, tex_layer));
if (texColor.a < 0.8)
discard;
}
//gl_FragDepth = gl_FragCoord.z;
}

View File

@@ -46,6 +46,7 @@ private:
int m_theme = 0;
int m_pre_set_day_tick = 0;
int m_pre_set_tick_speed = 1;
bool m_tick_frezze = false;
void show_about_table_bar();
void show_biome_table_bar();
void show_time_table_bar();
@@ -55,6 +56,7 @@ private:
void show_world_tab_item();
void show_player_tab_item();
void show_items_tab_item();
void show_shader_tab_item();
void update_config_view();
void update_player_profile();

View File

@@ -1,6 +1,7 @@
#pragma once
using TickType = unsigned long long;
// Prevent unsigned underflow issues in subtraction
using TickType = long long;
constexpr int DEFAULT_PER_TICK_TIME = 50;

View File

@@ -69,7 +69,7 @@ private:
std::atomic<bool> m_is_rebuilding{false};
std::atomic<bool> m_chunk_gen_finished{false};
std::atomic<bool> m_could_gen{true};
std::atomic<bool> m_day_night_cycle{true};
std::atomic<bool> m_tick_running{true};
std::atomic<int> m_rendering_distance{24};
std::atomic<float> m_chunk_gen_fraction{0.0f};
@@ -149,6 +149,9 @@ public:
void day_tick(TickType tick);
int per_tick_time() const;
void per_tick_time(int ms);
bool is_tick_running() const;
void tick_running(bool run);
};
} // namespace Cubed

View File

@@ -30,6 +30,11 @@ public:
void updata_framebuffer(int width, int height);
float& ambient_strength();
bool& discard_transparent();
bool& shader_on();
int& shadow_mode();
int& light_cull_face();
private:
static constexpr glm::vec3 SUNLIGHT_COLOR{1.0f, 1.0f, 1.0f};
static constexpr glm::vec3 SUN_COLOR{1.00f, 0.95f, 0.80f};
@@ -39,8 +44,8 @@ private:
static constexpr float NEAR_PLANE = 0.1f;
static constexpr float SUN_SIZE = 50.0f;
static constexpr float MOON_SIZE = 50.0f;
static constexpr float DEPTH_MAP_WIDTH = 2048.0f;
static constexpr float DEPTH_MAP_HEIGHT = 2048.0f;
static constexpr float DEPTH_MAP_SIZE = 4096.0f;
static constexpr float ANGLE_STEP_DEG = 0.5f;
float m_ambient_strength = 0.1f;
const Camera& m_camera;
@@ -48,6 +53,10 @@ private:
const TextureManager& m_texture_manager;
World& m_world;
bool m_discard_tranparent = true;
bool m_shader_on = true;
int m_shadow_mode = 0;
int m_light_cull_face = 0;
float m_aspect = 0.0f;
float m_fov = DEFAULT_FOV;
@@ -85,6 +94,12 @@ private:
glm::mat4 m_ui_m_matrix;
std::unordered_map<std::size_t, Shader> m_shaders;
glm::vec3 m_blend_from_sundir;
glm::vec3 m_blend_to_sundir;
float m_blend_t = 1.0f;
bool m_blend_initialized = false;
static constexpr float BLEND_DURATION = 0.15f;
/*
0 - quad vao
1 - sky vao
@@ -106,6 +121,11 @@ private:
void render_world();
void render_underwater();
void render_dev_panel();
glm::vec3 quantize_sun_direction(const glm::vec3& sundir,
float angle_step_deg) const;
glm::vec3 get_smoothed_shadow_sundir(const glm::vec3& raw_shadow_sundir,
float dt);
};
} // namespace Cubed

View File

@@ -12,6 +12,8 @@ float smootherstep(float edge0, float edge1, float x);
bool is_aabb_in_frustum(const glm::vec3& center, const glm::vec3& half_extents,
const std::vector<glm::vec4>& planes);
float deterministic_random(int x, int z, uint64_t seed);
glm::vec3 slerp(const glm::vec3& from, const glm::vec3& to, float t);
} // namespace Math
} // namespace Cubed

View File

@@ -84,6 +84,7 @@ void DevPanel::render() {
show_world_tab_item();
show_player_tab_item();
show_items_tab_item();
show_shader_tab_item();
show_about_table_bar();
ImGui::EndTabBar();
@@ -107,6 +108,7 @@ void DevPanel::show_about_table_bar() {
ImGui::Text("FreeType");
ImGui::Text("toml++");
ImGui::Text("Dear ImGui");
ImGui::Text("Tbb");
ImGui::Separator();
ImGui::Text("Special Thanks");
ImGui::Text("TANGERIME");
@@ -266,7 +268,13 @@ void DevPanel::show_biome_table_bar() {
void DevPanel::show_time_table_bar() {
World& world = m_app.world();
ImGui::Text("Game Tick %llu", world.game_tick());
ImGui::SameLine();
ImGui::Text("Day Tick %llu", world.day_tick());
m_tick_frezze = !world.is_tick_running();
ImGui::SameLine();
if (ImGui::Checkbox("Tick Frezze", &m_tick_frezze)) {
world.tick_running(!m_tick_frezze);
}
if (ImGui::SliderInt("SetDayTick", &m_pre_set_day_tick, 0, DAY_TIME)) {
}
ImGui::SameLine();
@@ -355,10 +363,7 @@ void DevPanel::show_settings_tab_item() {
static_cast<double>(m_config.mouse_sensitivity));
m_player->hot_reload();
}
if (ImGui::SliderFloat("AmbientStrength",
&m_app.renderer().ambient_strength(), 0.0f,
0.35f))
;
if (ImGui::SliderInt("Distance", &m_config.rendering_distance, 2,
128)) {
Config::get().set("world.rendering_distance",
@@ -604,6 +609,27 @@ void DevPanel::show_items_tab_item() {
}
}
void DevPanel::show_shader_tab_item() {
static const char* shader_mode[] = {"Rotated Poisson Disk PCF",
"3x3 Square Grid PCF", "PCF off"};
static const char* cull_face_mode[] = {"Front", "Back"};
if (ImGui::BeginTabItem("shader")) {
ImGui::Checkbox("Shader", &m_app.renderer().shader_on());
if (ImGui::SliderFloat("AmbientStrength",
&m_app.renderer().ambient_strength(), 0.0f,
0.35f))
;
ImGui::Checkbox("Discard Transparent",
&m_app.renderer().discard_transparent());
ImGui::Combo("ShaderMode", &m_app.renderer().shadow_mode(), shader_mode,
IM_ARRAYSIZE(shader_mode));
ImGui::Combo("LightCullFaceMode", &m_app.renderer().light_cull_face(),
cull_face_mode, IM_ARRAYSIZE(cull_face_mode));
ImGui::EndTabItem();
}
}
void DevPanel::update_config_view() {
auto config = Config::get();
m_config.fov =

View File

@@ -6,7 +6,8 @@
#include "Cubed/tools/cubed_hash.hpp"
#include <execution>
#include <glm/gtc/constants.hpp>
#include <numbers>
using namespace std::chrono;
namespace Cubed {
@@ -757,9 +758,9 @@ void World::serever_run(std::stop_token stoken) {
Logger::info("Server Thread Started!");
while (!stoken.stop_requested()) {
std::this_thread::sleep_for(milliseconds(m_per_tick_time));
if (m_day_night_cycle) {
if (m_tick_running) {
++m_game_ticks;
m_day_tick = (++m_day_tick) % DAY_TIME;
m_day_tick = (m_day_tick + 1) % DAY_TIME;
}
}
Logger::info("Server Thread Stopped!");
@@ -1047,7 +1048,7 @@ glm::vec3 World::sunlight_dir() const {
glm::vec3 dir{cos(altitude) * cos(azimuth), sin(altitude),
cos(altitude) * sin(azimuth)};
return glm::normalize(dir);
return glm::normalize(-dir);
}
*/
@@ -1078,4 +1079,6 @@ void World::day_tick(TickType tick) {
int World::per_tick_time() const { return m_per_tick_time.load(); }
void World::per_tick_time(int ms) { m_per_tick_time = ms; }
bool World::is_tick_running() const { return m_tick_running.load(); }
void World::tick_running(bool run) { m_tick_running = run; }
} // namespace Cubed

View File

@@ -18,6 +18,7 @@
#include <GLFW/glfw3.h>
#include <format>
#include <glm/gtc/type_ptr.hpp>
namespace Cubed {
Renderer::Renderer(const Camera& camera, World& world,
@@ -470,8 +471,8 @@ void Renderer::updata_framebuffer(int width, int height) {
glGenTextures(1, &m_depth_map_texture);
glBindTexture(GL_TEXTURE_2D, m_depth_map_texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, DEPTH_MAP_WIDTH,
DEPTH_MAP_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, DEPTH_MAP_SIZE,
DEPTH_MAP_SIZE, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
@@ -501,45 +502,65 @@ void Renderer::updata_framebuffer(int width, int height) {
void Renderer::render_world() {
// shader map
glm::mat4 light_space_matrix;
auto& m_render_snapshots = m_world.render_snapshots();
auto& camera_pos = m_camera.get_camera_pos();
if (m_shader_on) {
const auto& depth_shader = get_shader("depth_shader");
depth_shader.use();
glm::vec3 cam_pos = m_camera.get_camera_pos();
glm::vec3 cam_fwd = m_camera.get_camera_front();
float half_extent = 256.0f;
float half_extent = 128.0f;
glm::vec3 center = cam_pos + cam_fwd * (half_extent * 0.5f);
glm::vec3 sundir = glm::normalize(m_world.sunlight_dir());
glm::vec3 up =
fabs(sundir.y) > 0.99f ? glm::vec3(0, 0, 1) : glm::vec3(0, 1, 0);
glm::vec3 raw_shadow_sundir =
quantize_sun_direction(sundir, ANGLE_STEP_DEG);
glm::vec3 shadow_sundir =
get_smoothed_shadow_sundir(raw_shadow_sundir, m_delta_time);
glm::vec3 up = fabs(shadow_sundir.y) > 0.99f ? glm::vec3(0, 0, 1)
: glm::vec3(0, 1, 0);
glm::mat4 light_basis = glm::lookAt(glm::vec3(0.0f), sundir, up);
float texels_per_unit = DEPTH_MAP_WIDTH / (half_extent * 2.0f);
glm::mat4 light_basis = glm::lookAt(glm::vec3(0.0f), shadow_sundir, up);
float texels_per_unit = DEPTH_MAP_SIZE / (half_extent * 2.0f);
glm::vec3 ls_center = glm::vec3(light_basis * glm::vec4(center, 1.0f));
ls_center.x = std::round(ls_center.x * texels_per_unit) / texels_per_unit;
ls_center.y = std::round(ls_center.y * texels_per_unit) / texels_per_unit;
ls_center.x =
std::round(ls_center.x * texels_per_unit) / texels_per_unit;
ls_center.y =
std::round(ls_center.y * texels_per_unit) / texels_per_unit;
glm::vec3 snapped_center =
glm::vec3(glm::inverse(light_basis) * glm::vec4(ls_center, 1.0f));
float distance = 128.0f;
glm::vec3 light_pos = snapped_center - sundir * distance;
float distance = half_extent * 1.5f;
float near_plane = 1.0f;
float far_plane = distance + half_extent * 2.0f;
glm::vec3 light_pos = snapped_center - shadow_sundir * distance;
glm::mat4 light_view = glm::lookAt(light_pos, snapped_center, up);
glm::mat4 light_projection = glm::ortho(
-half_extent, half_extent, -half_extent, half_extent, 1.0f, 500.0f);
glm::mat4 light_projection =
glm::ortho(-half_extent, half_extent, -half_extent, half_extent,
near_plane, far_plane);
glm::mat4 light_space_matrix = light_projection * light_view;
light_space_matrix = light_projection * light_view;
glUniformMatrix4fv(depth_shader.loc("lightSpaceMatrix"), 1, GL_FALSE,
glm::value_ptr(light_space_matrix));
glViewport(0, 0, DEPTH_MAP_WIDTH, DEPTH_MAP_WIDTH);
glUniform1i(depth_shader.loc("is_discard_tranparent"),
m_discard_tranparent);
glViewport(0, 0, DEPTH_MAP_SIZE, DEPTH_MAP_SIZE);
if (m_light_cull_face == 0) {
glCullFace(GL_FRONT);
} else if (m_light_cull_face == 1) {
glCullFace(GL_BACK);
} else {
Logger::warn("Light Cull Face {} Over The Max Selection",
m_light_cull_face);
glCullFace(GL_BACK);
}
glBindFramebuffer(GL_FRAMEBUFFER, m_depth_map_fbo);
glClear(GL_DEPTH_BUFFER_BIT);
auto& m_render_snapshots = m_world.render_snapshots();
auto& camera_pos = m_camera.get_camera_pos();
glActiveTexture(GL_TEXTURE1);
glEnable(GL_DEPTH_TEST);
for (const auto& snapshot : m_render_snapshots) {
@@ -563,7 +584,8 @@ void Renderer::render_world() {
m_texture_manager.get_texture_array());
glBindVertexArray(snapshot.cross_vao);
glDrawArrays(GL_TRIANGLES, 0, snapshot.cross_vertices_count);
glDrawArrays(GL_TRIANGLES, 0,
snapshot.cross_vertices_count);
}
}
if (snapshot.normal_discard_vertices_count != 0) {
@@ -575,6 +597,7 @@ void Renderer::render_world() {
snapshot.normal_discard_vertices_count);
}
}
}
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
@@ -608,7 +631,8 @@ void Renderer::render_world() {
glm::value_ptr(SUNLIGHT_COLOR));
glUniform3fv(normal_block_shader.loc("sunlightDir"), 1,
glm::value_ptr(light_dir_view));
glUniform1i(normal_block_shader.loc("shadowMode"), m_shadow_mode);
glUniform1i(normal_block_shader.loc("shader_on"), m_shader_on);
m_mvp_mat = m_p_mat * m_mv_mat;
auto& m_planes = m_world.planes();
@@ -738,6 +762,56 @@ void Renderer::render_dev_panel() {
glEnable(GL_DEPTH_TEST);
}
float& Renderer::ambient_strength() { return m_ambient_strength; }
glm::vec3 Renderer::quantize_sun_direction(const glm::vec3& sundir,
float angle_step_deg) const {
float elevation = std::asin(glm::clamp(sundir.y, -1.0f, 1.0f));
float azimuth = std::atan2(sundir.z, sundir.x);
float step = glm::radians(angle_step_deg);
float quantized_elevation = std::round(elevation / step) * step;
float quantized_azimuth = std::round(azimuth / step) * step;
glm::vec3 quantized_dir;
quantized_dir.x =
std::cos(quantized_elevation) * std::cos(quantized_azimuth);
quantized_dir.z =
std::cos(quantized_elevation) * std::sin(quantized_azimuth);
quantized_dir.y = std::sin(quantized_elevation);
return glm::normalize(quantized_dir);
}
glm::vec3
Renderer::get_smoothed_shadow_sundir(const glm::vec3& raw_shadow_sundir,
float dt) {
if (!m_blend_initialized) {
m_blend_from_sundir = raw_shadow_sundir;
m_blend_to_sundir = raw_shadow_sundir;
m_blend_t = 1.0f;
m_blend_initialized = true;
return raw_shadow_sundir;
}
if (raw_shadow_sundir != m_blend_to_sundir) {
glm::vec3 current_displayed = glm::normalize(
Math::slerp(m_blend_from_sundir, m_blend_to_sundir, m_blend_t));
m_blend_from_sundir = current_displayed;
m_blend_to_sundir = raw_shadow_sundir;
m_blend_t = 0.0f;
}
m_blend_t = glm::min(m_blend_t + dt / BLEND_DURATION, 1.0f);
return glm::normalize(
Math::slerp(m_blend_from_sundir, m_blend_to_sundir, m_blend_t));
}
float& Renderer::ambient_strength() { return m_ambient_strength; }
bool& Renderer::discard_transparent() { return m_discard_tranparent; }
bool& Renderer::shader_on() { return m_shader_on; }
int& Renderer::shadow_mode() { return m_shadow_mode; }
int& Renderer::light_cull_face() { return m_light_cull_face; }
} // namespace Cubed

View File

@@ -164,6 +164,8 @@ void TextureManager::init_block() {
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
if (m_aniso >= 1) {
glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY,

View File

@@ -1,7 +1,9 @@
#include "Cubed/tools/math_tools.hpp"
#include <algorithm>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
namespace Cubed {
namespace Math {
@@ -65,6 +67,39 @@ float deterministic_random(int x, int z, uint64_t seed) {
h = h * 6364136223846793005ULL + (uint64_t)z;
return (float)(h >> 40) / (float)(1 << 24);
}
glm::vec3 slerp(const glm::vec3& from, const glm::vec3& to, float t) {
float cos_theta = glm::clamp(glm::dot(from, to), -1.0f, 1.0f);
if (cos_theta > 0.9995f) {
return glm::normalize(glm::mix(from, to, t));
}
if (cos_theta < -0.9995f) {
glm::vec3 axis = (std::fabs(from.x) < 0.9f)
? glm::vec3(1.0f, 0.0f, 0.0f)
: glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 ortho = glm::normalize(glm::cross(from, axis));
float angle = glm::pi<float>() * t;
glm::vec3 rotated =
from * std::cos(angle) + glm::cross(ortho, from) * std::sin(angle);
return glm::normalize(rotated);
}
float theta = std::acos(cos_theta);
float sin_theta = std::sin(theta);
float a = std::sin((1.0f - t) * theta) / sin_theta;
float b = std::sin(t * theta) / sin_theta;
return glm::normalize(a * from + b * to);
}
} // namespace Math
} // namespace Cubed