mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Tweaks to shadows.
Added shadow map resolution configuration, added seamless cubemaps, documented all existing rendering options, and fixed a few Clippy errors.
This commit is contained in:
parent
23b4058906
commit
be438657c3
@ -298,7 +298,7 @@ magischen Gegenstände ergattern?"#,
|
||||
"hud.settings.cloud_rendering_mode.regular": "Realistisch",
|
||||
"hud.settings.fullscreen": "Vollbild",
|
||||
"hud.settings.lighting_rendering_mode": "Beleuchtung",
|
||||
"hud.settings.lighting_rendering_mode.ashikmin": "Typ A",
|
||||
"hud.settings.lighting_rendering_mode.ashikhmin": "Typ A",
|
||||
"hud.settings.lighting_rendering_mode.blinnphong": "Typ B",
|
||||
"hud.settings.lighting_rendering_mode.lambertian": "Typ L",
|
||||
"hud.settings.shadow_rendering_mode": "Schatten",
|
||||
|
@ -298,13 +298,14 @@ magically infused items?"#,
|
||||
"hud.settings.fullscreen": "Fullscreen",
|
||||
"hud.settings.save_window_size": "Save window size",
|
||||
"hud.settings.lighting_rendering_mode": "Lighting Rendering Mode",
|
||||
"hud.settings.lighting_rendering_mode.ashikmin": "Type A",
|
||||
"hud.settings.lighting_rendering_mode.ashikhmin": "Type A",
|
||||
"hud.settings.lighting_rendering_mode.blinnphong": "Type B",
|
||||
"hud.settings.lighting_rendering_mode.lambertian": "Type L",
|
||||
"hud.settings.shadow_rendering_mode": "Shadow Rendering Mode",
|
||||
"hud.settings.shadow_rendering_mode.none": "None",
|
||||
"hud.settings.shadow_rendering_mode.cheap": "Cheap",
|
||||
"hud.settings.shadow_rendering_mode.map": "Map",
|
||||
"hud.settings.shadow_rendering_mode.map.resolution": "Resolution",
|
||||
"hud.settings.lod_detail": "LoD Detail",
|
||||
"hud.settings.save_window_size": "Save window size",
|
||||
|
||||
|
@ -30,8 +30,8 @@ uniform u_locals {
|
||||
out vec4 tgt_color;
|
||||
|
||||
void main() {
|
||||
// tgt_color = vec4(MU_SCATTER, 1.0);
|
||||
// return;
|
||||
tgt_color = vec4(MU_SCATTER, 1.0);
|
||||
return;
|
||||
vec4 _clouds;
|
||||
|
||||
vec3 cam_dir = normalize(f_pos - cam_pos.xyz);
|
||||
|
@ -134,8 +134,8 @@ impl Client {
|
||||
// We reduce the thread count by 1 to keep rendering smooth
|
||||
thread_pool.set_num_threads((num_cpus::get() - 1).max(1));
|
||||
|
||||
let (network, f) = Network::new(Pid::new(), None);
|
||||
thread_pool.execute(f);
|
||||
let (network, scheduler) = Network::new(Pid::new(), None);
|
||||
thread_pool.execute(scheduler);
|
||||
|
||||
let participant = block_on(network.connect(Address::Tcp(addr.into())))?;
|
||||
let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?;
|
||||
|
@ -4,7 +4,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
i18n::{list_localizations, LanguageMetadata, VoxygenLocalization},
|
||||
render::{AaMode, CloudMode, FluidMode, LightingMode, RenderMode, ShadowMode},
|
||||
render::{AaMode, CloudMode, FluidMode, LightingMode, RenderMode, ShadowMapMode, ShadowMode},
|
||||
ui::{fonts::ConrodVoxygenFonts, ImageSlider, ScaleMode, ToggleButton},
|
||||
window::GameInput,
|
||||
GlobalState,
|
||||
@ -16,6 +16,7 @@ use conrod_core::{
|
||||
widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
|
||||
WidgetCommon,
|
||||
};
|
||||
use core::convert::TryFrom;
|
||||
|
||||
const FPS_CHOICES: [u32; 11] = [15, 30, 40, 50, 60, 90, 120, 144, 240, 300, 500];
|
||||
|
||||
@ -119,6 +120,9 @@ widget_ids! {
|
||||
lighting_mode_list,
|
||||
shadow_mode_text,
|
||||
shadow_mode_list,
|
||||
shadow_mode_map_resolution_text,
|
||||
shadow_mode_map_resolution_slider,
|
||||
shadow_mode_map_resolution_value,
|
||||
save_window_size_button,
|
||||
audio_volume_slider,
|
||||
audio_volume_text,
|
||||
@ -1884,6 +1888,8 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.figure_dist_value, ui);
|
||||
|
||||
let render_mode = self.global_state.settings.graphics.render_mode;
|
||||
|
||||
// AaMode
|
||||
Text::new(&self.localized_strings.get("hud.settings.antialiasing_mode"))
|
||||
.down_from(state.ids.gamma_slider, 8.0)
|
||||
@ -1910,9 +1916,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
];
|
||||
|
||||
// Get which AA mode is currently active
|
||||
let selected = mode_list
|
||||
.iter()
|
||||
.position(|x| *x == self.global_state.settings.graphics.render_mode.aa);
|
||||
let selected = mode_list.iter().position(|x| *x == render_mode.aa);
|
||||
|
||||
if let Some(clicked) = DropDownList::new(&mode_label_list, selected)
|
||||
.w_h(400.0, 22.0)
|
||||
@ -1924,7 +1928,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
{
|
||||
events.push(Event::ChangeRenderMode(RenderMode {
|
||||
aa: mode_list[clicked],
|
||||
..self.global_state.settings.graphics.render_mode
|
||||
..render_mode
|
||||
}));
|
||||
}
|
||||
|
||||
@ -1949,9 +1953,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
];
|
||||
|
||||
// Get which cloud rendering mode is currently active
|
||||
let selected = mode_list
|
||||
.iter()
|
||||
.position(|x| *x == self.global_state.settings.graphics.render_mode.cloud);
|
||||
let selected = mode_list.iter().position(|x| *x == render_mode.cloud);
|
||||
|
||||
if let Some(clicked) = DropDownList::new(&mode_label_list, selected)
|
||||
.w_h(400.0, 22.0)
|
||||
@ -1963,7 +1965,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
{
|
||||
events.push(Event::ChangeRenderMode(RenderMode {
|
||||
cloud: mode_list[clicked],
|
||||
..self.global_state.settings.graphics.render_mode
|
||||
..render_mode
|
||||
}));
|
||||
}
|
||||
|
||||
@ -1990,9 +1992,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
];
|
||||
|
||||
// Get which fluid rendering mode is currently active
|
||||
let selected = mode_list
|
||||
.iter()
|
||||
.position(|x| *x == self.global_state.settings.graphics.render_mode.fluid);
|
||||
let selected = mode_list.iter().position(|x| *x == render_mode.fluid);
|
||||
|
||||
if let Some(clicked) = DropDownList::new(&mode_label_list, selected)
|
||||
.w_h(400.0, 22.0)
|
||||
@ -2004,7 +2004,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
{
|
||||
events.push(Event::ChangeRenderMode(RenderMode {
|
||||
fluid: mode_list[clicked],
|
||||
..self.global_state.settings.graphics.render_mode
|
||||
..render_mode
|
||||
}));
|
||||
}
|
||||
|
||||
@ -2021,14 +2021,14 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
.set(state.ids.lighting_mode_text, ui);
|
||||
|
||||
let mode_list = [
|
||||
LightingMode::Ashikmin,
|
||||
LightingMode::Ashikhmin,
|
||||
LightingMode::BlinnPhong,
|
||||
LightingMode::Lambertian,
|
||||
];
|
||||
let mode_label_list = [
|
||||
&self
|
||||
.localized_strings
|
||||
.get("hud.settings.lighting_rendering_mode.ashikmin"),
|
||||
.get("hud.settings.lighting_rendering_mode.ashikhmin"),
|
||||
&self
|
||||
.localized_strings
|
||||
.get("hud.settings.lighting_rendering_mode.blinnphong"),
|
||||
@ -2038,9 +2038,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
];
|
||||
|
||||
// Get which lighting rendering mode is currently active
|
||||
let selected = mode_list
|
||||
.iter()
|
||||
.position(|x| *x == self.global_state.settings.graphics.render_mode.lighting);
|
||||
let selected = mode_list.iter().position(|x| *x == render_mode.lighting);
|
||||
|
||||
if let Some(clicked) = DropDownList::new(&mode_label_list, selected)
|
||||
.w_h(400.0, 22.0)
|
||||
@ -2052,7 +2050,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
{
|
||||
events.push(Event::ChangeRenderMode(RenderMode {
|
||||
lighting: mode_list[clicked],
|
||||
..self.global_state.settings.graphics.render_mode
|
||||
..render_mode
|
||||
}));
|
||||
}
|
||||
|
||||
@ -2068,7 +2066,12 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.shadow_mode_text, ui);
|
||||
|
||||
let mode_list = [ShadowMode::None, ShadowMode::Cheap, ShadowMode::Map];
|
||||
let shadow_map_mode = ShadowMapMode::try_from(render_mode.shadow).ok();
|
||||
let mode_list = [
|
||||
ShadowMode::None,
|
||||
ShadowMode::Cheap,
|
||||
ShadowMode::Map(shadow_map_mode.unwrap_or_default()),
|
||||
];
|
||||
let mode_label_list = [
|
||||
&self
|
||||
.localized_strings
|
||||
@ -2082,9 +2085,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
];
|
||||
|
||||
// Get which shadow rendering mode is currently active
|
||||
let selected = mode_list
|
||||
.iter()
|
||||
.position(|x| *x == self.global_state.settings.graphics.render_mode.shadow);
|
||||
let selected = mode_list.iter().position(|x| *x == render_mode.shadow);
|
||||
|
||||
if let Some(clicked) = DropDownList::new(&mode_label_list, selected)
|
||||
.w_h(400.0, 22.0)
|
||||
@ -2096,10 +2097,56 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
{
|
||||
events.push(Event::ChangeRenderMode(RenderMode {
|
||||
shadow: mode_list[clicked],
|
||||
..self.global_state.settings.graphics.render_mode
|
||||
..render_mode
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(shadow_map_mode) = shadow_map_mode {
|
||||
// Display the shadow map mode if selected.
|
||||
Text::new(
|
||||
&self
|
||||
.localized_strings
|
||||
.get("hud.settings.shadow_rendering_mode.map.resolution"),
|
||||
)
|
||||
.right_from(state.ids.shadow_mode_list, 10.0)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.shadow_mode_map_resolution_text, ui);
|
||||
|
||||
if let Some(new_val) = ImageSlider::discrete(
|
||||
(shadow_map_mode.resolution.log2() * 4.0).round() as i8,
|
||||
-8,
|
||||
8,
|
||||
self.imgs.slider_indicator,
|
||||
self.imgs.slider,
|
||||
)
|
||||
.w_h(104.0, 22.0)
|
||||
.right_from(state.ids.shadow_mode_map_resolution_text, 8.0)
|
||||
.track_breadth(12.0)
|
||||
.slider_length(10.0)
|
||||
.pad_track((5.0, 5.0))
|
||||
.set(state.ids.shadow_mode_map_resolution_slider, ui)
|
||||
{
|
||||
events.push(Event::ChangeRenderMode(RenderMode {
|
||||
shadow: ShadowMode::Map(ShadowMapMode {
|
||||
resolution: 2.0f32.powf(f32::from(new_val) / 4.0),
|
||||
..shadow_map_mode
|
||||
}),
|
||||
..render_mode
|
||||
}));
|
||||
}
|
||||
|
||||
// TODO: Consider fixing to avoid allocation (it's probably not a bottleneck but
|
||||
// there's no reason to allocate for numbers).
|
||||
Text::new(&format!("{}", shadow_map_mode.resolution))
|
||||
.right_from(state.ids.shadow_mode_map_resolution_slider, 8.0)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.shadow_mode_map_resolution_value, ui);
|
||||
}
|
||||
|
||||
// Fullscreen
|
||||
Text::new(&self.localized_strings.get("hud.settings.fullscreen"))
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
|
@ -10,6 +10,7 @@ pub enum RenderError {
|
||||
IncludeError(glsl_include::Error),
|
||||
MappingError(gfx::mapping::Error),
|
||||
CopyError(gfx::CopyError<[u16; 3], usize>),
|
||||
CustomError(String),
|
||||
}
|
||||
|
||||
impl From<gfx::PipelineStateError<String>> for RenderError {
|
||||
|
@ -63,13 +63,38 @@ pub trait Pipeline {
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
/// Anti-aliasing modes
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum AaMode {
|
||||
None,
|
||||
/// Fast approximate antialiasing.
|
||||
///
|
||||
/// This is a screen-space technique, and therefore
|
||||
Fxaa,
|
||||
/// Multisampling AA, up to 4 samples per pixel.
|
||||
///
|
||||
/// NOTE: MSAA modes don't (currently) work with greedy meshing, and will
|
||||
/// also struggle in the futrue with deferred shading, so they may be
|
||||
/// removed in the future.
|
||||
MsaaX4,
|
||||
/// Multisampling AA, up to 8 samples per pixel.
|
||||
///
|
||||
/// NOTE: MSAA modes don't (currently) work with greedy meshing, and will
|
||||
/// also struggle in the futrue with deferred shading, so they may be
|
||||
/// removed in the future.
|
||||
MsaaX8,
|
||||
/// Multisampling AA, up to 16 samples per pixel.
|
||||
///
|
||||
/// NOTE: MSAA modes don't (currently) work with greedy meshing, and will
|
||||
/// also struggle in the futrue with deferred shading, so they may be
|
||||
/// removed in the future.
|
||||
MsaaX16,
|
||||
/// Super-sampling antialiasing, 4 samples per pixel.
|
||||
///
|
||||
/// Unlike MSAA, SSAA *always* performs 4 samples per pixel, rather than
|
||||
/// trying to choose importance samples at boundary regions, so it works
|
||||
/// much better with techniques like deferred rendering and greedy
|
||||
/// meshing that (without significantly more work) invalidate the
|
||||
/// GPU's assumptions about importance sampling.
|
||||
SsaaX4,
|
||||
}
|
||||
|
||||
@ -78,9 +103,38 @@ impl Default for AaMode {
|
||||
}
|
||||
|
||||
/// Cloud modes
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum CloudMode {
|
||||
/// No clouds. On computers that can't handle loops well, have performance
|
||||
/// issues in fragment shaders in general, or just have large
|
||||
/// resolutions, this can be a *very* impactful performance difference.
|
||||
/// Part of that is because of inefficiencies in how we implement
|
||||
/// regular clouds. It is still not all that cheap on low-end machines, due
|
||||
/// to many calculations being performed that use relatively expensive
|
||||
/// functions, and at some point I'd like to both optimize the regular
|
||||
/// sky shader further and create an even cheaper option.
|
||||
None,
|
||||
/// Volumetric clouds. This option can be *very* expensive on low-end
|
||||
/// machines, to the point of making the game unusable, for several
|
||||
/// reasons:
|
||||
///
|
||||
/// - The volumetric clouds use raymarching, which will cause catastrophic
|
||||
/// performance degradation on GPUs without good support for loops. There
|
||||
/// is an attempt to minimize the impact of this using a z-range check,
|
||||
/// but on some low-end GPUs (such as some integraetd graphics cards) this
|
||||
/// test doesn't appear to be able to be predicted well at shader
|
||||
/// invocation time.
|
||||
/// - The cloud computations themselves are fairly involved, further
|
||||
/// degrading performance.
|
||||
/// - Although the sky shader is always drawn at the outer edges of the
|
||||
/// skybox, the clouds themselves are supposed to be positioned much
|
||||
/// lower, which means the depth check for the skybox incorrectly cuts off
|
||||
/// clouds in some places. To compensate for these cases (e.g. where
|
||||
/// terrain is occluded by clouds from above, and the camera is above the
|
||||
/// clouds), we currently branch to see if we need to render the clouds in
|
||||
/// *every* fragment shader. For machines that can't optimize the check,
|
||||
/// this is absurdly expensive, so we should look at alternatives in the
|
||||
/// future that player better iwth the GPU.
|
||||
Regular,
|
||||
}
|
||||
|
||||
@ -89,9 +143,28 @@ impl Default for CloudMode {
|
||||
}
|
||||
|
||||
/// Fluid modes
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum FluidMode {
|
||||
/// "Cheap" water. This water implements no waves, no reflections, no
|
||||
/// diffraction, and no light attenuation through water. As a result,
|
||||
/// it can be much cheaper than shiny reflection.
|
||||
Cheap,
|
||||
/// "Shiny" water. This water implements waves on the surfaces, some
|
||||
/// attempt at reflections, and tries to compute accurate light
|
||||
/// attenuation through water (this is what results in the
|
||||
/// colors changing as you descend into deep water).
|
||||
///
|
||||
/// Unfortunately, the way the engine is currently set up, calculating
|
||||
/// accurate attenuation is a bit difficult; we use estimates from
|
||||
/// horizon maps for the current water altitude, which can both be off
|
||||
/// by up to (max_altitude / 255) meters, only has per-chunk horizontal
|
||||
/// resolution, and cannot handle edge cases like horizontal water (e.g.
|
||||
/// waterfalls) well. We are okay with the latter, and will try to fix
|
||||
/// the former soon.
|
||||
///
|
||||
/// Another issue is that we don't always know whether light is *blocked*,
|
||||
/// which causes attenuation to be computed incorrectly; this can be
|
||||
/// addressed by using shadow maps (at least for terrain).
|
||||
Shiny,
|
||||
}
|
||||
|
||||
@ -100,10 +173,22 @@ impl Default for FluidMode {
|
||||
}
|
||||
|
||||
/// Lighting modes
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum LightingMode {
|
||||
Ashikmin,
|
||||
/// Ashikhmin-Shirley BRDF lighting model. Attempts to generate a
|
||||
/// physically plausible (to some extent) lighting distribution.
|
||||
///
|
||||
/// This mdoel may not work as well with purely directional lighting, and is
|
||||
/// more expensive than the otehr models.
|
||||
Ashikhmin,
|
||||
/// Standard Blinn-Phong shading, combing Lambertian diffuse reflections and
|
||||
/// specular highlights.
|
||||
BlinnPhong,
|
||||
/// Standard Lambertian lighting model, with only diffuse reflections. The
|
||||
/// cheapest lighting model by a decent margin, but the performance
|
||||
/// dfifference between it and Blinn-Phong will probably only be
|
||||
/// significant on low-end machines that are bottlenecked on fragment
|
||||
/// shading.
|
||||
Lambertian,
|
||||
}
|
||||
|
||||
@ -111,25 +196,67 @@ impl Default for LightingMode {
|
||||
fn default() -> Self { LightingMode::BlinnPhong }
|
||||
}
|
||||
|
||||
/// Shadow map settings.
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct ShadowMapMode {
|
||||
/// Multiple of default resolution (default is currenttly the closest higher
|
||||
/// power of two above the length of the longest diagonal of the screen
|
||||
/// resolution, but this may change).
|
||||
pub resolution: f32,
|
||||
}
|
||||
|
||||
impl Default for ShadowMapMode {
|
||||
fn default() -> Self { Self { resolution: 1.0 } }
|
||||
}
|
||||
|
||||
/// Shadow modes
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum ShadowMode {
|
||||
/// No shadows at all. By far the cheapest option.
|
||||
None,
|
||||
/// Point shadows (draw circles under figures, up to a configured maximum;
|
||||
/// also render LOD shadows using horizon maps). Can be expensive on
|
||||
/// some machines, probably mostly due to horizon mapping; the point
|
||||
/// shadows are not rendered too efficiently, but that can probably
|
||||
/// be addressed later.
|
||||
Cheap,
|
||||
/// Multiple of resolution.
|
||||
Map, /* (f32) */
|
||||
/// Shadow map (render the scene from each light source, and also renders
|
||||
/// LOD shadows using horizon maps).
|
||||
Map(ShadowMapMode),
|
||||
}
|
||||
|
||||
impl Default for ShadowMode {
|
||||
fn default() -> Self { ShadowMode::Cheap }
|
||||
}
|
||||
|
||||
impl core::convert::TryFrom<ShadowMode> for ShadowMapMode {
|
||||
type Error = ();
|
||||
|
||||
/// Get the shadow map details if they exist.
|
||||
fn try_from(value: ShadowMode) -> Result<Self, Self::Error> {
|
||||
if let ShadowMode::Map(map) = value {
|
||||
Ok(map)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShadowMode {
|
||||
pub fn is_map(&self) -> bool { if let Self::Map(_) = self { true } else { false } }
|
||||
}
|
||||
|
||||
/// Render modes
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct RenderMode {
|
||||
#[serde(default)]
|
||||
pub aa: AaMode,
|
||||
#[serde(default)]
|
||||
pub cloud: CloudMode,
|
||||
#[serde(default)]
|
||||
pub fluid: FluidMode,
|
||||
#[serde(default)]
|
||||
pub lighting: LightingMode,
|
||||
#[serde(default)]
|
||||
pub shadow: ShadowMode,
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use super::{
|
||||
},
|
||||
texture::Texture,
|
||||
AaMode, CloudMode, FilterMethod, FluidMode, LightingMode, Pipeline, RenderError, RenderMode,
|
||||
ShadowMode, WrapMode,
|
||||
ShadowMapMode, ShadowMode, WrapMode,
|
||||
};
|
||||
use common::assets::{self, watch::ReloadIndicator};
|
||||
use core::convert::TryFrom;
|
||||
@ -144,20 +144,29 @@ impl Renderer {
|
||||
/// Create a new `Renderer` from a variety of backend-specific components
|
||||
/// and the window targets.
|
||||
pub fn new(
|
||||
device: gfx_backend::Device,
|
||||
mut device: gfx_backend::Device,
|
||||
mut factory: gfx_backend::Factory,
|
||||
win_color_view: WinColorView,
|
||||
win_depth_view: WinDepthView,
|
||||
mode: RenderMode,
|
||||
) -> Result<Self, RenderError> {
|
||||
// Enable seamless cubemaps globally, where available--they are essentially a
|
||||
// strict improvement on regular cube maps.
|
||||
//
|
||||
// Note that since we only have to enable this once globally, there is no point
|
||||
// in doing this on rerender.
|
||||
Self::enable_seamless_cube_maps(&mut device);
|
||||
|
||||
let dims = win_color_view.get_dimensions();
|
||||
|
||||
let mut shader_reload_indicator = ReloadIndicator::new();
|
||||
let shadow_views = Self::create_shadow_views(&mut factory, (dims.0, dims.1))
|
||||
.map_err(|err| {
|
||||
warn!("Could not create shadow map views: {:?}", err);
|
||||
})
|
||||
.ok();
|
||||
let shadow_views = ShadowMapMode::try_from(mode.shadow).ok().and_then(|mode| {
|
||||
Self::create_shadow_views(&mut factory, (dims.0, dims.1), &mode)
|
||||
.map_err(|err| {
|
||||
warn!("Could not create shadow map views: {:?}", err);
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let (
|
||||
skybox_pipeline,
|
||||
@ -320,8 +329,10 @@ impl Renderer {
|
||||
self.tgt_color_res = tgt_color_res;
|
||||
self.tgt_color_view = tgt_color_view;
|
||||
self.tgt_depth_stencil_view = tgt_depth_stencil_view;
|
||||
if let Some(shadow_map) = self.shadow_map.as_mut() {
|
||||
match Self::create_shadow_views(&mut self.factory, (dims.0, dims.1)) {
|
||||
if let (Some(shadow_map), ShadowMode::Map(mode)) =
|
||||
(self.shadow_map.as_mut(), self.mode.shadow)
|
||||
{
|
||||
match Self::create_shadow_views(&mut self.factory, (dims.0, dims.1), &mode) {
|
||||
Ok((
|
||||
point_depth_stencil_view,
|
||||
point_res,
|
||||
@ -407,6 +418,7 @@ impl Renderer {
|
||||
fn create_shadow_views(
|
||||
factory: &mut gfx_device_gl::Factory,
|
||||
size: (u16, u16),
|
||||
mode: &ShadowMapMode,
|
||||
) -> Result<
|
||||
(
|
||||
ShadowDepthStencilView,
|
||||
@ -418,17 +430,40 @@ impl Renderer {
|
||||
),
|
||||
RenderError,
|
||||
> {
|
||||
let size = Vec2::new(size.0, size.1);
|
||||
// (Attempt to) apply resolution factor to shadow map resolution.
|
||||
let resolution_factor = mode.resolution.clamped(0.25, 4.0);
|
||||
fn vec2_result<T, E>(v: Vec2<Result<T, E>>) -> Result<Vec2<T>, E> {
|
||||
Ok(Vec2::new(v.x?, v.y?))
|
||||
};
|
||||
|
||||
let max_texture_size = Self::max_texture_size_raw(factory);
|
||||
let size = vec2_result(Vec2::new(size.0, size.1).map(|e| {
|
||||
let size = f32::from(e) * resolution_factor;
|
||||
// NOTE: We know 0 <= e since we clamped the resolution factor to be between
|
||||
// 0.25 and 4.0.
|
||||
if size <= f32::from(max_texture_size) {
|
||||
Ok(size as u16)
|
||||
} else {
|
||||
Err(RenderError::CustomError(format!(
|
||||
"Resolution factor {:?} multiplied by screen resolution axis {:?} does not \
|
||||
fit in a texture on this machine.",
|
||||
resolution_factor, e
|
||||
)))
|
||||
}
|
||||
}))?;
|
||||
|
||||
let levels = 1; //10;
|
||||
let two_size = size.map(|e| {
|
||||
let two_size = vec2_result(size.map(|e| {
|
||||
u16::checked_next_power_of_two(e)
|
||||
.filter(|&e| e <= max_texture_size)
|
||||
.expect(
|
||||
"Next power of two for max screen resolution axis does not fit in a texture \
|
||||
on this machine.",
|
||||
)
|
||||
});
|
||||
.ok_or_else(|| {
|
||||
RenderError::CustomError(format!(
|
||||
"Next power of two for shadow map resolution axis {:?} does not fit in a \
|
||||
texture on this machine.",
|
||||
e
|
||||
))
|
||||
})
|
||||
}))?;
|
||||
let min_size = size.reduce_min();
|
||||
let max_size = size.reduce_max();
|
||||
let _min_two_size = two_size.reduce_min();
|
||||
@ -437,23 +472,30 @@ impl Renderer {
|
||||
// size of a diagonal along that axis.
|
||||
let diag_size = size.map(f64::from).magnitude();
|
||||
let diag_cross_size = f64::from(min_size) / f64::from(max_size) * diag_size;
|
||||
let (diag_size, _diag_cross_size) =
|
||||
if 0.0 < diag_size && diag_size <= f64::from(max_texture_size) {
|
||||
// NOTE: diag_cross_size must be non-negative, since it is the ratio of a
|
||||
// non-negative and a positive number (if max_size were zero,
|
||||
// diag_size would be 0 too). And it must be <= diag_size,
|
||||
// since min_size <= max_size. Therefore, if diag_size fits in a
|
||||
// u16, so does diag_cross_size.
|
||||
(diag_size as u16, diag_cross_size as u16)
|
||||
} else {
|
||||
panic!("Resolution of screen diagonal does not fit in a texture on this machine.");
|
||||
};
|
||||
let (diag_size, _diag_cross_size) = if 0.0 < diag_size
|
||||
&& diag_size <= f64::from(max_texture_size)
|
||||
{
|
||||
// NOTE: diag_cross_size must be non-negative, since it is the ratio of a
|
||||
// non-negative and a positive number (if max_size were zero,
|
||||
// diag_size would be 0 too). And it must be <= diag_size,
|
||||
// since min_size <= max_size. Therefore, if diag_size fits in a
|
||||
// u16, so does diag_cross_size.
|
||||
(diag_size as u16, diag_cross_size as u16)
|
||||
} else {
|
||||
return Err(RenderError::CustomError(format!(
|
||||
"Resolution of shadow map diagonal {:?} does not fit in a texture on this machine.",
|
||||
diag_size
|
||||
)));
|
||||
};
|
||||
let diag_two_size = u16::checked_next_power_of_two(diag_size)
|
||||
.filter(|&e| e <= max_texture_size)
|
||||
.expect(
|
||||
"Next power of two for resolution of screen diagonal does not fit in a texture on \
|
||||
this machine.",
|
||||
);
|
||||
.ok_or_else(|| {
|
||||
RenderError::CustomError(format!(
|
||||
"Next power of two for resolution of shadow map diagonal {:?} does not fit in \
|
||||
a texture on this machine.",
|
||||
diag_size
|
||||
))
|
||||
})?;
|
||||
/* let color_cty = <<TgtColorFmt as gfx::format::Formatted>::Channel as gfx::format::ChannelTyped
|
||||
>::get_channel_type();
|
||||
let tgt_color_tex = factory.create_texture(
|
||||
@ -619,7 +661,7 @@ impl Renderer {
|
||||
/// Queue the clearing of the shadow targets ready for a new frame to be
|
||||
/// rendered.
|
||||
pub fn clear_shadows(&mut self) {
|
||||
if self.mode.shadow != ShadowMode::Map {
|
||||
if !self.mode.shadow.is_map() {
|
||||
return;
|
||||
}
|
||||
if let Some(shadow_map) = self.shadow_map.as_mut() {
|
||||
@ -633,18 +675,45 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// NOTE: Supported by Vulkan (by default), DirectX 10+ (it seems--it's hard
|
||||
/// to find proof of this, but Direct3D 10 apparently does it by
|
||||
/// default, and 11 definitely does, so I assume it's natively supported
|
||||
/// by DirectX itself), OpenGL 3.2+, and Metal (done by default). While
|
||||
/// there may be some GPUs that don't quite support it correctly, the
|
||||
/// impact is relatively small, so there is no reaosn not to enable it where
|
||||
/// available.
|
||||
#[allow(unsafe_code)]
|
||||
fn enable_seamless_cube_maps(device: &mut gfx_backend::Device) {
|
||||
unsafe {
|
||||
// NOTE: Currently just fail silently rather than complain if the computer is on
|
||||
// a version lower than 3.2, where seamless cubemaps were introduced.
|
||||
if !device.get_info().is_version_supported(3, 2) {
|
||||
// println!("whoops");
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: Safe because GL_TEXTURE_CUBE_MAP_SEAMLESS is supported by OpenGL 3.2+
|
||||
// (see https://www.khronos.org/opengl/wiki/Cubemap_Texture#Seamless_cubemap);
|
||||
// enabling seamless cube maps should always be safe regardless of the state of
|
||||
// the OpenGL context, so no further checks are needd.
|
||||
device.with_gl(|gl| {
|
||||
gl.Enable(gfx_gl::TEXTURE_CUBE_MAP_SEAMLESS);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// NOTE: Supported by all but a handful of mobile GPUs
|
||||
/// (see https://github.com/gpuweb/gpuweb/issues/480)
|
||||
/// so wgpu should support it too.
|
||||
#[allow(unsafe_code)]
|
||||
fn set_depth_clamp(&mut self, depth_clamp: bool) {
|
||||
fn set_depth_clamp(device: &mut gfx_backend::Device, depth_clamp: bool) {
|
||||
unsafe {
|
||||
// NOTE: Currently just fail silently rather than complain if the computer is on
|
||||
// a version lower than 3.3, though we probably will complain
|
||||
// elsewhere regardless, since shadow mapping is an optional feature
|
||||
// and having depth clamping disabled won't cause undefined
|
||||
// behavior, just incorrect shadowing from objects behind the viewer.
|
||||
if !self.device.get_info().is_version_supported(3, 3) {
|
||||
if !device.get_info().is_version_supported(3, 3) {
|
||||
// println!("whoops");
|
||||
return;
|
||||
}
|
||||
@ -654,7 +723,7 @@ impl Renderer {
|
||||
// may use different extensions. Also, enabling depth clamping should
|
||||
// essentially always be safe regardless of the state of the OpenGL
|
||||
// context, so no further checks are needed.
|
||||
self.device.with_gl(|gl| {
|
||||
device.with_gl(|gl| {
|
||||
// println!("gl.Enable(gfx_gl::DEPTH_CLAMP) = {:?}",
|
||||
// gl.IsEnabled(gfx_gl::DEPTH_CLAMP));
|
||||
if depth_clamp {
|
||||
@ -676,18 +745,18 @@ impl Renderer {
|
||||
|
||||
/// Set up shadow rendering.
|
||||
pub fn start_shadows(&mut self) {
|
||||
if self.mode.shadow != ShadowMode::Map {
|
||||
if !self.mode.shadow.is_map() {
|
||||
return;
|
||||
}
|
||||
if let Some(_shadow_map) = self.shadow_map.as_mut() {
|
||||
self.encoder.flush(&mut self.device);
|
||||
self.set_depth_clamp(true);
|
||||
Self::set_depth_clamp(&mut self.device, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform all queued draw calls for shadows.
|
||||
pub fn flush_shadows(&mut self) {
|
||||
if self.mode.shadow != ShadowMode::Map {
|
||||
if !self.mode.shadow.is_map() {
|
||||
return;
|
||||
}
|
||||
if let Some(_shadow_map) = self.shadow_map.as_mut() {
|
||||
@ -697,7 +766,7 @@ impl Renderer {
|
||||
// let directed_encoder = &mut shadow_map.directed_encoder;
|
||||
// directed_encoder.flush(&mut self.device);
|
||||
// Reset depth clamping.
|
||||
self.set_depth_clamp(false);
|
||||
Self::set_depth_clamp(&mut self.device, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1322,7 +1391,7 @@ impl Renderer {
|
||||
* map: &Texture<LodColorFmt>,
|
||||
* horizon: &Texture<LodTextureFmt>, */
|
||||
) {
|
||||
if self.mode.shadow != ShadowMode::Map {
|
||||
if !self.mode.shadow.is_map() {
|
||||
return;
|
||||
}
|
||||
// NOTE: Don't render shadows if the shader is not supported.
|
||||
@ -1377,7 +1446,7 @@ impl Renderer {
|
||||
* map: &Texture<LodColorFmt>,
|
||||
* horizon: &Texture<LodTextureFmt>, */
|
||||
) {
|
||||
if self.mode.shadow != ShadowMode::Map {
|
||||
if !self.mode.shadow.is_map() {
|
||||
return;
|
||||
}
|
||||
// NOTE: Don't render shadows if the shader is not supported.
|
||||
@ -1433,7 +1502,7 @@ impl Renderer {
|
||||
* map: &Texture<LodColorFmt>,
|
||||
* horizon: &Texture<LodTextureFmt>, */
|
||||
) {
|
||||
if self.mode.shadow != ShadowMode::Map {
|
||||
if !self.mode.shadow.is_map() {
|
||||
return;
|
||||
}
|
||||
// NOTE: Don't render shadows if the shader is not supported.
|
||||
@ -1783,14 +1852,14 @@ fn create_pipelines(
|
||||
CloudMode::Regular => "CLOUD_MODE_REGULAR",
|
||||
},
|
||||
match mode.lighting {
|
||||
LightingMode::Ashikmin => "LIGHTING_ALGORITHM_ASHIKHMIN",
|
||||
LightingMode::Ashikhmin => "LIGHTING_ALGORITHM_ASHIKHMIN",
|
||||
LightingMode::BlinnPhong => "LIGHTING_ALGORITHM_BLINN_PHONG",
|
||||
LightingMode::Lambertian => "CLOUD_MODE_NONE",
|
||||
},
|
||||
match mode.shadow {
|
||||
ShadowMode::None => "SHADOW_MODE_NONE",
|
||||
ShadowMode::Map if has_shadow_views => "SHADOW_MODE_MAP",
|
||||
ShadowMode::Cheap | ShadowMode::Map => "SHADOW_MODE_CHEAP",
|
||||
ShadowMode::Map(_) if has_shadow_views => "SHADOW_MODE_MAP",
|
||||
ShadowMode::Cheap | ShadowMode::Map(_) => "SHADOW_MODE_CHEAP",
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -98,7 +98,7 @@ where
|
||||
) -> Result<Self, RenderError> {
|
||||
let (tex, srv) = factory
|
||||
.create_texture_immutable::<F>(kind, mipmap, data)
|
||||
.map_err(|err| RenderError::CombinedError(err))?;
|
||||
.map_err(RenderError::CombinedError)?;
|
||||
|
||||
Ok(Self {
|
||||
tex,
|
||||
|
@ -8,8 +8,8 @@ use crate::{
|
||||
ecs::comp::Interpolated,
|
||||
mesh::greedy::GreedyMesh,
|
||||
render::{
|
||||
self, BoneMeshes, ColLightFmt, Consts, FigureBoneData, FigureLocals, FigureModel, Globals,
|
||||
Light, RenderError, Renderer, Shadow, ShadowLocals, ShadowPipeline, Texture,
|
||||
BoneMeshes, ColLightFmt, Consts, FigureBoneData, FigureLocals, FigureModel, Globals, Light,
|
||||
RenderError, Renderer, Shadow, ShadowLocals, ShadowPipeline, Texture,
|
||||
},
|
||||
scene::{
|
||||
camera::{Camera, CameraMode},
|
||||
@ -1805,7 +1805,7 @@ impl FigureMgr {
|
||||
) {
|
||||
let ecs = state.ecs();
|
||||
|
||||
if is_daylight && renderer.render_mode().shadow == render::ShadowMode::Map {
|
||||
if is_daylight && renderer.render_mode().shadow.is_map() {
|
||||
(
|
||||
&ecs.entities(),
|
||||
&ecs.read_storage::<Pos>(),
|
||||
|
@ -1,10 +1,9 @@
|
||||
use core::{iter, mem};
|
||||
use hashbrown::HashMap;
|
||||
use num::traits::Float;
|
||||
/* pub use vek::{
|
||||
geom::repr_simd::*, mat::repr_simd::column_major::Mat4, ops::*, vec::repr_simd::*,
|
||||
}; */
|
||||
pub use vek::{geom::repr_c::*, mat::repr_c::column_major::Mat4, ops::*, vec::repr_c::*};
|
||||
pub use vek::{geom::repr_simd::*, mat::repr_simd::column_major::Mat4, ops::*, vec::repr_simd::*};
|
||||
// pub use vek::{geom::repr_c::*, mat::repr_c::column_major::Mat4, ops::*,
|
||||
// vec::repr_c::*};
|
||||
|
||||
pub fn aabb_to_points<T: Float>(bounds: Aabb<T>) -> [Vec3<T>; 8] {
|
||||
[
|
||||
|
@ -14,7 +14,7 @@ use self::{
|
||||
use crate::{
|
||||
audio::{music::MusicMgr, sfx::SfxMgr, AudioFrontend},
|
||||
render::{
|
||||
self, create_pp_mesh, create_skybox_mesh, Consts, Globals, Light, Model, PostProcessLocals,
|
||||
create_pp_mesh, create_skybox_mesh, Consts, Globals, Light, Model, PostProcessLocals,
|
||||
PostProcessPipeline, Renderer, Shadow, ShadowLocals, SkyboxLocals, SkyboxPipeline,
|
||||
},
|
||||
settings::Settings,
|
||||
@ -553,9 +553,7 @@ impl Scene {
|
||||
|
||||
let sun_dir = scene_data.get_sun_dir();
|
||||
let is_daylight = sun_dir.z < 0.0/*0.6*/;
|
||||
if renderer.render_mode().shadow == render::ShadowMode::Map
|
||||
&& (is_daylight || !lights.is_empty())
|
||||
{
|
||||
if renderer.render_mode().shadow.is_map() && (is_daylight || !lights.is_empty()) {
|
||||
/* // We treat the actual scene bounds as being clipped by the horizontal terrain bounds, but
|
||||
// expanded to contain the z values of all NPCs. This is potentially important to make
|
||||
// sure we don't clip out figures in front of the camera.
|
||||
@ -1270,9 +1268,7 @@ impl Scene {
|
||||
let cam_pos = self.camera.dependents().cam_pos + focus_pos.map(|e| e.trunc());
|
||||
|
||||
// would instead have this as an extension.
|
||||
if renderer.render_mode().shadow == render::ShadowMode::Map
|
||||
&& (is_daylight || self.light_data.len() > 0)
|
||||
{
|
||||
if renderer.render_mode().shadow.is_map() && (is_daylight || self.light_data.len() > 0) {
|
||||
// Set up shadow mapping.
|
||||
renderer.start_shadows();
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
use crate::{
|
||||
mesh::{greedy::GreedyMesh, Meshable},
|
||||
render::{
|
||||
self, ColLightFmt, ColLightInfo, Consts, FluidPipeline, Globals, Instances, Light, Mesh,
|
||||
Model, RenderError, Renderer, Shadow, ShadowLocals, ShadowPipeline, SpriteInstance,
|
||||
SpriteLocals, SpritePipeline, TerrainLocals, TerrainPipeline, Texture,
|
||||
ColLightFmt, ColLightInfo, Consts, FluidPipeline, Globals, Instances, Light, Mesh, Model,
|
||||
RenderError, Renderer, Shadow, ShadowLocals, ShadowPipeline, SpriteInstance, SpriteLocals,
|
||||
SpritePipeline, TerrainLocals, TerrainPipeline, Texture,
|
||||
},
|
||||
};
|
||||
|
||||
@ -2855,7 +2855,7 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
let collides_with_aabr = |a: math::Aabr<f32>, b: math::Aabr<f32>| {
|
||||
a.min.partial_cmple(&b.max).reduce_and() && a.max.partial_cmpge(&b.min).reduce_and()
|
||||
};
|
||||
if ray_direction.z < 0.0 && renderer.render_mode().shadow == render::ShadowMode::Map {
|
||||
if ray_direction.z < 0.0 && renderer.render_mode().shadow.is_map() {
|
||||
let visible_bounding_box = Aabb {
|
||||
min: visible_bounding_box.min - focus_off,
|
||||
max: visible_bounding_box.max - focus_off,
|
||||
@ -2962,7 +2962,7 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
min: -0.5,
|
||||
max: 0.5,
|
||||
}; */
|
||||
/* if ray_direction.z < 0.0 && renderer.render_mode().shadow == render::ShadowMode::Map {
|
||||
/* if ray_direction.z < 0.0 && renderer.render_mode().shadow.is_map() {
|
||||
let ray = if ray_direction.x.abs() * scene_bounding_box.size().d > ray_direction.z.abs() * chunk_sz {
|
||||
-ray_direction / ray_direction.x * chunk_sz
|
||||
} else {
|
||||
@ -3029,7 +3029,7 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
is_daylight: bool,
|
||||
focus_pos: Vec3<f32>,
|
||||
) {
|
||||
if !(renderer.render_mode().shadow == render::ShadowMode::Map) {
|
||||
if !renderer.render_mode().shadow.is_map() {
|
||||
return;
|
||||
};
|
||||
|
||||
|
@ -832,7 +832,6 @@ impl PlayState for SessionState {
|
||||
},
|
||||
HudEvent::ChangeRenderMode(new_render_mode) => {
|
||||
// Do this first so if it crashes the setting isn't saved :)
|
||||
println!("Changing render mode: {:?}", new_render_mode);
|
||||
global_state
|
||||
.window
|
||||
.renderer_mut()
|
||||
|
@ -317,7 +317,7 @@ impl<'a> MapConfig<'a> {
|
||||
let water_color_factor = 2.0;
|
||||
let g_water = 32.0 * water_color_factor;
|
||||
let b_water = 64.0 * water_color_factor;
|
||||
let column_rgb = column_rgb.unwrap_or(Rgb::new(
|
||||
let default_rgb = Rgb::new(
|
||||
if is_shaded || is_temperature {
|
||||
1.0
|
||||
} else {
|
||||
@ -325,7 +325,8 @@ impl<'a> MapConfig<'a> {
|
||||
},
|
||||
if is_shaded { 1.0 } else { alt },
|
||||
if is_shaded || is_humidity { 1.0 } else { 0.0 },
|
||||
));
|
||||
);
|
||||
let column_rgb = column_rgb.unwrap_or(default_rgb);
|
||||
let mut connections = [None; 8];
|
||||
let mut has_connections = false;
|
||||
// TODO: Support non-river connections.
|
||||
|
@ -122,6 +122,11 @@ pub fn cdf_irwin_hall<const N: usize>(weights: &[f32; N], samples: [f32; N]) ->
|
||||
/// NOTE: Length should always be WORLD_SIZE.x * WORLD_SIZE.y.
|
||||
pub type InverseCdf<F = f32> = Box<[(f32, F)]>;
|
||||
|
||||
/// NOTE: First component is estimated horizon angles at each chunk; second
|
||||
/// component is estimated heights of maximal occluder at each chunk (used
|
||||
/// for making shadows volumetric).
|
||||
pub type HorizonMap<A, H> = (Vec<A>, Vec<H>);
|
||||
|
||||
/// Computes the position Vec2 of a SimChunk from an index, where the index was
|
||||
/// generated by uniform_noise.
|
||||
pub fn uniform_idx_as_vec2(idx: usize) -> Vec2<i32> {
|
||||
@ -395,7 +400,7 @@ pub fn get_horizon_map<F: Float + Sync, A: Send, H: Send>(
|
||||
h: impl Fn(usize) -> F + Sync,
|
||||
to_angle: impl Fn(F) -> A + Sync,
|
||||
to_height: impl Fn(F) -> H + Sync,
|
||||
) -> Result<[(Vec<A>, Vec<H>); 2], ()> {
|
||||
) -> Result<[HorizonMap<A, H>; 2], ()> {
|
||||
if maxh < minh {
|
||||
// maxh must be greater than minh
|
||||
return Err(());
|
||||
|
Loading…
Reference in New Issue
Block a user