feat(renderer): add PCSS shadow mode with configurable samples and softness

Implement Percentage Closer Soft Shadows (PCSS) as shadow mode 3.
Add a FindBlocker function and three Poisson disk arrays (8, 16, 32 samples).
Expose new uniforms (lightSizeUV, minRadius, maxRadius, samples) and provide
slider/combo controls in the dev panel for sample count, light size, and penumbra radius limits.
This commit is contained in:
2026-06-18 17:47:23 +08:00
parent c00f05aafd
commit 8bba170984
5 changed files with 214 additions and 12 deletions

View File

@@ -15,17 +15,117 @@ uniform vec3 sunlightDir;
uniform bool shader_on; uniform bool shader_on;
uniform int shadowMode; uniform int shadowMode;
const vec2 poissonDisk[8] = vec2[]( uniform float lightSizeUV;
uniform float minRadius;
uniform float maxRadius;
const vec2 poissonDisk32[32] = vec2[](
vec2(-0.975402, -0.071138),
vec2(-0.920347, -0.411420),
vec2(-0.883908, 0.217872),
vec2(-0.815442, -0.879125),
vec2(-0.775043, 0.543896),
vec2(-0.698126, -0.227570),
vec2(-0.682433, 0.801894),
vec2(-0.563905, 0.021517),
vec2(-0.443233, -0.975116),
vec2(-0.412231, 0.361307),
vec2(-0.264969, -0.418930),
vec2(-0.241888, 0.997065),
vec2(-0.094184, -0.929389),
vec2(-0.019101, 0.680997),
vec2( 0.143832, -0.141008),
vec2( 0.199841, 0.786414),
vec2( 0.344959, 0.293878),
vec2( 0.443233, -0.475115),
vec2( 0.537430, -0.473734),
vec2( 0.589349, 0.569135),
vec2( 0.674281, -0.178897),
vec2( 0.791975, 0.190902),
vec2( 0.815442, 0.879125),
vec2( 0.896420, -0.613392),
vec2( 0.945586, -0.768907),
vec2( 0.974844, 0.756484),
vec2(-0.814100, 0.914376),
vec2(-0.382775, 0.276768),
vec2(-0.915886, 0.457714),
vec2( 0.537800, 0.912200),
vec2(-0.620000, -0.650000),
vec2( 0.120000, -0.780000)
);
const vec2 poissonDisk16[16] = vec2[](
vec2(-0.94201624, -0.39906216), vec2(0.94558609, -0.76890725),
vec2(-0.09418410, -0.92938870), vec2(0.34495938, 0.29387760),
vec2(-0.91588581, 0.45771432), vec2(-0.81544232, -0.87912464),
vec2(-0.38277543, 0.27676845), vec2(0.97484398, 0.75648379),
vec2(0.44323325, -0.97511554), vec2(0.53742981, -0.47373420),
vec2(-0.26496911, -0.41893023), vec2(0.79197514, 0.19090188),
vec2(-0.24188840, 0.99706507), vec2(-0.81409955, 0.91437590),
vec2(0.19984126, 0.78641367), vec2(0.14383161, -0.14100790)
);
const vec2 poissonDisk8[8] = vec2[](
vec2( 0.1440, 0.7659), vec2(-0.5761, 0.4479), vec2( 0.1440, 0.7659), vec2(-0.5761, 0.4479),
vec2(-0.3220, -0.6058), vec2( 0.5693, -0.4048), vec2(-0.3220, -0.6058), vec2( 0.5693, -0.4048),
vec2(-0.1276, 0.1657), vec2(-0.0649, -0.0165), vec2(-0.1276, 0.1657), vec2(-0.0649, -0.0165),
vec2( 0.2773, -0.0305), vec2(-0.1134, -0.2122) vec2( 0.2773, -0.0305), vec2(-0.1134, -0.2122)
); );
uniform int samples;
float random(vec3 seed) { float random(vec3 seed) {
return fract(sin(dot(seed, vec3(12.9898,78.233,45.5432))) * 43758.5453); return fract(sin(dot(seed, vec3(12.9898,78.233,45.5432))) * 43758.5453);
} }
float FindBlocker(vec2 uv,
float zReceiver,
vec2 texelSize,
float bias,
float lightSizeUV)
{
float avgDepth = 0.0;
int blockers = 0;
float searchRadius = lightSizeUV * 0.5;
for(int i = 0; i < samples; i++)
{
vec2 offset;
if (samples == 32) {
offset =
poissonDisk32[i]
* searchRadius
* texelSize;
} else if (samples == 16) {
offset =
poissonDisk16[i]
* searchRadius
* texelSize;
} else if (samples == 8) {
offset =
poissonDisk8[i]
* searchRadius
* texelSize;
} else {
offset =
poissonDisk32[i]
* searchRadius
* texelSize;
}
float depth =
texture(shadowMap, uv + offset).r;
if(depth < zReceiver - bias)
{
avgDepth += depth;
blockers++;
}
}
if(blockers == 0)
return -1.0;
return avgDepth / blockers;
}
float ShadowCalculation(vec4 fragPosLightSpace, vec3 norm, vec3 lightDir) float ShadowCalculation(vec4 fragPosLightSpace, vec3 norm, vec3 lightDir)
{ {
@@ -40,10 +140,12 @@ float ShadowCalculation(vec4 fragPosLightSpace, vec3 norm, vec3 lightDir)
float currentDepth = projCoords.z; float currentDepth = projCoords.z;
vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0)); vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));
float shadow = 0.0; float shadow = 0.0;
float bias = float bias =
max( clamp(
0.001 * (1.0 - dot(norm, lightDir)),
0.0003, 0.0003,
0.001 * (1.0 - dot(norm, lightDir)) 0.003
); );
if (shadowMode == 0) { if (shadowMode == 0) {
@@ -51,13 +153,20 @@ float ShadowCalculation(vec4 fragPosLightSpace, vec3 norm, vec3 lightDir)
float angle = random(seed) * 6.2831853;; // 2*PI float angle = random(seed) * 6.2831853;; // 2*PI
float s = sin(angle), c = cos(angle); float s = sin(angle), c = cos(angle);
mat2 rot = mat2(c, -s, s, c); mat2 rot = mat2(c, -s, s, c);
//float radius = 0.7;
float radius = 0.7; float radius = mix(1.0, 4.0, currentDepth);
const int samples = 8;
for (int i = 0; i < samples; ++i) { for (int i = 0; i < samples; ++i) {
vec2 offset = rot * poissonDisk[i] * radius * texelSize; vec2 offset;
if (samples == 32) {
offset = rot * poissonDisk32[i] * radius * texelSize;
} else if (samples == 16) {
offset = rot * poissonDisk16[i] * radius * texelSize;
} else if (samples == 8) {
offset = rot * poissonDisk8[i] * radius * texelSize;
} else {
offset = rot * poissonDisk32[i] * radius * texelSize;
}
float pcfDepth = texture(shadowMap, projCoords.xy + offset).r; float pcfDepth = texture(shadowMap, projCoords.xy + offset).r;
shadow += (currentDepth - bias > pcfDepth ? 1.0 : 0.0); shadow += (currentDepth - bias > pcfDepth ? 1.0 : 0.0);
} }
@@ -80,6 +189,59 @@ float ShadowCalculation(vec4 fragPosLightSpace, vec3 norm, vec3 lightDir)
currentDepth - bias > pcfDepth currentDepth - bias > pcfDepth
? 1.0 ? 1.0
: 0.0; : 0.0;
} else if (shadowMode == 3) {
float avgBlockerDepth =
FindBlocker(
projCoords.xy,
currentDepth,
texelSize,
bias,
lightSizeUV
);
if(avgBlockerDepth < 0.0)
{
return 0.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 penumbraRatio = (currentDepth - avgBlockerDepth);
float radius = clamp(
penumbraRatio * lightSizeUV,
minRadius,
maxRadius
);
*/
float radius =
mix(
minRadius,
maxRadius,
smoothstep(
0.0,
0.05,
currentDepth - avgBlockerDepth
)
);
for (int i = 0; i < samples; ++i) {
vec2 offset;
if (samples == 32) {
offset = rot * poissonDisk32[i] * radius * texelSize;
} else if (samples == 16) {
offset = rot * poissonDisk16[i] * radius * texelSize;
} else if (samples == 8) {
offset = rot * poissonDisk8[i] * radius * texelSize;
} else {
offset = rot * poissonDisk32[i] * radius * texelSize;
}
float pcfDepth = texture(shadowMap, projCoords.xy + offset).r;
shadow += (currentDepth - bias > pcfDepth ? 1.0 : 0.0);
}
shadow /= float(samples);
} else { } else {
float pcfDepth = float pcfDepth =
texture(shadowMap, projCoords.xy).r; texture(shadowMap, projCoords.xy).r;

View File

@@ -47,6 +47,7 @@ private:
int m_pre_set_day_tick = 0; int m_pre_set_day_tick = 0;
int m_pre_set_tick_speed = 1; int m_pre_set_tick_speed = 1;
bool m_tick_frezze = false; bool m_tick_frezze = false;
int m_samples_idx = 1;
void show_about_table_bar(); void show_about_table_bar();
void show_biome_table_bar(); void show_biome_table_bar();
void show_time_table_bar(); void show_time_table_bar();

View File

@@ -34,6 +34,10 @@ public:
bool& shader_on(); bool& shader_on();
int& shadow_mode(); int& shadow_mode();
int& light_cull_face(); int& light_cull_face();
int& light_size_uv();
float& min_radius();
float& max_radius();
int& samples();
private: private:
static constexpr glm::vec3 SUNLIGHT_COLOR{1.0f, 1.0f, 1.0f}; static constexpr glm::vec3 SUNLIGHT_COLOR{1.0f, 1.0f, 1.0f};
@@ -99,7 +103,11 @@ private:
float m_blend_t = 1.0f; float m_blend_t = 1.0f;
bool m_blend_initialized = false; bool m_blend_initialized = false;
static constexpr float BLEND_DURATION = 0.15f; static constexpr float BLEND_DURATION = 0.15f;
int m_light_size_uv = 20;
float m_min_radius = 2.0f;
float m_max_radius = 20.0f;
int m_samples = 16;
/* /*
0 - quad vao 0 - quad vao
1 - sky vao 1 - sky vao

View File

@@ -611,9 +611,10 @@ void DevPanel::show_items_tab_item() {
void DevPanel::show_shader_tab_item() { void DevPanel::show_shader_tab_item() {
static const char* shader_mode[] = {"Rotated Poisson Disk PCF", static const char* shader_mode[] = {
"3x3 Square Grid PCF", "PCF off"}; "Rotated Poisson Disk PCF", "3x3 Square Grid PCF", "PCF off", "PCSS"};
static const char* cull_face_mode[] = {"Front", "Back"}; static const char* cull_face_mode[] = {"Front", "Back"};
static const char* samples[] = {"8", "16", "32"};
if (ImGui::BeginTabItem("shader")) { if (ImGui::BeginTabItem("shader")) {
ImGui::Checkbox("Shader", &m_app.renderer().shader_on()); ImGui::Checkbox("Shader", &m_app.renderer().shader_on());
if (ImGui::SliderFloat("AmbientStrength", if (ImGui::SliderFloat("AmbientStrength",
@@ -626,6 +627,25 @@ void DevPanel::show_shader_tab_item() {
IM_ARRAYSIZE(shader_mode)); IM_ARRAYSIZE(shader_mode));
ImGui::Combo("LightCullFaceMode", &m_app.renderer().light_cull_face(), ImGui::Combo("LightCullFaceMode", &m_app.renderer().light_cull_face(),
cull_face_mode, IM_ARRAYSIZE(cull_face_mode)); cull_face_mode, IM_ARRAYSIZE(cull_face_mode));
if (ImGui::Combo("samples", &m_samples_idx, samples,
IM_ARRAYSIZE(samples))) {
if (m_samples_idx == 0) {
m_app.renderer().samples() = 8;
} else if (m_samples_idx == 1) {
m_app.renderer().samples() = 16;
} else if (m_samples_idx == 2) {
m_app.renderer().samples() = 32;
} else {
Logger::warn("Samples Index {} is invaild", m_samples_idx);
m_app.renderer().samples() = 16;
}
}
ImGui::SliderInt("LightSizeUV", &m_app.renderer().light_size_uv(), 0,
800);
ImGui::SliderFloat("MinRaduis", &m_app.renderer().min_radius(), 0.0f,
20.0f);
ImGui::SliderFloat("MaxRadius", &m_app.renderer().max_radius(), 0.0f,
100.0f);
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
} }

View File

@@ -505,6 +505,7 @@ void Renderer::render_world() {
glm::mat4 light_space_matrix; glm::mat4 light_space_matrix;
auto& m_render_snapshots = m_world.render_snapshots(); auto& m_render_snapshots = m_world.render_snapshots();
auto& camera_pos = m_camera.get_camera_pos(); auto& camera_pos = m_camera.get_camera_pos();
float texels_per_unit = 0.0f;
if (m_shader_on) { if (m_shader_on) {
const auto& depth_shader = get_shader("depth_shader"); const auto& depth_shader = get_shader("depth_shader");
depth_shader.use(); depth_shader.use();
@@ -524,7 +525,7 @@ void Renderer::render_world() {
: glm::vec3(0, 1, 0); : glm::vec3(0, 1, 0);
glm::mat4 light_basis = glm::lookAt(glm::vec3(0.0f), shadow_sundir, up); glm::mat4 light_basis = glm::lookAt(glm::vec3(0.0f), shadow_sundir, up);
float texels_per_unit = DEPTH_MAP_SIZE / (half_extent * 2.0f); texels_per_unit = DEPTH_MAP_SIZE / (half_extent * 2.0f);
glm::vec3 ls_center = glm::vec3(light_basis * glm::vec4(center, 1.0f)); glm::vec3 ls_center = glm::vec3(light_basis * glm::vec4(center, 1.0f));
ls_center.x = ls_center.x =
std::round(ls_center.x * texels_per_unit) / texels_per_unit; std::round(ls_center.x * texels_per_unit) / texels_per_unit;
@@ -633,6 +634,12 @@ void Renderer::render_world() {
glm::value_ptr(light_dir_view)); glm::value_ptr(light_dir_view));
glUniform1i(normal_block_shader.loc("shadowMode"), m_shadow_mode); glUniform1i(normal_block_shader.loc("shadowMode"), m_shadow_mode);
glUniform1i(normal_block_shader.loc("shader_on"), m_shader_on); glUniform1i(normal_block_shader.loc("shader_on"), m_shader_on);
glUniform1f(normal_block_shader.loc("texelsPerUnit"), texels_per_unit);
glUniform1f(normal_block_shader.loc("lightSizeUV"),
static_cast<GLfloat>(m_light_size_uv));
glUniform1f(normal_block_shader.loc("minRadius"), m_min_radius);
glUniform1f(normal_block_shader.loc("maxRadius"), m_max_radius);
glUniform1i(normal_block_shader.loc("samples"), m_samples);
m_mvp_mat = m_p_mat * m_mv_mat; m_mvp_mat = m_p_mat * m_mv_mat;
auto& m_planes = m_world.planes(); auto& m_planes = m_world.planes();
@@ -814,4 +821,8 @@ bool& Renderer::discard_transparent() { return m_discard_tranparent; }
bool& Renderer::shader_on() { return m_shader_on; } bool& Renderer::shader_on() { return m_shader_on; }
int& Renderer::shadow_mode() { return m_shadow_mode; } int& Renderer::shadow_mode() { return m_shadow_mode; }
int& Renderer::light_cull_face() { return m_light_cull_face; } int& Renderer::light_cull_face() { return m_light_cull_face; }
int& Renderer::light_size_uv() { return m_light_size_uv; }
float& Renderer::min_radius() { return m_min_radius; }
float& Renderer::max_radius() { return m_max_radius; }
int& Renderer::samples() { return m_samples; }
} // namespace Cubed } // namespace Cubed