1use glam::{Vec2, Vec3, Vec4};
5
6#[derive(Clone, Debug)]
9pub struct DepthEffectsConfig {
10 pub self_shadow: bool,
11 pub self_shadow_bias: f32,
12 pub ao_strength: f32,
13 pub ao_radius: f32,
14 pub ao_samples: u32,
15 pub rim_light: bool,
16 pub rim_color: Vec3,
17 pub rim_power: f32,
18 pub rim_intensity: f32,
19 pub inner_glow: bool,
20 pub glow_strength: f32,
21 pub glow_color: Vec3,
22}
23
24impl Default for DepthEffectsConfig {
25 fn default() -> Self {
26 Self {
27 self_shadow: true,
28 self_shadow_bias: 0.005,
29 ao_strength: 0.5,
30 ao_radius: 0.1,
31 ao_samples: 8,
32 rim_light: true,
33 rim_color: Vec3::new(0.8, 0.9, 1.0),
34 rim_power: 3.0,
35 rim_intensity: 0.5,
36 inner_glow: false,
37 glow_strength: 0.3,
38 glow_color: Vec3::new(1.0, 0.8, 0.5),
39 }
40 }
41}
42
43pub fn compute_self_shadow(normal: Vec3, light_dir: Vec3, bias: f32) -> f32 {
48 let n_dot_l = normal.dot(light_dir.normalize_or_zero());
49 let shadow = ((n_dot_l + bias) / (bias * 2.0 + 0.01)).clamp(0.0, 1.0);
51 shadow
52}
53
54pub fn compute_ssao_for_glyph(
58 normal: Vec3,
59 position: Vec3,
60 neighbors: &[Vec3],
61 config: &DepthEffectsConfig,
62) -> f32 {
63 if neighbors.is_empty() || config.ao_samples == 0 {
64 return 1.0;
65 }
66
67 let mut occlusion = 0.0f32;
68 let mut samples = 0u32;
69
70 for neighbor in neighbors {
71 let to_neighbor = *neighbor - position;
72 let dist = to_neighbor.length();
73 if dist < 0.0001 || dist > config.ao_radius {
74 continue;
75 }
76
77 let dir = to_neighbor / dist;
78 let n_dot_d = normal.dot(dir).max(0.0);
79
80 let distance_factor = 1.0 - (dist / config.ao_radius).clamp(0.0, 1.0);
82 occlusion += n_dot_d * distance_factor;
83 samples += 1;
84
85 if samples >= config.ao_samples {
86 break;
87 }
88 }
89
90 let ao = if samples > 0 {
91 1.0 - (occlusion / samples as f32 * config.ao_strength).clamp(0.0, 1.0)
92 } else {
93 1.0
94 };
95
96 ao
97}
98
99pub fn compute_rim_light(normal: Vec3, view_dir: Vec3, config: &DepthEffectsConfig) -> Vec3 {
102 if !config.rim_light {
103 return Vec3::ZERO;
104 }
105
106 let n = normal.normalize_or_zero();
107 let v = view_dir.normalize_or_zero();
108 let n_dot_v = n.dot(v).max(0.0);
109
110 let rim = (1.0 - n_dot_v).powf(config.rim_power);
112
113 config.rim_color * rim * config.rim_intensity
114}
115
116pub fn compute_subsurface(
119 normal: Vec3,
120 light_dir: Vec3,
121 view_dir: Vec3,
122 thickness: f32,
123 config: &DepthEffectsConfig,
124) -> f32 {
125 if !config.inner_glow || thickness <= 0.0 {
126 return 0.0;
127 }
128
129 let n = normal.normalize_or_zero();
130 let l = light_dir.normalize_or_zero();
131 let v = view_dir.normalize_or_zero();
132
133 let wrap = 0.5;
135 let n_dot_l = (n.dot(l) + wrap) / (1.0 + wrap);
136 let wrap_diffuse = n_dot_l.max(0.0);
137
138 let half = (l + v).normalize_or_zero();
140 let v_dot_h = v.dot(-half).max(0.0);
141 let back_light = v_dot_h.powf(2.0);
142
143 let thickness_factor = (1.0 - thickness.clamp(0.0, 1.0)).powf(2.0);
145
146 (wrap_diffuse * 0.3 + back_light * 0.7) * thickness_factor * config.glow_strength
147}
148
149pub fn apply_depth_effects(
151 albedo: Vec4,
152 normal: Vec3,
153 position: Vec3,
154 view_dir: Vec3,
155 light_dir: Vec3,
156 neighbors: &[Vec3],
157 thickness: f32,
158 config: &DepthEffectsConfig,
159) -> Vec4 {
160 let mut color = Vec3::new(albedo.x, albedo.y, albedo.z);
161
162 if config.self_shadow {
164 let shadow = compute_self_shadow(normal, light_dir, config.self_shadow_bias);
165 color *= shadow * 0.7 + 0.3; }
167
168 let ao = compute_ssao_for_glyph(normal, position, neighbors, config);
170 color *= ao;
171
172 let rim = compute_rim_light(normal, view_dir, config);
174 color += rim;
175
176 let sss = compute_subsurface(normal, light_dir, view_dir, thickness, config);
178 color += config.glow_color * sss;
179
180 Vec4::new(color.x.min(1.0), color.y.min(1.0), color.z.min(1.0), albedo.w)
181}
182
183pub const SELF_SHADOW_GLSL: &str = r#"
187float compute_self_shadow(vec3 normal, vec3 light_dir, float bias) {
188 float n_dot_l = dot(normal, normalize(light_dir));
189 return clamp((n_dot_l + bias) / (bias * 2.0 + 0.01), 0.0, 1.0);
190}
191"#;
192
193pub const SSAO_GLSL: &str = r#"
195uniform sampler2D u_depth_tex;
196uniform sampler2D u_normal_tex;
197uniform vec2 u_screen_size;
198uniform float u_ao_radius;
199uniform float u_ao_strength;
200
201float hash(vec2 p) {
202 return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
203}
204
205float compute_ssao(vec2 uv, vec3 position, vec3 normal) {
206 float occlusion = 0.0;
207 const int SAMPLES = 8;
208 for (int i = 0; i < SAMPLES; i++) {
209 float angle = float(i) * 6.2831853 / float(SAMPLES) + hash(uv + float(i));
210 float r = u_ao_radius * (float(i + 1) / float(SAMPLES));
211 vec2 offset = vec2(cos(angle), sin(angle)) * r / u_screen_size;
212 float neighbor_depth = texture(u_depth_tex, uv + offset).r;
213 vec3 neighbor_normal = texture(u_normal_tex, uv + offset).rgb * 2.0 - 1.0;
214 float depth_diff = position.z - neighbor_depth;
215 if (depth_diff > 0.001 && depth_diff < u_ao_radius) {
216 occlusion += (1.0 - depth_diff / u_ao_radius) * max(dot(normal, neighbor_normal), 0.0);
217 }
218 }
219 return 1.0 - clamp(occlusion / float(SAMPLES) * u_ao_strength, 0.0, 1.0);
220}
221"#;
222
223pub const RIM_LIGHT_GLSL: &str = r#"
225uniform vec3 u_rim_color;
226uniform float u_rim_power;
227uniform float u_rim_intensity;
228
229vec3 compute_rim(vec3 normal, vec3 view_dir) {
230 float n_dot_v = max(dot(normalize(normal), normalize(view_dir)), 0.0);
231 float rim = pow(1.0 - n_dot_v, u_rim_power);
232 return u_rim_color * rim * u_rim_intensity;
233}
234"#;
235
236pub const SUBSURFACE_GLSL: &str = r#"
238uniform vec3 u_sss_color;
239uniform float u_sss_strength;
240
241float compute_sss(vec3 normal, vec3 light_dir, vec3 view_dir, float thickness) {
242 float wrap = 0.5;
243 float n_dot_l = (dot(normal, light_dir) + wrap) / (1.0 + wrap);
244 float wrap_diffuse = max(n_dot_l, 0.0);
245
246 vec3 half_vec = normalize(light_dir + view_dir);
247 float back_light = pow(max(dot(view_dir, -half_vec), 0.0), 2.0);
248
249 float thick_factor = pow(1.0 - clamp(thickness, 0.0, 1.0), 2.0);
250 return (wrap_diffuse * 0.3 + back_light * 0.7) * thick_factor * u_sss_strength;
251}
252"#;
253
254#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn self_shadow_facing_light() {
262 let shadow = compute_self_shadow(Vec3::Z, Vec3::Z, 0.005);
263 assert!(shadow > 0.9, "Face toward light should be lit: {}", shadow);
264 }
265
266 #[test]
267 fn self_shadow_facing_away() {
268 let shadow = compute_self_shadow(Vec3::Z, -Vec3::Z, 0.005);
269 assert!(shadow < 0.1, "Face away from light should be shadowed: {}", shadow);
270 }
271
272 #[test]
273 fn rim_light_strongest_at_edge() {
274 let config = DepthEffectsConfig::default();
275 let edge_rim = compute_rim_light(Vec3::Z, Vec3::X, &config); let center_rim = compute_rim_light(Vec3::Z, Vec3::Z, &config); assert!(edge_rim.length() > center_rim.length(),
278 "Rim should be stronger at edge: edge={}, center={}", edge_rim.length(), center_rim.length());
279 }
280
281 #[test]
282 fn ao_no_neighbors_returns_one() {
283 let config = DepthEffectsConfig::default();
284 let ao = compute_ssao_for_glyph(Vec3::Z, Vec3::ZERO, &[], &config);
285 assert_eq!(ao, 1.0);
286 }
287
288 #[test]
289 fn ao_surrounded_is_darker() {
290 let config = DepthEffectsConfig { ao_radius: 1.0, ao_strength: 1.0, ..Default::default() };
291 let neighbors = vec![
292 Vec3::new(0.1, 0.0, 0.05),
293 Vec3::new(-0.1, 0.0, 0.05),
294 Vec3::new(0.0, 0.1, 0.05),
295 Vec3::new(0.0, -0.1, 0.05),
296 ];
297 let ao = compute_ssao_for_glyph(Vec3::Z, Vec3::ZERO, &neighbors, &config);
298 assert!(ao < 1.0, "Surrounded point should have AO < 1: {}", ao);
299 }
300
301 #[test]
302 fn subsurface_thin_is_positive() {
303 let config = DepthEffectsConfig { inner_glow: true, glow_strength: 1.0, ..Default::default() };
304 let sss = compute_subsurface(Vec3::Z, Vec3::Z, -Vec3::Z, 0.1, &config);
305 assert!(sss > 0.0, "Thin geometry should transmit light: {}", sss);
306 }
307
308 #[test]
309 fn subsurface_thick_is_less() {
310 let config = DepthEffectsConfig { inner_glow: true, glow_strength: 1.0, ..Default::default() };
311 let thin = compute_subsurface(Vec3::Z, Vec3::Z, -Vec3::Z, 0.1, &config);
312 let thick = compute_subsurface(Vec3::Z, Vec3::Z, -Vec3::Z, 0.9, &config);
313 assert!(thin > thick, "Thin should transmit more: thin={}, thick={}", thin, thick);
314 }
315
316 #[test]
317 fn apply_all_effects() {
318 let config = DepthEffectsConfig {
319 self_shadow: true,
320 rim_light: true,
321 inner_glow: true,
322 glow_strength: 0.5,
323 ..Default::default()
324 };
325 let result = apply_depth_effects(
326 Vec4::new(0.5, 0.5, 0.5, 1.0),
327 Vec3::Z,
328 Vec3::ZERO,
329 Vec3::new(0.5, 0.0, 0.5).normalize(),
330 Vec3::Z,
331 &[],
332 0.3,
333 &config,
334 );
335 assert!(result.w == 1.0, "Alpha should be preserved");
336 assert!(result.x >= 0.0 && result.x <= 1.0);
337 }
338}