three_d/renderer/light/
spot_light.rs

1use crate::core::*;
2use crate::renderer::light::*;
3use crate::renderer::*;
4
5///
6/// A light which shines from the given position and in the given direction.
7/// The light will cast shadows if you [generate a shadow map](SpotLight::generate_shadow_map).
8///
9pub struct SpotLight {
10    context: Context,
11    shadow_texture: Option<DepthTexture2D>,
12    shadow_matrix: Mat4,
13    /// The intensity of the light. This allows for higher intensity than 1 which can be used to simulate high intensity light sources like the sun.
14    pub intensity: f32,
15    /// The base color of the light.
16    pub color: Srgba,
17    /// The position of the light.
18    pub position: Vec3,
19    /// The direction the light shines.
20    pub direction: Vec3,
21    /// The cutoff angle for the light.
22    pub cutoff: Radians,
23    /// The [Attenuation] of the light.
24    pub attenuation: Attenuation,
25}
26
27impl SpotLight {
28    /// Constructs a new spot light.
29    pub fn new(
30        context: &Context,
31        intensity: f32,
32        color: Srgba,
33        position: Vec3,
34        direction: Vec3,
35        cutoff: impl Into<Radians>,
36        attenuation: Attenuation,
37    ) -> SpotLight {
38        SpotLight {
39            context: context.clone(),
40            shadow_texture: None,
41            intensity,
42            color,
43            position,
44            direction,
45            cutoff: cutoff.into(),
46            attenuation,
47            shadow_matrix: Mat4::identity(),
48        }
49    }
50
51    ///
52    /// Clear the shadow map, effectively disable the shadow.
53    /// Only necessary if you want to disable the shadow, if you want to update the shadow, just use [SpotLight::generate_shadow_map].
54    ///
55    pub fn clear_shadow_map(&mut self) {
56        self.shadow_texture = None;
57        self.shadow_matrix = Mat4::identity();
58    }
59
60    ///
61    /// Generate a shadow map which is used to simulate shadows from the spot light onto the geometries given as input.
62    /// It is recomended that the texture size is power of 2.
63    /// If the shadows are too low resolution (the edges between shadow and non-shadow are pixelated) try to increase the texture size.
64    ///
65    pub fn generate_shadow_map(
66        &mut self,
67        texture_size: u32,
68        geometries: impl IntoIterator<Item = impl Geometry> + Clone,
69    ) {
70        let position = self.position;
71        let target = position + self.direction.normalize();
72        let up = compute_up_direction(self.direction);
73
74        let viewport = Viewport::new_at_origo(texture_size, texture_size);
75
76        let mut z_far = 0.0f32;
77        let mut z_near = f32::MAX;
78        for geometry in geometries.clone() {
79            let aabb = geometry.aabb();
80            if !aabb.is_empty() {
81                z_far = z_far.max(aabb.distance_max(self.position));
82                z_near = z_near.min(aabb.distance(self.position));
83            }
84        }
85
86        let shadow_camera = Camera::new_perspective(
87            viewport,
88            position,
89            target,
90            up,
91            self.cutoff,
92            z_near.max(0.01),
93            z_far,
94        );
95        self.shadow_matrix = shadow_matrix(&shadow_camera);
96
97        let mut shadow_texture = DepthTexture2D::new::<f32>(
98            &self.context,
99            texture_size,
100            texture_size,
101            Wrapping::ClampToEdge,
102            Wrapping::ClampToEdge,
103        );
104        let depth_material = DepthMaterial {
105            render_states: RenderStates {
106                write_mask: WriteMask::DEPTH,
107                ..Default::default()
108            },
109            ..Default::default()
110        };
111        let frustum = shadow_camera.frustum();
112        shadow_texture
113            .as_depth_target()
114            .clear(ClearState::default())
115            .write::<RendererError>(|| {
116                for geometry in geometries
117                    .into_iter()
118                    .filter(|g| frustum.contains(g.aabb()))
119                {
120                    render_with_material(
121                        &self.context,
122                        &shadow_camera,
123                        &geometry,
124                        &depth_material,
125                        &[],
126                    );
127                }
128                Ok(())
129            })
130            .unwrap();
131        self.shadow_texture = Some(shadow_texture);
132    }
133
134    ///
135    /// Returns a reference to the shadow map if it has been generated.
136    ///
137    pub fn shadow_map(&self) -> Option<&DepthTexture2D> {
138        self.shadow_texture.as_ref()
139    }
140}
141
142impl Light for SpotLight {
143    fn shader_source(&self, i: u32) -> String {
144        if self.shadow_texture.is_some() {
145            format!(
146                "
147                    uniform sampler2D shadowMap{};
148                    uniform mat4 shadowMVP{};
149
150                    uniform vec3 color{};
151                    uniform vec3 attenuation{};
152                    uniform vec3 position{};
153                    uniform float cutoff{};
154                    uniform vec3 direction{};
155                    vec3 calculate_lighting{}(vec3 surface_color, vec3 position, vec3 normal, vec3 view_direction, float metallic, float roughness, float occlusion)
156                    {{
157                        vec3 light_direction = position{} - position;
158                        float distance = length(light_direction);
159                        light_direction = light_direction / distance;
160
161                        float angle = acos(dot(-light_direction, normalize(direction{})));
162                        float cutoff = cutoff{};
163
164                        vec3 result = vec3(0.0);
165                        if (angle < cutoff) {{
166                            vec3 light_color = attenuate(color{}, attenuation{}, distance);
167                            result = calculate_light(light_color, light_direction, surface_color, view_direction, normal,
168                                metallic, roughness) * (1.0 - smoothstep(0.75 * cutoff, cutoff, angle));
169                            result *= calculate_shadow(light_direction, normal, shadowMap{}, shadowMVP{}, position);
170                        }}
171                        return result;
172                    }}
173
174                ", i, i, i, i, i, i, i, i, i, i, i, i, i, i, i)
175        } else {
176            format!(
177                "
178                    uniform vec3 color{};
179                    uniform vec3 attenuation{};
180                    uniform vec3 position{};
181                    uniform float cutoff{};
182                    uniform vec3 direction{};
183                    vec3 calculate_lighting{}(vec3 surface_color, vec3 position, vec3 normal, vec3 view_direction, float metallic, float roughness, float occlusion)
184                    {{
185                        vec3 light_direction = position{} - position;
186                        float distance = length(light_direction);
187                        light_direction = light_direction / distance;
188
189                        float angle = acos(dot(-light_direction, normalize(direction{})));
190                        float cutoff = cutoff{};
191
192                        vec3 result = vec3(0.0);
193                        if (angle < cutoff) {{
194                            vec3 light_color = attenuate(color{}, attenuation{}, distance);
195                            result = calculate_light(light_color, light_direction, surface_color, view_direction, normal,
196                                metallic, roughness) * (1.0 - smoothstep(0.75 * cutoff, cutoff, angle));
197                        }}
198                        return result;
199                    }}
200
201                ", i, i, i, i, i, i, i, i, i, i, i)
202        }
203    }
204    fn use_uniforms(&self, program: &Program, i: u32) {
205        if let Some(ref tex) = self.shadow_texture {
206            program.use_depth_texture(&format!("shadowMap{}", i), tex);
207            program.use_uniform(&format!("shadowMVP{}", i), self.shadow_matrix);
208        }
209        program.use_uniform(
210            &format!("color{}", i),
211            self.color.to_linear_srgb().truncate() * self.intensity,
212        );
213        program.use_uniform(
214            &format!("attenuation{}", i),
215            vec3(
216                self.attenuation.constant,
217                self.attenuation.linear,
218                self.attenuation.quadratic,
219            ),
220        );
221        program.use_uniform(&format!("position{}", i), self.position);
222        program.use_uniform(&format!("direction{}", i), self.direction.normalize());
223        program.use_uniform(&format!("cutoff{}", i), self.cutoff.0);
224    }
225
226    fn id(&self) -> LightId {
227        LightId::SpotLight(self.shadow_texture.is_some())
228    }
229}