use crate::value::{Value, ValueContainer};
use crate::value_toml::ValueTomlLoader;
use rustc_hash::FxHashMap;
use scenevm::{Atom, RenderMode as SceneVmRenderMode, SceneVM};
use vek::Vec4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RendererBackend {
Compute,
Raster,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderQualityPreset {
Low,
Medium,
High,
Ultra,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FadeMode {
OrderedDither,
Uniform,
}
impl FadeMode {
fn as_code(self) -> u32 {
match self {
FadeMode::OrderedDither => 0,
FadeMode::Uniform => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LightingModel {
Lambert,
CookTorrance,
Pbr,
}
impl LightingModel {
fn as_code(self) -> u32 {
match self {
LightingModel::Lambert => 0,
LightingModel::CookTorrance => 1,
LightingModel::Pbr => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderStyle {
Clean,
Retro,
Grimy,
}
impl RenderStyle {
fn lighting_code(self, lighting_model: LightingModel) -> u32 {
match self {
RenderStyle::Clean => lighting_model.as_code(),
RenderStyle::Retro => 3,
RenderStyle::Grimy => 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PostToneMapper {
None,
Reinhard,
Aces,
}
impl PostToneMapper {
fn as_code(self) -> u32 {
match self {
PostToneMapper::None => 0,
PostToneMapper::Reinhard => 1,
PostToneMapper::Aces => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PostTarget {
Both,
D2,
D3,
}
#[derive(Debug, Clone)]
pub struct PostEffectSettings {
pub name: String,
pub enabled: bool,
pub target: PostTarget,
pub intensity: f32,
pub distance_start: Option<f32>,
pub distance_end: Option<f32>,
}
#[derive(Debug, Clone, Default)]
pub struct PostStackSettings {
pub enabled: bool,
pub effects: Vec<PostEffectSettings>,
}
#[derive(Debug, Clone)]
pub struct RenderSettings {
pub backend_2d: RendererBackend,
pub backend_3d: RendererBackend,
pub quality: RenderQualityPreset,
pub post: PostStackSettings,
pub sky_color: [f32; 3],
pub background_color_2d: [f32; 4],
pub visibility_range_2d: f32,
pub visibility_alpha_2d: f32,
pub sun_color: [f32; 3],
pub sun_intensity: f32,
pub sun_direction: [f32; 3],
pub sun_enabled: bool,
pub ambient_color: [f32; 3],
pub ambient_strength: f32,
pub fog_color: [f32; 3],
pub fog_density: f32,
pub ao_samples: f32,
pub ao_radius: f32,
pub bump_strength: f32,
pub msaa_samples: u32,
pub max_transparency_bounces: f32,
pub max_shadow_distance: f32,
pub max_sky_distance: f32,
pub max_shadow_steps: f32,
pub reflection_samples: f32,
pub firstp_blur_near: f32,
pub firstp_blur_far: f32,
pub raster_shadow_enabled: bool,
pub raster_shadow_strength: f32,
pub raster_shadow_resolution: f32,
pub raster_shadow_bias: f32,
pub fade_mode: FadeMode,
pub lighting_model: LightingModel,
pub render_style: RenderStyle,
pub avatar_highlight_enabled: bool,
pub avatar_highlight_lift: f32,
pub avatar_highlight_fill: f32,
pub avatar_highlight_rim: f32,
pub avatar_shading_enabled: bool,
pub avatar_skin_shading_enabled: bool,
pub post_enabled: bool,
pub post_tone_mapper: PostToneMapper,
pub post_exposure: f32,
pub post_gamma: f32,
pub post_saturation: f32,
pub post_luminance: f32,
pub post_grit: f32,
pub post_posterize: f32,
pub post_palette_bias: f32,
pub post_shadow_lift: f32,
pub post_edge_soften: f32,
pub frame_time_ms: f32,
transitions: FxHashMap<SettingKey, Transition>,
pub simulation: DaylightSimulation,
}
#[derive(Debug, Clone)]
pub struct DaylightSimulation {
pub enabled: bool,
pub night_sky_color: [f32; 3],
pub morning_sky_color: [f32; 3],
pub midday_sky_color: [f32; 3],
pub evening_sky_color: [f32; 3],
pub night_sun_color: [f32; 3],
pub morning_sun_color: [f32; 3],
pub midday_sun_color: [f32; 3],
pub evening_sun_color: [f32; 3],
pub sunrise_time: f32,
pub sunset_time: f32,
pub color_transition_duration_hours: f32,
}
impl Default for DaylightSimulation {
fn default() -> Self {
Self {
enabled: false,
night_sky_color: [0.02, 0.02, 0.05], morning_sky_color: [1.0, 0.6, 0.4], midday_sky_color: [0.529, 0.808, 0.922], evening_sky_color: [1.0, 0.5, 0.3], night_sun_color: [0.1, 0.1, 0.15], morning_sun_color: [1.0, 0.8, 0.6], midday_sun_color: [1.0, 1.0, 0.95], evening_sun_color: [1.0, 0.7, 0.5], sunrise_time: 6.0,
sunset_time: 18.0,
color_transition_duration_hours: 0.5,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum SettingKey {
BackgroundColor2D,
VisibilityRange2D,
VisibilityAlpha2D,
SkyColor,
SunColor,
SunIntensity,
SunDirection,
SunEnabled,
ShadowEnabled,
AmbientColor,
AmbientStrength,
FogColor,
FogDensity,
AoSamples,
AoRadius,
BumpStrength,
MsaaSamples,
MaxTransparencyBounces,
MaxShadowDistance,
MaxSkyDistance,
MaxShadowSteps,
ReflectionSamples,
FirstPBlurNear,
FirstPBlurFar,
ShadowStrength,
ShadowResolution,
ShadowBias,
FadeMode,
LightingModel,
RenderStyle,
AvatarHighlightEnabled,
AvatarHighlightLift,
AvatarHighlightFill,
AvatarHighlightRim,
AvatarShadingEnabled,
AvatarSkinShadingEnabled,
PostEnabled,
PostToneMapper,
PostExposure,
PostGamma,
PostSaturation,
PostLuminance,
PostGrit,
PostPosterize,
PostPaletteBias,
PostShadowLift,
PostEdgeSoften,
FrameTimeMs,
}
#[derive(Debug, Clone)]
enum Transition {
Float {
start: f32,
target: f32,
duration: f32,
elapsed: f32,
},
Vec3 {
start: [f32; 3],
target: [f32; 3],
duration: f32,
elapsed: f32,
},
Bool {
start: bool,
target: bool,
duration: f32,
elapsed: f32,
},
}
#[derive(Debug, Clone)]
enum SettingValue {
Float(f32),
Vec3([f32; 3]),
Vec4([f32; 4]),
Bool(bool),
FadeMode(FadeMode),
LightingModel(LightingModel),
RenderStyle(RenderStyle),
ToneMapper(PostToneMapper),
}
impl Default for RenderSettings {
fn default() -> Self {
Self {
backend_2d: RendererBackend::Raster,
backend_3d: RendererBackend::Raster,
quality: RenderQualityPreset::Custom,
post: PostStackSettings::default(),
sky_color: [0.529, 0.808, 0.922], background_color_2d: [0.0, 0.0, 0.0, 1.0],
visibility_range_2d: 0.0,
visibility_alpha_2d: 0.82,
sun_color: [1.0, 0.980, 0.804], sun_intensity: 1.0,
sun_direction: [-0.5, -1.0, -0.3],
sun_enabled: true,
ambient_color: [0.8, 0.8, 0.8],
ambient_strength: 0.3,
fog_color: [0.502, 0.502, 0.502], fog_density: 0.0,
ao_samples: 8.0,
ao_radius: 0.5,
bump_strength: 1.0,
msaa_samples: 4,
max_transparency_bounces: 8.0,
max_shadow_distance: 10.0,
max_sky_distance: 50.0,
max_shadow_steps: 2.0,
reflection_samples: 0.0,
firstp_blur_near: 3.0,
firstp_blur_far: 8.0,
raster_shadow_enabled: true,
raster_shadow_strength: 0.8,
raster_shadow_resolution: 1024.0,
raster_shadow_bias: 0.0015,
fade_mode: FadeMode::OrderedDither,
lighting_model: LightingModel::CookTorrance,
render_style: RenderStyle::Clean,
avatar_highlight_enabled: true,
avatar_highlight_lift: 1.12,
avatar_highlight_fill: 0.20,
avatar_highlight_rim: 0.18,
avatar_shading_enabled: true,
avatar_skin_shading_enabled: false,
post_enabled: true,
post_tone_mapper: PostToneMapper::Reinhard,
post_exposure: 1.0,
post_gamma: 2.2,
post_saturation: 1.0,
post_luminance: 1.0,
post_grit: 0.0,
post_posterize: 0.0,
post_palette_bias: 0.0,
post_shadow_lift: 0.0,
post_edge_soften: 0.0,
frame_time_ms: 1000.0 / 30.0,
transitions: FxHashMap::default(),
simulation: DaylightSimulation::default(),
}
}
}
impl RenderSettings {
pub fn scenevm_mode_2d(&self) -> SceneVmRenderMode {
match self.backend_2d {
RendererBackend::Compute => SceneVmRenderMode::Compute2D,
RendererBackend::Raster => SceneVmRenderMode::Raster2D,
}
}
pub fn scenevm_mode_3d(&self) -> SceneVmRenderMode {
match self.backend_3d {
RendererBackend::Compute => SceneVmRenderMode::Compute3D,
RendererBackend::Raster => SceneVmRenderMode::Raster3D,
}
}
pub fn read(&mut self, toml_content: &str) -> Result<(), Box<dyn std::error::Error>> {
let groups = ValueTomlLoader::from_str(toml_content)
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
self.read_renderer_and_post_sections(toml_content)?;
if let Some(render) = groups.get("render") {
self.apply_render_values(render)?;
}
if let Some(sim) = groups.get("simulation") {
self.apply_simulation_values(sim)?;
}
Ok(())
}
fn read_renderer_and_post_sections(
&mut self,
toml_content: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let doc: toml::Value = toml::from_str(toml_content)?;
if let Some(renderer) = doc.get("renderer").and_then(toml::Value::as_table) {
if let Some(v) = renderer.get("backend_2d").and_then(toml::Value::as_str) {
self.backend_2d = parse_backend(v);
}
if let Some(v) = renderer.get("backend_3d").and_then(toml::Value::as_str) {
self.backend_3d = parse_backend(v);
}
if let Some(v) = renderer.get("style").and_then(toml::Value::as_str) {
self.render_style = parse_render_style(v);
}
if let Some(v) = renderer.get("quality").and_then(toml::Value::as_str) {
self.quality = parse_quality(v);
self.apply_quality_preset(self.quality);
}
}
if let Some(viewport) = doc.get("viewport").and_then(toml::Value::as_table)
&& let Some(v) = viewport
.get("background_color_2d")
.and_then(toml::Value::as_str)
{
self.background_color_2d = parse_hex_color_rgba(v)?;
}
if let Some(viewport) = doc.get("viewport").and_then(toml::Value::as_table)
&& let Some(v) = viewport
.get("visibility_range_2d")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
{
self.visibility_range_2d = (v as f32).max(0.0);
}
if let Some(viewport) = doc.get("viewport").and_then(toml::Value::as_table)
&& let Some(v) = viewport
.get("visibility_alpha_2d")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
{
self.visibility_alpha_2d = (v as f32).clamp(0.0, 1.0);
}
if let Some(raster3d) = doc.get("raster_3d").and_then(toml::Value::as_table) {
if let Some(v) = raster3d
.get("shadow_enabled")
.and_then(toml::Value::as_bool)
{
self.raster_shadow_enabled = v;
}
if let Some(v) = raster3d
.get("shadow_strength")
.and_then(toml::Value::as_float)
{
self.raster_shadow_strength = v as f32;
}
if let Some(v) = raster3d
.get("shadow_resolution")
.and_then(toml::Value::as_integer)
{
self.raster_shadow_resolution = v as f32;
} else if let Some(v) = raster3d
.get("shadow_resolution")
.and_then(toml::Value::as_float)
{
self.raster_shadow_resolution = v as f32;
}
if let Some(v) = raster3d.get("shadow_bias").and_then(toml::Value::as_float) {
self.raster_shadow_bias = v as f32;
}
if let Some(v) = raster3d
.get("avatar_highlight_enabled")
.and_then(toml::Value::as_bool)
{
self.avatar_highlight_enabled = v;
}
if let Some(v) = raster3d
.get("avatar_highlight_lift")
.and_then(toml::Value::as_float)
{
self.avatar_highlight_lift = v as f32;
}
if let Some(v) = raster3d
.get("avatar_highlight_fill")
.and_then(toml::Value::as_float)
{
self.avatar_highlight_fill = v as f32;
}
if let Some(v) = raster3d
.get("avatar_highlight_rim")
.and_then(toml::Value::as_float)
{
self.avatar_highlight_rim = v as f32;
}
}
if let Some(game) = doc.get("game").and_then(toml::Value::as_table) {
if let Some(v) = game.get("avatar_shading").and_then(toml::Value::as_bool) {
self.avatar_shading_enabled = v;
}
if let Some(v) = game
.get("avatar_skin_auto_shading")
.and_then(toml::Value::as_bool)
{
self.avatar_skin_shading_enabled = v;
}
}
self.post.enabled = doc
.get("post")
.and_then(toml::Value::as_table)
.and_then(|t| t.get("enabled"))
.and_then(toml::Value::as_bool)
.unwrap_or(self.post.enabled);
self.post_enabled = self.post.enabled;
if let Some(post) = doc.get("post").and_then(toml::Value::as_table) {
if let Some(v) = post.get("enabled").and_then(toml::Value::as_bool) {
self.post_enabled = v;
self.post.enabled = v;
}
if let Some(v) = post.get("tone_mapper").and_then(toml::Value::as_str) {
self.post_tone_mapper = parse_tone_mapper(v);
}
if let Some(v) = post.get("exposure").and_then(toml::Value::as_float) {
self.post_exposure = v as f32;
}
if let Some(v) = post.get("gamma").and_then(toml::Value::as_float) {
self.post_gamma = v as f32;
}
if let Some(v) = post.get("saturation").and_then(toml::Value::as_float) {
self.post_saturation = v as f32;
}
if let Some(v) = post.get("luminance").and_then(toml::Value::as_float) {
self.post_luminance = v as f32;
}
if let Some(v) = post.get("grit").and_then(toml::Value::as_float) {
self.post_grit = v as f32;
}
if let Some(v) = post.get("posterize").and_then(toml::Value::as_float) {
self.post_posterize = v as f32;
}
if let Some(v) = post.get("palette_bias").and_then(toml::Value::as_float) {
self.post_palette_bias = v as f32;
}
if let Some(v) = post.get("shadow_lift").and_then(toml::Value::as_float) {
self.post_shadow_lift = v as f32;
}
if let Some(v) = post.get("edge_soften").and_then(toml::Value::as_float) {
self.post_edge_soften = v as f32;
}
}
self.post.effects.clear();
if let Some(effects) = doc
.get("post")
.and_then(toml::Value::as_table)
.and_then(|t| t.get("effects"))
.and_then(toml::Value::as_array)
{
for effect in effects {
let Some(tbl) = effect.as_table() else {
continue;
};
let Some(name) = tbl.get("name").and_then(toml::Value::as_str) else {
continue;
};
let enabled = tbl
.get("enabled")
.and_then(toml::Value::as_bool)
.unwrap_or(true);
let target = tbl
.get("target")
.and_then(toml::Value::as_str)
.map(parse_post_target)
.unwrap_or(PostTarget::Both);
let intensity = tbl
.get("intensity")
.and_then(toml::Value::as_float)
.map(|v| v as f32)
.unwrap_or(1.0);
let distance_start = tbl
.get("distance_start")
.and_then(toml::Value::as_float)
.map(|v| v as f32);
let distance_end = tbl
.get("distance_end")
.and_then(toml::Value::as_float)
.map(|v| v as f32);
self.post.effects.push(PostEffectSettings {
name: name.to_string(),
enabled,
target,
intensity,
distance_start,
distance_end,
});
}
}
Ok(())
}
fn apply_quality_preset(&mut self, preset: RenderQualityPreset) {
match preset {
RenderQualityPreset::Low => {
self.ao_samples = 0.0;
self.bump_strength = 0.0;
self.max_shadow_distance = 0.0;
self.reflection_samples = 0.0;
self.max_sky_distance = 15.0;
}
RenderQualityPreset::Medium => {
self.ao_samples = 2.0;
self.bump_strength = 0.25;
self.max_shadow_distance = 5.0;
self.reflection_samples = 0.0;
self.max_sky_distance = 30.0;
}
RenderQualityPreset::High => {
self.ao_samples = 4.0;
self.bump_strength = 0.6;
self.max_shadow_distance = 10.0;
self.reflection_samples = 1.0;
self.max_sky_distance = 50.0;
}
RenderQualityPreset::Ultra => {
self.ao_samples = 8.0;
self.bump_strength = 1.0;
self.max_shadow_distance = 15.0;
self.reflection_samples = 2.0;
self.max_sky_distance = 75.0;
}
RenderQualityPreset::Custom => {}
}
}
pub fn set(
&mut self,
name: &str,
value: Value,
time: f32,
) -> Result<(), Box<dyn std::error::Error>> {
let Some(key) = Self::key_from_name(name) else {
return Err(format!("Unknown render setting '{}'", name).into());
};
if key == SettingKey::FrameTimeMs {
let ms = Self::value_to_f32(&value)
.ok_or_else(|| format!("Expected numeric value for '{}'", name))?;
self.frame_time_ms = ms.max(0.0);
return Ok(());
}
let target = Self::parse_value_for_key(key, value)?;
let duration = time.max(0.0);
if duration == 0.0 {
self.apply_setting_value(key, target);
self.transitions.remove(&key);
return Ok(());
}
let start = self.current_value(key);
let transition = match (start, target) {
(SettingValue::Float(s), SettingValue::Float(t)) => Transition::Float {
start: s,
target: t,
duration,
elapsed: 0.0,
},
(SettingValue::Vec3(s), SettingValue::Vec3(t)) => Transition::Vec3 {
start: s,
target: t,
duration,
elapsed: 0.0,
},
(SettingValue::Bool(s), SettingValue::Bool(t)) => Transition::Bool {
start: s,
target: t,
duration,
elapsed: 0.0,
},
_ => {
return Err("Mismatched setting value types".into());
}
};
self.transitions.insert(key, transition);
Ok(())
}
pub fn apply_hour(&mut self, hour: f32) {
if !self.simulation.enabled {
return;
}
let sim = &self.simulation;
let hour = hour.rem_euclid(24.0);
let transition = sim.color_transition_duration_hours.max(0.0);
let morning_start = sim.sunrise_time - transition;
let morning_end = sim.sunrise_time + transition;
let midday_to_evening_start = (sim.sunset_time - transition).max(morning_end);
let evening_end = sim.sunset_time + transition;
let (sky_color, sun_color) = if hour >= morning_start && hour < sim.sunrise_time {
let t = (hour - morning_start) / transition.max(f32::EPSILON);
let sky = lerp_color(sim.night_sky_color, sim.morning_sky_color, t);
let sun = lerp_color(sim.night_sun_color, sim.morning_sun_color, t);
(sky, sun)
} else if hour >= sim.sunrise_time && hour < morning_end {
let t = (hour - sim.sunrise_time) / transition.max(f32::EPSILON);
let sky = lerp_color(sim.morning_sky_color, sim.midday_sky_color, t);
let sun = lerp_color(sim.morning_sun_color, sim.midday_sun_color, t);
(sky, sun)
} else if hour >= morning_end && hour < midday_to_evening_start {
(sim.midday_sky_color, sim.midday_sun_color)
} else if hour >= midday_to_evening_start && hour < sim.sunset_time {
let t = (hour - midday_to_evening_start)
/ (sim.sunset_time - midday_to_evening_start).max(f32::EPSILON);
let sky = lerp_color(sim.midday_sky_color, sim.evening_sky_color, t);
let sun = lerp_color(sim.midday_sun_color, sim.evening_sun_color, t);
(sky, sun)
} else if hour >= sim.sunset_time && hour < evening_end {
let t = (hour - sim.sunset_time) / transition.max(f32::EPSILON);
let sky = lerp_color(sim.evening_sky_color, sim.night_sky_color, t);
let sun = lerp_color(sim.evening_sun_color, sim.night_sun_color, t);
(sky, sun)
} else {
(sim.night_sky_color, sim.night_sun_color)
};
self.sky_color = sky_color;
self.sun_color = sun_color;
let day_length = (sim.sunset_time - sim.sunrise_time).max(f32::EPSILON);
let daylight_progress = ((hour - sim.sunrise_time) / day_length).clamp(0.0, 1.0);
let daylight_angle = (daylight_progress * std::f32::consts::PI).sin() * 90.0;
let sun_angle = if hour >= sim.sunrise_time && hour < sim.sunset_time {
daylight_angle
} else if hour >= morning_start && hour < sim.sunrise_time {
let t = (hour - morning_start) / transition.max(f32::EPSILON);
lerp(-30.0, 0.0, t)
} else if hour >= sim.sunset_time && hour < evening_end {
let t = (hour - sim.sunset_time) / transition.max(f32::EPSILON);
lerp(0.0, -30.0, t)
} else {
-30.0
};
let angle_rad = sun_angle.to_radians();
let progress = daylight_progress;
let x = lerp(-1.0, 1.0, progress);
let y = -angle_rad.sin();
let z = -0.3;
self.sun_direction = [x, y, z];
}
pub fn apply_2d(&mut self, vm: &mut SceneVM) {
self.update_transitions();
let to_linear = |c: f32| c.powf(2.2);
vm.execute(Atom::SetBackground(Vec4::new(
to_linear(self.background_color_2d[0]),
to_linear(self.background_color_2d[1]),
to_linear(self.background_color_2d[2]),
self.background_color_2d[3],
)));
vm.execute(Atom::SetGP0(Vec4::zero()));
vm.execute(Atom::SetGP1(Vec4::new(
to_linear(self.sun_color[0]),
to_linear(self.sun_color[1]),
to_linear(self.sun_color[2]),
self.sun_intensity,
)));
let sun_dir = vek::Vec3::from(self.sun_direction).normalized();
vm.execute(Atom::SetGP2(Vec4::new(
sun_dir.x,
sun_dir.y,
sun_dir.z,
if self.sun_enabled { 1.0 } else { 0.0 },
)));
vm.execute(Atom::SetGP3(Vec4::new(
to_linear(self.ambient_color[0]),
to_linear(self.ambient_color[1]),
to_linear(self.ambient_color[2]),
self.ambient_strength,
)));
vm.execute(Atom::SetGP8(Vec4::new(
self.fade_mode.as_code() as f32,
self.render_style.lighting_code(self.lighting_model) as f32,
self.post_saturation.max(0.0),
self.post_luminance.max(0.0),
)));
vm.execute(Atom::SetGP9(Vec4::new(
if self.post_enabled { 1.0 } else { 0.0 },
self.post_tone_mapper.as_code() as f32,
self.post_exposure.max(0.0),
self.post_gamma.max(0.001),
)));
vm.vm.set_raster3d_post_style_params(
Vec4::new(
self.post_grit.clamp(0.0, 1.0),
self.post_posterize.clamp(0.0, 1.0),
self.post_palette_bias.clamp(0.0, 1.0),
self.post_shadow_lift.clamp(0.0, 1.0),
),
Vec4::new(self.post_edge_soften.clamp(0.0, 1.0), 0.0, 0.0, 0.0),
);
}
pub fn apply_3d(&mut self, vm: &mut SceneVM) {
self.update_transitions();
let to_linear = |c: f32| c.powf(2.2);
vm.execute(Atom::SetGP0(Vec4::new(
to_linear(self.sky_color[0]),
to_linear(self.sky_color[1]),
to_linear(self.sky_color[2]),
0.0,
)));
vm.execute(Atom::SetGP1(Vec4::new(
to_linear(self.sun_color[0]),
to_linear(self.sun_color[1]),
to_linear(self.sun_color[2]),
self.sun_intensity,
)));
let sun_dir = vek::Vec3::from(self.sun_direction).normalized();
vm.execute(Atom::SetGP2(Vec4::new(
sun_dir.x,
sun_dir.y,
sun_dir.z,
if self.sun_enabled { 1.0 } else { 0.0 },
)));
vm.execute(Atom::SetGP3(Vec4::new(
to_linear(self.ambient_color[0]),
to_linear(self.ambient_color[1]),
to_linear(self.ambient_color[2]),
self.ambient_strength,
)));
vm.execute(Atom::SetGP4(Vec4::new(
to_linear(self.fog_color[0]),
to_linear(self.fog_color[1]),
to_linear(self.fog_color[2]),
self.fog_density,
)));
vm.execute(Atom::SetGP5(Vec4::new(
self.ao_samples,
self.ao_radius,
self.bump_strength,
self.max_transparency_bounces,
)));
vm.execute(Atom::SetRaster3DMsaaSamples(self.msaa_samples));
vm.execute(Atom::SetGP6(Vec4::new(
self.max_shadow_distance,
self.max_sky_distance,
self.firstp_blur_near.max(0.0),
self.firstp_blur_far
.max(self.firstp_blur_near.max(0.0) + 0.001),
)));
vm.execute(Atom::SetGP7(Vec4::new(
if self.raster_shadow_enabled { 1.0 } else { 0.0 },
self.raster_shadow_strength.clamp(0.0, 1.0),
self.raster_shadow_resolution.max(64.0),
self.raster_shadow_bias.max(0.0),
)));
vm.execute(Atom::SetGP8(Vec4::new(
self.fade_mode.as_code() as f32,
self.render_style.lighting_code(self.lighting_model) as f32,
self.post_saturation.max(0.0),
self.post_luminance.max(0.0),
)));
vm.execute(Atom::SetGP9(Vec4::new(
if self.post_enabled { 1.0 } else { 0.0 },
self.post_tone_mapper.as_code() as f32,
self.post_exposure.max(0.0),
self.post_gamma.max(0.001),
)));
vm.vm.set_raster3d_avatar_highlight_params(Vec4::new(
self.avatar_highlight_lift.max(0.0),
self.avatar_highlight_fill.max(0.0),
self.avatar_highlight_rim.max(0.0),
if self.avatar_highlight_enabled {
1.0
} else {
0.0
},
));
vm.vm.set_raster3d_post_style_params(
Vec4::new(
self.post_grit.clamp(0.0, 1.0),
self.post_posterize.clamp(0.0, 1.0),
self.post_palette_bias.clamp(0.0, 1.0),
self.post_shadow_lift.clamp(0.0, 1.0),
),
Vec4::new(self.post_edge_soften.clamp(0.0, 1.0), 0.0, 0.0, 0.0),
);
}
fn update_transitions(&mut self) {
if self.transitions.is_empty() {
return;
}
let dt = (self.frame_time_ms / 1000.0).max(0.0001);
let mut finished = Vec::new();
let mut updates = Vec::new();
for (key, transition) in self.transitions.iter_mut() {
match transition {
Transition::Float {
start,
target,
duration,
elapsed,
} => {
*elapsed += dt;
let progress = if *duration == 0.0 {
1.0
} else {
(*elapsed / *duration).clamp(0.0, 1.0)
};
let value = lerp(*start, *target, progress);
updates.push((*key, SettingValue::Float(value)));
if progress >= 1.0 {
finished.push(*key);
}
}
Transition::Vec3 {
start,
target,
duration,
elapsed,
} => {
*elapsed += dt;
let progress = if *duration == 0.0 {
1.0
} else {
(*elapsed / *duration).clamp(0.0, 1.0)
};
let value = lerp_color(*start, *target, progress);
updates.push((*key, SettingValue::Vec3(value)));
if progress >= 1.0 {
finished.push(*key);
}
}
Transition::Bool {
start,
target,
duration,
elapsed,
} => {
*elapsed += dt;
let done = *duration == 0.0 || *elapsed >= *duration;
let value = if done { *target } else { *start };
updates.push((*key, SettingValue::Bool(value)));
if done {
finished.push(*key);
}
}
}
}
for (key, value) in updates {
self.apply_setting_value(key, value);
}
for key in finished {
self.transitions.remove(&key);
}
}
fn current_value(&self, key: SettingKey) -> SettingValue {
match key {
SettingKey::BackgroundColor2D => SettingValue::Vec4(self.background_color_2d),
SettingKey::VisibilityRange2D => SettingValue::Float(self.visibility_range_2d),
SettingKey::VisibilityAlpha2D => SettingValue::Float(self.visibility_alpha_2d),
SettingKey::SkyColor => SettingValue::Vec3(self.sky_color),
SettingKey::SunColor => SettingValue::Vec3(self.sun_color),
SettingKey::SunIntensity => SettingValue::Float(self.sun_intensity),
SettingKey::SunDirection => SettingValue::Vec3(self.sun_direction),
SettingKey::SunEnabled => SettingValue::Bool(self.sun_enabled),
SettingKey::ShadowEnabled => SettingValue::Bool(self.raster_shadow_enabled),
SettingKey::AmbientColor => SettingValue::Vec3(self.ambient_color),
SettingKey::AmbientStrength => SettingValue::Float(self.ambient_strength),
SettingKey::FogColor => SettingValue::Vec3(self.fog_color),
SettingKey::FogDensity => SettingValue::Float(self.fog_density),
SettingKey::AoSamples => SettingValue::Float(self.ao_samples),
SettingKey::AoRadius => SettingValue::Float(self.ao_radius),
SettingKey::BumpStrength => SettingValue::Float(self.bump_strength),
SettingKey::MsaaSamples => SettingValue::Float(self.msaa_samples as f32),
SettingKey::MaxTransparencyBounces => {
SettingValue::Float(self.max_transparency_bounces)
}
SettingKey::MaxShadowDistance => SettingValue::Float(self.max_shadow_distance),
SettingKey::MaxSkyDistance => SettingValue::Float(self.max_sky_distance),
SettingKey::MaxShadowSteps => SettingValue::Float(self.max_shadow_steps),
SettingKey::ReflectionSamples => SettingValue::Float(self.reflection_samples),
SettingKey::FirstPBlurNear => SettingValue::Float(self.firstp_blur_near),
SettingKey::FirstPBlurFar => SettingValue::Float(self.firstp_blur_far),
SettingKey::ShadowStrength => SettingValue::Float(self.raster_shadow_strength),
SettingKey::ShadowResolution => SettingValue::Float(self.raster_shadow_resolution),
SettingKey::ShadowBias => SettingValue::Float(self.raster_shadow_bias),
SettingKey::FadeMode => SettingValue::FadeMode(self.fade_mode),
SettingKey::LightingModel => SettingValue::LightingModel(self.lighting_model),
SettingKey::RenderStyle => SettingValue::RenderStyle(self.render_style),
SettingKey::AvatarHighlightEnabled => SettingValue::Bool(self.avatar_highlight_enabled),
SettingKey::AvatarHighlightLift => SettingValue::Float(self.avatar_highlight_lift),
SettingKey::AvatarHighlightFill => SettingValue::Float(self.avatar_highlight_fill),
SettingKey::AvatarHighlightRim => SettingValue::Float(self.avatar_highlight_rim),
SettingKey::AvatarShadingEnabled => SettingValue::Bool(self.avatar_shading_enabled),
SettingKey::AvatarSkinShadingEnabled => {
SettingValue::Bool(self.avatar_skin_shading_enabled)
}
SettingKey::PostEnabled => SettingValue::Bool(self.post_enabled),
SettingKey::PostToneMapper => SettingValue::ToneMapper(self.post_tone_mapper),
SettingKey::PostExposure => SettingValue::Float(self.post_exposure),
SettingKey::PostGamma => SettingValue::Float(self.post_gamma),
SettingKey::PostSaturation => SettingValue::Float(self.post_saturation),
SettingKey::PostLuminance => SettingValue::Float(self.post_luminance),
SettingKey::PostGrit => SettingValue::Float(self.post_grit),
SettingKey::PostPosterize => SettingValue::Float(self.post_posterize),
SettingKey::PostPaletteBias => SettingValue::Float(self.post_palette_bias),
SettingKey::PostShadowLift => SettingValue::Float(self.post_shadow_lift),
SettingKey::PostEdgeSoften => SettingValue::Float(self.post_edge_soften),
SettingKey::FrameTimeMs => SettingValue::Float(self.frame_time_ms),
}
}
fn apply_setting_value(&mut self, key: SettingKey, value: SettingValue) {
match (key, value) {
(SettingKey::BackgroundColor2D, SettingValue::Vec4(v)) => self.background_color_2d = v,
(SettingKey::VisibilityRange2D, SettingValue::Float(v)) => {
self.visibility_range_2d = v.max(0.0)
}
(SettingKey::VisibilityAlpha2D, SettingValue::Float(v)) => {
self.visibility_alpha_2d = v.clamp(0.0, 1.0)
}
(SettingKey::SkyColor, SettingValue::Vec3(v)) => self.sky_color = v,
(SettingKey::SunColor, SettingValue::Vec3(v)) => self.sun_color = v,
(SettingKey::SunIntensity, SettingValue::Float(v)) => self.sun_intensity = v,
(SettingKey::SunDirection, SettingValue::Vec3(v)) => self.sun_direction = v,
(SettingKey::SunEnabled, SettingValue::Bool(v)) => self.sun_enabled = v,
(SettingKey::ShadowEnabled, SettingValue::Bool(v)) => self.raster_shadow_enabled = v,
(SettingKey::AmbientColor, SettingValue::Vec3(v)) => self.ambient_color = v,
(SettingKey::AmbientStrength, SettingValue::Float(v)) => self.ambient_strength = v,
(SettingKey::FogColor, SettingValue::Vec3(v)) => self.fog_color = v,
(SettingKey::FogDensity, SettingValue::Float(v)) => self.fog_density = v,
(SettingKey::AoSamples, SettingValue::Float(v)) => self.ao_samples = v,
(SettingKey::AoRadius, SettingValue::Float(v)) => self.ao_radius = v,
(SettingKey::BumpStrength, SettingValue::Float(v)) => self.bump_strength = v,
(SettingKey::MsaaSamples, SettingValue::Float(v)) => {
self.msaa_samples = if v.round().max(0.0) as u32 == 0 { 0 } else { 4 }
}
(SettingKey::MaxTransparencyBounces, SettingValue::Float(v)) => {
self.max_transparency_bounces = v
}
(SettingKey::MaxShadowDistance, SettingValue::Float(v)) => self.max_shadow_distance = v,
(SettingKey::MaxSkyDistance, SettingValue::Float(v)) => self.max_sky_distance = v,
(SettingKey::MaxShadowSteps, SettingValue::Float(v)) => self.max_shadow_steps = v,
(SettingKey::ReflectionSamples, SettingValue::Float(v)) => self.reflection_samples = v,
(SettingKey::FirstPBlurNear, SettingValue::Float(v)) => self.firstp_blur_near = v,
(SettingKey::FirstPBlurFar, SettingValue::Float(v)) => self.firstp_blur_far = v,
(SettingKey::ShadowStrength, SettingValue::Float(v)) => self.raster_shadow_strength = v,
(SettingKey::ShadowResolution, SettingValue::Float(v)) => {
self.raster_shadow_resolution = v
}
(SettingKey::ShadowBias, SettingValue::Float(v)) => self.raster_shadow_bias = v,
(SettingKey::FadeMode, SettingValue::FadeMode(v)) => self.fade_mode = v,
(SettingKey::LightingModel, SettingValue::LightingModel(v)) => self.lighting_model = v,
(SettingKey::RenderStyle, SettingValue::RenderStyle(v)) => self.render_style = v,
(SettingKey::AvatarHighlightEnabled, SettingValue::Bool(v)) => {
self.avatar_highlight_enabled = v
}
(SettingKey::AvatarHighlightLift, SettingValue::Float(v)) => {
self.avatar_highlight_lift = v
}
(SettingKey::AvatarHighlightFill, SettingValue::Float(v)) => {
self.avatar_highlight_fill = v
}
(SettingKey::AvatarHighlightRim, SettingValue::Float(v)) => {
self.avatar_highlight_rim = v
}
(SettingKey::AvatarShadingEnabled, SettingValue::Bool(v)) => {
self.avatar_shading_enabled = v
}
(SettingKey::AvatarSkinShadingEnabled, SettingValue::Bool(v)) => {
self.avatar_skin_shading_enabled = v
}
(SettingKey::PostEnabled, SettingValue::Bool(v)) => self.post_enabled = v,
(SettingKey::PostToneMapper, SettingValue::ToneMapper(v)) => self.post_tone_mapper = v,
(SettingKey::PostExposure, SettingValue::Float(v)) => self.post_exposure = v,
(SettingKey::PostGamma, SettingValue::Float(v)) => self.post_gamma = v,
(SettingKey::PostSaturation, SettingValue::Float(v)) => self.post_saturation = v,
(SettingKey::PostLuminance, SettingValue::Float(v)) => self.post_luminance = v,
(SettingKey::PostGrit, SettingValue::Float(v)) => self.post_grit = v.clamp(0.0, 1.0),
(SettingKey::PostPosterize, SettingValue::Float(v)) => {
self.post_posterize = v.clamp(0.0, 1.0)
}
(SettingKey::PostPaletteBias, SettingValue::Float(v)) => {
self.post_palette_bias = v.clamp(0.0, 1.0)
}
(SettingKey::PostShadowLift, SettingValue::Float(v)) => {
self.post_shadow_lift = v.clamp(0.0, 1.0)
}
(SettingKey::PostEdgeSoften, SettingValue::Float(v)) => {
self.post_edge_soften = v.clamp(0.0, 1.0)
}
(SettingKey::FrameTimeMs, SettingValue::Float(v)) => self.frame_time_ms = v,
_ => {}
}
}
fn parse_value_for_key(
key: SettingKey,
value: Value,
) -> Result<SettingValue, Box<dyn std::error::Error>> {
match key {
SettingKey::BackgroundColor2D => match value {
Value::Vec4(v) => Ok(SettingValue::Vec4(v)),
Value::Vec3(v) => Ok(SettingValue::Vec4([v[0], v[1], v[2], 1.0])),
Value::Str(s) => Ok(SettingValue::Vec4(parse_hex_color_rgba(&s)?)),
_ => Err("Expected Vec4 or hex color for background_color_2d".into()),
},
SettingKey::VisibilityRange2D => {
let Some(v) = Self::value_to_f32(&value) else {
return Err("Expected numeric value for visibility_range_2d".into());
};
Ok(SettingValue::Float(v.max(0.0)))
}
SettingKey::VisibilityAlpha2D => {
let Some(v) = Self::value_to_f32(&value) else {
return Err("Expected numeric value for visibility_alpha_2d".into());
};
Ok(SettingValue::Float(v.clamp(0.0, 1.0)))
}
SettingKey::SkyColor
| SettingKey::SunColor
| SettingKey::SunDirection
| SettingKey::AmbientColor
| SettingKey::FogColor => match value {
Value::Vec3(v) => Ok(SettingValue::Vec3(v)),
Value::Vec4(v) => Ok(SettingValue::Vec3([v[0], v[1], v[2]])),
Value::Str(s) => Ok(SettingValue::Vec3(parse_hex_color(&s)?)),
_ => Err(format!("Expected Vec3 or hex color for {:?}", key).into()),
},
SettingKey::SunEnabled
| SettingKey::ShadowEnabled
| SettingKey::AvatarHighlightEnabled
| SettingKey::AvatarShadingEnabled
| SettingKey::AvatarSkinShadingEnabled
| SettingKey::PostEnabled => match value {
Value::Bool(b) => Ok(SettingValue::Bool(b)),
_ => Err("Expected bool for render setting".into()),
},
SettingKey::SunIntensity
| SettingKey::AmbientStrength
| SettingKey::FogDensity
| SettingKey::AoSamples
| SettingKey::AoRadius
| SettingKey::BumpStrength
| SettingKey::MsaaSamples
| SettingKey::MaxTransparencyBounces
| SettingKey::MaxShadowDistance
| SettingKey::MaxSkyDistance
| SettingKey::MaxShadowSteps
| SettingKey::ReflectionSamples
| SettingKey::FirstPBlurNear
| SettingKey::FirstPBlurFar
| SettingKey::ShadowStrength
| SettingKey::ShadowResolution
| SettingKey::ShadowBias
| SettingKey::AvatarHighlightLift
| SettingKey::AvatarHighlightFill
| SettingKey::AvatarHighlightRim
| SettingKey::PostExposure
| SettingKey::PostGamma
| SettingKey::PostSaturation
| SettingKey::PostLuminance
| SettingKey::PostGrit
| SettingKey::PostPosterize
| SettingKey::PostPaletteBias
| SettingKey::PostShadowLift
| SettingKey::PostEdgeSoften
| SettingKey::FrameTimeMs => {
let Some(v) = Self::value_to_f32(&value) else {
return Err(format!("Expected numeric value for {:?}", key).into());
};
Ok(SettingValue::Float(v))
}
SettingKey::FadeMode => match value {
Value::Str(s) => Ok(SettingValue::FadeMode(parse_fade_mode(&s))),
_ => Err("Expected string for fade_mode".into()),
},
SettingKey::LightingModel => match value {
Value::Str(s) => Ok(SettingValue::LightingModel(parse_lighting_model(&s))),
_ => Err("Expected string for lighting_model".into()),
},
SettingKey::RenderStyle => match value {
Value::Str(s) => Ok(SettingValue::RenderStyle(parse_render_style(&s))),
_ => Err("Expected string for style".into()),
},
SettingKey::PostToneMapper => match value {
Value::Str(s) => Ok(SettingValue::ToneMapper(parse_tone_mapper(&s))),
_ => Err("Expected string for tone_mapper".into()),
},
}
}
fn value_to_f32(value: &Value) -> Option<f32> {
match value {
Value::Float(v) => Some(*v),
Value::Int(v) => Some(*v as f32),
Value::UInt(v) => Some(*v as f32),
Value::Int64(v) => Some(*v as f32),
_ => None,
}
}
fn key_from_name(name: &str) -> Option<SettingKey> {
match name {
"background_color_2d" => Some(SettingKey::BackgroundColor2D),
"visibility_range_2d" => Some(SettingKey::VisibilityRange2D),
"visibility_alpha_2d" => Some(SettingKey::VisibilityAlpha2D),
"sky_color" => Some(SettingKey::SkyColor),
"sun_color" => Some(SettingKey::SunColor),
"sun_intensity" => Some(SettingKey::SunIntensity),
"sun_direction" => Some(SettingKey::SunDirection),
"sun_enabled" => Some(SettingKey::SunEnabled),
"shadow_enabled" => Some(SettingKey::ShadowEnabled),
"ambient_color" => Some(SettingKey::AmbientColor),
"ambient_strength" => Some(SettingKey::AmbientStrength),
"fog_color" => Some(SettingKey::FogColor),
"fog_density" => Some(SettingKey::FogDensity),
"ao_samples" => Some(SettingKey::AoSamples),
"ao_radius" => Some(SettingKey::AoRadius),
"bump_strength" => Some(SettingKey::BumpStrength),
"msaa_samples" => Some(SettingKey::MsaaSamples),
"max_transparency_bounces" => Some(SettingKey::MaxTransparencyBounces),
"max_shadow_distance" => Some(SettingKey::MaxShadowDistance),
"max_sky_distance" => Some(SettingKey::MaxSkyDistance),
"max_shadow_steps" => Some(SettingKey::MaxShadowSteps),
"reflection_samples" => Some(SettingKey::ReflectionSamples),
"firstp_blur_near" => Some(SettingKey::FirstPBlurNear),
"firstp_blur_far" => Some(SettingKey::FirstPBlurFar),
"shadow_strength" => Some(SettingKey::ShadowStrength),
"shadow_resolution" => Some(SettingKey::ShadowResolution),
"shadow_bias" => Some(SettingKey::ShadowBias),
"fade_mode" => Some(SettingKey::FadeMode),
"lighting_model" => Some(SettingKey::LightingModel),
"style" | "render_style" => Some(SettingKey::RenderStyle),
"avatar_highlight_enabled" => Some(SettingKey::AvatarHighlightEnabled),
"avatar_highlight_lift" => Some(SettingKey::AvatarHighlightLift),
"avatar_highlight_fill" => Some(SettingKey::AvatarHighlightFill),
"avatar_highlight_rim" => Some(SettingKey::AvatarHighlightRim),
"avatar_shading_enabled" => Some(SettingKey::AvatarShadingEnabled),
"avatar_skin_shading_enabled" => Some(SettingKey::AvatarSkinShadingEnabled),
"enabled" | "post_enabled" => Some(SettingKey::PostEnabled),
"tone_mapper" | "post_tone_mapper" => Some(SettingKey::PostToneMapper),
"exposure" | "post_exposure" => Some(SettingKey::PostExposure),
"gamma" | "post_gamma" => Some(SettingKey::PostGamma),
"saturation" | "post_saturation" => Some(SettingKey::PostSaturation),
"luminance" | "post_luminance" => Some(SettingKey::PostLuminance),
"grit" | "post_grit" => Some(SettingKey::PostGrit),
"posterize" | "post_posterize" => Some(SettingKey::PostPosterize),
"palette_bias" | "post_palette_bias" => Some(SettingKey::PostPaletteBias),
"shadow_lift" | "post_shadow_lift" => Some(SettingKey::PostShadowLift),
"edge_soften" | "post_edge_soften" => Some(SettingKey::PostEdgeSoften),
"ms_per_frame" => Some(SettingKey::FrameTimeMs),
_ => None,
}
}
pub fn runtime_override_names() -> &'static [&'static str] {
&[
"background_color_2d",
"visibility_range_2d",
"visibility_alpha_2d",
"sky_color",
"sun_color",
"sun_intensity",
"sun_direction",
"sun_enabled",
"shadow_enabled",
"ambient_color",
"ambient_strength",
"fog_color",
"fog_density",
"ao_samples",
"ao_radius",
"bump_strength",
"msaa_samples",
"max_transparency_bounces",
"max_shadow_distance",
"max_sky_distance",
"max_shadow_steps",
"reflection_samples",
"firstp_blur_near",
"firstp_blur_far",
"shadow_strength",
"shadow_resolution",
"shadow_bias",
"fade_mode",
"lighting_model",
"style",
"avatar_highlight_enabled",
"avatar_highlight_lift",
"avatar_highlight_fill",
"avatar_highlight_rim",
"avatar_shading_enabled",
"avatar_skin_shading_enabled",
"ms_per_frame",
]
}
pub fn runtime_post_override_names() -> &'static [&'static str] {
&[
"enabled",
"tone_mapper",
"exposure",
"gamma",
"saturation",
"luminance",
"grit",
"posterize",
"palette_bias",
"shadow_lift",
"edge_soften",
]
}
pub fn value_for_name(&self, name: &str) -> Option<Value> {
match name {
"background_color_2d" => Some(Value::Vec4(self.background_color_2d)),
"visibility_range_2d" => Some(Value::Float(self.visibility_range_2d)),
"visibility_alpha_2d" => Some(Value::Float(self.visibility_alpha_2d)),
"sky_color" => Some(Value::Vec3(self.sky_color)),
"sun_color" => Some(Value::Vec3(self.sun_color)),
"sun_intensity" => Some(Value::Float(self.sun_intensity)),
"sun_direction" => Some(Value::Vec3(self.sun_direction)),
"sun_enabled" => Some(Value::Bool(self.sun_enabled)),
"shadow_enabled" => Some(Value::Bool(self.raster_shadow_enabled)),
"ambient_color" => Some(Value::Vec3(self.ambient_color)),
"ambient_strength" => Some(Value::Float(self.ambient_strength)),
"fog_color" => Some(Value::Vec3(self.fog_color)),
"fog_density" => Some(Value::Float(self.fog_density)),
"ao_samples" => Some(Value::Float(self.ao_samples)),
"ao_radius" => Some(Value::Float(self.ao_radius)),
"bump_strength" => Some(Value::Float(self.bump_strength)),
"msaa_samples" => Some(Value::Float(self.msaa_samples as f32)),
"max_transparency_bounces" => Some(Value::Float(self.max_transparency_bounces)),
"max_shadow_distance" => Some(Value::Float(self.max_shadow_distance)),
"max_sky_distance" => Some(Value::Float(self.max_sky_distance)),
"max_shadow_steps" => Some(Value::Float(self.max_shadow_steps)),
"reflection_samples" => Some(Value::Float(self.reflection_samples)),
"firstp_blur_near" => Some(Value::Float(self.firstp_blur_near)),
"firstp_blur_far" => Some(Value::Float(self.firstp_blur_far)),
"shadow_strength" => Some(Value::Float(self.raster_shadow_strength)),
"shadow_resolution" => Some(Value::Float(self.raster_shadow_resolution)),
"shadow_bias" => Some(Value::Float(self.raster_shadow_bias)),
"fade_mode" => Some(Value::Str(match self.fade_mode {
FadeMode::OrderedDither => "ordered_dither".into(),
FadeMode::Uniform => "uniform".into(),
})),
"lighting_model" => Some(Value::Str(match self.lighting_model {
LightingModel::Lambert => "lambert".into(),
LightingModel::CookTorrance => "cook_torrance".into(),
LightingModel::Pbr => "pbr".into(),
})),
"style" | "render_style" => Some(Value::Str(match self.render_style {
RenderStyle::Clean => "clean".into(),
RenderStyle::Retro => "retro".into(),
RenderStyle::Grimy => "grimy".into(),
})),
"avatar_highlight_enabled" => Some(Value::Bool(self.avatar_highlight_enabled)),
"avatar_highlight_lift" => Some(Value::Float(self.avatar_highlight_lift)),
"avatar_highlight_fill" => Some(Value::Float(self.avatar_highlight_fill)),
"avatar_highlight_rim" => Some(Value::Float(self.avatar_highlight_rim)),
"avatar_shading_enabled" => Some(Value::Bool(self.avatar_shading_enabled)),
"avatar_skin_shading_enabled" => Some(Value::Bool(self.avatar_skin_shading_enabled)),
"post_grit" => Some(Value::Float(self.post_grit)),
"post_posterize" => Some(Value::Float(self.post_posterize)),
"post_palette_bias" => Some(Value::Float(self.post_palette_bias)),
"post_shadow_lift" => Some(Value::Float(self.post_shadow_lift)),
"post_edge_soften" => Some(Value::Float(self.post_edge_soften)),
"ms_per_frame" => Some(Value::Float(self.frame_time_ms)),
_ => None,
}
}
pub fn post_value_for_name(&self, name: &str) -> Option<Value> {
match name {
"enabled" => Some(Value::Bool(self.post_enabled)),
"tone_mapper" => Some(Value::Str(match self.post_tone_mapper {
PostToneMapper::None => "none".into(),
PostToneMapper::Reinhard => "reinhard".into(),
PostToneMapper::Aces => "aces".into(),
})),
"exposure" => Some(Value::Float(self.post_exposure)),
"gamma" => Some(Value::Float(self.post_gamma)),
"saturation" => Some(Value::Float(self.post_saturation)),
"luminance" => Some(Value::Float(self.post_luminance)),
"grit" => Some(Value::Float(self.post_grit)),
"posterize" => Some(Value::Float(self.post_posterize)),
"palette_bias" => Some(Value::Float(self.post_palette_bias)),
"shadow_lift" => Some(Value::Float(self.post_shadow_lift)),
"edge_soften" => Some(Value::Float(self.post_edge_soften)),
_ => None,
}
}
}
impl RenderSettings {
pub fn apply_render_values(
&mut self,
render: &ValueContainer,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(v) = render.get_str("background_color_2d") {
self.background_color_2d = parse_hex_color_rgba(v)?;
} else if let Some(v) = render.get_vec4("background_color_2d") {
self.background_color_2d = v;
} else if let Some(v) = render.get_vec3("background_color_2d") {
self.background_color_2d = [v[0], v[1], v[2], 1.0];
}
self.visibility_range_2d =
render.get_float_default("visibility_range_2d", self.visibility_range_2d);
self.visibility_alpha_2d = render
.get_float_default("visibility_alpha_2d", self.visibility_alpha_2d)
.clamp(0.0, 1.0);
if let Some(v) = render.get_str("sky_color") {
self.sky_color = parse_hex_color(v)?;
} else if let Some(v) = render.get_vec3("sky_color") {
self.sky_color = v;
}
if let Some(v) = render.get_str("sun_color") {
self.sun_color = parse_hex_color(v)?;
} else if let Some(v) = render.get_vec3("sun_color") {
self.sun_color = v;
}
self.sun_intensity = render.get_float_default("sun_intensity", self.sun_intensity);
self.sun_direction = render.get_vec3_default("sun_direction", self.sun_direction);
self.sun_enabled = render.get_bool_default("sun_enabled", self.sun_enabled);
if let Some(v) = render.get_str("ambient_color") {
self.ambient_color = parse_hex_color(v)?;
} else if let Some(v) = render.get_vec3("ambient_color") {
self.ambient_color = v;
}
self.ambient_strength = render.get_float_default("ambient_strength", self.ambient_strength);
if let Some(v) = render.get_str("fog_color") {
self.fog_color = parse_hex_color(v)?;
} else if let Some(v) = render.get_vec3("fog_color") {
self.fog_color = v;
}
if let Some(d) = render.get_float("fog_density") {
self.fog_density = d / 100.0;
}
self.ao_samples = render.get_float_default("ao_samples", self.ao_samples);
self.ao_radius = render.get_float_default("ao_radius", self.ao_radius);
self.bump_strength = render.get_float_default("bump_strength", self.bump_strength);
self.msaa_samples = render
.get_int("msaa_samples")
.map(|v| v.max(0) as u32)
.or_else(|| {
render
.get_float("msaa_samples")
.map(|v| v.max(0.0).round() as u32)
})
.unwrap_or(self.msaa_samples);
self.msaa_samples = if self.msaa_samples == 0 { 0 } else { 4 };
self.max_transparency_bounces =
render.get_float_default("max_transparency_bounces", self.max_transparency_bounces);
self.max_shadow_distance =
render.get_float_default("max_shadow_distance", self.max_shadow_distance);
self.max_sky_distance = render.get_float_default("max_sky_distance", self.max_sky_distance);
self.max_shadow_steps = render.get_float_default("max_shadow_steps", self.max_shadow_steps);
self.reflection_samples =
render.get_float_default("reflection_samples", self.reflection_samples);
self.firstp_blur_near = render.get_float_default("firstp_blur_near", self.firstp_blur_near);
self.firstp_blur_far = render.get_float_default("firstp_blur_far", self.firstp_blur_far);
self.raster_shadow_enabled = render.get_bool_default(
"shadow_enabled",
render.get_bool_default("raster_shadow_enabled", self.raster_shadow_enabled),
);
self.raster_shadow_strength = render.get_float_default(
"shadow_strength",
render.get_float_default("raster_shadow_strength", self.raster_shadow_strength),
);
self.raster_shadow_resolution = render.get_float_default(
"shadow_resolution",
render.get_float_default("raster_shadow_resolution", self.raster_shadow_resolution),
);
self.raster_shadow_bias = render.get_float_default(
"shadow_bias",
render.get_float_default("raster_shadow_bias", self.raster_shadow_bias),
);
self.fade_mode = render
.get_str("fade_mode")
.map(parse_fade_mode)
.unwrap_or(self.fade_mode);
self.lighting_model = render
.get_str("lighting_model")
.map(parse_lighting_model)
.unwrap_or(self.lighting_model);
self.render_style = render
.get_str("style")
.or_else(|| render.get_str("render_style"))
.map(parse_render_style)
.unwrap_or(self.render_style);
self.avatar_highlight_enabled =
render.get_bool_default("avatar_highlight_enabled", self.avatar_highlight_enabled);
self.avatar_highlight_lift =
render.get_float_default("avatar_highlight_lift", self.avatar_highlight_lift);
self.avatar_highlight_fill =
render.get_float_default("avatar_highlight_fill", self.avatar_highlight_fill);
self.avatar_highlight_rim =
render.get_float_default("avatar_highlight_rim", self.avatar_highlight_rim);
self.avatar_shading_enabled =
render.get_bool_default("avatar_shading_enabled", self.avatar_shading_enabled);
self.avatar_skin_shading_enabled = render.get_bool_default(
"avatar_skin_shading_enabled",
self.avatar_skin_shading_enabled,
);
self.frame_time_ms = render.get_float_default("ms_per_frame", self.frame_time_ms);
if let Some(fps) = render.get_float("fps") {
if fps > 0.0 {
self.frame_time_ms = 1000.0 / fps;
}
}
Ok(())
}
pub fn apply_post_values(
&mut self,
post: &ValueContainer,
) -> Result<(), Box<dyn std::error::Error>> {
self.post_enabled = post.get_bool_default("enabled", self.post_enabled);
self.post.enabled = self.post_enabled;
if let Some(v) = post.get_str("tone_mapper") {
self.post_tone_mapper = parse_tone_mapper(v);
}
self.post_exposure = post.get_float_default("exposure", self.post_exposure);
self.post_gamma = post.get_float_default("gamma", self.post_gamma);
self.post_saturation = post.get_float_default("saturation", self.post_saturation);
self.post_luminance = post.get_float_default("luminance", self.post_luminance);
self.post_grit = post
.get_float_default("grit", self.post_grit)
.clamp(0.0, 1.0);
self.post_posterize = post
.get_float_default("posterize", self.post_posterize)
.clamp(0.0, 1.0);
self.post_palette_bias = post
.get_float_default("palette_bias", self.post_palette_bias)
.clamp(0.0, 1.0);
self.post_shadow_lift = post
.get_float_default("shadow_lift", self.post_shadow_lift)
.clamp(0.0, 1.0);
self.post_edge_soften = post
.get_float_default("edge_soften", self.post_edge_soften)
.clamp(0.0, 1.0);
Ok(())
}
fn apply_simulation_values(
&mut self,
sim: &ValueContainer,
) -> Result<(), Box<dyn std::error::Error>> {
self.simulation.enabled = sim.get_bool_default("enabled", self.simulation.enabled);
if let Some(v) = sim.get_str("night_sky_color") {
self.simulation.night_sky_color = parse_hex_color(v)?;
} else if let Some(v) = sim.get_vec3("night_sky_color") {
self.simulation.night_sky_color = v;
}
if let Some(v) = sim.get_str("morning_sky_color") {
self.simulation.morning_sky_color = parse_hex_color(v)?;
} else if let Some(v) = sim.get_vec3("morning_sky_color") {
self.simulation.morning_sky_color = v;
}
if let Some(v) = sim.get_str("midday_sky_color") {
self.simulation.midday_sky_color = parse_hex_color(v)?;
} else if let Some(v) = sim.get_vec3("midday_sky_color") {
self.simulation.midday_sky_color = v;
}
if let Some(v) = sim.get_str("evening_sky_color") {
self.simulation.evening_sky_color = parse_hex_color(v)?;
} else if let Some(v) = sim.get_vec3("evening_sky_color") {
self.simulation.evening_sky_color = v;
}
if let Some(v) = sim.get_str("night_sun_color") {
self.simulation.night_sun_color = parse_hex_color(v)?;
} else if let Some(v) = sim.get_vec3("night_sun_color") {
self.simulation.night_sun_color = v;
}
if let Some(v) = sim.get_str("morning_sun_color") {
self.simulation.morning_sun_color = parse_hex_color(v)?;
} else if let Some(v) = sim.get_vec3("morning_sun_color") {
self.simulation.morning_sun_color = v;
}
if let Some(v) = sim.get_str("midday_sun_color") {
self.simulation.midday_sun_color = parse_hex_color(v)?;
} else if let Some(v) = sim.get_vec3("midday_sun_color") {
self.simulation.midday_sun_color = v;
}
if let Some(v) = sim.get_str("evening_sun_color") {
self.simulation.evening_sun_color = parse_hex_color(v)?;
} else if let Some(v) = sim.get_vec3("evening_sun_color") {
self.simulation.evening_sun_color = v;
}
self.simulation.sunrise_time =
sim.get_float_default("sunrise_time", self.simulation.sunrise_time);
self.simulation.sunset_time =
sim.get_float_default("sunset_time", self.simulation.sunset_time);
self.simulation.color_transition_duration_hours = sim.get_float_default(
"color_transition_duration_hours",
self.simulation.color_transition_duration_hours,
);
Ok(())
}
}
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
fn lerp_color(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
[
lerp(a[0], b[0], t),
lerp(a[1], b[1], t),
lerp(a[2], b[2], t),
]
}
fn parse_hex_color(hex: &str) -> Result<[f32; 3], Box<dyn std::error::Error>> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err(format!(
"Invalid hex color: expected 6 characters, got {}",
hex.len()
)
.into());
}
let r = u8::from_str_radix(&hex[0..2], 16)?;
let g = u8::from_str_radix(&hex[2..4], 16)?;
let b = u8::from_str_radix(&hex[4..6], 16)?;
Ok([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0])
}
fn parse_hex_color_rgba(hex: &str) -> Result<[f32; 4], Box<dyn std::error::Error>> {
let hex = hex.trim_start_matches('#');
match hex.len() {
6 => {
let rgb = parse_hex_color(hex)?;
Ok([rgb[0], rgb[1], rgb[2], 1.0])
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16)?;
let g = u8::from_str_radix(&hex[2..4], 16)?;
let b = u8::from_str_radix(&hex[4..6], 16)?;
let a = u8::from_str_radix(&hex[6..8], 16)?;
Ok([
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a as f32 / 255.0,
])
}
_ => Err(format!(
"Invalid hex color: expected 6 or 8 characters, got {}",
hex.len()
)
.into()),
}
}
fn parse_backend(v: &str) -> RendererBackend {
match v.to_ascii_lowercase().as_str() {
"raster" => RendererBackend::Raster,
_ => RendererBackend::Compute,
}
}
fn parse_quality(v: &str) -> RenderQualityPreset {
match v.to_ascii_lowercase().as_str() {
"low" => RenderQualityPreset::Low,
"medium" => RenderQualityPreset::Medium,
"high" => RenderQualityPreset::High,
"ultra" => RenderQualityPreset::Ultra,
_ => RenderQualityPreset::Custom,
}
}
fn parse_post_target(v: &str) -> PostTarget {
match v.to_ascii_lowercase().as_str() {
"2d" => PostTarget::D2,
"3d" => PostTarget::D3,
_ => PostTarget::Both,
}
}
fn parse_fade_mode(v: &str) -> FadeMode {
match v.to_ascii_lowercase().as_str() {
"uniform" | "uniformn" => FadeMode::Uniform,
_ => FadeMode::OrderedDither,
}
}
fn parse_lighting_model(v: &str) -> LightingModel {
match v.to_ascii_lowercase().as_str() {
"lambert" => LightingModel::Lambert,
"cook_torrance" => LightingModel::CookTorrance,
"pbr" => LightingModel::Pbr,
_ => LightingModel::CookTorrance,
}
}
fn parse_render_style(v: &str) -> RenderStyle {
match v.to_ascii_lowercase().as_str() {
"retro" => RenderStyle::Retro,
"grimy" | "gritty" | "dirty" => RenderStyle::Grimy,
_ => RenderStyle::Clean,
}
}
fn parse_tone_mapper(v: &str) -> PostToneMapper {
match v.to_ascii_lowercase().as_str() {
"none" => PostToneMapper::None,
"aces" => PostToneMapper::Aces,
_ => PostToneMapper::Reinhard,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_example_toml() {
let example = include_str!("../render_settings_example.toml");
let mut settings = RenderSettings::default();
settings.read(example).expect("render settings parse");
assert_eq!(settings.sky_color, [0.5294118, 0.80784315, 0.92156863]); assert_eq!(settings.sun_color, [1.0, 0.98039216, 0.8039216]); assert_eq!(settings.sun_intensity, 1.0);
assert_eq!(settings.sun_direction, [-0.5, -1.0, -0.3]);
assert!(settings.sun_enabled);
assert!(settings.simulation.enabled);
assert_eq!(settings.simulation.sunrise_time, 6.0);
assert_eq!(settings.simulation.sunset_time, 18.0);
assert_eq!(settings.simulation.color_transition_duration_hours, 0.5);
}
#[test]
fn interpolates_with_set() {
let mut settings = RenderSettings::default();
settings.frame_time_ms = 1000.0;
settings
.set("sun_intensity", Value::Float(3.0), 2.0)
.expect("set sun_intensity");
settings.update_transitions();
assert!((settings.sun_intensity - 2.0).abs() < f32::EPSILON);
settings.update_transitions();
assert!((settings.sun_intensity - 3.0).abs() < f32::EPSILON);
assert!(settings.transitions.is_empty());
}
}