feat: lighting effects (#18)

* feat(rendering): add basic diffuse and ambient lighting to block rendering

* feat(world): add day/night cycle with server tick system

* feat(renderer): make ambient strength adjustable via dev panel

* fix(game_time): use unsigned tick type and enforce positive tick speed

* feat(renderer): add shadow mapping with PCF soft shadows

Introduce shadow mapping using a dedicated depth framebuffer and shader. The block fragment shader now performs percentage-closer filtering (PCF) with Poisson disk sampling and random rotation for soft shadows. The vertex shader outputs light-space coordinates. A new depth shader pair handles rendering from the light's perspective, discarding transparent fragments. The renderer sets up the light projection based on the camera position and sun direction, and applies the shadow factor to diffuse lighting. Day/night cycle can now be toggled off in the world server thread.

* perf(shadow): increase depth map resolution and refine PCF sampling

* 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.

* chore(world): add missing <numbers> include

* 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.

* fix(texture): set texture wrap mode to clamp to edge

* feat(renderer): smooth shadow sun direction transitions using quantized directions and slerp

* 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.

* feat(renderer): add roughness-based specular lighting to blocks

* feat(renderer): compute ambient and sunlight colors based on sun height

* feat(renderer): add moonlight and smooth day/night light blending

* refactor(renderer): extract day/night calculation and add billboard shaders

Add a new ParallelLight struct to encapsulate lighting parameters.
Move day-night cycle computation from render_world() to a new
day_night_calculation() method. Update sky shaders to use procedural
colors based on sun position. Add separate billboard shaders for sun/moon.

* feat(sky): add animated clouds to sky shader with speed control

* feat(renderer): add configurable cloud thresholds and white mix
This commit is contained in:
zhenyan121
2026-06-19 11:34:22 +08:00
committed by GitHub
parent f4114c2699
commit ca3fc5e3bf
38 changed files with 1305 additions and 52 deletions

View File

@@ -7,4 +7,5 @@ is_liquid = false
is_passable = true
is_transitional = false
is_transparent = true
name = 'air'
name = 'air'
roughness = 1.0

View File

@@ -7,4 +7,5 @@ is_liquid = false
is_passable = false
is_transitional = true
is_transparent = false
name = 'dirt'
name = 'dirt'
roughness = 1.0

View File

@@ -7,4 +7,5 @@ is_liquid = false
is_passable = true
is_transitional = false
is_transparent = true
name = 'grass'
name = 'grass'
roughness = 0.9

View File

@@ -7,4 +7,5 @@ is_liquid = false
is_passable = false
is_transitional = true
is_transparent = false
name = 'grass_block'
name = 'grass_block'
roughness = 0.9

View File

@@ -7,4 +7,5 @@ is_liquid = false
is_passable = false
is_transitional = false
is_transparent = true
name = 'leaf'
name = 'leaf'
roughness = 0.7

View File

@@ -7,4 +7,5 @@ is_liquid = false
is_passable = false
is_transitional = false
is_transparent = false
name = 'log'
name = 'log'
roughness = 0.7

View File

@@ -7,4 +7,5 @@ is_liquid = false
is_passable = false
is_transitional = true
is_transparent = false
name = 'sand'
name = 'sand'
roughness = 0.8

View File

@@ -7,4 +7,5 @@ is_liquid = false
is_passable = false
is_transitional = true
is_transparent = false
name = 'snowy_grass_block'
name = 'snowy_grass_block'
roughness = 0.9

View File

@@ -7,4 +7,5 @@ is_liquid = false
is_passable = false
is_transitional = true
is_transparent = false
name = 'stone'
name = 'stone'
roughness = 0.75

View File

@@ -8,3 +8,4 @@ is_transparent = false
is_discard = false
is_blend = false
is_transitional = false
roughness = 1.0

View File

@@ -7,4 +7,5 @@ is_liquid = true
is_passable = true
is_transitional = false
is_transparent = true
name = 'water'
name = 'water'
roughness = 0.02

View File

@@ -0,0 +1,11 @@
#version 460
out vec4 frag_color;
uniform vec3 color;
void main(void) {
frag_color = vec4(color, 1.0);
}

View File

@@ -0,0 +1,10 @@
#version 460
layout (location = 0) in vec3 vertices_pos;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
void main(void) {
gl_Position = proj_matrix * mv_matrix * vec4(vertices_pos, 1.0);
}

View File

@@ -1,15 +1,329 @@
#version 460
in vec2 tc;
in vec3 normal;
in vec3 vert_pos;
in vec4 FragPosLightSpace;
in float roughness;
flat in int tex_layer;
out vec4 color;
layout (binding = 0) uniform sampler2D shadowMap;
layout (binding = 1) uniform sampler2DArray samp;
uniform float ambientStrength;
uniform vec3 sunlightColor;
uniform vec3 ambientColor;
uniform vec3 sunlightDir;
uniform vec3 cameraPos;
uniform bool shader_on;
uniform int shadowMode;
uniform float specularStrength;
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.3220, -0.6058), vec2( 0.5693, -0.4048),
vec2(-0.1276, 0.1657), vec2(-0.0649, -0.0165),
vec2( 0.2773, -0.0305), vec2(-0.1134, -0.2122)
);
uniform int samples;
float random(vec3 seed) {
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)
{
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
projCoords = projCoords * 0.5 + 0.5;
if (projCoords.x < 0.0 || projCoords.x > 1.0 ||
projCoords.y < 0.0 || projCoords.y > 1.0 ||
projCoords.z < 0.0 || projCoords.z > 1.0) {
return 0.0;
}
float currentDepth = projCoords.z;
vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));
float shadow = 0.0;
float bias =
clamp(
0.001 * (1.0 - dot(norm, lightDir)),
0.0003,
0.003
);
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 = 0.7;
float radius = mix(1.0, 4.0, currentDepth);
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 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 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 {
float pcfDepth =
texture(shadowMap, projCoords.xy).r;
shadow =
currentDepth - bias > pcfDepth
? 1.0
: 0.0;
}
return shadow;
}
layout (binding = 0) uniform sampler2DArray samp;
void main(void) {
color = texture(samp, vec3(tc, tex_layer));
if (color.a < 0.8) {
vec4 objectColor = texture(samp, vec3(tc, tex_layer));
if (objectColor.a < 0.8) {
discard;
}
if (!shader_on) {
color = objectColor;
return;
}
vec3 lightDir = normalize(-sunlightDir);
vec3 norm = normalize(normal);
vec3 V =
normalize(cameraPos - vert_pos);
vec3 H =
normalize(lightDir + V);
vec3 ambient = ambientStrength * ambientColor;
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * sunlightColor;
float r =
clamp(roughness, 0.0, 1.0);
float shininess =
mix(
512.0,
4.0,
r
);
float ks =
mix(
0.8,
0.02,
r
);
float spec = 0.0;
if(diff > 0.0)
{
spec =
ks *
pow(
max(dot(norm,H),0.0),
shininess
);
}
vec3 specular = spec * sunlightColor * specularStrength;
float shadow = ShadowCalculation(FragPosLightSpace, norm, lightDir);
color = vec4((ambient + (1.0 - shadow) * (diffuse)) * objectColor.rgb + (1.0-shadow) * specular, objectColor.a);
//color = varyingColor;
}

View File

@@ -3,8 +3,15 @@
layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 texCoord;
layout (location = 2) in float layer;
layout (location = 3) in vec3 aNormal;
layout (location = 4) in float Roughness;
out vec2 tc;
out vec3 normal;
out vec3 vert_pos;
flat out int tex_layer;
out vec4 FragPosLightSpace;
out float roughness;
mat4 buildRotateX(float rad);
mat4 buildRotateY(float rad);
@@ -13,13 +20,21 @@ mat4 buildTranslate(float x, float y, float z);
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
uniform mat4 lightSpaceMatrix;
void main(void) {
gl_Position = proj_matrix * mv_matrix * vec4(pos, 1.0);
vec4 viewPos = mv_matrix * vec4(pos, 1.0);
vert_pos = pos;
tc = texCoord;
tex_layer = int(layer);
roughness = Roughness;
normal = mat3(norm_matrix) * aNormal;
FragPosLightSpace = lightSpaceMatrix * vec4(pos, 1.0);
gl_Position = proj_matrix * viewPos;
}
mat4 buildTranslate(float x, float y, float z) {

View File

@@ -0,0 +1,16 @@
#version 460
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

@@ -0,0 +1,12 @@
#version 460
layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 texCoord;
layout (location = 2) in float layer;
uniform mat4 lightSpaceMatrix;
out vec2 tc;
flat out int tex_layer;
void main() {
tc = texCoord;
tex_layer = int(layer);
gl_Position = lightSpaceMatrix * vec4(pos, 1.0);
}

View File

@@ -1,9 +1,95 @@
#version 460
in vec3 dir;
out vec4 frag_color;
void main(void) {
frag_color = vec4(0.529, 0.808, 0.922, 1.0);
uniform vec3 skyTop;
uniform vec3 skyBottom;
uniform vec3 sunDir;
uniform vec3 sunColor;
uniform float horizonSharpness;
uniform float cloudWhiteMix;
uniform float cloudThresholdLow;
uniform float cloudThresholdHigh;
uniform float time;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p) {
float v = 0.0;
float amp = 0.5;
for (int i = 0; i < 5; i++) {
v += amp * noise(p);
p *= 2.0;
amp *= 0.5;
}
return v;
}
void main(void) {
vec3 sund = normalize(sunDir);
float t =
clamp(
dir.y * 0.5 + 0.5,
0.0,
1.0
);
vec3 sky =
mix(
skyBottom,
skyTop,
pow(t, horizonSharpness)
);
// cloud
if (dir.y > 0.0) {
vec2 cloud_uv = dir.xz / (dir.y + 0.15) * 0.5 + vec2(time * 0.005, time * 0.002);
float cloud_density = fbm(cloud_uv * 2.0);
float safeLow = cloudThresholdLow;
float safeHigh = max(cloudThresholdHigh, cloudThresholdLow + 0.001);
cloud_density = smoothstep(safeLow,safeHigh, cloud_density);
float fade = smoothstep(0.0, 0.3, dir.y) * (1.0 - smoothstep(0.85, 1.0, dir.y));
cloud_density *= fade;
vec3 cloud_color = mix(skyBottom, vec3(1.0), cloudWhiteMix);
sky = mix(sky, cloud_color, cloud_density * 0.6);
}
float sunAmount = max(dot(dir, sund), 0.0);
//float glow = pow(sunAmount, 8.0) * 0.15;
float glow = pow(sunAmount, 8.0) * 0.15 + pow(sunAmount, 32.0) * 0.3;
sky += glow * sunColor;
frag_color = vec4(sky, 1.0);
//frag_color = vec4(vec3(sunAmount), 1.0);
//frag_color = vec4(t,0,0,1);
}

View File

@@ -5,6 +5,12 @@ layout (location = 0) in vec3 vertices_pos;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
out vec3 dir;
void main(void) {
// Our skybox mesh uses [0,1] coordinates instead of the usual [-1,1].
// Shift to the cube center before normalizing to obtain the correct
// view direction for atmospheric calculations.
dir = normalize(vertices_pos - vec3(0.5));
gl_Position = proj_matrix * mv_matrix * vec4(vertices_pos, 1.0);
}