veloren/voxygen/src/scene/mod.rs
2020-09-19 19:14:23 +08:00

1069 lines
40 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

pub mod camera;
pub mod figure;
pub mod lod;
pub mod math;
pub mod particle;
pub mod simple;
pub mod terrain;
pub use self::{
camera::{Camera, CameraMode},
figure::FigureMgr,
lod::Lod,
particle::ParticleMgr,
terrain::Terrain,
};
use crate::{
audio::{music::MusicMgr, sfx::SfxMgr, AudioFrontend},
render::{
create_pp_mesh, create_skybox_mesh, Consts, GlobalModel, Globals, Light, LodData, Model,
PostProcessLocals, PostProcessPipeline, Renderer, Shadow, ShadowLocals, SkyboxLocals,
SkyboxPipeline,
},
settings::Settings,
window::{AnalogGameInput, Event},
};
use client::Client;
use common::{
comp,
comp::humanoid::DEFAULT_HUMANOID_EYE_HEIGHT,
outcome::Outcome,
span,
state::{DeltaTime, State},
terrain::{BlockKind, TerrainChunk},
vol::ReadVol,
};
use comp::item::Reagent;
use num::traits::{Float, FloatConst};
use specs::{Entity as EcsEntity, Join, WorldExt};
use vek::*;
// TODO: Don't hard-code this.
const CURSOR_PAN_SCALE: f32 = 0.005;
const MAX_LIGHT_COUNT: usize = 31;
const MAX_SHADOW_COUNT: usize = 24;
const NUM_DIRECTED_LIGHTS: usize = 1;
const LIGHT_DIST_RADIUS: f32 = 64.0; // The distance beyond which lights may not emit light from their origin
const SHADOW_DIST_RADIUS: f32 = 8.0;
const SHADOW_MAX_DIST: f32 = 96.0; // The distance beyond which shadows may not be visible
/// The minimum sin γ we will use before switching to uniform mapping.
const EPSILON_UPSILON: f64 = -1.0;
const SHADOW_NEAR: f32 = 0.25; // Near plane for shadow map point light rendering.
const SHADOW_FAR: f32 = 128.0; // Far plane for shadow map point light rendering.
/// Above this speed is considered running
/// Used for first person camera effects
const RUNNING_THRESHOLD: f32 = 0.7;
/// is_daylight, array of active lights.
pub type LightData<'a> = (bool, &'a [Light]);
struct EventLight {
light: Light,
timeout: f32,
fadeout: fn(f32) -> f32,
}
struct Skybox {
model: Model<SkyboxPipeline>,
locals: Consts<SkyboxLocals>,
}
struct PostProcess {
model: Model<PostProcessPipeline>,
locals: Consts<PostProcessLocals>,
}
pub struct Scene {
data: GlobalModel,
camera: Camera,
camera_input_state: Vec2<f32>,
event_lights: Vec<EventLight>,
skybox: Skybox,
postprocess: PostProcess,
terrain: Terrain<TerrainChunk>,
pub lod: Lod,
loaded_distance: f32,
/// x coordinate is sea level (minimum height for any land chunk), and y
/// coordinate is the maximum height above the mnimimum for any land
/// chunk.
map_bounds: Vec2<f32>,
select_pos: Option<Vec3<i32>>,
light_data: Vec<Light>,
particle_mgr: ParticleMgr,
figure_mgr: FigureMgr,
sfx_mgr: SfxMgr,
music_mgr: MusicMgr,
}
pub struct SceneData<'a> {
pub state: &'a State,
pub player_entity: specs::Entity,
pub target_entity: Option<specs::Entity>,
pub loaded_distance: f32,
pub view_distance: u32,
pub tick: u64,
pub thread_pool: &'a uvth::ThreadPool,
pub gamma: f32,
pub ambiance: f32,
pub mouse_smoothing: bool,
pub sprite_render_distance: f32,
pub particles_enabled: bool,
pub figure_lod_render_distance: f32,
pub is_aiming: bool,
}
impl<'a> SceneData<'a> {
pub fn get_sun_dir(&self) -> Vec3<f32> { Globals::get_sun_dir(self.state.get_time_of_day()) }
pub fn get_moon_dir(&self) -> Vec3<f32> { Globals::get_moon_dir(self.state.get_time_of_day()) }
}
/// Approximate a scalar field of view angle using the parameterization from
/// section 4.3 of Lloyd's thesis:
///
/// W_e = 2 n_e tan θ
///
/// where
///
/// W_e = 2 is the width of the image plane (for our projections, since they go
/// from -1 to 1) n_e = near_plane is the near plane for the view frustum
/// θ = (fov / 2) is the half-angle of the FOV (the one passed to
/// Mat4::projection_rh_no).
///
/// Although the widths for the x and y image planes are the same, they are
/// different in this framework due to the introduction of an aspect ratio:
///
/// y'(p) = 1.0 / tan(fov / 2) * p.y / -p.z
/// x'(p) = 1.0 / (aspect * tan(fov / 2)) * p.x / -p.z
///
/// i.e.
///
/// y'(x, y, -near, w) = 1 / tan(fov / 2) p.y / near
/// x'(x, y, -near, w) = 1 / (aspect * tan(fov / 2)) p.x / near
///
/// W_e,y = 2 * near_plane * tan(fov / 2)
/// W_e,x = 2 * near_plane * aspect * W_e,y
///
/// Θ_x = atan(W_e_y / 2 / near_plane) = atanfov / t()
///
/// i.e. we have an "effective" W_e_x of
///
/// 2 = 2 * near_plane * tan Θ
///
/// atan(1 / near_plane) = θ
///
/// y'
/// x(-near)
/// W_e = 2 * near_plane *
///
/// W_e_y / n_e = tan (fov / 2)
/// W_e_x = 2 n
fn compute_scalar_fov<F: Float>(_near_plane: F, fov: F, aspect: F) -> F {
let two = F::one() + F::one();
let theta_y = fov / two;
let theta_x = (aspect * theta_y.tan()).atan();
theta_x.min(theta_y)
}
/// Compute a near-optimal warping parameter that helps minimize error in a
/// shadow map.
///
/// See section 5.2 of Brandon Lloyd's thesis:
///
/// [http://gamma.cs.unc.edu/papers/documents/dissertations/lloyd07.pdf](Logarithmic Perspective Shadow Maps).
///
/// η =
/// 0 γ < γ_a
/// -1 + (η_b + 1)(1 + cos(90 (γ - γ_a)/(γ_b - γ_a))) γ_a ≤ γ < γ_b
/// η_b + (η_c - η_b) sin(90 (γ - γ_b)/(γ_c - γ_b)) γ_b ≤ γ < γ_c
/// η_c γ_c ≤ γ
///
/// NOTE: Equation's described behavior is *wrong!* I have pieced together a
/// slightly different function that seems to more closely satisfy the author's
/// intent:
///
/// η =
/// -1 γ < γ_a
/// -1 + (η_b + 1) (γ - γ_a)/(γ_b - γ_a) γ_a ≤ γ < γ_b
/// η_b + (η_c - η_b) sin(90 (γ - γ_b)/(γ_c - γ_b)) γ_b ≤ γ < γ_c
/// η_c γ_c ≤ γ
///
/// There are other alternatives that may have more desirable properties, such
/// as:
///
/// η =
/// -1 γ < γ_a
/// -1 + (η_b + 1)(1 - cos(90 (γ - γ_a)/(γ_b - γ_a))) γ_a ≤ γ < γ_b
/// η_b + (η_c - η_b) sin(90 (γ - γ_b)/(γ_c - γ_b)) γ_b ≤ γ < γ_c
/// η_c γ_c ≤ γ
fn compute_warping_parameter<F: Float + FloatConst>(
gamma: F,
(gamma_a, gamma_b, gamma_c): (F, F, F),
(eta_b, eta_c): (F, F),
) -> F {
if gamma < gamma_a {
-F::one()
/* F::zero() */
} else if gamma_a <= gamma && gamma < gamma_b {
/* -F::one() + (eta_b + F::one()) * (F::one() + (F::FRAC_PI_2() * (gamma - gamma_a) / (gamma_b - gamma_a)).cos()) */
-F::one() + (eta_b + F::one()) * (F::one() - (F::FRAC_PI_2() * (gamma - gamma_a) / (gamma_b - gamma_a)).cos())
// -F::one() + (eta_b + F::one()) * ((gamma - gamma_a) / (gamma_b - gamma_a))
} else if gamma_b <= gamma && gamma < gamma_c {
eta_b + (eta_c - eta_b) * (F::FRAC_PI_2() * (gamma - gamma_b) / (gamma_c - gamma_b)).sin()
} else {
eta_c
}
// NOTE: Just in case we go out of range due to floating point imprecision.
.max(-F::one()).min(F::one())
}
/// Compute a near-optimal warping parameter that falls off quickly enough
/// when the warp angle goes past the minimum field of view angle, for
/// perspective projections.
///
/// For F_p (perspective warping) and view fov angle θ,the parameters are:
///
/// γ_a = θ / 3
/// γ_b = θ
/// γ_c = θ + 0.3(90 - θ)
///
/// η_b = -0.2
/// η_c = 0
///
/// See compute_warping_parameter.
fn compute_warping_parameter_perspective<F: Float + FloatConst>(
gamma: F,
near_plane: F,
fov: F,
aspect: F,
) -> F {
let theta = compute_scalar_fov(near_plane, fov, aspect);
let two = F::one() + F::one();
let three = two + F::one();
let ten = three + three + three + F::one();
compute_warping_parameter(
gamma,
(
theta / three,
theta,
theta + (three / ten) * (F::FRAC_PI_2() - theta),
),
(-two / ten, F::zero()),
)
}
impl Scene {
/// Create a new `Scene` with default parameters.
pub fn new(renderer: &mut Renderer, client: &Client, settings: &Settings) -> Self {
let resolution = renderer.get_resolution().map(|e| e as f32);
Self {
data: GlobalModel {
globals: renderer.create_consts(&[Globals::default()]).unwrap(),
lights: renderer
.create_consts(&[Light::default(); MAX_LIGHT_COUNT])
.unwrap(),
shadows: renderer
.create_consts(&[Shadow::default(); MAX_SHADOW_COUNT])
.unwrap(),
shadow_mats: renderer
.create_consts(&[ShadowLocals::default(); MAX_LIGHT_COUNT * 6 + 6])
.unwrap(),
},
camera: Camera::new(resolution.x / resolution.y, CameraMode::ThirdPerson),
camera_input_state: Vec2::zero(),
event_lights: Vec::new(),
skybox: Skybox {
model: renderer.create_model(&create_skybox_mesh()).unwrap(),
locals: renderer.create_consts(&[SkyboxLocals::default()]).unwrap(),
},
postprocess: PostProcess {
model: renderer.create_model(&create_pp_mesh()).unwrap(),
locals: renderer
.create_consts(&[PostProcessLocals::default()])
.unwrap(),
},
terrain: Terrain::new(renderer),
lod: Lod::new(renderer, client, settings),
loaded_distance: 0.0,
map_bounds: client.world_map.2,
select_pos: None,
light_data: Vec::new(),
particle_mgr: ParticleMgr::new(renderer),
figure_mgr: FigureMgr::new(renderer),
sfx_mgr: SfxMgr::new(),
music_mgr: MusicMgr::new(),
}
}
/// Get a reference to the scene's globals.
pub fn globals(&self) -> &Consts<Globals> { &self.data.globals }
/// Get a reference to the scene's camera.
pub fn camera(&self) -> &Camera { &self.camera }
/// Get a reference to the scene's terrain.
pub fn terrain(&self) -> &Terrain<TerrainChunk> { &self.terrain }
/// Get a reference to the scene's lights.
pub fn lights(&self) -> &Vec<Light> { &self.light_data }
/// Get a reference to the scene's particle manager.
pub fn particle_mgr(&self) -> &ParticleMgr { &self.particle_mgr }
/// Get a reference to the scene's figure manager.
pub fn figure_mgr(&self) -> &FigureMgr { &self.figure_mgr }
/// Get a mutable reference to the scene's camera.
pub fn camera_mut(&mut self) -> &mut Camera { &mut self.camera }
/// Set the block position that the player is interacting with
pub fn set_select_pos(&mut self, pos: Option<Vec3<i32>>) { self.select_pos = pos; }
pub fn select_pos(&self) -> Option<Vec3<i32>> { self.select_pos }
/// Handle an incoming user input event (e.g.: cursor moved, key pressed,
/// window closed).
///
/// If the event is handled, return true.
pub fn handle_input_event(&mut self, event: Event) -> bool {
match event {
// When the window is resized, change the camera's aspect ratio
Event::Resize(dims) => {
self.camera.set_aspect_ratio(dims.x as f32 / dims.y as f32);
true
},
// Panning the cursor makes the camera rotate
Event::CursorPan(delta) => {
self.camera.rotate_by(Vec3::from(delta) * CURSOR_PAN_SCALE);
true
},
// Zoom the camera when a zoom event occurs
Event::Zoom(delta) => {
// when zooming in the distance the camera travelles should be based on the
// final distance. This is to make sure the camera travelles the
// same distance when zooming in and out
if delta < 0.0 {
self.camera.zoom_switch(
delta * (0.05 + self.camera.get_distance() * 0.01) / (1.0 - delta * 0.01),
); // Thank you Imbris for doing the math
} else {
self.camera
.zoom_switch(delta * (0.05 + self.camera.get_distance() * 0.01));
}
true
},
Event::AnalogGameInput(input) => match input {
AnalogGameInput::CameraX(d) => {
self.camera_input_state.x = d;
true
},
AnalogGameInput::CameraY(d) => {
self.camera_input_state.y = d;
true
},
_ => false,
},
// All other events are unhandled
_ => false,
}
}
pub fn handle_outcome(
&mut self,
outcome: &Outcome,
scene_data: &SceneData,
audio: &mut AudioFrontend,
) {
span!(_guard, "handle_outcome", "Scene::handle_outcome");
self.particle_mgr.handle_outcome(&outcome, &scene_data);
self.sfx_mgr.handle_outcome(&outcome, audio);
match outcome {
Outcome::Explosion {
pos,
power,
reagent,
} => self.event_lights.push(EventLight {
light: Light::new(
*pos,
match reagent {
Some(Reagent::Blue) => Rgb::new(0.15, 0.4, 1.0),
Some(Reagent::Green) => Rgb::new(0.0, 1.0, 0.0),
Some(Reagent::Purple) => Rgb::new(0.7, 0.0, 1.0),
Some(Reagent::Red) => Rgb::new(1.0, 0.0, 0.0),
Some(Reagent::Yellow) => Rgb::new(1.0, 1.0, 0.0),
None => Rgb::new(1.0, 0.5, 0.0),
},
*power
* match reagent {
Some(_) => 5.0,
None => 2.5,
},
),
timeout: match reagent {
Some(_) => 1.0,
None => 0.5,
},
fadeout: |timeout| timeout * 2.0,
}),
Outcome::ProjectileShot { .. } => {},
}
}
/// Maintain data such as GPU constant buffers, models, etc. To be called
/// once per tick.
pub fn maintain(
&mut self,
renderer: &mut Renderer,
audio: &mut AudioFrontend,
scene_data: &SceneData,
) {
span!(_guard, "maintain", "Scene::maintain");
// Get player position.
let ecs = scene_data.state.ecs();
let player_pos = ecs
.read_storage::<comp::Pos>()
.get(scene_data.player_entity)
.map_or(Vec3::zero(), |pos| pos.0);
let player_rolling = ecs
.read_storage::<comp::CharacterState>()
.get(scene_data.player_entity)
.map_or(false, |cs| cs.is_dodge());
let is_running = ecs
.read_storage::<comp::Vel>()
.get(scene_data.player_entity)
.map(|v| v.0.magnitude_squared() > RUNNING_THRESHOLD.powi(2))
.unwrap_or(false);
let on_ground = ecs
.read_storage::<comp::PhysicsState>()
.get(scene_data.player_entity)
.map(|p| p.on_ground);
let player_scale = match scene_data
.state
.ecs()
.read_storage::<comp::Body>()
.get(scene_data.player_entity)
{
Some(comp::Body::Humanoid(body)) => body.scale(),
_ => 1_f32,
};
let eye_height = match scene_data
.state
.ecs()
.read_storage::<comp::Body>()
.get(scene_data.player_entity)
{
Some(comp::Body::Humanoid(body)) => body.eye_height(),
_ => DEFAULT_HUMANOID_EYE_HEIGHT,
};
// Add the analog input to camera
self.camera
.rotate_by(Vec3::from([self.camera_input_state.x, 0.0, 0.0]));
self.camera
.rotate_by(Vec3::from([0.0, self.camera_input_state.y, 0.0]));
// Alter camera position to match player.
let tilt = self.camera.get_orientation().y;
let dist = self.camera.get_distance();
let up = match self.camera.get_mode() {
CameraMode::FirstPerson => {
if player_rolling {
player_scale * 0.8
} else if is_running && on_ground.unwrap_or(false) {
eye_height + (scene_data.state.get_time() as f32 * 17.0).sin() * 0.05
} else {
eye_height
}
},
CameraMode::ThirdPerson if scene_data.is_aiming => player_scale * 2.2,
CameraMode::ThirdPerson => eye_height,
CameraMode::Freefly => 0.0,
};
match self.camera.get_mode() {
CameraMode::FirstPerson | CameraMode::ThirdPerson => {
self.camera.set_focus_pos(
player_pos + Vec3::unit_z() * (up - tilt.min(0.0).sin() * dist * 0.6),
);
},
CameraMode::Freefly => {},
};
// Tick camera for interpolation.
self.camera.update(
scene_data.state.get_time(),
scene_data.state.get_delta_time(),
scene_data.mouse_smoothing,
);
// Compute camera matrices.
self.camera.compute_dependents(&*scene_data.state.terrain());
let camera::Dependents {
view_mat,
proj_mat,
cam_pos,
..
} = self.camera.dependents();
// Update chunk loaded distance smoothly for nice shader fog
let loaded_distance =
(0.98 * self.loaded_distance + 0.02 * scene_data.loaded_distance).max(0.01);
// Update light constants
let lights = &mut self.light_data;
lights.clear();
lights.extend(
(
&scene_data.state.ecs().read_storage::<comp::Pos>(),
scene_data.state.ecs().read_storage::<comp::Ori>().maybe(),
scene_data
.state
.ecs()
.read_storage::<crate::ecs::comp::Interpolated>()
.maybe(),
&scene_data
.state
.ecs()
.read_storage::<comp::LightAnimation>(),
)
.join()
.filter(|(pos, _, _, light_anim)| {
light_anim.col != Rgb::zero()
&& light_anim.strength > 0.0
&& (pos.0.distance_squared(player_pos) as f32)
< loaded_distance.powf(2.0) + LIGHT_DIST_RADIUS
})
.map(|(pos, ori, interpolated, light_anim)| {
// Use interpolated values if they are available
let (pos, ori) =
interpolated.map_or((pos.0, ori.map(|o| o.0)), |i| (i.pos, Some(i.ori)));
let rot = {
if let Some(o) = ori {
Mat3::rotation_z(-o.x.atan2(o.y))
} else {
Mat3::identity()
}
};
Light::new(
pos + (rot * light_anim.offset),
light_anim.col,
light_anim.strength,
)
})
.chain(
self.event_lights
.iter()
.map(|el| el.light.with_strength((el.fadeout)(el.timeout))),
),
);
lights.sort_by_key(|light| light.get_pos().distance_squared(player_pos) as i32);
lights.truncate(MAX_LIGHT_COUNT);
renderer
.update_consts(&mut self.data.lights, &lights)
.expect("Failed to update light constants");
// Update event lights
let dt = ecs.fetch::<DeltaTime>().0;
self.event_lights.drain_filter(|el| {
el.timeout -= dt;
el.timeout <= 0.0
});
// Update shadow constants
let mut shadows = (
&scene_data.state.ecs().read_storage::<comp::Pos>(),
scene_data
.state
.ecs()
.read_storage::<crate::ecs::comp::Interpolated>()
.maybe(),
scene_data.state.ecs().read_storage::<comp::Scale>().maybe(),
&scene_data.state.ecs().read_storage::<comp::Body>(),
&scene_data.state.ecs().read_storage::<comp::Stats>(),
)
.join()
.filter(|(_, _, _, _, stats)| !stats.is_dead)
.filter(|(pos, _, _, _, _)| {
(pos.0.distance_squared(player_pos) as f32)
< (loaded_distance.min(SHADOW_MAX_DIST) + SHADOW_DIST_RADIUS).powf(2.0)
})
.map(|(pos, interpolated, scale, _, _)| {
Shadow::new(
// Use interpolated values pos if it is available
interpolated.map_or(pos.0, |i| i.pos),
scale.map_or(1.0, |s| s.0),
)
})
.collect::<Vec<_>>();
shadows.sort_by_key(|shadow| shadow.get_pos().distance_squared(player_pos) as i32);
shadows.truncate(MAX_SHADOW_COUNT);
renderer
.update_consts(&mut self.data.shadows, &shadows)
.expect("Failed to update light constants");
// Remember to put the new loaded distance back in the scene.
self.loaded_distance = loaded_distance;
// Update light projection matrices for the shadow map.
let time_of_day = scene_data.state.get_time_of_day();
let focus_pos = self.camera.get_focus_pos();
let focus_off = focus_pos.map(|e| e.trunc());
// Update global constants.
renderer
.update_consts(&mut self.data.globals, &[Globals::new(
view_mat,
proj_mat,
cam_pos,
focus_pos,
self.loaded_distance,
self.lod.get_data().tgt_detail as f32,
self.map_bounds,
time_of_day,
scene_data.state.get_time(),
renderer.get_resolution(),
Vec2::new(SHADOW_NEAR, SHADOW_FAR),
lights.len(),
shadows.len(),
NUM_DIRECTED_LIGHTS,
scene_data
.state
.terrain()
.get((cam_pos + focus_off).map(|e| e.floor() as i32))
.map(|b| b.kind())
.unwrap_or(BlockKind::Air),
self.select_pos.map(|e| e - focus_off.map(|e| e as i32)),
scene_data.gamma,
scene_data.ambiance,
self.camera.get_mode(),
scene_data.sprite_render_distance as f32 - 20.0,
)])
.expect("Failed to update global constants");
// Maintain LoD.
self.lod.maintain(renderer);
// Maintain the terrain.
let (_visible_bounds, visible_light_volume, visible_psr_bounds) = self.terrain.maintain(
renderer,
&scene_data,
focus_pos,
self.loaded_distance,
view_mat,
proj_mat,
);
// Maintain the figures.
let _figure_bounds =
self.figure_mgr
.maintain(renderer, scene_data, visible_psr_bounds, &self.camera);
let sun_dir = scene_data.get_sun_dir();
let is_daylight = sun_dir.z < 0.0;
if renderer.render_mode().shadow.is_map() && (is_daylight || !lights.is_empty()) {
let fov = self.camera.get_fov();
let aspect_ratio = self.camera.get_aspect_ratio();
let view_dir = ((focus_pos.map(f32::fract)) - cam_pos).normalized();
let (point_shadow_res, _directed_shadow_res) = renderer.get_shadow_resolution();
// NOTE: The aspect ratio is currently always 1 for our cube maps, since they
// are equal on all sides.
let point_shadow_aspect = point_shadow_res.x as f32 / point_shadow_res.y as f32;
// Construct matrices to transform from world space to light space for the sun
// and moon.
let directed_light_dir = math::Vec3::from(sun_dir);
// Optimal warping for directed lights:
//
// n_opt = 1 / sin y (z_n + √(z_n + (f - n) sin y))
//
// where n is near plane, f is far plane, y is the tilt angle between view and
// light direction, and n_opt is the optimal near plane.
// We also want a way to transform and scale this matrix (* 0.5 + 0.5) in order
// to transform it correctly into texture coordinates, as well as
// OpenGL coordinates. Note that the matrix for directional light
// is *already* linear in the depth buffer.
let texture_mat = Mat4::scaling_3d(0.5f32) * Mat4::translation_3d(1.0f32);
// We need to compute these offset matrices to transform world space coordinates
// to the translated ones we use when multiplying by the light space
// matrix; this helps avoid precision loss during the
// multiplication.
let look_at = math::Vec3::from(cam_pos);
// We upload view matrices as well, to assist in linearizing vertex positions.
// (only for directional lights, so far).
let mut directed_shadow_mats = Vec::with_capacity(6);
let new_dir = math::Vec3::from(view_dir);
let new_dir = new_dir.normalized();
let up: math::Vec3<f32> = math::Vec3::up();
directed_shadow_mats.push(math::Mat4::look_at_rh(
look_at,
look_at + directed_light_dir,
up,
));
// This leaves us with five dummy slots, which we push as defaults.
directed_shadow_mats
.extend_from_slice(&[math::Mat4::default(); 6 - NUM_DIRECTED_LIGHTS] as _);
// Now, construct the full projection matrices in the first two directed light
// slots.
let mut shadow_mats = Vec::with_capacity(6 * (lights.len() + 1));
shadow_mats.extend(directed_shadow_mats.iter().enumerate().map(
move |(idx, &light_view_mat)| {
if idx >= NUM_DIRECTED_LIGHTS {
return ShadowLocals::new(Mat4::identity(), Mat4::identity());
}
let v_p_orig =
math::Vec3::from(light_view_mat * math::Vec4::from_direction(new_dir));
let mut v_p = v_p_orig.normalized();
let cos_gamma = new_dir
.map(f64::from)
.dot(directed_light_dir.map(f64::from));
let sin_gamma = (1.0 - cos_gamma * cos_gamma).sqrt();
let gamma = sin_gamma.asin();
let view_mat = math::Mat4::from_col_array(view_mat.into_col_array());
let bounds1 = math::fit_psr(
view_mat.map_cols(math::Vec4::from),
visible_light_volume.iter().copied(),
math::Vec4::homogenized,
);
let n_e = f64::from(-bounds1.max.z);
let factor = compute_warping_parameter_perspective(
gamma,
n_e,
f64::from(fov),
f64::from(aspect_ratio),
);
v_p.z = 0.0;
v_p.normalize();
let l_r: math::Mat4<f32> = if factor > EPSILON_UPSILON {
math::Mat4::look_at_rh(math::Vec3::zero(), math::Vec3::forward_rh(), v_p)
} else {
math::Mat4::identity()
};
let directed_proj_mat = math::Mat4::new(
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0,
1.0,
);
let light_all_mat = l_r * directed_proj_mat * light_view_mat;
let bounds0 = math::fit_psr(
light_all_mat,
visible_light_volume.iter().copied(),
math::Vec4::homogenized,
);
// Vague idea: project z_n from the camera view to the light view (where it's
// tilted by γ).
let (z_0, z_1) = {
let p_z = bounds1.max.z;
let p_y = bounds0.min.y;
let p_x = bounds0.center().x;
let view_inv = view_mat.inverted();
let light_all_inv = light_all_mat.inverted();
let view_point = view_inv * math::Vec4::new(0.0, 0.0, p_z, 1.0);
let view_plane =
view_inv * math::Vec4::from_direction(math::Vec3::unit_z());
let light_point = light_all_inv * math::Vec4::new(0.0, p_y, 0.0, 1.0);
let light_plane =
light_all_inv * math::Vec4::from_direction(math::Vec3::unit_y());
let shadow_point = light_all_inv * math::Vec4::new(p_x, 0.0, 0.0, 1.0);
let shadow_plane =
light_all_inv * math::Vec4::from_direction(math::Vec3::unit_x());
let solve_p0 = math::Mat4::new(
view_plane.x,
view_plane.y,
view_plane.z,
-view_plane.dot(view_point),
light_plane.x,
light_plane.y,
light_plane.z,
-light_plane.dot(light_point),
shadow_plane.x,
shadow_plane.y,
shadow_plane.z,
-shadow_plane.dot(shadow_point),
0.0,
0.0,
0.0,
1.0,
);
let p0_world = solve_p0.inverted() * math::Vec4::unit_w();
let p0 = light_all_mat * p0_world;
let mut p1 = p0;
p1.y = bounds0.max.y;
let view_from_light_mat = view_mat * light_all_inv;
let z0 = view_from_light_mat * p0;
let z1 = view_from_light_mat * p1;
(f64::from(z0.z), f64::from(z1.z))
};
let mut light_focus_pos: math::Vec3<f32> = math::Vec3::zero();
light_focus_pos.x = bounds0.center().x;
light_focus_pos.y = bounds0.min.y;
light_focus_pos.z = bounds0.center().z;
let d = f64::from(bounds0.max.y - bounds0.min.y).abs();
let w_l_y = d;
// NOTE: See section 5.1.2.2 of Lloyd's thesis.
let alpha = z_1 / z_0;
let alpha_sqrt = alpha.sqrt();
let directed_near_normal = if factor < 0.0 {
// Standard shadow map to LiSPSM
(1.0 + alpha_sqrt - factor * (alpha - 1.0))
/ ((alpha - 1.0) * (factor + 1.0))
} else {
// LiSPSM to PSM
((alpha_sqrt - 1.0) * (factor * alpha_sqrt + 1.0)).recip()
};
// Equation 5.14 - 5.16
let y_ = |v: f64| w_l_y * (v + directed_near_normal).abs();
let directed_near = y_(0.0) as f32;
let directed_far = y_(1.0) as f32;
light_focus_pos.y = if factor > EPSILON_UPSILON {
light_focus_pos.y - directed_near
} else {
light_focus_pos.y
};
let w_v: math::Mat4<f32> = math::Mat4::translation_3d(-math::Vec3::new(
light_focus_pos.x,
light_focus_pos.y,
light_focus_pos.z,
));
let shadow_view_mat: math::Mat4<f32> = w_v * light_all_mat;
let w_p: math::Mat4<f32> = {
if factor > EPSILON_UPSILON {
// Projection for y
let near = directed_near;
let far = directed_far;
let left = -1.0;
let right = 1.0;
let bottom = -1.0;
let top = 1.0;
let s_x = 2.0 * near / (right - left);
let o_x = (right + left) / (right - left);
let s_z = 2.0 * near / (top - bottom);
let o_z = (top + bottom) / (top - bottom);
let s_y = (far + near) / (far - near);
let o_y = -2.0 * far * near / (far - near);
math::Mat4::new(
s_x, o_x, 0.0, 0.0, 0.0, s_y, 0.0, o_y, 0.0, o_z, s_z, 0.0, 0.0,
1.0, 0.0, 0.0,
)
} else {
math::Mat4::identity()
}
};
let shadow_all_mat: math::Mat4<f32> = w_p * shadow_view_mat;
let math::Aabb::<f32> {
min:
math::Vec3 {
x: xmin,
y: ymin,
z: zmin,
},
max:
math::Vec3 {
x: xmax,
y: ymax,
z: zmax,
},
} = math::fit_psr(
shadow_all_mat,
visible_light_volume.iter().copied(),
math::Vec4::homogenized,
);
let s_x = 2.0 / (xmax - xmin);
let s_y = 2.0 / (ymax - ymin);
let s_z = 2.0 / (zmax - zmin);
let o_x = -(xmax + xmin) / (xmax - xmin);
let o_y = -(ymax + ymin) / (ymax - ymin);
let o_z = -(zmax + zmin) / (zmax - zmin);
let directed_proj_mat = Mat4::new(
s_x, 0.0, 0.0, o_x, 0.0, s_y, 0.0, o_y, 0.0, 0.0, s_z, o_z, 0.0, 0.0, 0.0,
1.0,
);
let shadow_all_mat: Mat4<f32> =
Mat4::from_col_arrays(shadow_all_mat.into_col_arrays());
let directed_texture_proj_mat = texture_mat * directed_proj_mat;
ShadowLocals::new(
directed_proj_mat * shadow_all_mat,
directed_texture_proj_mat * shadow_all_mat,
)
},
));
// Now, we tackle point lights.
// First, create a perspective projection matrix at 90 degrees (to cover a whole
// face of the cube map we're using).
let shadow_proj = Mat4::perspective_rh_no(
90.0f32.to_radians(),
point_shadow_aspect,
SHADOW_NEAR,
SHADOW_FAR,
);
// Next, construct the 6 orientations we'll use for the six faces, in terms of
// their (forward, up) vectors.
let orientations = [
(Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, -1.0, 0.0)),
(Vec3::new(-1.0, 0.0, 0.0), Vec3::new(0.0, -1.0, 0.0)),
(Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 0.0, 1.0)),
(Vec3::new(0.0, -1.0, 0.0), Vec3::new(0.0, 0.0, -1.0)),
(Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.0, -1.0, 0.0)),
(Vec3::new(0.0, 0.0, -1.0), Vec3::new(0.0, -1.0, 0.0)),
];
// NOTE: We could create the shadow map collection at the same time as the
// lights, but then we'd have to sort them both, which wastes time. Plus, we
// want to prepend our directed lights.
shadow_mats.extend(lights.iter().flat_map(|light| {
// Now, construct the full projection matrix by making the light look at each
// cube face.
let eye = Vec3::new(light.pos[0], light.pos[1], light.pos[2]) - focus_off;
orientations.iter().map(move |&(forward, up)| {
// NOTE: We don't currently try to linearize point lights or need a separate
// transform for them.
ShadowLocals::new(
shadow_proj * Mat4::look_at_rh(eye, eye + forward, up),
Mat4::identity(),
)
})
}));
renderer
.update_consts(&mut self.data.shadow_mats, &shadow_mats)
.expect("Failed to update light constants");
}
// Remove unused figures.
self.figure_mgr.clean(scene_data.tick);
// Maintain the particles.
self.particle_mgr
.maintain(renderer, &scene_data, &self.terrain);
// Maintain audio
self.sfx_mgr.maintain(
audio,
scene_data.state,
scene_data.player_entity,
&self.camera,
);
self.music_mgr.maintain(audio, scene_data.state);
}
/// Render the scene using the provided `Renderer`.
pub fn render(
&mut self,
renderer: &mut Renderer,
state: &State,
player_entity: EcsEntity,
tick: u64,
scene_data: &SceneData,
) {
span!(_guard, "render", "Scene::render");
let sun_dir = scene_data.get_sun_dir();
let is_daylight = sun_dir.z < 0.0;
let focus_pos = self.camera.get_focus_pos();
let cam_pos = self.camera.dependents().cam_pos + focus_pos.map(|e| e.trunc());
let global = &self.data;
let light_data = (is_daylight, &*self.light_data);
let camera_data = (&self.camera, scene_data.figure_lod_render_distance);
// would instead have this as an extension.
if renderer.render_mode().shadow.is_map() && (is_daylight || !light_data.1.is_empty()) {
if is_daylight {
// Set up shadow mapping.
renderer.start_shadows();
}
// Render terrain shadows.
self.terrain
.render_shadows(renderer, global, light_data, focus_pos);
// Render figure shadows.
self.figure_mgr
.render_shadows(renderer, state, tick, global, light_data, camera_data);
if is_daylight {
// Flush shadows.
renderer.flush_shadows();
}
}
let lod = self.lod.get_data();
self.figure_mgr.render_player(
renderer,
state,
player_entity,
tick,
global,
lod,
camera_data,
);
// Render terrain and figures.
self.terrain.render(renderer, global, lod, focus_pos);
self.figure_mgr.render(
renderer,
state,
player_entity,
tick,
global,
lod,
camera_data,
);
self.lod.render(renderer, global);
// Render the skybox.
renderer.render_skybox(&self.skybox.model, global, &self.skybox.locals, lod);
self.terrain.render_translucent(
renderer,
global,
lod,
focus_pos,
cam_pos,
scene_data.sprite_render_distance,
);
// Render particle effects.
self.particle_mgr.render(renderer, scene_data, global, lod);
renderer.render_post_process(
&self.postprocess.model,
&global.globals,
&self.postprocess.locals,
);
}
}