From 0c08187fda9e91cbe80ab4247b8ccdce8621386e Mon Sep 17 00:00:00 2001 From: b-guild Date: Tue, 18 Jun 2024 22:52:51 -0700 Subject: [PATCH 01/10] First version of the improved terrain brush system --- editor/src/interaction/terrain.rs | 292 +++---- editor/src/scene/commands/terrain.rs | 99 +-- fyrox-impl/src/scene/terrain/brushstroke.rs | 652 ++++++++++++++ fyrox-impl/src/scene/terrain/geometry.rs | 114 ++- fyrox-impl/src/scene/terrain/mod.rs | 891 ++++++++++++++++---- fyrox-impl/src/scene/terrain/quadtree.rs | 58 +- fyrox-math/Cargo.toml | 2 +- fyrox-math/src/lib.rs | 27 +- fyrox-ui/src/inspector/editors/matrix2.rs | 96 +++ fyrox-ui/src/inspector/editors/mod.rs | 4 + fyrox-ui/src/lib.rs | 7 +- fyrox-ui/src/matrix2.rs | 264 ++++++ 12 files changed, 2056 insertions(+), 450 deletions(-) create mode 100644 fyrox-impl/src/scene/terrain/brushstroke.rs create mode 100644 fyrox-ui/src/inspector/editors/matrix2.rs create mode 100644 fyrox-ui/src/matrix2.rs diff --git a/editor/src/interaction/terrain.rs b/editor/src/interaction/terrain.rs index 83fdf4002..38cfb9e69 100644 --- a/editor/src/interaction/terrain.rs +++ b/editor/src/interaction/terrain.rs @@ -1,10 +1,12 @@ +use fyrox::scene::terrain::ChunkData; + use crate::fyrox::core::uuid::{uuid, Uuid}; use crate::fyrox::core::TypeUuidProvider; use crate::fyrox::graph::BaseSceneGraph; use crate::fyrox::gui::{HorizontalAlignment, Thickness, VerticalAlignment}; use crate::fyrox::{ core::{ - algebra::{Matrix4, Vector2, Vector3}, + algebra::{Matrix2, Matrix4, Vector2, Vector3}, arrayvec::ArrayVec, color::Color, log::{Log, MessageKind}, @@ -34,7 +36,9 @@ use crate::fyrox::{ MeshBuilder, RenderPath, }, node::Node, - terrain::{Brush, BrushMode, BrushShape, Terrain, TerrainRayCastResult}, + terrain::{ + Brush, BrushMode, BrushShape, BrushStroke, BrushTarget, Terrain, TerrainRayCastResult, + }, }, }; use crate::interaction::make_interaction_mode_button; @@ -54,13 +58,18 @@ use crate::{ use fyrox::asset::untyped::ResourceKind; use std::sync::Arc; +fn modify_clamp(x: &mut f32, delta: f32, min: f32, max: f32) { + *x = (*x + delta).clamp(min, max) +} + pub struct TerrainInteractionMode { - heightmaps: Vec>, - masks: Vec>, + undo_chunks: Vec, message_sender: MessageSender, interacting: bool, brush_gizmo: BrushGizmo, + brush_position: Vector3, brush: Brush, + stroke: BrushStroke, brush_panel: BrushPanel, scene_viewer_frame: Handle, } @@ -73,9 +82,12 @@ impl TerrainInteractionMode { scene_viewer_frame: Handle, ) -> Self { let brush = Brush { - center: Default::default(), shape: BrushShape::Circle { radius: 1.0 }, - mode: BrushMode::ModifyHeightMap { amount: 1.0 }, + mode: BrushMode::Raise { amount: 1.0 }, + target: BrushTarget::HeightMap, + alpha: 1.0, + hardness: 1.0, + transform: Matrix2::identity(), }; let brush_panel = @@ -83,15 +95,34 @@ impl TerrainInteractionMode { Self { brush_panel, - heightmaps: Default::default(), + undo_chunks: Default::default(), brush_gizmo: BrushGizmo::new(game_scene, engine), interacting: false, message_sender, brush, - masks: Default::default(), + brush_position: Vector3::default(), + stroke: BrushStroke::default(), scene_viewer_frame, } } + fn modify_brush_opacity(&mut self, direction: f32) { + modify_clamp(&mut self.brush.alpha, 0.01 * direction, 0.0, 1.0); + } + fn draw(&mut self, terrain: &mut Terrain, shift: bool) { + let mut brush_copy = self.brush.clone(); + if let BrushMode::Raise { amount } = &mut brush_copy.mode { + if shift { + *amount *= -1.0; + } + } + + self.undo_chunks = terrain.draw( + self.brush_position, + &brush_copy, + &mut self.stroke, + std::mem::take(&mut self.undo_chunks), + ); + } } pub struct BrushGizmo { @@ -129,19 +160,6 @@ impl BrushGizmo { } } -fn copy_layer_masks(terrain: &Terrain, layer: usize) -> Vec> { - let mut masks = Vec::new(); - - for chunk in terrain.chunks_ref() { - match chunk.layer_masks.get(layer) { - Some(mask) => masks.push(mask.data_ref().data().to_vec()), - None => Log::err("layer index out of range"), - } - } - - masks -} - impl TypeUuidProvider for TerrainInteractionMode { fn type_uuid() -> Uuid { uuid!("bc19eff3-3e3a-49c0-9a9d-17d36fccc34e") @@ -164,37 +182,38 @@ impl InteractionMode for TerrainInteractionMode { if let Some(selection) = editor_selection.as_graph() { if selection.is_single_selection() { + let shift = engine + .user_interfaces + .first_mut() + .keyboard_modifiers() + .shift; let graph = &mut engine.scenes[game_scene.scene].graph; let handle = selection.nodes()[0]; - if let Some(terrain) = &graph[handle].cast::() { + let ray = graph[game_scene.camera_controller.camera] + .cast::() + .map(|cam| cam.make_ray(mouse_pos, frame_size)); + if let Some(terrain) = graph[handle].cast_mut::() { // Pick height value at the point of interaction. - if let BrushMode::FlattenHeightMap { height } = &mut self.brush.mode { - let camera = &graph[game_scene.camera_controller.camera]; - if let Some(camera) = camera.cast::() { - let ray = camera.make_ray(mouse_pos, frame_size); - + if let BrushMode::Flatten { .. } = &mut self.brush.mode { + if let Some(ray) = ray { let mut intersections = ArrayVec::::new(); terrain.raycast(ray, &mut intersections, true); - if let Some(closest) = intersections.first() { - *height = closest.height; + let first = intersections.first(); + if let (Some(closest), BrushTarget::HeightMap) = + (first, self.brush.target) + { + self.stroke.value = closest.height; + } else if let Some(closest) = first { + let p = closest.position; + self.stroke.value = terrain + .interpolate_value(Vector2::new(p.x, p.z), self.brush.target); + } else { + self.stroke.value = 0.0; } } } - - match self.brush.mode { - BrushMode::ModifyHeightMap { .. } | BrushMode::FlattenHeightMap { .. } => { - self.heightmaps = terrain - .chunks_ref() - .iter() - .map(|c| c.heightmap_owned()) - .collect(); - } - BrushMode::DrawOnMask { layer, .. } => { - self.masks = copy_layer_masks(terrain, layer); - } - } - + self.draw(terrain, shift); self.interacting = true; } } @@ -204,52 +223,37 @@ impl InteractionMode for TerrainInteractionMode { fn on_left_mouse_button_up( &mut self, editor_selection: &Selection, - controller: &mut dyn SceneController, - engine: &mut Engine, + _controller: &mut dyn SceneController, + _engine: &mut Engine, _mouse_pos: Vector2, _frame_size: Vector2, _settings: &Settings, ) { - let Some(game_scene) = controller.downcast_mut::() else { - return; - }; - + self.stroke.clear(); if let Some(selection) = editor_selection.as_graph() { if selection.is_single_selection() { - let graph = &mut engine.scenes[game_scene.scene].graph; let handle = selection.nodes()[0]; - if let Some(terrain) = &graph[handle].cast::() { - if self.interacting { - let new_heightmaps = terrain - .chunks_ref() - .iter() - .map(|c| c.heightmap_owned()) - .collect(); - - match self.brush.mode { - BrushMode::ModifyHeightMap { .. } - | BrushMode::FlattenHeightMap { .. } => { - self.message_sender - .do_command(ModifyTerrainHeightCommand::new( - handle, - std::mem::take(&mut self.heightmaps), - new_heightmaps, - )); - } - BrushMode::DrawOnMask { layer, .. } => { - self.message_sender - .do_command(ModifyTerrainLayerMaskCommand::new( - handle, - std::mem::take(&mut self.masks), - copy_layer_masks(terrain, layer), - layer, - )); - } + if self.interacting { + match self.brush.target { + BrushTarget::HeightMap => { + self.message_sender + .do_command(ModifyTerrainHeightCommand::new( + handle, + std::mem::take(&mut self.undo_chunks), + )); + } + BrushTarget::LayerMask { layer, .. } => { + self.message_sender + .do_command(ModifyTerrainLayerMaskCommand::new( + handle, + std::mem::take(&mut self.undo_chunks), + layer, + )); } - - self.interacting = false; } + + self.interacting = false; } } } @@ -268,10 +272,17 @@ impl InteractionMode for TerrainInteractionMode { let Some(game_scene) = controller.downcast_mut::() else { return; }; + let graph = &mut engine.scenes[game_scene.scene].graph; + + let mut gizmo_visible = false; if let Some(selection) = editor_selection.as_graph() { if selection.is_single_selection() { - let graph = &mut engine.scenes[game_scene.scene].graph; + let shift = engine + .user_interfaces + .first_mut() + .keyboard_modifiers() + .shift; let handle = selection.nodes()[0]; let camera = &graph[game_scene.camera_controller.camera]; @@ -282,44 +293,11 @@ impl InteractionMode for TerrainInteractionMode { terrain.raycast(ray, &mut intersections, true); if let Some(closest) = intersections.first() { - self.brush.center = closest.position; - - let mut brush_copy = self.brush.clone(); - match &mut brush_copy.mode { - BrushMode::ModifyHeightMap { amount } => { - if engine - .user_interfaces - .first_mut() - .keyboard_modifiers() - .shift - { - *amount *= -1.0; - } - } - BrushMode::DrawOnMask { alpha, .. } => { - if engine - .user_interfaces - .first_mut() - .keyboard_modifiers() - .shift - { - *alpha = -1.0; - } - } - BrushMode::FlattenHeightMap { height } => { - if engine - .user_interfaces - .first_mut() - .keyboard_modifiers() - .shift - { - *height *= -1.0; - } - } - } + self.brush_position = closest.position; + gizmo_visible = true; if self.interacting { - terrain.draw(&brush_copy); + self.draw(terrain, shift); } let scale = match self.brush.shape { @@ -339,6 +317,10 @@ impl InteractionMode for TerrainInteractionMode { } } } + let gizmo = &mut graph[self.brush_gizmo.brush]; + if gizmo.visibility() != gizmo_visible { + gizmo.set_visibility(gizmo_visible); + } } fn activate(&mut self, controller: &dyn SceneController, engine: &mut Engine) { @@ -417,22 +399,15 @@ impl InteractionMode for TerrainInteractionMode { ) -> bool { let mut processed = false; - fn modify_clamp(x: &mut f32, delta: f32, min: f32, max: f32) { - *x = (*x + delta).clamp(min, max) - } - let key_bindings = &settings.key_bindings.terrain_key_bindings; if hotkey == &key_bindings.draw_on_mask_mode { - self.brush.mode = BrushMode::DrawOnMask { - layer: 0, - alpha: 1.0, - }; + self.brush.target = BrushTarget::LayerMask { layer: 0 }; processed = true; } else if hotkey == &key_bindings.modify_height_map_mode { - self.brush.mode = BrushMode::ModifyHeightMap { amount: 1.0 }; + self.brush.target = BrushTarget::HeightMap; processed = true; } else if hotkey == &key_bindings.flatten_slopes_mode { - self.brush.mode = BrushMode::FlattenHeightMap { height: 0.0 }; + self.brush.mode = BrushMode::Flatten; processed = true; } else if hotkey == &key_bindings.increase_brush_size { match &mut self.brush.shape { @@ -453,34 +428,18 @@ impl InteractionMode for TerrainInteractionMode { } processed = true; } else if hotkey == &key_bindings.decrease_brush_opacity { - match &mut self.brush.mode { - BrushMode::ModifyHeightMap { amount } => { - *amount -= 0.01; - } - BrushMode::FlattenHeightMap { height } => { - *height -= 0.01; - } - BrushMode::DrawOnMask { alpha, .. } => modify_clamp(alpha, -0.01, 0.0, 1.0), - } + self.modify_brush_opacity(-1.0); processed = true; } else if hotkey == &key_bindings.increase_brush_opacity { - match &mut self.brush.mode { - BrushMode::ModifyHeightMap { amount } => { - *amount += 0.01; - } - BrushMode::FlattenHeightMap { height } => { - *height += 0.01; - } - BrushMode::DrawOnMask { alpha, .. } => modify_clamp(alpha, 0.01, 0.0, 1.0), - } + self.modify_brush_opacity(1.0); processed = true; } else if hotkey == &key_bindings.prev_layer { - if let BrushMode::DrawOnMask { layer, .. } = &mut self.brush.mode { + if let BrushTarget::LayerMask { layer, .. } = &mut self.brush.target { *layer = layer.saturating_sub(1); } processed = true; } else if hotkey == &key_bindings.next_layer { - if let BrushMode::DrawOnMask { layer, .. } = &mut self.brush.mode { + if let BrushTarget::LayerMask { layer, .. } = &mut self.brush.target { *layer = layer.saturating_add(1); } processed = true; @@ -520,29 +479,45 @@ struct BrushPanel { fn make_brush_mode_enum_property_editor_definition() -> EnumPropertyEditorDefinition { EnumPropertyEditorDefinition { variant_generator: |i| match i { - 0 => BrushMode::ModifyHeightMap { amount: 0.1 }, - 1 => BrushMode::DrawOnMask { - layer: 0, - alpha: 1.0, - }, - 2 => BrushMode::FlattenHeightMap { height: 0.0 }, + 0 => BrushMode::Raise { amount: 0.1 }, + 1 => BrushMode::Assign { value: 0.0 }, + 2 => BrushMode::Flatten, + 3 => BrushMode::Smooth { kernel_radius: 1 }, _ => unreachable!(), }, index_generator: |v| match v { - BrushMode::ModifyHeightMap { .. } => 0, - BrushMode::DrawOnMask { .. } => 1, - BrushMode::FlattenHeightMap { .. } => 2, + BrushMode::Raise { .. } => 0, + BrushMode::Assign { .. } => 1, + BrushMode::Flatten { .. } => 2, + BrushMode::Smooth { .. } => 3, }, names_generator: || { vec![ - "Modify Height Map".to_string(), - "Draw On Mask".to_string(), - "Flatten Height Map".to_string(), + "Raise or Lower".to_string(), + "Assign Value".to_string(), + "Flatten".to_string(), + "Smooth".to_string(), ] }, } } +fn make_brush_target_enum_property_editor_definition() -> EnumPropertyEditorDefinition +{ + EnumPropertyEditorDefinition { + variant_generator: |i| match i { + 0 => BrushTarget::HeightMap, + 1 => BrushTarget::LayerMask { layer: 0 }, + _ => unreachable!(), + }, + index_generator: |v| match v { + BrushTarget::HeightMap => 0, + BrushTarget::LayerMask { .. } => 1, + }, + names_generator: || vec!["Height Map".to_string(), "Layer Mask".to_string()], + } +} + fn make_brush_shape_enum_property_editor_definition() -> EnumPropertyEditorDefinition { EnumPropertyEditorDefinition { variant_generator: |i| match i { @@ -565,6 +540,7 @@ impl BrushPanel { fn new(ctx: &mut BuildContext, brush: &Brush) -> Self { let property_editors = PropertyEditorDefinitionContainer::with_default_editors(); property_editors.insert(make_brush_mode_enum_property_editor_definition()); + property_editors.insert(make_brush_target_enum_property_editor_definition()); property_editors.insert(make_brush_shape_enum_property_editor_definition()); let context = InspectorContext::from_object( diff --git a/editor/src/scene/commands/terrain.rs b/editor/src/scene/commands/terrain.rs index 10b10d527..25fa89c89 100644 --- a/editor/src/scene/commands/terrain.rs +++ b/editor/src/scene/commands/terrain.rs @@ -1,8 +1,6 @@ +use fyrox::scene::terrain::ChunkData; + use crate::command::CommandContext; -use crate::fyrox::core::log::Log; -use crate::fyrox::resource::texture::{ - TextureKind, TexturePixelKind, TextureResourceExtension, TextureWrapMode, -}; use crate::fyrox::{ core::pool::Handle, resource::texture::TextureResource, @@ -100,54 +98,27 @@ impl CommandTrait for DeleteTerrainLayerCommand { #[derive(Debug)] pub struct ModifyTerrainHeightCommand { terrain: Handle, - // TODO: This is very memory-inefficient solution, it could be done - // better by either pack/unpack data on the fly, or by saving changes - // for sub-chunks. - old_heightmaps: Vec>, - new_heightmaps: Vec>, + heightmaps: Vec, + skip_first_execute: bool, } impl ModifyTerrainHeightCommand { - pub fn new( - terrain: Handle, - old_heightmaps: Vec>, - new_heightmaps: Vec>, - ) -> Self { + pub fn new(terrain: Handle, heightmaps: Vec) -> Self { Self { terrain, - old_heightmaps, - new_heightmaps, + heightmaps, + skip_first_execute: true, } } pub fn swap(&mut self, context: &mut dyn CommandContext) { let context = context.get_mut::(); let terrain = context.scene.graph[self.terrain].as_terrain_mut(); - let heigth_map_size = terrain.height_map_size(); - for (chunk, (old, new)) in terrain.chunks_mut().iter_mut().zip( - self.old_heightmaps - .iter_mut() - .zip(self.new_heightmaps.iter_mut()), - ) { - let height_map = TextureResource::from_bytes( - TextureKind::Rectangle { - width: heigth_map_size.x, - height: heigth_map_size.y, - }, - TexturePixelKind::R32F, - fyrox::core::transmute_vec_as_bytes(new.clone()), - Default::default(), - ) - .unwrap(); - - let mut data = height_map.data_ref(); - data.set_s_wrap_mode(TextureWrapMode::ClampToEdge); - data.set_t_wrap_mode(TextureWrapMode::ClampToEdge); - drop(data); - - chunk.replace_height_map(height_map).unwrap(); - std::mem::swap(old, new); + let current_chunks = terrain.chunks_mut(); + for c in self.heightmaps.iter_mut() { + c.swap_height_from_list(current_chunks); } + terrain.update_quad_trees(); } } @@ -157,6 +128,10 @@ impl CommandTrait for ModifyTerrainHeightCommand { } fn execute(&mut self, context: &mut dyn CommandContext) { + if self.skip_first_execute { + self.skip_first_execute = false; + return; + } self.swap(context); } @@ -168,51 +143,27 @@ impl CommandTrait for ModifyTerrainHeightCommand { #[derive(Debug)] pub struct ModifyTerrainLayerMaskCommand { terrain: Handle, - // TODO: This is very memory-inefficient solution, it could be done - // better by either pack/unpack data on the fly, or by saving changes - // for sub-chunks. - old_masks: Vec>, - new_masks: Vec>, + masks: Vec, layer: usize, + skip_first_execute: bool, } impl ModifyTerrainLayerMaskCommand { - pub fn new( - terrain: Handle, - old_masks: Vec>, - new_masks: Vec>, - layer: usize, - ) -> Self { + pub fn new(terrain: Handle, masks: Vec, layer: usize) -> Self { Self { terrain, - old_masks, - new_masks, + masks, layer, + skip_first_execute: true, } } pub fn swap(&mut self, context: &mut dyn CommandContext) { let context = context.get_mut::(); let terrain = context.scene.graph[self.terrain].as_terrain_mut(); - - for (i, chunk) in terrain.chunks_mut().iter_mut().enumerate() { - if i >= self.old_masks.len() || i >= self.new_masks.len() { - Log::err("Invalid mask index.") - } else { - let old = &mut self.old_masks[i]; - let new = &mut self.new_masks[i]; - let chunk_mask = &mut chunk.layer_masks[self.layer]; - - let mut texture_data = chunk_mask.data_ref(); - - for (mask_pixel, new_pixel) in - texture_data.modify().data_mut().iter_mut().zip(new.iter()) - { - *mask_pixel = *new_pixel; - } - - std::mem::swap(old, new); - } + let current_chunks = terrain.chunks_mut(); + for c in self.masks.iter_mut() { + c.swap_layer_mask_from_list(current_chunks, self.layer); } } } @@ -223,6 +174,10 @@ impl CommandTrait for ModifyTerrainLayerMaskCommand { } fn execute(&mut self, context: &mut dyn CommandContext) { + if self.skip_first_execute { + self.skip_first_execute = false; + return; + } self.swap(context); } diff --git a/fyrox-impl/src/scene/terrain/brushstroke.rs b/fyrox-impl/src/scene/terrain/brushstroke.rs new file mode 100644 index 000000000..c214968fa --- /dev/null +++ b/fyrox-impl/src/scene/terrain/brushstroke.rs @@ -0,0 +1,652 @@ +use crate::core::{ + algebra::{Matrix2, Vector2}, + math::{OptionRect, Rect}, + reflect::prelude::*, +}; +use crate::fxhash::FxHashMap; +use fyrox_core::uuid_provider; + +/// A value that is stored in a terrain and can be edited by a brush. +pub trait BrushValue { + /// Increase the value by the given amount, or decrease it if the amount is negative. + fn raise(self, amount: f32) -> Self; + /// Create a value based upon a float representation. + fn from_f32(value: f32) -> Self; + /// Create an f32 representation of this value. + fn into_f32(self) -> f32; +} + +impl BrushValue for f32 { + #[inline] + fn raise(self, amount: f32) -> Self { + self + amount + } + #[inline] + fn from_f32(value: f32) -> Self { + value + } + #[inline] + fn into_f32(self) -> f32 { + self + } +} + +impl BrushValue for u8 { + #[inline] + fn raise(self, amount: f32) -> Self { + (self as f32 + amount * 255.0).clamp(0.0, 255.0) as Self + } + #[inline] + fn from_f32(value: f32) -> Self { + (value * 255.0).clamp(0.0, 255.0) as Self + } + #[inline] + fn into_f32(self) -> f32 { + self as f32 / 255.0 + } +} + +/// Trait for any of the various data properties that may be edited +/// by a brush. V is the type of the elements of the data, +/// such as f32 for the height data and u8 for the mask data. +/// +/// This trait encapsulates and hides the concept of chunks. +/// It pretends that terrain data is a continuous infinite array, and +/// accessing any data in that array requires only a Vector2 index. +/// This simplifies brush algorithms. +pub trait BrushableTerrainData { + /// Returns the value at the given coordinates as it was *before* the current brushstroke. + /// Previous calls to [BrushableTerrainData::update] will not affect the value returned until the current + /// stroke ends and the changes to the terrain are completed. + fn get_value(&self, position: Vector2) -> V; + /// Updates the value of the terrain according to the given function + /// if the given strength is greater than the current brush strength at + /// the given coordinates. func is not called otherwise. + /// + /// If the value is updated, then the brush strength is increased to match + /// the given strength at those coordinates so that the same position will + /// not be updated again unless by a brush of greater strength than this one. + /// + /// If strength is 0.0 or less, nothing is done, since 0.0 is the minimum valid brush strength. + /// + /// The value passed to func is the same value that would be returned by [BrushableTerrainData::get_value], + /// which is the value at that position *before* the current stroke began. + /// Even if update is called multiple times on a single position, the value passed to func will be + /// the same each time until the current stroke is completed. + fn update(&mut self, position: Vector2, strength: f32, func: F) + where + F: FnOnce(&Self, V) -> V; + /// Calculate a value for a kernel of the given radius around the given position + /// by summing the values of the neighborhood surrounding the position. + fn sum_kernel(&self, position: Vector2, kernel_radius: u32) -> f32; +} + +#[derive(Debug, Default)] +/// Data for an in-progress terrain painting operation +pub struct BrushStroke { + /// The height pixels that have been drawn to + pub height_pixels: StrokeData, + /// The mask pixels that have been drawn to + pub mask_pixels: StrokeData, + /// A value that may change over the course of a stroke. + pub value: f32, +} + +/// The pixels for a stroke, generalized over the type of data being edited. +#[derive(Debug, Default)] +pub struct StrokeData(FxHashMap, StrokeElement>); + +/// A single pixel data of a brush stroke +#[derive(Debug, Copy, Clone)] +pub struct StrokeElement { + /// The intensity of the brush stroke, with 0.0 indicating a pixel that brush has not touched + /// and 1.0 indicates a pixel fully covered by the brush. + pub strength: f32, + /// The value of the pixel before the stroke began. + pub original_value: V, +} + +/// A pixel within a brush shape. +#[derive(Debug, Copy, Clone)] +pub struct BrushPixel { + /// The position of the pixel + pub position: Vector2, + /// The strength of the brush at this pixel, with 0.0 indicating the pixel is outside the bounds of the brush, + /// and 1.0 indicating the maximum strength of the brush. + pub strength: f32, +} + +impl BrushStroke { + /// Prepare this object for a new brushstroke. + pub fn clear(&mut self) { + self.height_pixels.clear(); + self.mask_pixels.clear(); + } +} + +impl StrokeData { + /// Reset the brush stroke so it is ready to begin a new stroke. + #[inline] + pub fn clear(&mut self) { + self.0.clear() + } + /// Return the StrokeElement stored at the give position, if there is one. + #[inline] + pub fn get(&self, position: Vector2) -> Option<&StrokeElement> { + self.0.get(&position) + } + /// Stores or modifies the StrokeElement at the given position. + /// If the element is updated, return the original pixel value of the element. + /// - `position`: The position of the data to modify within the terrain. + /// - `strength`: The strength of the brush at the position, from 0.0 to 1.0. + /// The element is updated if the stored strength is less than the given strength. + /// If there is no stored strength, that is treated as a strength of 0.0. + /// - `pixel_value`: The current value of the data. + /// This may be stored in the StrokeData if no pixel value is currently recorded for the given position. + /// Otherwise, this value is ignored. + #[inline] + pub fn update_pixel( + &mut self, + position: Vector2, + strength: f32, + pixel_value: V, + ) -> Option + where + V: Clone, + { + if strength == 0.0 { + None + } else if let Some(element) = self.0.get_mut(&position) { + if element.strength < strength { + element.strength = strength; + Some(element.original_value.clone()) + } else { + None + } + } else { + let element = StrokeElement { + strength, + original_value: pixel_value.clone(), + }; + self.0.insert(position, element); + Some(pixel_value) + } + } +} + +/// An iterator that produces coordinates by scanning an integer Rect. +#[derive(Debug, Clone)] +pub struct RectIter { + bounds: Rect, + next_pos: Vector2, +} + +impl RectIter { + /// Create an iterator that returns coordinates within the given bounds. + pub fn new(bounds: Rect) -> Self { + Self { + bounds, + next_pos: Vector2::default(), + } + } + /// The Rect that this iter is scanning. + pub fn bounds(&self) -> Rect { + self.bounds + } +} + +impl Iterator for RectIter { + type Item = Vector2; + fn next(&mut self) -> Option { + if self.next_pos.y > self.bounds.size.y { + return None; + } + let result = self.next_pos + self.bounds.position; + if self.next_pos.x < self.bounds.size.x { + self.next_pos.x += 1; + } else { + self.next_pos.y += 1; + self.next_pos.x = 0; + } + Some(result) + } +} + +fn apply_hardness(hardness: f32, strength: f32) -> f32 { + if strength == 0.0 { + return 0.0; + } + let h = 1.0 - hardness; + if strength < h { + strength / h + } else { + 1.0 + } +} + +/// An iterator of the pixels of a round brush. +#[derive(Debug, Clone)] +pub struct CircleBrushPixels { + center: Vector2, + radius: f32, + hardness: f32, + inv_transform: Matrix2, + bounds_iter: RectIter, +} + +impl CircleBrushPixels { + /// The bounding rectangle of the pixels. + pub fn bounds(&self) -> Rect { + self.bounds_iter.bounds() + } +} + +impl CircleBrushPixels { + /// Construct a new pixel iterator for a round brush at the given position, radius, + /// and 2x2 transform matrix. + pub fn new(center: Vector2, radius: f32, hardness: f32, transform: Matrix2) -> Self { + let mut bounds: OptionRect = Default::default(); + let transform = if transform.is_invertible() { + transform + } else { + Matrix2::identity() + }; + let inv_transform = transform.try_inverse().unwrap(); + for p in [ + Vector2::new(radius, radius), + Vector2::new(radius, -radius), + Vector2::new(-radius, radius), + Vector2::new(-radius, -radius), + ] { + let p1 = transform * p + center; + let ceil = p1.map(|x| x.ceil() as i32); + let floor = p1.map(|x| x.floor() as i32); + let rect = Rect::new(floor.x, floor.y, ceil.x - floor.x, ceil.y - floor.y); + bounds.extend_to_contain(rect); + } + Self { + center, + radius, + hardness, + inv_transform, + bounds_iter: RectIter::new(bounds.unwrap()), + } + } +} + +impl Iterator for CircleBrushPixels { + type Item = BrushPixel; + + fn next(&mut self) -> Option { + let position = self.bounds_iter.next()?; + let fx = position.x as f32; + let fy = position.y as f32; + let p = Vector2::new(fx, fy) - self.center; + let p1 = self.inv_transform * p; + let dist_sqr = p1.magnitude_squared(); + let radius = self.radius; + let strength = if dist_sqr >= radius * radius { + 0.0 + } else { + (1.0 - dist_sqr.sqrt() / radius).max(0.0) + }; + let strength = apply_hardness(self.hardness, strength); + Some(BrushPixel { position, strength }) + } +} + +impl RectBrushPixels { + /// The bounds of the pixels. + pub fn bounds(&self) -> Rect { + self.bounds_iter.bounds() + } +} + +/// An iterator of the pixels of a rectangular brush. +#[derive(Debug, Clone)] +pub struct RectBrushPixels { + center: Vector2, + radius: Vector2, + hardness: f32, + inv_transform: Matrix2, + bounds_iter: RectIter, +} + +impl RectBrushPixels { + /// Construct a new pixel iterator for a rectangle brush at the given position, + /// x-radius, y-radius, and 2x2 transform matrix for rotation. + pub fn new( + center: Vector2, + radius: Vector2, + hardness: f32, + transform: Matrix2, + ) -> Self { + let mut bounds: Option> = None; + let transform = if transform.is_invertible() { + transform + } else { + Matrix2::identity() + }; + let inv_transform = transform.try_inverse().unwrap(); + for p in [ + center + radius, + center + Vector2::new(radius.x, -radius.y), + center + Vector2::new(-radius.x, radius.y), + center - radius, + ] { + let p = transform * p; + let ceil = p.map(|x| x.ceil() as i32); + let floor = p.map(|x| x.floor() as i32); + let rect = Rect::new(floor.x, floor.y, ceil.x - floor.x, ceil.y - floor.y); + if let Some(bounds) = &mut bounds { + bounds.extend_to_contain(rect); + } else { + bounds = Some(rect); + } + } + Self { + center, + radius, + hardness, + inv_transform, + bounds_iter: RectIter::new(bounds.unwrap()), + } + } +} + +impl Iterator for RectBrushPixels { + type Item = BrushPixel; + + fn next(&mut self) -> Option { + let position = self.bounds_iter.next()?; + let fx = position.x as f32; + let fy = position.y as f32; + let p = Vector2::new(fx, fy) - self.center; + let p = self.inv_transform * p; + let radius = self.radius; + let p = p.abs(); + let min = radius.min(); + let radius = Vector2::new(radius.x - min, radius.y - min); + let p = Vector2::new(p.x - min, p.y - min).sup(&Vector2::new(0.0, 0.0)); + let strength = if p.x > radius.x || p.y > radius.y { + 0.0 + } else { + 1.0 - (p.x / radius.x).max(p.y / radius.y) + }; + let strength = apply_hardness(self.hardness, strength); + Some(BrushPixel { position, strength }) + } +} + +/// Shape of a brush. +#[derive(Copy, Clone, Reflect, Debug)] +pub enum BrushShape { + /// Circle with given radius. + Circle { + /// Radius of the circle. + radius: f32, + }, + /// Rectangle with given width and height. + Rectangle { + /// Width of the rectangle. + width: f32, + /// Length of the rectangle. + length: f32, + }, +} + +uuid_provider!(BrushShape = "a4dbfba0-077c-4658-9972-38384a8432f9"); + +impl BrushShape { + /// Return true if the given point is within the shape when positioned at the given center point. + pub fn contains(&self, brush_center: Vector2, pixel_position: Vector2) -> bool { + match *self { + BrushShape::Circle { radius } => (brush_center - pixel_position).norm() < radius, + BrushShape::Rectangle { width, length } => Rect::new( + brush_center.x - width * 0.5, + brush_center.y - length * 0.5, + width, + length, + ) + .contains(pixel_position), + } + } +} + +/// Paint mode of a brush. It defines operation that will be performed on the terrain. +#[derive(Clone, PartialEq, PartialOrd, Reflect, Debug)] +pub enum BrushMode { + /// Raise or lower the value + Raise { + /// An offset to change the value by + amount: f32, + }, + /// Flattens value of the terrain data + Flatten, + /// Assigns a particular value to anywhere the brush touches. + Assign { + /// Fixed value to paint into the data + value: f32, + }, + /// Reduce sharp changes in the data. + Smooth { + /// Determines the size of each pixel's neighborhood in terms of + /// distance from the pixel. + /// 0 means no smoothing at all. + /// 1 means taking the mean of the 3x3 square of pixels surrounding each smoothed pixel. + /// 2 means using a 5x5 square of pixels. And so on. + kernel_radius: u32, + }, +} + +uuid_provider!(BrushMode = "48ad4cac-05f3-485a-b2a3-66812713841f"); + +impl BrushMode { + /// Perform the operation represented by this BrushMode. + /// - `pixels`: An iterator over the pixels that are covered by the shape of the brush and the position of the brush. + /// - `data`: An abstraction of the terrain data that allows the brush mode to edit the terrain data without concern + /// for chunks of what kind of data is being edited. + /// - `value`: A value that is used to control some BrushModes, especially `Flatten` where it represents the level of flattened terrain. + /// - `alpha`: A value between 0.0 and 1.0 that represents how much the brush's effect is weighted when combining it with the original + /// value of the pixels, with 0.0 meaning the brush has no effect and 1.0 meaning that the original value of the pixels is completely covered. + pub fn draw(&self, pixels: I, data: &mut D, value: f32, alpha: f32) + where + I: Iterator, + D: BrushableTerrainData, + V: BrushValue, + { + match self { + BrushMode::Raise { amount } => { + for BrushPixel { position, strength } in pixels { + data.update(position, strength, |_, x| { + x.raise(amount * strength * alpha) + }); + } + } + BrushMode::Flatten => { + for BrushPixel { position, strength } in pixels { + let alpha = strength * alpha; + data.update(position, strength, |_, x| { + V::from_f32(x.into_f32() * (1.0 - alpha) + value * alpha) + }); + } + } + BrushMode::Assign { value } => { + for BrushPixel { position, strength } in pixels { + let alpha = strength * alpha; + data.update(position, strength, |_, x| { + V::from_f32(x.into_f32() * (1.0 - alpha) + value * alpha) + }); + } + } + BrushMode::Smooth { kernel_radius } => { + if *kernel_radius == 0 || alpha == 0.0 { + return; + } + let size = kernel_radius * 2 + 1; + let scale = 1.0 / (size * size) as f32; + for BrushPixel { position, strength } in pixels { + let alpha = strength * alpha; + data.update(position, strength, |data, x| { + let value = data.sum_kernel(position, *kernel_radius) * scale; + V::from_f32(x.into_f32() * (1.0 - alpha) + value * alpha) + }); + } + } + } + } +} + +/// Paint target of a brush. It defines the data that the brush will operate on. +#[derive(Copy, Clone, Reflect, Debug, PartialEq, Eq)] +pub enum BrushTarget { + /// Modifies the height map + HeightMap, + /// Draws on a given layer + LayerMask { + /// The number of the layer to modify + layer: usize, + }, +} + +uuid_provider!(BrushTarget = "461c1be7-189e-44ee-b8fd-00b8fdbc668f"); + +/// Brush is used to modify terrain. It supports multiple shapes and modes. +#[derive(Clone, Reflect, Debug)] +pub struct Brush { + /// Shape of the brush. + pub shape: BrushShape, + /// Paint mode of the brush. + pub mode: BrushMode, + /// The data to modify with the brush + pub target: BrushTarget, + /// Transform that can modify the shape of the brush + pub transform: Matrix2, + /// The softness of the edges of the brush. + /// 0.0 means that the brush fades very gradually from opaque to transparent. + /// 1.0 means that the edges of the brush do not fade. + pub hardness: f32, + /// The transparency of the brush, allowing the values beneath the brushstroke to show throw. + /// 0.0 means the brush is fully transparent and does not draw. + /// 1.0 means the brush is fully opaque. + pub alpha: f32, +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn rect_extend_to_contain_f64() { + let mut rect = Rect::new(0.0, 0.0, 1.0, 1.0); + + //rect.extend_to_contain(Rect::new(1.0, 1.0, 1.0, 1.0)); + //assert_eq!(rect, Rect::new(0.0, 0.0, 2.0, 2.0)); + + rect.extend_to_contain(Rect::new(-1.0, -1.0, 1.0, 1.0)); + assert_eq!(rect, Rect::new(-1.0, -1.0, 2.0, 2.0)); + } + #[test] + fn rect_extend_to_contain_i32() { + let mut rect: Rect = Rect::new(0, 0, 1, 1); + + rect.extend_to_contain(Rect::new(1, 1, 1, 1)); + assert_eq!(rect, Rect::::new(0, 0, 2, 2)); + + rect.extend_to_contain(Rect::new(-1, -1, 1, 1)); + assert_eq!(rect, Rect::::new(-1, -1, 2, 2)); + } + #[test] + fn bounds_iter() { + let result: Vec> = RectIter::new(Rect::new(-1, -2, 3, 2)).collect(); + let expected: Vec> = vec![ + (-1, -2), + (0, -2), + (1, -2), + (2, -2), + (-1, -1), + (0, -1), + (1, -1), + (2, -1), + (-1, 0), + (0, 0), + (1, 0), + (2, 0), + ] + .into_iter() + .map(|(x, y)| Vector2::new(x, y)) + .collect(); + assert_eq!(result, expected); + } + #[test] + fn finite_pixels_circle() { + let mut iter = CircleBrushPixels::new(Vector2::default(), 2.0, 1.0, Matrix2::identity()); + for _ in 0..100 { + if iter.next().is_none() { + return; + } + } + panic!("Iter went over 100."); + } + #[test] + fn finite_pixels_rect() { + let mut iter = RectBrushPixels::new( + Vector2::default(), + Vector2::new(2.0, 2.0), + 1.0, + Matrix2::identity(), + ); + for _ in 0..100 { + if iter.next().is_none() { + return; + } + } + panic!("Iter went over 100."); + } + #[test] + fn pixels_range_circle() { + let mut iter = CircleBrushPixels::new(Vector2::default(), 2.0, 1.0, Matrix2::identity()); + let mut rect = Rect::new(0, 0, 0, 0); + let mut points: Vec> = Vec::default(); + for _ in 0..100 { + if let Some(BrushPixel { position, .. }) = iter.next() { + points.push(Vector2::new(position.x, position.y)); + rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0)); + } else { + break; + } + } + assert_eq!( + rect, + Rect::new(-2, -2, 4, 4), + "{:?} {:?}", + iter.bounds(), + points + ); + } + #[test] + fn pixels_range_rect() { + let mut iter = RectBrushPixels::new( + Vector2::default(), + Vector2::new(2.0, 2.0), + 1.0, + Matrix2::identity(), + ); + let mut rect = Rect::new(0, 0, 0, 0); + let mut points: Vec> = Vec::default(); + for _ in 0..100 { + if let Some(BrushPixel { position, .. }) = iter.next() { + points.push(Vector2::new(position.x, position.y)); + rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0)); + } else { + break; + } + } + assert_eq!( + rect, + Rect::new(-2, -2, 4, 4), + "{:?} {:?}", + iter.bounds(), + points + ); + } +} diff --git a/fyrox-impl/src/scene/terrain/geometry.rs b/fyrox-impl/src/scene/terrain/geometry.rs index d9baf7572..6a40556a0 100644 --- a/fyrox-impl/src/scene/terrain/geometry.rs +++ b/fyrox-impl/src/scene/terrain/geometry.rs @@ -1,6 +1,6 @@ use crate::{ core::{ - algebra::{Vector2, Vector3}, + algebra::{Vector2, Vector3, Vector4}, math::TriangleDefinition, }, renderer::framework::geometry_buffer::ElementRange, @@ -24,7 +24,22 @@ pub struct TerrainGeometry { } impl TerrainGeometry { - /// Create a grid mesh with the given number of rows and columns. + /// Create a grid mesh with the given number of rows and columns of vertices. + /// For example, if mesh_size were (3,3), the resulting mesh would have 9 vertices + /// and 4 quads, each made of two triangles, for a total of 8 triangles. + /// + /// Because the mesh will be divided into quadrants, the mesh_size *must not* be smaller than 3 + /// in either dimension. + /// + /// Quadrants are calculated by dividing each dimension of the mesh by 2. + /// This will only produce equal-sized quadrants if (mesh_size.x - 1) is even + /// and (mesh_size.y - 1) is even. + /// + /// If mesh_size is (3,3), then the 4 quads will be split evenly between + /// the four quadrants, one quad per quadrant. + /// If mesh_size is (4,4), then the 9 quads will be split unevenly, with one + /// quadrant getting 1 quad, two quadrants getting 2 quads, and one quardrant getting 4 quads. + /// If mesh_size is (5,5), then the 16 quads will be split evenly, with each quardant getting 4 quads. pub fn new(mesh_size: Vector2) -> Self { let mut surface_data = SurfaceData::new( VertexBuffer::new::(0, vec![]).unwrap(), @@ -43,9 +58,8 @@ impl TerrainGeometry { .push_vertex(&StaticVertex { position: Vector3::new(kx, 0.0, kz), tex_coord: Vector2::new(kx, kz), - // Normals and tangents will be calculated later. - normal: Default::default(), - tangent: Default::default(), + normal: Vector3::new(0.0, 1.0, 0.0), + tangent: Vector4::new(1.0, 0.0, 0.0, -1.0), }) .unwrap(); } @@ -54,26 +68,23 @@ impl TerrainGeometry { let mut geometry_buffer_mut = surface_data.geometry_buffer.modify(); - let half_size = mesh_size / 2; + let half_size = Vector2::new(mesh_size.x - 1, mesh_size.y - 1) / 2; let mut quadrants = [ElementRange::Full; 4]; for ((x_range, y_range), quadrant) in [ - (0..(half_size.x + 1), 0..(half_size.y + 1)), - ((half_size.x - 1)..mesh_size.x, 0..(half_size.y + 1)), - ( - (half_size.x - 1)..mesh_size.x, - (half_size.y - 1)..mesh_size.y, - ), - (0..(half_size.x + 1), (half_size.y - 1)..mesh_size.y), + (0..half_size.x, 0..half_size.y), + (half_size.x..mesh_size.x - 1, 0..half_size.y), + (half_size.x..mesh_size.x - 1, half_size.y..mesh_size.y - 1), + (0..half_size.x, half_size.y..mesh_size.y - 1), ] .into_iter() .zip(&mut quadrants) { let offset = geometry_buffer_mut.len(); - for iy in y_range.start..y_range.end - 1 { + for iy in y_range { let iy_next = iy + 1; - for x in x_range.start..x_range.end - 1 { + for x in x_range.clone() { let x_next = x + 1; let i0 = iy * mesh_size.x + x; @@ -93,8 +104,10 @@ impl TerrainGeometry { } drop(geometry_buffer_mut); - surface_data.calculate_normals().unwrap(); - surface_data.calculate_tangents().unwrap(); + // There is no need to calculate normals and tangents when they will always be the same for + // all vertices. + //surface_data.calculate_normals().unwrap(); + //surface_data.calculate_tangents().unwrap(); Self { data: SurfaceResource::new_ok(ResourceKind::Embedded, surface_data), @@ -102,3 +115,70 @@ impl TerrainGeometry { } } } + +#[cfg(test)] +mod tests { + use crate::scene::mesh::buffer::VertexAttributeUsage; + + use super::*; + + #[test] + fn geometry_3x3() { + let g = TerrainGeometry::new(Vector2::new(3, 3)); + for (i, q) in g.quadrants.iter().copied().enumerate() { + let ElementRange::Specific { count, .. } = q else { + panic!("Quadrant is full.") + }; + assert_eq!(count, 2, "Quadrant: {}", i); + } + } + #[test] + fn geometry_4x4() { + let g = TerrainGeometry::new(Vector2::new(4, 4)); + let counts = [2, 4, 8, 4]; + for (i, (q, c)) in g.quadrants.iter().copied().zip(counts).enumerate() { + let ElementRange::Specific { count, .. } = q else { + panic!("Quadrant is full.") + }; + assert_eq!(count, c, "Quadrant: {}", i); + } + } + #[test] + fn geometry_5x5() { + let g = TerrainGeometry::new(Vector2::new(5, 5)); + for (i, q) in g.quadrants.iter().copied().enumerate() { + let ElementRange::Specific { count, .. } = q else { + panic!("Quadrant is full.") + }; + assert_eq!(count, 8, "Quadrant: {}", i); + } + } + #[test] + fn normals() { + let g = TerrainGeometry::new(Vector2::new(3, 3)); + let vertices = &g.data.data_ref().vertex_buffer; + let attr_view = vertices + .attribute_view::>(VertexAttributeUsage::Normal) + .unwrap(); + for i in 0..vertices.vertex_count() as usize { + assert_eq!( + attr_view.get(i).unwrap().clone(), + Vector3::::new(0.0, 1.0, 0.0) + ); + } + } + #[test] + fn tangents() { + let g = TerrainGeometry::new(Vector2::new(3, 3)); + let vertices = &g.data.data_ref().vertex_buffer; + let attr_view = vertices + .attribute_view::>(VertexAttributeUsage::Tangent) + .unwrap(); + for i in 0..vertices.vertex_count() as usize { + assert_eq!( + attr_view.get(i).unwrap().clone(), + Vector4::::new(1.0, 0.0, 0.0, -1.0) + ); + } + } +} diff --git a/fyrox-impl/src/scene/terrain/mod.rs b/fyrox-impl/src/scene/terrain/mod.rs index 2e8dfeb33..d8a9f70bc 100644 --- a/fyrox-impl/src/scene/terrain/mod.rs +++ b/fyrox-impl/src/scene/terrain/mod.rs @@ -6,7 +6,7 @@ use crate::scene::node::RdcControlFlow; use crate::{ asset::Resource, core::{ - algebra::{Matrix4, Point3, Vector2, Vector3, Vector4}, + algebra::{Matrix2, Matrix4, Point3, Vector2, Vector3, Vector4}, arrayvec::ArrayVec, log::Log, math::{aabb::AxisAlignedBoundingBox, ray::Ray, ray_rect_intersection, Rect}, @@ -49,12 +49,155 @@ use std::{ ops::{Deref, DerefMut, Range}, }; +mod brushstroke; mod geometry; mod quadtree; +pub use brushstroke::*; + /// Current implementation version marker. pub const VERSION: u8 = 1; +/// Position of a single cell within terrain data. +#[derive(Debug, Clone)] +pub struct TerrainRect { + /// The pixel coordinates of the cell. + pub grid_position: Vector2, + /// The local 2D bounds of the cell. + pub bounds: Rect, +} + +impl TerrainRect { + /// Calculate the cell which contains the given local 2D coordinates when cells have the given size. + /// It is assumed that the (0,0) cell has its origin at local 2D point (0.0, 0.0). + pub fn from_local(position: Vector2, cell_size: Vector2) -> TerrainRect { + let cell_pos = Vector2::new(position.x / cell_size.x, position.y / cell_size.y); + let cell_pos = cell_pos.map(f32::floor); + let min = Vector2::new(cell_pos.x * cell_size.x, cell_pos.y * cell_size.y); + TerrainRect { + grid_position: cell_pos.map(|x| x as i32), + bounds: Rect::new(min.x, min.y, cell_size.x, cell_size.y), + } + } +} + +fn push_height_data(chunks: &mut Vec, new_chunk: &Chunk) { + if chunks + .iter() + .all(|c| c.grid_position != new_chunk.grid_position) + { + chunks.push(ChunkData::from_height(new_chunk)); + } +} + +fn push_layer_mask_data(chunks: &mut Vec, new_chunk: &Chunk, layer: usize) { + if chunks + .iter() + .all(|c| c.grid_position != new_chunk.grid_position) + { + chunks.push(ChunkData::from_layer_mask(new_chunk, layer)); + } +} + +/// Abstract access to terrain height data for use by brushes. +pub struct BrushableHeight<'a, 'b> { + /// The terrain to be edited + pub terrain: &'a mut Terrain, + /// The deta of the in-progress brushstroke. + pub stroke: &'b mut StrokeData, + /// Copies of the chunks that have been edited by the brushstroke, + /// as they were before being touched by the brush. + pub chunks: Vec, +} + +/// Abstract access to terrain layer mask data for use by brushes. +pub struct BrushableLayerMask<'a, 'b> { + /// The terrain to be edited + pub terrain: &'a mut Terrain, + /// The deta of the in-progress brushstroke. + pub stroke: &'b mut StrokeData, + /// The layer to edit. + pub layer: usize, + /// Copies of the chunks that have been edited by the brushstroke, + /// as they were before being touched by the brush. + pub chunks: Vec, +} + +impl<'a, 'b> BrushableTerrainData for BrushableHeight<'a, 'b> { + fn get_value(&self, position: Vector2) -> f32 { + self.stroke + .get(position) + .map(|x| x.original_value) + .or_else(|| self.terrain.get_height(position)) + .unwrap_or(0.0) + } + + fn update(&mut self, position: Vector2, strength: f32, func: F) + where + F: FnOnce(&Self, f32) -> f32, + { + if let Some(pixel_value) = self.terrain.get_height(position) { + if let Some(pixel_value) = self.stroke.update_pixel(position, strength, pixel_value) { + let value = func(self, pixel_value); + self.terrain.update_height_pixel( + position, + |_| value, + |c| push_height_data(&mut self.chunks, c), + ); + } + } + } + + fn sum_kernel(&self, position: Vector2, kernel_radius: u32) -> f32 { + let r = kernel_radius as i32; + let mut sum = 0.0; + for x in position.x - r..=position.x + r { + for y in position.y - r..=position.y + r { + sum += self.get_value(Vector2::new(x, y)); + } + } + sum + } +} + +impl<'a, 'b> BrushableTerrainData for BrushableLayerMask<'a, 'b> { + fn get_value(&self, position: Vector2) -> u8 { + self.stroke + .get(position) + .map(|x| x.original_value) + .or_else(|| self.terrain.get_layer_mask(position, self.layer)) + .unwrap_or(0) + } + + fn update(&mut self, position: Vector2, strength: f32, func: F) + where + F: FnOnce(&Self, u8) -> u8, + { + if let Some(pixel_value) = self.terrain.get_layer_mask(position, self.layer) { + if let Some(pixel_value) = self.stroke.update_pixel(position, strength, pixel_value) { + let value = func(self, pixel_value); + self.terrain + .update_mask_pixel(position, self.layer, |_| value); + let chunk_pos = self.terrain.chunk_containing_mask_pos(position); + if let Some(chunk) = self.terrain.find_chunk(chunk_pos) { + push_layer_mask_data(&mut self.chunks, chunk, self.layer); + } + } + } + } + + fn sum_kernel(&self, position: Vector2, kernel_radius: u32) -> f32 { + let r = kernel_radius as i32; + let mut sum: u32 = 0; + for x in position.x - r..=position.x + r { + for y in position.y - r..=position.y + r { + sum += self.get_value(Vector2::new(x, y)) as u32; + } + } + sum as f32 + } +} + /// Layers is a material Terrain can have as many layers as you want, but each layer slightly decreases /// performance, so keep amount of layers on reasonable level (1 - 5 should be enough for most /// cases). @@ -130,6 +273,81 @@ fn make_height_map_texture(height_map: Vec, size: Vector2) -> TextureR make_height_map_texture_internal(height_map, size).unwrap() } +/// A copy of a layer of data from a chunk. +/// It can be height data or mask data, since the type is erased. +/// The layer that this data represents must be remembered externally. +pub struct ChunkData { + /// The grid position of the original chunk. + pub grid_position: Vector2, + /// The type-erased data from either the height or one of the layers of the chunk. + pub content: Box<[u8]>, +} + +impl std::fmt::Debug for ChunkData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ChunkData") + .field("grid_position", &self.grid_position) + .field("content", &format!("[..](len: {})", &self.content.len())) + .finish() + } +} + +impl ChunkData { + /// Create a ChunkData object using data from the height map of the given chunk. + pub fn from_height(chunk: &Chunk) -> ChunkData { + let data = Box::<[u8]>::from(chunk.heightmap().data_ref().data()); + ChunkData { + grid_position: chunk.grid_position, + content: data, + } + } + /// Create a ChunkData object using data from a layer mask of the given chunk. + pub fn from_layer_mask(chunk: &Chunk, layer: usize) -> ChunkData { + let resource = &chunk.layer_masks[layer]; + let data = Box::<[u8]>::from(resource.data_ref().data()); + ChunkData { + grid_position: chunk.grid_position, + content: data, + } + } + /// Swap the content of this data with the content of the given chunk's height map. + pub fn swap_height(&mut self, chunk: &mut Chunk) { + let mut state = chunk.heightmap().state(); + let mut modify = state.data().unwrap().modify(); + for (a, b) in modify.data_mut().iter_mut().zip(self.content.iter_mut()) { + std::mem::swap(a, b); + } + } + /// Swap the content of this data with the content of the given chunk's mask layer. + pub fn swap_layer_mask(&mut self, chunk: &mut Chunk, layer: usize) { + let mut state = chunk.layer_masks[layer].state(); + let mut modify = state.data().unwrap().modify(); + for (a, b) in modify.data_mut().iter_mut().zip(self.content.iter_mut()) { + std::mem::swap(a, b); + } + } + /// Swap the height data of the a chunk from the list with the height data in this object. + /// The given list of chunks will be searched to find the chunk that matches `grid_position`. + pub fn swap_height_from_list(&mut self, chunks: &mut [Chunk]) { + for c in chunks { + if c.grid_position == self.grid_position { + self.swap_height(c); + break; + } + } + } + /// Swap the layer mask data of a particular layer of a chunk from the list with the data in this object. + /// The given list of chunks will be searched to find the chunk that matches `grid_position`. + pub fn swap_layer_mask_from_list(&mut self, chunks: &mut [Chunk], layer: usize) { + for c in chunks { + if c.grid_position == self.grid_position { + self.swap_layer_mask(c, layer); + break; + } + } + } +} + /// Chunk is smaller block of a terrain. Terrain can have as many chunks as you need, which always arranged in a /// grid. You can add chunks from any side of a terrain. Chunks could be considered as a "sub-terrain", which could /// use its own set of materials for layers. This could be useful for different biomes, to prevent high amount of @@ -229,11 +447,16 @@ impl Visit for Chunk { } VERSION => { self.heightmap.visit("Heightmap", &mut region)?; - self.position.visit("Position", &mut region)?; + // We do not need to visit position, since its value is implied by grid_position. + //self.position.visit("Position", &mut region)?; self.physical_size.visit("PhysicalSize", &mut region)?; self.height_map_size.visit("HeightMapSize", &mut region)?; self.layer_masks.visit("LayerMasks", &mut region)?; self.grid_position.visit("GridPosition", &mut region)?; + // Set position to have the value implied by grid_position + if region.is_reading() { + self.position = self.position() + } let _ = self.block_size.visit("BlockSize", &mut region); } _ => (), @@ -265,7 +488,16 @@ impl Chunk { /// Returns position of the chunk in local 2D coordinates relative to origin of the /// terrain. pub fn local_position(&self) -> Vector2 { - map_to_local(self.position) + map_to_local(self.position()) + } + + /// The position of the chunk within the terrain based on its `grid_position` and `physical_size`. + pub fn position(&self) -> Vector3 { + Vector3::new( + self.grid_position.x as f32 * self.physical_size.x, + 0.0, + self.grid_position.y as f32 * self.physical_size.y, + ) } /// Returns a reference to height map. @@ -501,7 +733,7 @@ impl Chunk { /// Performs debug drawing of the chunk. It draws internal quad-tree structure for debugging purposes. pub fn debug_draw(&self, transform: &Matrix4, ctx: &mut SceneDrawingContext) { - let transform = *transform * Matrix4::new_translation(&self.position); + let transform = *transform * Matrix4::new_translation(&self.position()); self.quad_tree .debug_draw(&transform, self.height_map_size, self.physical_size, ctx) @@ -509,7 +741,12 @@ impl Chunk { fn set_block_size(&mut self, block_size: Vector2) { self.block_size = block_size; - self.quad_tree = make_quad_tree(&self.heightmap, self.height_map_size, block_size); + self.update_quad_tree(); + } + + /// Recalculates the quad tree for this chunk. + pub fn update_quad_tree(&mut self) { + self.quad_tree = make_quad_tree(&self.heightmap, self.height_map_size, self.block_size); } } @@ -591,6 +828,36 @@ pub struct TerrainRayCastResult { /// As usual, to have collisions working you need to create a rigid body and add an appropriate collider to it. /// In case of terrains you need to create a collider with `Heightfield` shape and specify your terrain as a /// geometry source. +/// +/// ## Coordinate Spaces +/// +/// Terrains operate in several systems of coordinates depending upon which aspect of the terrain is being measured. +/// +/// - **Local:** These are the 3D `f32` coordinates of the Terrain node that are transformed to world space by the +/// [Base::global_transform]. It is measured in meters. +/// - **Local 2D:** These are the 2D `f32` coordinates formed by taking the (x,y,z) of local coordinates and turning them +/// into (x,z), with y removed and z becoming the new y. +/// The size of chunks in these coordinates is set by [Terrain::chunk_size]. +/// - **Grid Position:** These are the 2D `i32` coordinates that represent a chunk's position within the regular grid of +/// chunks that make up a terrain. The *local 2D* position of a chunk can be calculated from its *grid position* by +/// multiplying its x and y coordinates by the x and y of [Terrain::chunk_size]. +/// - **Height Pixel Position:** These are the 2D `i32` coordinates that measure position across the x and z axes of +/// the terrain using pixels in the height data of each chunk. (0,0) is the position of the Terrain node. +/// The *height pixel position* of a chunk can be calculated from its *grid position* by +/// multiplying its x and y coordinates by (x - 1) and (y - 1) of [Terrain::height_map_size]. +/// Subtracting 1 from each dimension is necessary because the height map data of chunks overlaps by one pixel +/// on each edge, so the distance between the origins of two adjacent chunks is one less than height_map_size. +/// - **Mask Pixel Position:** These are the 2D `i32` coordinates that measure position across the x and z axes of +/// the terrain using pixels of the mask data of each chunk. (0,0) is the position of the (0,0) pixel of the +/// mask texture of the (0,0) chunk. +/// This means that (0,0) is offset from the position of the Terrain node by a half-pixel in the x direction +/// and a half-pixel in the z direction. +/// The size of each pixel is determined by [Terrain::chunk_size] and [Terrain::mask_size]. +/// +/// The size of blocks and the size of quad tree nodes is measured in height pixel coordinates, and these measurements +/// count the number of pixels needed to render the vertices of that part of the terrain, which means that they +/// overlap with their neighbors just as chunks overlap. Two adjacent blocks share vertices along their edge, +/// so they also share pixels in the height map data. #[derive(Debug, Reflect, Clone)] pub struct Terrain { base: Base, @@ -602,6 +869,7 @@ pub struct Terrain { decal_layer_index: InheritableVariable, /// Size of the chunk, in meters. + /// This value becomes the [Chunk::physical_size] of newly created chunks. #[reflect( min_value = 0.001, description = "Size of the chunk, in meters.", @@ -626,14 +894,28 @@ pub struct Terrain { length_chunks: InheritableVariable>, /// Size of the height map per chunk, in pixels. Warning: any change to this value will result in resampling! + /// + /// Each dimension should be one greater than some power of 2, such as 5 = 4 + 1, 9 = 8 + 1, 17 = 16 + 1, and so on. + /// This is important because when chunks are being split into quadrants for LOD, the splits must always happen + /// along its vertices, and there should be an equal number of vertices on each side of each split. + /// If there cannot be an equal number of vertices on each side of the split, then the split will be made + /// so that the number of vertices is as close to equal as possible, but this may result in vertices not being + /// properly aligned between adjacent blocks. #[reflect( min_value = 2.0, step = 1.0, - description = "Size of the height map per chunk, in pixels. Warning: any change to this value will result in resampling!", + description = "Size of the height map per chunk, in pixels. Should be a power of 2 plus 1, for example: 5, 9, 17, etc. \ + Warning: any change to this value will result in resampling!", setter = "set_height_map_size" )] height_map_size: InheritableVariable>, + /// Size of the mesh block that will be scaled to various sizes to render the terrain at various levels of detail, + /// as measured by counting vertices along each dimension. + /// + /// Each dimension should be one greater than some power of 2, such as 5 = 4 + 1, 9 = 8 + 1, 17 = 16 + 1, and so on. + /// This helps the vertices of the block to align with the pixels of the height data texture, since the height data + /// texture should also have dimensions that are one greater than some power of 2. #[reflect(min_value = 8.0, step = 1.0, setter = "set_block_size")] block_size: InheritableVariable>, @@ -845,32 +1127,23 @@ impl TypeUuidProvider for Terrain { } impl Terrain { - /// Returns chunk size in meters. + /// Returns chunk size in meters. This is equivalent to [Chunk::physical_size]. pub fn chunk_size(&self) -> Vector2 { *self.chunk_size } /// Sets new chunk size of the terrain (in meters). All chunks in the terrain will be repositioned according - /// to their positions on the grid. + /// to their positions on the grid. Return the previous chunk size. pub fn set_chunk_size(&mut self, chunk_size: Vector2) -> Vector2 { let old = *self.chunk_size; self.chunk_size.set_value_and_mark_modified(chunk_size); // Re-position each chunk according to its position on the grid. - for (z, iy) in (*self.length_chunks) - .clone() - .zip(0..self.length_chunks.len()) - { - for (x, ix) in (*self.width_chunks).clone().zip(0..self.width_chunks.len()) { - let position = Vector3::new( - x as f32 * self.chunk_size.x, - 0.0, - z as f32 * self.chunk_size.y, - ); - + for iy in 0..self.length_chunks.len() { + for ix in 0..self.width_chunks.len() { let chunk = &mut self.chunks[iy * self.width_chunks.len() + ix]; - chunk.position = position; chunk.physical_size = chunk_size; + chunk.position = chunk.position(); } } @@ -880,6 +1153,8 @@ impl Terrain { } /// Returns height map dimensions along each axis. + /// This is measured in *pixels* and gives the size of each chunk, + /// including the 1 pixel overlap that each chunk shares with its neighbors. pub fn height_map_size(&self) -> Vector2 { *self.height_map_size } @@ -893,7 +1168,8 @@ impl Terrain { old } - /// Sets the new block size. Block size defines "granularity" of the terrain; the minimal terrain patch that + /// Sets the new block size, measured in height map pixels. + /// Block size defines "granularity" of the terrain; the minimal terrain patch that /// will be used for rendering. It directly affects level-of-detail system of the terrain. **Warning:** This /// method is very heavy and should not be used at every frame! pub fn set_block_size(&mut self, block_size: Vector2) -> Vector2 { @@ -906,12 +1182,12 @@ impl Terrain { old } - /// Returns current block size of the terrain. + /// Returns current block size of the terrain as measured by counting vertices along each axis of the block mesh. pub fn block_size(&self) -> Vector2 { *self.block_size } - /// Returns the total amount of pixels along each axis of the layer blending mask. + /// Returns the number of pixels along each axis of the layer blending mask. pub fn mask_size(&self) -> Vector2 { *self.mask_size } @@ -1018,6 +1294,27 @@ impl Terrain { &mut self.chunks } + /// Return the chunk with the matching [Chunk::grid_position]. + pub fn find_chunk(&self, grid_position: Vector2) -> Option<&Chunk> { + self.chunks + .iter() + .find(|c| c.grid_position == grid_position) + } + + /// Return the chunk with the matching [Chunk::grid_position]. + pub fn find_chunk_mut(&mut self, grid_position: Vector2) -> Option<&mut Chunk> { + self.chunks + .iter_mut() + .find(|c| c.grid_position == grid_position) + } + + /// Create new quad trees for every chunk in the terrain. + pub fn update_quad_trees(&mut self) { + for c in self.chunks.iter_mut() { + c.update_quad_tree(); + } + } + /// Sets new decal layer index. It defines which decals will be applies to the mesh, /// for example iff a decal has index == 0 and a mesh has index == 0, then decals will /// be applied. This allows you to apply decals only on needed surfaces. @@ -1036,6 +1333,331 @@ impl Terrain { project(self.global_transform(), p) } + /// The size of each cell of the height grid in local 2D units. + pub fn height_grid_scale(&self) -> Vector2 { + let cell_width = self.chunk_size.x / (self.height_map_size.x - 1) as f32; + let cell_length = self.chunk_size.y / (self.height_map_size.y - 1) as f32; + Vector2::new(cell_width, cell_length) + } + + /// The size of each cell of the mask grid in local 2D units. + pub fn mask_grid_scale(&self) -> Vector2 { + let cell_width = self.chunk_size.x / self.mask_size.x as f32; + let cell_length = self.chunk_size.y / self.mask_size.y as f32; + Vector2::new(cell_width, cell_length) + } + + /// Calculate which cell of the height grid contains the given local 2D position. + pub fn get_height_grid_square(&self, position: Vector2) -> TerrainRect { + TerrainRect::from_local(position, self.height_grid_scale()) + } + + /// Calculate which cell of the mask grid contains the given local 2D position. + pub fn get_mask_grid_square(&self, position: Vector2) -> TerrainRect { + let cell_size = self.mask_grid_scale(); + let half_size = cell_size / 2.0; + let position = position - half_size; + let mut rect = TerrainRect::from_local(position, cell_size); + rect.bounds.position += half_size; + rect + } + + /// Return the value of the layer mask at the given mask pixel position. + pub fn get_layer_mask(&self, position: Vector2, layer: usize) -> Option { + let chunk_pos = self.chunk_containing_mask_pos(position); + let chunk = self.find_chunk(chunk_pos)?; + let origin = self.chunk_mask_pos_origin(chunk_pos); + let pos = (position - origin).map(|x| x as usize); + let index = pos.y * self.mask_size.x as usize + pos.x; + let texture_data = chunk.layer_masks[layer].data_ref(); + let mask_data = texture_data.data(); + Some(mask_data[index]) + } + + /// Return the value of the height map at the given height pixel position. + pub fn get_height(&self, position: Vector2) -> Option { + let chunk_pos = self.chunk_containing_height_pos(position); + let origin = self.chunk_height_pos_origin(chunk_pos); + let pos = (position - origin).map(|x| x as usize); + let end = self.height_map_size.map(|x| (x - 1) as usize); + if let h @ Some(_) = self.get_height_in_chunk(chunk_pos, pos) { + return h; + } + if pos.x == 0 { + if let h @ Some(_) = self.get_height_in_chunk( + Vector2::new(chunk_pos.x - 1, chunk_pos.y), + Vector2::new(pos.x + end.x, pos.y), + ) { + return h; + } + } + if pos.y == 0 { + if let h @ Some(_) = self.get_height_in_chunk( + Vector2::new(chunk_pos.x, chunk_pos.y - 1), + Vector2::new(pos.x, pos.y + end.y), + ) { + return h; + } + } + if pos.x == 0 && pos.y == 0 { + if let h @ Some(_) = self.get_height_in_chunk( + Vector2::new(chunk_pos.x - 1, chunk_pos.y - 1), + Vector2::new(pos.x + end.x, pos.y + end.y), + ) { + return h; + } + } + None + } + + fn get_height_in_chunk( + &self, + chunk_pos: Vector2, + pixel_pos: Vector2, + ) -> Option { + let index = pixel_pos.y * self.height_map_size.x as usize + pixel_pos.x; + let chunk = self.find_chunk(chunk_pos)?; + let texture_data = chunk.heightmap.as_ref().unwrap().data_ref(); + let height_map = texture_data.data_of_type::().unwrap(); + Some(height_map[index]) + } + + /// Return an interpolation of that the value should be for the given brush target + /// at the given local 2D position. + /// For height target, it returns the height. + /// For mask targets, it returns 0.0 for transparent and 1.0 for opaque. + pub fn interpolate_value(&self, position: Vector2, target: BrushTarget) -> f32 { + let grid_square = match target { + BrushTarget::HeightMap => self.get_height_grid_square(position), + BrushTarget::LayerMask { .. } => self.get_mask_grid_square(position), + }; + let p = grid_square.grid_position; + let b = grid_square.bounds; + let x0 = b.position.x; + let y0 = b.position.y; + let x1 = b.position.x + b.size.x; + let y1 = b.position.y + b.size.y; + let dx0 = position.x - x0; + let dx1 = x1 - position.x; + let dy0 = position.y - y0; + let dy1 = y1 - position.y; + let p00 = p; + let p01 = Vector2::new(p.x, p.y + 1); + let p10 = Vector2::new(p.x + 1, p.y); + let p11 = Vector2::new(p.x + 1, p.y + 1); + let (f00, f01, f10, f11) = match target { + BrushTarget::HeightMap => ( + self.get_height(p00).unwrap_or(0.0), + self.get_height(p01).unwrap_or(0.0), + self.get_height(p10).unwrap_or(0.0), + self.get_height(p11).unwrap_or(0.0), + ), + BrushTarget::LayerMask { layer } => ( + self.get_layer_mask(p00, layer).unwrap_or(0) as f32 / 255.0, + self.get_layer_mask(p01, layer).unwrap_or(0) as f32 / 255.0, + self.get_layer_mask(p10, layer).unwrap_or(0) as f32 / 255.0, + self.get_layer_mask(p11, layer).unwrap_or(0) as f32 / 255.0, + ), + }; + let value = f00 * dx1 * dy1 + f10 * dx0 * dy1 + f01 * dx1 * dy0 + f11 * dx0 * dy0; + value / (b.size.x * b.size.y) + } + + /// Convert height pixel position into local 2D position. + pub fn height_pos_to_local(&self, position: Vector2) -> Vector2 { + let pos = position.map(|x| x as f32); + let chunk_size = self.height_map_size.map(|x| (x - 1) as f32); + let physical_size = &self.chunk_size; + Vector2::new( + pos.x / chunk_size.x * physical_size.x, + pos.y / chunk_size.y * physical_size.y, + ) + } + + /// Convert mask pixel position into local 2D position. + pub fn mask_pos_to_local(&self, position: Vector2) -> Vector2 { + let pos = position.map(|x| x as f32 + 0.5); + let chunk_size = self.mask_size.map(|x| x as f32); + let physical_size = &self.chunk_size; + Vector2::new( + pos.x / chunk_size.x * physical_size.x, + pos.y / chunk_size.y * physical_size.y, + ) + } + + /// Determines the chunk containing the given height pixel coordinate. + /// Be aware that the edges of chunks overlap because the vertices along each edge of a chunk + /// have the same height as the corresponding vertices of the next chunk in that direction. + /// Due to this, if `position.x` is on the x-axis origin of the chunk returned by this method, + /// then the position is also contained in the chunk at x - 1. + /// Similarly, if `position.y` is on the y-axis origin, then the position is also in the y - 1 chunk. + /// If position is on the origin in both the x and y axes, then the position is actually contained + /// in 4 chunks. + pub fn chunk_containing_height_pos(&self, position: Vector2) -> Vector2 { + // Subtract 1 from x and y to exclude the overlapping pixel along both axes from the chunk size. + let chunk_size = self.height_map_size.map(|x| x as i32 - 1); + let x = position.x / chunk_size.x; + let y = position.y / chunk_size.y; + // Correct for the possibility of x or y being negative. + let x = if position.x < 0 && position.x % chunk_size.x != 0 { + x - 1 + } else { + x + }; + let y = if position.y < 0 && position.y % chunk_size.y != 0 { + y - 1 + } else { + y + }; + Vector2::new(x, y) + } + + /// Determines the position of the (0,0) coordinate of the given chunk + /// as measured in height pixel coordinates. + pub fn chunk_height_pos_origin(&self, chunk_grid_position: Vector2) -> Vector2 { + let chunk_size = *self.height_map_size; + // Subtract 1 from x and y to exclude the overlapping pixel along both axes from the chunk size. + let x = chunk_grid_position.x * (chunk_size.x as i32 - 1); + let y = chunk_grid_position.y * (chunk_size.y as i32 - 1); + Vector2::new(x, y) + } + + /// Determines the chunk containing the given mask pixel coordinate. + /// This method makes no guarantee that there is actually a chunk at the returned coordinates. + /// It returns the grid_position that the chunk would have if it existed. + pub fn chunk_containing_mask_pos(&self, position: Vector2) -> Vector2 { + // Subtract 1 from x and y to exclude the overlapping pixel along both axes from the chunk size. + let chunk_size = self.mask_size.map(|x| x as i32); + let x = position.x / chunk_size.x; + let y = position.y / chunk_size.y; + // Correct for the possibility of x or y being negative. + let x = if position.x < 0 && position.x % chunk_size.x != 0 { + x - 1 + } else { + x + }; + let y = if position.y < 0 && position.y % chunk_size.y != 0 { + y - 1 + } else { + y + }; + Vector2::new(x, y) + } + + /// Determines the position of the (0,0) coordinate of the given chunk + /// as measured in mask pixel coordinates. + pub fn chunk_mask_pos_origin(&self, chunk_grid_position: Vector2) -> Vector2 { + let chunk_size = *self.mask_size; + let x = chunk_grid_position.x * chunk_size.x as i32; + let y = chunk_grid_position.y * chunk_size.y as i32; + Vector2::new(x, y) + } + + /// Applies the given function to the value at the given position in mask pixel coordinates. + /// This method calls the given function with the mask value of that pixel. + /// If no chunk contains the given position, then the function is not called. + pub fn update_mask_pixel(&mut self, position: Vector2, layer: usize, func: F) + where + F: FnOnce(u8) -> u8, + { + let chunk_pos = self.chunk_containing_mask_pos(position); + let origin = self.chunk_mask_pos_origin(chunk_pos); + let pos = position - origin; + let index = (pos.y * self.mask_size.x as i32 + pos.x) as usize; + let Some(chunk) = self.find_chunk_mut(chunk_pos) else { + return; + }; + let mut texture_data = chunk.layer_masks[layer].data_ref(); + let mut texture_modifier = texture_data.modify(); + let mask = texture_modifier.data_mut_of_type::().unwrap(); + let value = &mut mask[index]; + *value = func(*value); + } + + /// Applies the given function to the value at the given position in height pixel coordinates. + /// This method calls the given function with the height value of that pixel. + /// The returned value is written to every chunk that contains that pixel, replacing the current value. + /// Most pixels are contained in only one chunk, but some pixels are contained in anywhere from zero to four chunks, + /// due to chunks overlapping at the edges and corners. + /// If no chunk contains the given position, then the function is not called. + pub fn update_height_pixel( + &mut self, + position: Vector2, + mut pixel_func: F, + mut chunk_func: G, + ) where + F: FnMut(f32) -> f32, + G: FnMut(&Chunk), + { + let chunk_pos = self.chunk_containing_height_pos(position); + let origin = self.chunk_height_pos_origin(chunk_pos); + let pos = (position - origin).map(|x| x as usize); + let mut result: Option = None; + let end = self.height_map_size.map(|x| (x - 1) as usize); + self.update_pixel_in_chunk( + chunk_pos, + pos, + &mut result, + &mut pixel_func, + &mut chunk_func, + ); + if pos.x == 0 { + self.update_pixel_in_chunk( + Vector2::new(chunk_pos.x - 1, chunk_pos.y), + Vector2::new(pos.x + end.x, pos.y), + &mut result, + &mut pixel_func, + &mut chunk_func, + ); + } + if pos.y == 0 { + self.update_pixel_in_chunk( + Vector2::new(chunk_pos.x, chunk_pos.y - 1), + Vector2::new(pos.x, pos.y + end.y), + &mut result, + &mut pixel_func, + &mut chunk_func, + ); + } + if pos.x == 0 && pos.y == 0 { + self.update_pixel_in_chunk( + Vector2::new(chunk_pos.x - 1, chunk_pos.y - 1), + Vector2::new(pos.x + end.x, pos.y + end.y), + &mut result, + &mut pixel_func, + &mut chunk_func, + ); + } + } + + fn update_pixel_in_chunk( + &mut self, + chunk_pos: Vector2, + pixel_pos: Vector2, + result: &mut Option, + pixel_func: F, + chunk_func: G, + ) where + F: FnOnce(f32) -> f32, + G: FnOnce(&Chunk), + { + let index = pixel_pos.y * self.height_map_size.x as usize + pixel_pos.x; + let Some(chunk) = self.find_chunk_mut(chunk_pos) else { + return; + }; + chunk_func(chunk); + let mut texture_data = chunk.heightmap.as_ref().unwrap().data_ref(); + let mut texture_modifier = texture_data.modify(); + let height_map = texture_modifier.data_mut_of_type::().unwrap(); + let value = &mut height_map[index]; + if let Some(new_value) = result { + *value = *new_value; + } else { + *value = pixel_func(*value); + *result = Some(*value); + } + } + /// Applies the given function to each pixel of the height map. pub fn for_each_height_map_pixel(&mut self, mut func: F) where @@ -1070,79 +1692,81 @@ impl Terrain { self.bounding_box_dirty.set(true); } + fn draw_data( + center: Vector2, + scale: Vector2, + brush: &Brush, + value: f32, + data: &mut D, + ) where + D: BrushableTerrainData, + V: BrushValue, + { + let scale_matrix = Matrix2::new(1.0 / scale.x, 0.0, 0.0, 1.0 / scale.y); + let transform = brush.transform * scale_matrix; + match brush.shape { + BrushShape::Circle { radius } => brush.mode.draw( + CircleBrushPixels::new(scale_matrix * center, radius, brush.hardness, transform), + data, + value, + brush.alpha, + ), + BrushShape::Rectangle { width, length } => brush.mode.draw( + RectBrushPixels::new( + scale_matrix * center, + Vector2::new(width, length), + brush.hardness, + transform, + ), + data, + value, + brush.alpha, + ), + } + } + /// Multi-functional drawing method. It uses given brush to modify terrain, see [`Brush`] docs for /// more info. - pub fn draw(&mut self, brush: &Brush) { - let center = project(self.global_transform(), brush.center).unwrap(); - - match brush.mode { - BrushMode::ModifyHeightMap { amount } => { - self.for_each_height_map_pixel(|pixel, pixel_position| { - let k = match brush.shape { - BrushShape::Circle { radius } => { - 1.0 - ((center - pixel_position).norm() / radius).powf(2.0) - } - BrushShape::Rectangle { .. } => 1.0, - }; - - if brush.shape.contains(center, pixel_position) { - *pixel += k * amount; - } - }); - } - BrushMode::DrawOnMask { layer, alpha } => { - if layer >= self.layers.len() { - return; - } - - let alpha = alpha.clamp(-1.0, 1.0); - - for chunk in self.chunks.iter_mut() { - let chunk_position = chunk.local_position(); - let mut texture_data = chunk.layer_masks[layer].data_ref(); - let mut texture_data_mut = texture_data.modify(); - - let (texture_width, texture_height) = - if let TextureKind::Rectangle { width, height } = texture_data_mut.kind() { - (width as usize, height as usize) - } else { - unreachable!("Mask must be a 2D greyscale image!") - }; - - for z in 0..texture_height { - let kz = z as f32 / (texture_height - 1) as f32; - for x in 0..texture_width { - let kx = x as f32 / (texture_width - 1) as f32; - - let pixel_position = chunk_position - + Vector2::new( - kx * chunk.physical_size.x, - kz * chunk.physical_size.y, - ); - - let k = match brush.shape { - BrushShape::Circle { radius } => { - 1.0 - ((center - pixel_position).norm() / radius).powf(4.0) - } - BrushShape::Rectangle { .. } => 1.0, - }; - - if brush.shape.contains(center, pixel_position) { - // We can draw on mask directly, without any problems because it has R8 pixel format. - let data = texture_data_mut.data_mut(); - let pixel = &mut data[z * texture_width + x]; - *pixel = (*pixel as f32 + k * alpha * 255.0).min(255.0) as u8; - } - } - } - } + /// - `position`: The position of the brush in global space. + /// - `brush`: The Brush structure defines how the terrain will be modified around the given position. + /// - `stroke`: The BrushStroke structure remembers information about a brushstroke across multiple calls to `draw`. + /// - `chunks`: The vector in which to save copies of the data from modified chunks for the purpose of creating an undo command. + /// This vector will be returned. + pub fn draw( + &mut self, + position: Vector3, + brush: &Brush, + stroke: &mut BrushStroke, + chunks: Vec, + ) -> Vec { + let center = project(self.global_transform(), position).unwrap(); + match brush.target { + BrushTarget::HeightMap => { + let scale = self.height_grid_scale(); + let value = stroke.value; + let mut data = BrushableHeight { + stroke: &mut stroke.height_pixels, + terrain: self, + chunks, + }; + Terrain::draw_data(center, scale, brush, value, &mut data); + let chunks = data.chunks; + self.update_quad_trees(); + chunks } - BrushMode::FlattenHeightMap { height } => { - self.for_each_height_map_pixel(|pixel, pixel_position| { - if brush.shape.contains(center, pixel_position) { - *pixel = height; - } - }); + BrushTarget::LayerMask { layer } => { + let scale = self.mask_grid_scale(); + let value = stroke.value; + let mut data = BrushableLayerMask { + stroke: &mut stroke.mask_pixels, + terrain: self, + layer, + chunks, + }; + Terrain::draw_data(center, scale, brush, value, &mut data); + let chunks = data.chunks; + self.update_quad_trees(); + chunks } } } @@ -1507,7 +2131,7 @@ impl NodeTrait for Terrain { .collect::>(); let chunk_transform = - self.global_transform() * Matrix4::new_translation(&chunk.position); + self.global_transform() * Matrix4::new_translation(&chunk.position()); // Use the `levels` list and the camera position to generate a list of all the positions // and scales where instances of the terrain geometry should appear in the render. @@ -1638,77 +2262,6 @@ impl NodeTrait for Terrain { } } -/// Shape of a brush. -#[derive(Copy, Clone, Reflect, Debug)] -pub enum BrushShape { - /// Circle with given radius. - Circle { - /// Radius of the circle. - radius: f32, - }, - /// Rectangle with given width and height. - Rectangle { - /// Width of the rectangle. - width: f32, - /// Length of the rectangle. - length: f32, - }, -} - -uuid_provider!(BrushShape = "a4dbfba0-077c-4658-9972-38384a8432f9"); - -impl BrushShape { - fn contains(&self, brush_center: Vector2, pixel_position: Vector2) -> bool { - match *self { - BrushShape::Circle { radius } => (brush_center - pixel_position).norm() < radius, - BrushShape::Rectangle { width, length } => Rect::new( - brush_center.x - width * 0.5, - brush_center.y - length * 0.5, - width, - length, - ) - .contains(pixel_position), - } - } -} - -/// Paint mode of a brush. It defines operation that will be performed on the terrain. -#[derive(Clone, PartialEq, PartialOrd, Reflect, Debug)] -pub enum BrushMode { - /// Modifies height map. - ModifyHeightMap { - /// An offset for height map. - amount: f32, - }, - /// Flattens height map. - FlattenHeightMap { - /// Fixed height value for flattening. - height: f32, - }, - /// Draws on a given layer. - DrawOnMask { - /// A layer to draw on. - layer: usize, - /// A value to put on mask. Range is [-1.0; 1.0] where negative values "erase" - /// values from mask, and positive - paints. - alpha: f32, - }, -} - -uuid_provider!(BrushMode = "48ad4cac-05f3-485a-b2a3-66812713841f"); - -/// Brush is used to modify terrain. It supports multiple shapes and modes. -#[derive(Clone, Reflect, Debug)] -pub struct Brush { - /// Center of the brush. - #[reflect(hidden)] - pub center: Vector3, - /// Shape of the brush. - pub shape: BrushShape, - /// Paint mode of the brush. - pub mode: BrushMode, -} - /// Terrain builder allows you to quickly build a terrain with required features. pub struct TerrainBuilder { base_builder: BaseBuilder, @@ -1744,12 +2297,12 @@ impl TerrainBuilder { pub fn new(base_builder: BaseBuilder) -> Self { Self { base_builder, - chunk_size: Vector2::new(16.0, 16.0), + chunk_size: Vector2::new(16.0, 16.0), // Vector2::new(16.0, 16.0) width_chunks: 0..2, length_chunks: 0..2, - mask_size: Vector2::new(256, 256), - height_map_size: Vector2::new(256, 256), - block_size: Vector2::new(32, 32), + mask_size: Vector2::new(256, 256), // Vector2::new(256, 256) + height_map_size: Vector2::new(257, 257), // Vector2::new(257, 257) + block_size: Vector2::new(33, 33), // Vector2::new(33,33) layers: Default::default(), decal_layer_index: 0, } diff --git a/fyrox-impl/src/scene/terrain/quadtree.rs b/fyrox-impl/src/scene/terrain/quadtree.rs index a34282b2f..0b45c3922 100644 --- a/fyrox-impl/src/scene/terrain/quadtree.rs +++ b/fyrox-impl/src/scene/terrain/quadtree.rs @@ -96,6 +96,7 @@ impl QuadTreeNode { /// * height_map_size: The number of rows and columns of the height data. /// * position: The position of the area represented by this node within the data. /// * node_size: The size of the area represented by this ndoe within the data. + /// Each node should overlap with its neighbors along the edges by one pixel. /// * max_size: Any node below this size will be a leaf. /// * level: The level of detail of this node. /// * index: The mutable pointer to the current persistent index. @@ -113,8 +114,26 @@ impl QuadTreeNode { ) -> Self { let mut min_height = f32::MAX; let mut max_height = f32::MIN; - for y in position.y..((position.y + node_size.y).min(height_map_size.y)) { - for x in position.x..((position.x + node_size.x).min(height_map_size.x)) { + let x_max = position.x + node_size.x; + let y_max = position.y + node_size.y; + assert!( + x_max <= height_map_size.x, + "position.x({}) + node_size.x({}) = {} > {} (height_map_size.x)", + position.x, + node_size.x, + x_max, + height_map_size.x + ); + assert!( + y_max <= height_map_size.y, + "position.y({}) + node_size.y({}) = {} > {} (height_map_size.y)", + position.y, + node_size.y, + y_max, + height_map_size.y + ); + for y in position.y..y_max { + for x in position.x..x_max { let height = height_map[(y * height_map_size.x + x) as usize]; if height < min_height { min_height = height; @@ -125,11 +144,17 @@ impl QuadTreeNode { } } - let kind = if node_size.x < max_size.x && node_size.y < max_size.y { + let kind = if node_size.x <= max_size.x && node_size.y <= max_size.y { QuadTreeNodeKind::Leaf } else { // Build children nodes recursively. - let new_size = node_size / 2; + // Convert pixel size into mesh size, counting the edges between vertices instead of counting vertices. + let real_size = Vector2::new(node_size.x - 1, node_size.y - 1); + // Calculate child size by taking half of the real size and adding 1 to convert back into pixel size. + let new_size = Vector2::new(real_size.x / 2 + 1, real_size.y / 2 + 1); + // The first pixel of the next node starts on the last pixel of the previous node, not on the first pixel beyond the previous node. + // Therefore we position the node at node_size.x - 1 instead of node_size.x. + let center_pos = Vector2::new(new_size.x - 1, new_size.y - 1); let next_level = level + 1; QuadTreeNodeKind::Branch { leafs: [ @@ -145,7 +170,7 @@ impl QuadTreeNode { Box::new(QuadTreeNode::new( height_map, height_map_size, - position + Vector2::new(new_size.x, 0), + position + Vector2::new(center_pos.x, 0), new_size, max_size, next_level, @@ -154,7 +179,7 @@ impl QuadTreeNode { Box::new(QuadTreeNode::new( height_map, height_map_size, - position + new_size, + position + center_pos, new_size, max_size, next_level, @@ -163,7 +188,7 @@ impl QuadTreeNode { Box::new(QuadTreeNode::new( height_map, height_map_size, - position + Vector2::new(0, new_size.y), + position + Vector2::new(0, center_pos.y), new_size, max_size, next_level, @@ -199,13 +224,18 @@ impl QuadTreeNode { height_map_size: Vector2, physical_size: Vector2, ) -> AxisAlignedBoundingBox { - let min_x = (self.position.x as f32 / height_map_size.x as f32) * physical_size.x; - let min_y = (self.position.y as f32 / height_map_size.y as f32) * physical_size.y; - - let max_x = - ((self.position.x + self.size.x) as f32 / height_map_size.x as f32) * physical_size.x; - let max_y = - ((self.position.y + self.size.y) as f32 / height_map_size.y as f32) * physical_size.y; + // Convert sizes form pixel sizes to mesh sizes. + // For calculating AABB, we do not care about the number of vertices; + // we care about the number of edges between vertices, which is one fewer. + let real_map_size = Vector2::new(height_map_size.x - 1, height_map_size.y - 1); + let real_node_size = Vector2::new(self.size.x - 1, self.size.y - 1); + let min_x = (self.position.x as f32 / real_map_size.x as f32) * physical_size.x; + let min_y = (self.position.y as f32 / real_map_size.y as f32) * physical_size.y; + + let max_x = ((self.position.x + real_node_size.x) as f32 / real_map_size.x as f32) + * physical_size.x; + let max_y = ((self.position.y + real_node_size.y) as f32 / real_map_size.y as f32) + * physical_size.y; let min = Vector3::new(min_x, self.min_height, min_y); let max = Vector3::new(max_x, self.max_height, max_y); diff --git a/fyrox-math/Cargo.toml b/fyrox-math/Cargo.toml index 3c4bc5719..0c2185418 100644 --- a/fyrox-math/Cargo.toml +++ b/fyrox-math/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/FyroxEngine/Fyrox" rust-version = "1.72" [dependencies] -rectutils = "0.1.0" +rectutils = "0.2.0" nalgebra = "0.32.3" arrayvec = "0.7.4" num-traits = "0.2.18" diff --git a/fyrox-math/src/lib.rs b/fyrox-math/src/lib.rs index 146c0aee9..b47b5ac8d 100644 --- a/fyrox-math/src/lib.rs +++ b/fyrox-math/src/lib.rs @@ -974,29 +974,20 @@ mod test { fn rect_clip_by() { let rect = Rect::new(0, 0, 10, 10); - assert_eq!(rect.clip_by(Rect::new(2, 2, 1, 1)), Rect::new(2, 2, 1, 1)); assert_eq!( - rect.clip_by(Rect::new(0, 0, 15, 15)), - Rect::new(0, 0, 10, 10) - ); - - // When there is no intersection. - assert_eq!( - rect.clip_by(Rect::new(-2, 1, 1, 1)), - Rect::new(0, 0, 10, 10) - ); - assert_eq!( - rect.clip_by(Rect::new(11, 1, 1, 1)), - Rect::new(0, 0, 10, 10) + rect.clip_by(Rect::new(2, 2, 1, 1)).unwrap(), + Rect::new(2, 2, 1, 1) ); assert_eq!( - rect.clip_by(Rect::new(1, -2, 1, 1)), - Rect::new(0, 0, 10, 10) - ); - assert_eq!( - rect.clip_by(Rect::new(1, 11, 1, 1)), + rect.clip_by(Rect::new(0, 0, 15, 15)).unwrap(), Rect::new(0, 0, 10, 10) ); + + // When there is no intersection. + assert!(rect.clip_by(Rect::new(-2, 1, 1, 1)).is_none()); + assert!(rect.clip_by(Rect::new(11, 1, 1, 1)).is_none()); + assert!(rect.clip_by(Rect::new(1, -2, 1, 1)).is_none()); + assert!(rect.clip_by(Rect::new(1, 11, 1, 1)).is_none()); } #[test] diff --git a/fyrox-ui/src/inspector/editors/matrix2.rs b/fyrox-ui/src/inspector/editors/matrix2.rs new file mode 100644 index 000000000..9c9791684 --- /dev/null +++ b/fyrox-ui/src/inspector/editors/matrix2.rs @@ -0,0 +1,96 @@ +use crate::{ + core::{algebra::Matrix2, num_traits::NumCast}, + inspector::{ + editors::{ + PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance, + PropertyEditorMessageContext, PropertyEditorTranslationContext, + }, + FieldKind, InspectorError, PropertyChanged, + }, + message::{MessageDirection, UiMessage}, + numeric::NumericType, + matrix2::{Matrix2EditorBuilder, Matrix2EditorMessage}, + widget::WidgetBuilder, + Thickness, +}; +use std::{any::TypeId, marker::PhantomData}; + +#[derive(Debug)] +pub struct Matrix2PropertyEditorDefinition { + pub phantom: PhantomData, +} + +impl Default for Matrix2PropertyEditorDefinition { + fn default() -> Self { + Self { + phantom: PhantomData, + } + } +} + +impl PropertyEditorDefinition + for Matrix2PropertyEditorDefinition +{ + fn value_type_id(&self) -> TypeId { + TypeId::of::>() + } + + fn create_instance( + &self, + ctx: PropertyEditorBuildContext, + ) -> Result { + let value = ctx.property_info.cast_value::>()?; + Ok(PropertyEditorInstance::Simple { + editor: Matrix2EditorBuilder::new( + WidgetBuilder::new().with_margin(Thickness::uniform(1.0)), + ) + .with_min(Matrix2::repeat( + ctx.property_info + .min_value + .and_then(NumCast::from) + .unwrap_or_else(T::min_value), + )) + .with_max(Matrix2::repeat( + ctx.property_info + .max_value + .and_then(NumCast::from) + .unwrap_or_else(T::max_value), + )) + .with_step(Matrix2::repeat( + ctx.property_info + .step + .and_then(NumCast::from) + .unwrap_or_else(T::one), + )) + .with_value(*value) + .build(ctx.build_context), + }) + } + + fn create_message( + &self, + ctx: PropertyEditorMessageContext, + ) -> Result, InspectorError> { + let value = ctx.property_info.cast_value::>()?; + Ok(Some(Matrix2EditorMessage::value( + ctx.instance, + MessageDirection::ToWidget, + *value, + ))) + } + + fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option { + if ctx.message.direction() == MessageDirection::FromWidget { + if let Some(Matrix2EditorMessage::Value(value)) = + ctx.message.data::>() + { + return Some(PropertyChanged { + owner_type_id: ctx.owner_type_id, + name: ctx.name.to_string(), + value: FieldKind::object(*value), + }); + } + } + None + } +} diff --git a/fyrox-ui/src/inspector/editors/mod.rs b/fyrox-ui/src/inspector/editors/mod.rs index b66e21db8..bafdaaeab 100644 --- a/fyrox-ui/src/inspector/editors/mod.rs +++ b/fyrox-ui/src/inspector/editors/mod.rs @@ -39,6 +39,7 @@ use crate::{ inherit::InheritablePropertyEditorDefinition, inspectable::InspectablePropertyEditorDefinition, key::KeyBindingPropertyEditorDefinition, + matrix2::Matrix2PropertyEditorDefinition, numeric::NumericPropertyEditorDefinition, quat::QuatPropertyEditorDefinition, range::RangePropertyEditorDefinition, @@ -108,6 +109,7 @@ pub mod immutable_string; pub mod inherit; pub mod inspectable; pub mod key; +pub mod matrix2; pub mod numeric; pub mod path; pub mod quat; @@ -413,6 +415,8 @@ impl PropertyEditorDefinitionContainer { Vector2, Vector2, Vector2, Vector2, Vector2, Vector2 } + reg_property_editor! { container, Matrix2PropertyEditorDefinition: default, f64, f32, i64, u64, i32, u32, i16, u16, i8, u8, usize, isize } + // Range + InheritableVariable> reg_property_editor! { container, RangePropertyEditorDefinition: new, f64, f32, i64, u64, i32, u32, i16, u16, i8, u8, usize, isize } reg_property_editor! { container, InheritablePropertyEditorDefinition: new, diff --git a/fyrox-ui/src/lib.rs b/fyrox-ui/src/lib.rs index 5498ca95c..987d824f6 100644 --- a/fyrox-ui/src/lib.rs +++ b/fyrox-ui/src/lib.rs @@ -211,6 +211,7 @@ pub mod inspector; pub mod key; pub mod list_view; pub mod loader; +pub mod matrix2; pub mod menu; pub mod message; pub mod messagebox; @@ -1683,7 +1684,11 @@ impl UserInterface { Rect::new(0.0, 0.0, self.screen_size.x, self.screen_size.y) }; - node.clip_bounds.set(screen_bounds.clip_by(parent_bounds)); + node.clip_bounds.set( + screen_bounds + .clip_by(parent_bounds) + .unwrap_or(screen_bounds), + ); for &child in node.children() { self.calculate_clip_bounds(child, node.clip_bounds.get()); diff --git a/fyrox-ui/src/matrix2.rs b/fyrox-ui/src/matrix2.rs new file mode 100644 index 000000000..475e85cbb --- /dev/null +++ b/fyrox-ui/src/matrix2.rs @@ -0,0 +1,264 @@ +use crate::{ + core::{ + algebra::Matrix2, num_traits, pool::Handle, reflect::prelude::*, type_traits::prelude::*, + visitor::prelude::*, + }, + define_constructor, + grid::{Column, GridBuilder, Row}, + message::{MessageDirection, UiMessage}, + numeric::{NumericType, NumericUpDownBuilder, NumericUpDownMessage}, + widget::WidgetBuilder, + BuildContext, Control, Thickness, UiNode, UserInterface, Widget, +}; +use std::ops::{Deref, DerefMut}; + +fn make_numeric_input( + ctx: &mut BuildContext, + column: usize, + row: usize, + value: T, + min: T, + max: T, + step: T, + editable: bool, + precision: usize, +) -> Handle { + NumericUpDownBuilder::new( + WidgetBuilder::new() + .on_row(row) + .on_column(column) + .with_margin(Thickness { + left: 1.0, + top: 0.0, + right: 1.0, + bottom: 0.0, + }), + ) + .with_precision(precision) + .with_value(value) + .with_min_value(min) + .with_max_value(max) + .with_step(step) + .with_editable(editable) + .build(ctx) +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Matrix2EditorMessage +where + T: NumericType, +{ + Value(Matrix2), +} + +impl Matrix2EditorMessage +where + T: NumericType, +{ + define_constructor!(Matrix2EditorMessage:Value => fn value(Matrix2), layout: false); +} + +#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)] +pub struct Matrix2Editor +where + T: NumericType, +{ + pub widget: Widget, + pub fields: [Handle; 4], + #[reflect(hidden)] + #[visit(skip)] + pub value: Matrix2, + #[reflect(hidden)] + #[visit(skip)] + pub min: Matrix2, + #[reflect(hidden)] + #[visit(skip)] + pub max: Matrix2, + #[reflect(hidden)] + #[visit(skip)] + pub step: Matrix2, +} + +impl Deref for Matrix2Editor +where + T: NumericType, +{ + type Target = Widget; + + fn deref(&self) -> &Self::Target { + &self.widget + } +} + +impl DerefMut for Matrix2Editor +where + T: NumericType, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.widget + } +} + +impl TypeUuidProvider for Matrix2Editor { + fn type_uuid() -> Uuid { + combine_uuids( + uuid!("9f05427a-5862-4574-bb21-ebaf52aa8c72"), + T::type_uuid(), + ) + } +} + +impl Control for Matrix2Editor +where + T: NumericType, +{ + fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) { + self.widget.handle_routed_message(ui, message); + + if let Some(&NumericUpDownMessage::Value(value)) = message.data::>() + { + if message.direction() == MessageDirection::FromWidget { + for (i, field) in self.fields.iter().enumerate() { + if message.destination() == *field { + let mut new_value = self.value; + new_value[i] = value; + ui.send_message(Matrix2EditorMessage::value( + self.handle(), + MessageDirection::ToWidget, + new_value, + )); + } + } + } + } else if let Some(&Matrix2EditorMessage::Value(new_value)) = + message.data::>() + { + if message.direction() == MessageDirection::ToWidget { + let mut changed = false; + + for i in 0..4 { + let editor = self.fields[i]; + let current = &mut self.value[i]; + let min = self.min[i]; + let max = self.max[i]; + let new = num_traits::clamp(new_value[i], min, max); + + if *current != new { + *current = new; + ui.send_message(NumericUpDownMessage::value( + editor, + MessageDirection::ToWidget, + new, + )); + changed = true; + } + } + + if changed { + ui.send_message(message.reverse()); + } + } + } + } +} + +pub struct Matrix2EditorBuilder +where + T: NumericType, +{ + widget_builder: WidgetBuilder, + value: Matrix2, + editable: bool, + min: Matrix2, + max: Matrix2, + step: Matrix2, + precision: usize, +} + +impl Matrix2EditorBuilder +where + T: NumericType, +{ + pub fn new(widget_builder: WidgetBuilder) -> Self { + Self { + widget_builder, + value: Matrix2::identity(), + editable: true, + min: Matrix2::repeat(T::min_value()), + max: Matrix2::repeat(T::max_value()), + step: Matrix2::repeat(T::one()), + precision: 3, + } + } + + pub fn with_value(mut self, value: Matrix2) -> Self { + self.value = value; + self + } + + pub fn with_editable(mut self, editable: bool) -> Self { + self.editable = editable; + self + } + + pub fn with_min(mut self, min: Matrix2) -> Self { + self.min = min; + self + } + + pub fn with_max(mut self, max: Matrix2) -> Self { + self.max = max; + self + } + + pub fn with_step(mut self, step: Matrix2) -> Self { + self.step = step; + self + } + + pub fn with_precision(mut self, precision: usize) -> Self { + self.precision = precision; + self + } + + pub fn build(self, ctx: &mut BuildContext) -> Handle { + let mut fields = Vec::new(); + let mut children = Vec::new(); + + for y in 0..2 { + for x in 0..2 { + let field = make_numeric_input( + ctx, + x, + y, + self.value[(y, x)], + self.min[(y, x)], + self.max[(y, x)], + self.step[(y, x)], + self.editable, + self.precision, + ); + children.push(field); + fields.push(field); + } + } + + let grid = GridBuilder::new(WidgetBuilder::new().with_children(children)) + .add_row(Row::stretch()) + .add_row(Row::stretch()) + .add_column(Column::stretch()) + .add_column(Column::stretch()) + .build(ctx); + + let node = Matrix2Editor { + widget: self.widget_builder.with_child(grid).build(), + fields: fields.try_into().unwrap(), + value: self.value, + min: self.min, + max: self.max, + step: self.step, + }; + + ctx.add_node(UiNode::new(node)) + } +} From 763982d1b591dad9e481e2453562caf7e359ea5b Mon Sep 17 00:00:00 2001 From: b-guild Date: Sun, 30 Jun 2024 03:58:18 -0700 Subject: [PATCH 02/10] Multi-thread terrain brush --- editor/src/interaction/terrain.rs | 188 ++-- editor/src/scene/commands/terrain.rs | 2 +- fyrox-impl/src/scene/terrain/brushstroke.rs | 652 ------------ .../scene/terrain/brushstroke/brushraster.rs | 434 ++++++++ .../src/scene/terrain/brushstroke/mod.rs | 931 ++++++++++++++++++ .../scene/terrain/brushstroke/strokechunks.rs | 326 ++++++ fyrox-impl/src/scene/terrain/mod.rs | 369 ++----- fyrox-math/src/lib.rs | 1 + fyrox-math/src/segment.rs | 269 +++++ 9 files changed, 2152 insertions(+), 1020 deletions(-) delete mode 100644 fyrox-impl/src/scene/terrain/brushstroke.rs create mode 100644 fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs create mode 100644 fyrox-impl/src/scene/terrain/brushstroke/mod.rs create mode 100644 fyrox-impl/src/scene/terrain/brushstroke/strokechunks.rs create mode 100644 fyrox-math/src/segment.rs diff --git a/editor/src/interaction/terrain.rs b/editor/src/interaction/terrain.rs index 38cfb9e69..5ff77a45e 100644 --- a/editor/src/interaction/terrain.rs +++ b/editor/src/interaction/terrain.rs @@ -1,4 +1,8 @@ -use fyrox::scene::terrain::ChunkData; +use fyrox::fxhash::FxHashMap; +use fyrox::resource::texture::TextureResource; +use fyrox::scene::terrain::brushstroke::{ + BrushSender, BrushThreadMessage, TerrainTextureData, TerrainTextureKind, UndoData, +}; use crate::fyrox::core::uuid::{uuid, Uuid}; use crate::fyrox::core::TypeUuidProvider; @@ -36,9 +40,8 @@ use crate::fyrox::{ MeshBuilder, RenderPath, }, node::Node, - terrain::{ - Brush, BrushMode, BrushShape, BrushStroke, BrushTarget, Terrain, TerrainRayCastResult, - }, + terrain::brushstroke::{Brush, BrushMode, BrushShape, BrushStroke, BrushTarget}, + terrain::{Terrain, TerrainRayCastResult}, }, }; use crate::interaction::make_interaction_mode_button; @@ -56,20 +59,36 @@ use crate::{ MSG_SYNC_FLAG, }; use fyrox::asset::untyped::ResourceKind; +use std::sync::mpsc::channel; use std::sync::Arc; fn modify_clamp(x: &mut f32, delta: f32, min: f32, max: f32) { *x = (*x + delta).clamp(min, max) } +fn handle_undo_chunks(undo_chunks: UndoData, sender: &MessageSender) { + match undo_chunks.target { + BrushTarget::HeightMap => sender.do_command(ModifyTerrainHeightCommand::new( + undo_chunks.node, + undo_chunks.chunks, + )), + BrushTarget::LayerMask { layer } => sender.do_command(ModifyTerrainLayerMaskCommand::new( + undo_chunks.node, + undo_chunks.chunks, + layer, + )), + } +} + pub struct TerrainInteractionMode { - undo_chunks: Vec, message_sender: MessageSender, + brush_sender: Option, interacting: bool, brush_gizmo: BrushGizmo, + prev_brush_position: Option>, brush_position: Vector3, + brush_value: f32, brush: Brush, - stroke: BrushStroke, brush_panel: BrushPanel, scene_viewer_frame: Handle, } @@ -94,34 +113,108 @@ impl TerrainInteractionMode { BrushPanel::new(&mut engine.user_interfaces.first_mut().build_ctx(), &brush); Self { + message_sender, + brush_sender: None, brush_panel, - undo_chunks: Default::default(), brush_gizmo: BrushGizmo::new(game_scene, engine), interacting: false, - message_sender, brush, + brush_value: Default::default(), + prev_brush_position: None, brush_position: Vector3::default(), - stroke: BrushStroke::default(), scene_viewer_frame, } } fn modify_brush_opacity(&mut self, direction: f32) { modify_clamp(&mut self.brush.alpha, 0.01 * direction, 0.0, 1.0); } - fn draw(&mut self, terrain: &mut Terrain, shift: bool) { - let mut brush_copy = self.brush.clone(); - if let BrushMode::Raise { amount } = &mut brush_copy.mode { + fn start_background_thread(&mut self) { + let (sender, receiver) = channel::(); + self.brush_sender = Some(BrushSender::new(sender)); + let sender_clone = self.message_sender.clone(); + let mut stroke = BrushStroke::with_chunk_handler(Box::new(move |undo_chunks| { + handle_undo_chunks(undo_chunks, &sender_clone) + })); + match std::thread::Builder::new() + .name("Terrain Brush".into()) + .spawn(move || stroke.accept_messages(receiver)) + { + Ok(_) => (), + Err(_) => { + Log::err("Brush thread failed to start."); + self.brush_sender = None; + } + } + } + fn end_background_thread(&mut self) { + self.brush_sender = None; + } + fn start_stroke(&self, terrain: &mut Terrain, handle: Handle, shift: bool) { + let mut brush = self.brush.clone(); + // Ignore stroke with a non-existent layer index. + if let BrushTarget::LayerMask { layer } = brush.target { + if layer >= terrain.layers().len() { + return; + } + } + // Holding shift as start of stroke causes the stroke to reverse lowering and raising. + if let BrushMode::Raise { amount } = &mut brush.mode { if shift { *amount *= -1.0; } } - - self.undo_chunks = terrain.draw( - self.brush_position, - &brush_copy, - &mut self.stroke, - std::mem::take(&mut self.undo_chunks), - ); + let chunk_size = match brush.target { + BrushTarget::HeightMap => terrain.height_map_size(), + BrushTarget::LayerMask { .. } => terrain.mask_size(), + }; + let kind = match brush.target { + BrushTarget::HeightMap => TerrainTextureKind::Height, + BrushTarget::LayerMask { .. } => TerrainTextureKind::Mask, + }; + let resources: FxHashMap, TextureResource> = match brush.target { + BrushTarget::HeightMap => terrain + .chunks_ref() + .iter() + .map(|c| (c.grid_position(), c.heightmap().clone())) + .collect(), + BrushTarget::LayerMask { layer } => terrain + .chunks_ref() + .iter() + .map(|c| (c.grid_position(), c.layer_masks[layer].clone())) + .collect(), + }; + let data = TerrainTextureData { + chunk_size, + kind, + resources, + }; + if let Some(sender) = &self.brush_sender { + sender.start_stroke(brush, handle, data); + } else { + Log::err("Brush thread failure"); + } + } + fn draw(&mut self, terrain: &mut Terrain) { + let Some(position) = terrain.project(self.brush_position) else { + return; + }; + let position = match self.brush.target { + BrushTarget::HeightMap => terrain.local_to_height_pixel(position), + BrushTarget::LayerMask { .. } => terrain.local_to_mask_pixel(position), + }; + let scale = match self.brush.target { + BrushTarget::HeightMap => terrain.height_grid_scale(), + BrushTarget::LayerMask { .. } => terrain.mask_grid_scale(), + }; + if let Some(sender) = &self.brush_sender { + if let Some(start) = self.prev_brush_position.take() { + self.brush + .smear(start, position, scale, self.brush_value, sender); + } else { + self.brush.stamp(position, scale, self.brush_value, sender); + } + self.prev_brush_position = Some(position); + } } } @@ -203,17 +296,19 @@ impl InteractionMode for TerrainInteractionMode { if let (Some(closest), BrushTarget::HeightMap) = (first, self.brush.target) { - self.stroke.value = closest.height; + self.brush_value = closest.height; } else if let Some(closest) = first { let p = closest.position; - self.stroke.value = terrain + self.brush_value = terrain .interpolate_value(Vector2::new(p.x, p.z), self.brush.target); } else { - self.stroke.value = 0.0; + self.brush_value = 0.0; } } } - self.draw(terrain, shift); + self.start_stroke(terrain, handle, shift); + self.prev_brush_position = None; + self.draw(terrain); self.interacting = true; } } @@ -222,40 +317,18 @@ impl InteractionMode for TerrainInteractionMode { fn on_left_mouse_button_up( &mut self, - editor_selection: &Selection, + _editor_selection: &Selection, _controller: &mut dyn SceneController, _engine: &mut Engine, _mouse_pos: Vector2, _frame_size: Vector2, _settings: &Settings, ) { - self.stroke.clear(); - if let Some(selection) = editor_selection.as_graph() { - if selection.is_single_selection() { - let handle = selection.nodes()[0]; - - if self.interacting { - match self.brush.target { - BrushTarget::HeightMap => { - self.message_sender - .do_command(ModifyTerrainHeightCommand::new( - handle, - std::mem::take(&mut self.undo_chunks), - )); - } - BrushTarget::LayerMask { layer, .. } => { - self.message_sender - .do_command(ModifyTerrainLayerMaskCommand::new( - handle, - std::mem::take(&mut self.undo_chunks), - layer, - )); - } - } - - self.interacting = false; - } + if self.interacting { + if let Some(s) = &self.brush_sender { + s.end_stroke() } + self.interacting = false; } } @@ -278,11 +351,6 @@ impl InteractionMode for TerrainInteractionMode { if let Some(selection) = editor_selection.as_graph() { if selection.is_single_selection() { - let shift = engine - .user_interfaces - .first_mut() - .keyboard_modifiers() - .shift; let handle = selection.nodes()[0]; let camera = &graph[game_scene.camera_controller.camera]; @@ -297,7 +365,7 @@ impl InteractionMode for TerrainInteractionMode { gizmo_visible = true; if self.interacting { - self.draw(terrain, shift); + self.draw(terrain); } let scale = match self.brush.shape { @@ -328,6 +396,8 @@ impl InteractionMode for TerrainInteractionMode { return; }; + self.start_background_thread(); + self.brush_gizmo .set_visible(&mut engine.scenes[game_scene.scene].graph, true); @@ -354,6 +424,8 @@ impl InteractionMode for TerrainInteractionMode { return; }; + self.end_background_thread(); + self.brush_gizmo .set_visible(&mut engine.scenes[game_scene.scene].graph, false); @@ -482,7 +554,7 @@ fn make_brush_mode_enum_property_editor_definition() -> EnumPropertyEditorDefini 0 => BrushMode::Raise { amount: 0.1 }, 1 => BrushMode::Assign { value: 0.0 }, 2 => BrushMode::Flatten, - 3 => BrushMode::Smooth { kernel_radius: 1 }, + 3 => BrushMode::Smooth { kernel_radius: 5 }, _ => unreachable!(), }, index_generator: |v| match v { @@ -555,7 +627,7 @@ impl BrushPanel { ); let inspector; - let window = WindowBuilder::new(WidgetBuilder::new().with_width(300.0).with_height(150.0)) + let window = WindowBuilder::new(WidgetBuilder::new().with_width(300.0).with_height(250.0)) .can_minimize(false) .can_maximize(false) .with_content({ diff --git a/editor/src/scene/commands/terrain.rs b/editor/src/scene/commands/terrain.rs index 25fa89c89..088ce9b29 100644 --- a/editor/src/scene/commands/terrain.rs +++ b/editor/src/scene/commands/terrain.rs @@ -1,4 +1,4 @@ -use fyrox::scene::terrain::ChunkData; +use fyrox::scene::terrain::brushstroke::ChunkData; use crate::command::CommandContext; use crate::fyrox::{ diff --git a/fyrox-impl/src/scene/terrain/brushstroke.rs b/fyrox-impl/src/scene/terrain/brushstroke.rs deleted file mode 100644 index c214968fa..000000000 --- a/fyrox-impl/src/scene/terrain/brushstroke.rs +++ /dev/null @@ -1,652 +0,0 @@ -use crate::core::{ - algebra::{Matrix2, Vector2}, - math::{OptionRect, Rect}, - reflect::prelude::*, -}; -use crate::fxhash::FxHashMap; -use fyrox_core::uuid_provider; - -/// A value that is stored in a terrain and can be edited by a brush. -pub trait BrushValue { - /// Increase the value by the given amount, or decrease it if the amount is negative. - fn raise(self, amount: f32) -> Self; - /// Create a value based upon a float representation. - fn from_f32(value: f32) -> Self; - /// Create an f32 representation of this value. - fn into_f32(self) -> f32; -} - -impl BrushValue for f32 { - #[inline] - fn raise(self, amount: f32) -> Self { - self + amount - } - #[inline] - fn from_f32(value: f32) -> Self { - value - } - #[inline] - fn into_f32(self) -> f32 { - self - } -} - -impl BrushValue for u8 { - #[inline] - fn raise(self, amount: f32) -> Self { - (self as f32 + amount * 255.0).clamp(0.0, 255.0) as Self - } - #[inline] - fn from_f32(value: f32) -> Self { - (value * 255.0).clamp(0.0, 255.0) as Self - } - #[inline] - fn into_f32(self) -> f32 { - self as f32 / 255.0 - } -} - -/// Trait for any of the various data properties that may be edited -/// by a brush. V is the type of the elements of the data, -/// such as f32 for the height data and u8 for the mask data. -/// -/// This trait encapsulates and hides the concept of chunks. -/// It pretends that terrain data is a continuous infinite array, and -/// accessing any data in that array requires only a Vector2 index. -/// This simplifies brush algorithms. -pub trait BrushableTerrainData { - /// Returns the value at the given coordinates as it was *before* the current brushstroke. - /// Previous calls to [BrushableTerrainData::update] will not affect the value returned until the current - /// stroke ends and the changes to the terrain are completed. - fn get_value(&self, position: Vector2) -> V; - /// Updates the value of the terrain according to the given function - /// if the given strength is greater than the current brush strength at - /// the given coordinates. func is not called otherwise. - /// - /// If the value is updated, then the brush strength is increased to match - /// the given strength at those coordinates so that the same position will - /// not be updated again unless by a brush of greater strength than this one. - /// - /// If strength is 0.0 or less, nothing is done, since 0.0 is the minimum valid brush strength. - /// - /// The value passed to func is the same value that would be returned by [BrushableTerrainData::get_value], - /// which is the value at that position *before* the current stroke began. - /// Even if update is called multiple times on a single position, the value passed to func will be - /// the same each time until the current stroke is completed. - fn update(&mut self, position: Vector2, strength: f32, func: F) - where - F: FnOnce(&Self, V) -> V; - /// Calculate a value for a kernel of the given radius around the given position - /// by summing the values of the neighborhood surrounding the position. - fn sum_kernel(&self, position: Vector2, kernel_radius: u32) -> f32; -} - -#[derive(Debug, Default)] -/// Data for an in-progress terrain painting operation -pub struct BrushStroke { - /// The height pixels that have been drawn to - pub height_pixels: StrokeData, - /// The mask pixels that have been drawn to - pub mask_pixels: StrokeData, - /// A value that may change over the course of a stroke. - pub value: f32, -} - -/// The pixels for a stroke, generalized over the type of data being edited. -#[derive(Debug, Default)] -pub struct StrokeData(FxHashMap, StrokeElement>); - -/// A single pixel data of a brush stroke -#[derive(Debug, Copy, Clone)] -pub struct StrokeElement { - /// The intensity of the brush stroke, with 0.0 indicating a pixel that brush has not touched - /// and 1.0 indicates a pixel fully covered by the brush. - pub strength: f32, - /// The value of the pixel before the stroke began. - pub original_value: V, -} - -/// A pixel within a brush shape. -#[derive(Debug, Copy, Clone)] -pub struct BrushPixel { - /// The position of the pixel - pub position: Vector2, - /// The strength of the brush at this pixel, with 0.0 indicating the pixel is outside the bounds of the brush, - /// and 1.0 indicating the maximum strength of the brush. - pub strength: f32, -} - -impl BrushStroke { - /// Prepare this object for a new brushstroke. - pub fn clear(&mut self) { - self.height_pixels.clear(); - self.mask_pixels.clear(); - } -} - -impl StrokeData { - /// Reset the brush stroke so it is ready to begin a new stroke. - #[inline] - pub fn clear(&mut self) { - self.0.clear() - } - /// Return the StrokeElement stored at the give position, if there is one. - #[inline] - pub fn get(&self, position: Vector2) -> Option<&StrokeElement> { - self.0.get(&position) - } - /// Stores or modifies the StrokeElement at the given position. - /// If the element is updated, return the original pixel value of the element. - /// - `position`: The position of the data to modify within the terrain. - /// - `strength`: The strength of the brush at the position, from 0.0 to 1.0. - /// The element is updated if the stored strength is less than the given strength. - /// If there is no stored strength, that is treated as a strength of 0.0. - /// - `pixel_value`: The current value of the data. - /// This may be stored in the StrokeData if no pixel value is currently recorded for the given position. - /// Otherwise, this value is ignored. - #[inline] - pub fn update_pixel( - &mut self, - position: Vector2, - strength: f32, - pixel_value: V, - ) -> Option - where - V: Clone, - { - if strength == 0.0 { - None - } else if let Some(element) = self.0.get_mut(&position) { - if element.strength < strength { - element.strength = strength; - Some(element.original_value.clone()) - } else { - None - } - } else { - let element = StrokeElement { - strength, - original_value: pixel_value.clone(), - }; - self.0.insert(position, element); - Some(pixel_value) - } - } -} - -/// An iterator that produces coordinates by scanning an integer Rect. -#[derive(Debug, Clone)] -pub struct RectIter { - bounds: Rect, - next_pos: Vector2, -} - -impl RectIter { - /// Create an iterator that returns coordinates within the given bounds. - pub fn new(bounds: Rect) -> Self { - Self { - bounds, - next_pos: Vector2::default(), - } - } - /// The Rect that this iter is scanning. - pub fn bounds(&self) -> Rect { - self.bounds - } -} - -impl Iterator for RectIter { - type Item = Vector2; - fn next(&mut self) -> Option { - if self.next_pos.y > self.bounds.size.y { - return None; - } - let result = self.next_pos + self.bounds.position; - if self.next_pos.x < self.bounds.size.x { - self.next_pos.x += 1; - } else { - self.next_pos.y += 1; - self.next_pos.x = 0; - } - Some(result) - } -} - -fn apply_hardness(hardness: f32, strength: f32) -> f32 { - if strength == 0.0 { - return 0.0; - } - let h = 1.0 - hardness; - if strength < h { - strength / h - } else { - 1.0 - } -} - -/// An iterator of the pixels of a round brush. -#[derive(Debug, Clone)] -pub struct CircleBrushPixels { - center: Vector2, - radius: f32, - hardness: f32, - inv_transform: Matrix2, - bounds_iter: RectIter, -} - -impl CircleBrushPixels { - /// The bounding rectangle of the pixels. - pub fn bounds(&self) -> Rect { - self.bounds_iter.bounds() - } -} - -impl CircleBrushPixels { - /// Construct a new pixel iterator for a round brush at the given position, radius, - /// and 2x2 transform matrix. - pub fn new(center: Vector2, radius: f32, hardness: f32, transform: Matrix2) -> Self { - let mut bounds: OptionRect = Default::default(); - let transform = if transform.is_invertible() { - transform - } else { - Matrix2::identity() - }; - let inv_transform = transform.try_inverse().unwrap(); - for p in [ - Vector2::new(radius, radius), - Vector2::new(radius, -radius), - Vector2::new(-radius, radius), - Vector2::new(-radius, -radius), - ] { - let p1 = transform * p + center; - let ceil = p1.map(|x| x.ceil() as i32); - let floor = p1.map(|x| x.floor() as i32); - let rect = Rect::new(floor.x, floor.y, ceil.x - floor.x, ceil.y - floor.y); - bounds.extend_to_contain(rect); - } - Self { - center, - radius, - hardness, - inv_transform, - bounds_iter: RectIter::new(bounds.unwrap()), - } - } -} - -impl Iterator for CircleBrushPixels { - type Item = BrushPixel; - - fn next(&mut self) -> Option { - let position = self.bounds_iter.next()?; - let fx = position.x as f32; - let fy = position.y as f32; - let p = Vector2::new(fx, fy) - self.center; - let p1 = self.inv_transform * p; - let dist_sqr = p1.magnitude_squared(); - let radius = self.radius; - let strength = if dist_sqr >= radius * radius { - 0.0 - } else { - (1.0 - dist_sqr.sqrt() / radius).max(0.0) - }; - let strength = apply_hardness(self.hardness, strength); - Some(BrushPixel { position, strength }) - } -} - -impl RectBrushPixels { - /// The bounds of the pixels. - pub fn bounds(&self) -> Rect { - self.bounds_iter.bounds() - } -} - -/// An iterator of the pixels of a rectangular brush. -#[derive(Debug, Clone)] -pub struct RectBrushPixels { - center: Vector2, - radius: Vector2, - hardness: f32, - inv_transform: Matrix2, - bounds_iter: RectIter, -} - -impl RectBrushPixels { - /// Construct a new pixel iterator for a rectangle brush at the given position, - /// x-radius, y-radius, and 2x2 transform matrix for rotation. - pub fn new( - center: Vector2, - radius: Vector2, - hardness: f32, - transform: Matrix2, - ) -> Self { - let mut bounds: Option> = None; - let transform = if transform.is_invertible() { - transform - } else { - Matrix2::identity() - }; - let inv_transform = transform.try_inverse().unwrap(); - for p in [ - center + radius, - center + Vector2::new(radius.x, -radius.y), - center + Vector2::new(-radius.x, radius.y), - center - radius, - ] { - let p = transform * p; - let ceil = p.map(|x| x.ceil() as i32); - let floor = p.map(|x| x.floor() as i32); - let rect = Rect::new(floor.x, floor.y, ceil.x - floor.x, ceil.y - floor.y); - if let Some(bounds) = &mut bounds { - bounds.extend_to_contain(rect); - } else { - bounds = Some(rect); - } - } - Self { - center, - radius, - hardness, - inv_transform, - bounds_iter: RectIter::new(bounds.unwrap()), - } - } -} - -impl Iterator for RectBrushPixels { - type Item = BrushPixel; - - fn next(&mut self) -> Option { - let position = self.bounds_iter.next()?; - let fx = position.x as f32; - let fy = position.y as f32; - let p = Vector2::new(fx, fy) - self.center; - let p = self.inv_transform * p; - let radius = self.radius; - let p = p.abs(); - let min = radius.min(); - let radius = Vector2::new(radius.x - min, radius.y - min); - let p = Vector2::new(p.x - min, p.y - min).sup(&Vector2::new(0.0, 0.0)); - let strength = if p.x > radius.x || p.y > radius.y { - 0.0 - } else { - 1.0 - (p.x / radius.x).max(p.y / radius.y) - }; - let strength = apply_hardness(self.hardness, strength); - Some(BrushPixel { position, strength }) - } -} - -/// Shape of a brush. -#[derive(Copy, Clone, Reflect, Debug)] -pub enum BrushShape { - /// Circle with given radius. - Circle { - /// Radius of the circle. - radius: f32, - }, - /// Rectangle with given width and height. - Rectangle { - /// Width of the rectangle. - width: f32, - /// Length of the rectangle. - length: f32, - }, -} - -uuid_provider!(BrushShape = "a4dbfba0-077c-4658-9972-38384a8432f9"); - -impl BrushShape { - /// Return true if the given point is within the shape when positioned at the given center point. - pub fn contains(&self, brush_center: Vector2, pixel_position: Vector2) -> bool { - match *self { - BrushShape::Circle { radius } => (brush_center - pixel_position).norm() < radius, - BrushShape::Rectangle { width, length } => Rect::new( - brush_center.x - width * 0.5, - brush_center.y - length * 0.5, - width, - length, - ) - .contains(pixel_position), - } - } -} - -/// Paint mode of a brush. It defines operation that will be performed on the terrain. -#[derive(Clone, PartialEq, PartialOrd, Reflect, Debug)] -pub enum BrushMode { - /// Raise or lower the value - Raise { - /// An offset to change the value by - amount: f32, - }, - /// Flattens value of the terrain data - Flatten, - /// Assigns a particular value to anywhere the brush touches. - Assign { - /// Fixed value to paint into the data - value: f32, - }, - /// Reduce sharp changes in the data. - Smooth { - /// Determines the size of each pixel's neighborhood in terms of - /// distance from the pixel. - /// 0 means no smoothing at all. - /// 1 means taking the mean of the 3x3 square of pixels surrounding each smoothed pixel. - /// 2 means using a 5x5 square of pixels. And so on. - kernel_radius: u32, - }, -} - -uuid_provider!(BrushMode = "48ad4cac-05f3-485a-b2a3-66812713841f"); - -impl BrushMode { - /// Perform the operation represented by this BrushMode. - /// - `pixels`: An iterator over the pixels that are covered by the shape of the brush and the position of the brush. - /// - `data`: An abstraction of the terrain data that allows the brush mode to edit the terrain data without concern - /// for chunks of what kind of data is being edited. - /// - `value`: A value that is used to control some BrushModes, especially `Flatten` where it represents the level of flattened terrain. - /// - `alpha`: A value between 0.0 and 1.0 that represents how much the brush's effect is weighted when combining it with the original - /// value of the pixels, with 0.0 meaning the brush has no effect and 1.0 meaning that the original value of the pixels is completely covered. - pub fn draw(&self, pixels: I, data: &mut D, value: f32, alpha: f32) - where - I: Iterator, - D: BrushableTerrainData, - V: BrushValue, - { - match self { - BrushMode::Raise { amount } => { - for BrushPixel { position, strength } in pixels { - data.update(position, strength, |_, x| { - x.raise(amount * strength * alpha) - }); - } - } - BrushMode::Flatten => { - for BrushPixel { position, strength } in pixels { - let alpha = strength * alpha; - data.update(position, strength, |_, x| { - V::from_f32(x.into_f32() * (1.0 - alpha) + value * alpha) - }); - } - } - BrushMode::Assign { value } => { - for BrushPixel { position, strength } in pixels { - let alpha = strength * alpha; - data.update(position, strength, |_, x| { - V::from_f32(x.into_f32() * (1.0 - alpha) + value * alpha) - }); - } - } - BrushMode::Smooth { kernel_radius } => { - if *kernel_radius == 0 || alpha == 0.0 { - return; - } - let size = kernel_radius * 2 + 1; - let scale = 1.0 / (size * size) as f32; - for BrushPixel { position, strength } in pixels { - let alpha = strength * alpha; - data.update(position, strength, |data, x| { - let value = data.sum_kernel(position, *kernel_radius) * scale; - V::from_f32(x.into_f32() * (1.0 - alpha) + value * alpha) - }); - } - } - } - } -} - -/// Paint target of a brush. It defines the data that the brush will operate on. -#[derive(Copy, Clone, Reflect, Debug, PartialEq, Eq)] -pub enum BrushTarget { - /// Modifies the height map - HeightMap, - /// Draws on a given layer - LayerMask { - /// The number of the layer to modify - layer: usize, - }, -} - -uuid_provider!(BrushTarget = "461c1be7-189e-44ee-b8fd-00b8fdbc668f"); - -/// Brush is used to modify terrain. It supports multiple shapes and modes. -#[derive(Clone, Reflect, Debug)] -pub struct Brush { - /// Shape of the brush. - pub shape: BrushShape, - /// Paint mode of the brush. - pub mode: BrushMode, - /// The data to modify with the brush - pub target: BrushTarget, - /// Transform that can modify the shape of the brush - pub transform: Matrix2, - /// The softness of the edges of the brush. - /// 0.0 means that the brush fades very gradually from opaque to transparent. - /// 1.0 means that the edges of the brush do not fade. - pub hardness: f32, - /// The transparency of the brush, allowing the values beneath the brushstroke to show throw. - /// 0.0 means the brush is fully transparent and does not draw. - /// 1.0 means the brush is fully opaque. - pub alpha: f32, -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn rect_extend_to_contain_f64() { - let mut rect = Rect::new(0.0, 0.0, 1.0, 1.0); - - //rect.extend_to_contain(Rect::new(1.0, 1.0, 1.0, 1.0)); - //assert_eq!(rect, Rect::new(0.0, 0.0, 2.0, 2.0)); - - rect.extend_to_contain(Rect::new(-1.0, -1.0, 1.0, 1.0)); - assert_eq!(rect, Rect::new(-1.0, -1.0, 2.0, 2.0)); - } - #[test] - fn rect_extend_to_contain_i32() { - let mut rect: Rect = Rect::new(0, 0, 1, 1); - - rect.extend_to_contain(Rect::new(1, 1, 1, 1)); - assert_eq!(rect, Rect::::new(0, 0, 2, 2)); - - rect.extend_to_contain(Rect::new(-1, -1, 1, 1)); - assert_eq!(rect, Rect::::new(-1, -1, 2, 2)); - } - #[test] - fn bounds_iter() { - let result: Vec> = RectIter::new(Rect::new(-1, -2, 3, 2)).collect(); - let expected: Vec> = vec![ - (-1, -2), - (0, -2), - (1, -2), - (2, -2), - (-1, -1), - (0, -1), - (1, -1), - (2, -1), - (-1, 0), - (0, 0), - (1, 0), - (2, 0), - ] - .into_iter() - .map(|(x, y)| Vector2::new(x, y)) - .collect(); - assert_eq!(result, expected); - } - #[test] - fn finite_pixels_circle() { - let mut iter = CircleBrushPixels::new(Vector2::default(), 2.0, 1.0, Matrix2::identity()); - for _ in 0..100 { - if iter.next().is_none() { - return; - } - } - panic!("Iter went over 100."); - } - #[test] - fn finite_pixels_rect() { - let mut iter = RectBrushPixels::new( - Vector2::default(), - Vector2::new(2.0, 2.0), - 1.0, - Matrix2::identity(), - ); - for _ in 0..100 { - if iter.next().is_none() { - return; - } - } - panic!("Iter went over 100."); - } - #[test] - fn pixels_range_circle() { - let mut iter = CircleBrushPixels::new(Vector2::default(), 2.0, 1.0, Matrix2::identity()); - let mut rect = Rect::new(0, 0, 0, 0); - let mut points: Vec> = Vec::default(); - for _ in 0..100 { - if let Some(BrushPixel { position, .. }) = iter.next() { - points.push(Vector2::new(position.x, position.y)); - rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0)); - } else { - break; - } - } - assert_eq!( - rect, - Rect::new(-2, -2, 4, 4), - "{:?} {:?}", - iter.bounds(), - points - ); - } - #[test] - fn pixels_range_rect() { - let mut iter = RectBrushPixels::new( - Vector2::default(), - Vector2::new(2.0, 2.0), - 1.0, - Matrix2::identity(), - ); - let mut rect = Rect::new(0, 0, 0, 0); - let mut points: Vec> = Vec::default(); - for _ in 0..100 { - if let Some(BrushPixel { position, .. }) = iter.next() { - points.push(Vector2::new(position.x, position.y)); - rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0)); - } else { - break; - } - } - assert_eq!( - rect, - Rect::new(-2, -2, 4, 4), - "{:?} {:?}", - iter.bounds(), - points - ); - } -} diff --git a/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs b/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs new file mode 100644 index 000000000..5e5778da1 --- /dev/null +++ b/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs @@ -0,0 +1,434 @@ +//! The brushraster module converts floating-point brush positions into rasterized +//! pixels, with each pixel given a strength based on how close it is to the center of the +//! brush and the hardness of the brush. Soft brushes gradually decrease in strength +//! from the center to the edge. Hard brushes have full strength until very close to the edge. +//! +//! This rasterization tool is used by the UI to convert mouse events into pixel messages +//! that get sent to the brush painting thread. +use fyrox_core::math::segment::LineSegment2; + +use crate::core::{ + algebra::{Matrix2, Vector2}, + math::{OptionRect, Rect}, +}; + +fn apply_hardness(hardness: f32, strength: f32) -> f32 { + if strength == 0.0 { + return 0.0; + } + let h = 1.0 - hardness; + if strength < h { + strength / h + } else { + 1.0 + } +} + +/// Trait for objects that are capable of assigning strengths to pixels. +pub trait BrushRaster { + /// Calculate the strength of a pixel at the given position, + /// as measured relative to the center of the brush, so that point (0.0, 0.0) + /// is the exact center of the brush. + fn strength_at(&self, point: Vector2) -> f32; + /// An AABB that contains all the pixels of the brush, with (0.0, 0.0) being + /// at the center of the brush. + fn bounds(&self) -> Rect; + fn transformed_bounds(&self, center: Vector2, transform: &Matrix2) -> Rect { + let mut bounds = OptionRect::::default(); + let rect = self.bounds(); + for p in [ + rect.left_top_corner(), + rect.left_bottom_corner(), + rect.right_top_corner(), + rect.right_bottom_corner(), + ] { + let p1 = transform * p + center; + let ceil = p1.map(|x| x.ceil() as i32); + let floor = p1.map(|x| x.floor() as i32); + let rect = Rect::new(floor.x, floor.y, ceil.x - floor.x, ceil.y - floor.y); + bounds.extend_to_contain(rect); + } + bounds.unwrap() + } +} + +/// Rasterize a round brush with the given radius. +#[derive(Debug, Clone)] +pub struct CircleRaster(pub f32); + +impl BrushRaster for CircleRaster { + fn strength_at(&self, point: Vector2) -> f32 { + let radius = self.0; + let dist_sqr = point.magnitude_squared(); + if dist_sqr >= radius * radius { + 0.0 + } else { + (1.0 - dist_sqr.sqrt() / radius).max(0.0) + } + } + fn bounds(&self) -> Rect { + let radius = self.0; + Rect::from_points(Vector2::new(-radius, -radius), Vector2::new(radius, radius)) + } +} + +/// Rasterize a rectangular brush with the given x-radius and y-radius. +#[derive(Debug, Clone)] +pub struct RectRaster(pub f32, pub f32); + +impl BrushRaster for RectRaster { + fn strength_at(&self, point: Vector2) -> f32 { + let radius = Vector2::new(self.0, self.1); + // Flip p so that it is on the positive side of both axes. + let p = point.abs(); + let min = radius.min(); + let inner = Vector2::new(radius.x - min, radius.y - min); + let outer = radius - inner; + if outer.min() == 0.0 { + return 0.0; + } + let p = (p - inner).sup(&Vector2::new(0.0, 0.0)); + if p.x > outer.x || p.y > outer.y { + 0.0 + } else { + 1.0 - (p.x / outer.x).max(p.y / outer.y) + } + } + fn bounds(&self) -> Rect { + let RectRaster(x, y) = self; + Rect::from_points(Vector2::new(-x, -y), Vector2::new(*x, *y)) + } +} + +/// A pixel within a brush shape. +#[derive(Debug, Copy, Clone)] +pub struct BrushPixel { + /// The position of the pixel + pub position: Vector2, + /// The strength of the brush at this pixel, with 0.0 indicating the pixel is outside the bounds of the brush, + /// and 1.0 indicating the maximum strength of the brush. + pub strength: f32, +} + +/// An iterator that produces coordinates by scanning an integer Rect. +#[derive(Debug, Clone)] +pub struct RectIter { + bounds: Rect, + next_pos: Vector2, +} + +impl RectIter { + /// Create an iterator that returns coordinates within the given bounds. + pub fn new(bounds: Rect) -> Self { + Self { + bounds, + next_pos: Vector2::default(), + } + } + /// The Rect that this iter is scanning. + pub fn bounds(&self) -> Rect { + self.bounds + } +} + +impl Iterator for RectIter { + type Item = Vector2; + fn next(&mut self) -> Option { + if self.next_pos.y > self.bounds.size.y { + return None; + } + let result = self.next_pos + self.bounds.position; + if self.next_pos.x < self.bounds.size.x { + self.next_pos.x += 1; + } else { + self.next_pos.y += 1; + self.next_pos.x = 0; + } + Some(result) + } +} + +#[derive(Debug, Clone)] +pub struct StampPixels { + brush_raster: R, + center: Vector2, + hardness: f32, + inv_transform: Matrix2, + bounds_iter: RectIter, +} + +impl StampPixels +where + R: BrushRaster, +{ + pub fn bounds(&self) -> Rect { + self.bounds_iter.bounds() + } + /// Construct a new pixel iterator for a round brush at the given position, radius, + /// and 2x2 transform matrix. + pub fn new( + brush_raster: R, + center: Vector2, + hardness: f32, + transform: Matrix2, + ) -> Self { + let (transform, inv_transform) = transform + .try_inverse() + .map(|m| (transform, m)) + .unwrap_or((Matrix2::identity(), Matrix2::identity())); + let bounds = brush_raster.transformed_bounds(center, &transform); + Self { + brush_raster, + center, + hardness, + inv_transform, + bounds_iter: RectIter::new(bounds), + } + } +} + +impl Iterator for StampPixels +where + R: BrushRaster, +{ + type Item = BrushPixel; + + fn next(&mut self) -> Option { + let position = self.bounds_iter.next()?; + let fx = position.x as f32; + let fy = position.y as f32; + let p = Vector2::new(fx, fy) - self.center; + let p = self.inv_transform * p; + let strength = self.brush_raster.strength_at(p); + let strength = apply_hardness(self.hardness, strength); + Some(BrushPixel { position, strength }) + } +} + +/// An iterator of the pixels of a round brush. +#[derive(Debug, Clone)] +pub struct SmearPixels { + brush_raster: R, + start: Vector2, + end: Vector2, + hardness: f32, + inv_transform: Matrix2, + bounds_iter: RectIter, +} + +impl SmearPixels { + /// The bounding rectangle of the pixels. + pub fn bounds(&self) -> Rect { + self.bounds_iter.bounds() + } + /// Construct a new pixel iterator for a brush at the given position, radius, + /// and 2x2 transform matrix. + pub fn new( + brush_raster: R, + start: Vector2, + end: Vector2, + hardness: f32, + transform: Matrix2, + ) -> Self + where + R: BrushRaster, + { + let mut bounds: OptionRect = Default::default(); + let (transform, inv_transform) = transform + .try_inverse() + .map(|m| (transform, m)) + .unwrap_or((Matrix2::identity(), Matrix2::identity())); + bounds.extend_to_contain(brush_raster.transformed_bounds(start, &transform)); + bounds.extend_to_contain(brush_raster.transformed_bounds(end, &transform)); + Self { + brush_raster, + start, + end, + hardness, + inv_transform, + bounds_iter: RectIter::new(bounds.unwrap()), + } + } +} + +impl Iterator for SmearPixels +where + R: BrushRaster, +{ + type Item = BrushPixel; + + fn next(&mut self) -> Option { + let position = self.bounds_iter.next()?; + let fx = position.x as f32; + let fy = position.y as f32; + let segment = LineSegment2::new(&self.start, &self.end); + let center = segment.nearest_point(&Vector2::new(fx, fy)); + let p = Vector2::new(fx, fy) - center; + let p = self.inv_transform * p; + let strength = self.brush_raster.strength_at(p); + let strength = apply_hardness(self.hardness, strength); + Some(BrushPixel { position, strength }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn rect_extend_to_contain_f64() { + let mut rect = Rect::new(0.0, 0.0, 1.0, 1.0); + + //rect.extend_to_contain(Rect::new(1.0, 1.0, 1.0, 1.0)); + //assert_eq!(rect, Rect::new(0.0, 0.0, 2.0, 2.0)); + + rect.extend_to_contain(Rect::new(-1.0, -1.0, 1.0, 1.0)); + assert_eq!(rect, Rect::new(-1.0, -1.0, 2.0, 2.0)); + } + #[test] + fn rect_extend_to_contain_i32() { + let mut rect: Rect = Rect::new(0, 0, 1, 1); + + rect.extend_to_contain(Rect::new(1, 1, 1, 1)); + assert_eq!(rect, Rect::::new(0, 0, 2, 2)); + + rect.extend_to_contain(Rect::new(-1, -1, 1, 1)); + assert_eq!(rect, Rect::::new(-1, -1, 3, 3)); + } + #[test] + fn bounds_iter() { + let result: Vec> = RectIter::new(Rect::new(-1, -2, 3, 2)).collect(); + let expected: Vec> = vec![ + (-1, -2), + (0, -2), + (1, -2), + (2, -2), + (-1, -1), + (0, -1), + (1, -1), + (2, -1), + (-1, 0), + (0, 0), + (1, 0), + (2, 0), + ] + .into_iter() + .map(|(x, y)| Vector2::new(x, y)) + .collect(); + assert_eq!(result, expected); + } + #[test] + fn finite_pixels_circle() { + let mut iter = StampPixels::new( + CircleRaster(2.0), + Vector2::default(), + 1.0, + Matrix2::identity(), + ); + for _ in 0..100 { + if iter.next().is_none() { + return; + } + } + panic!("Iter went over 100."); + } + #[test] + fn finite_pixels_rect() { + let mut iter = StampPixels::new( + RectRaster(2.0, 2.0), + Vector2::default(), + 1.0, + Matrix2::identity(), + ); + for _ in 0..100 { + if iter.next().is_none() { + return; + } + } + panic!("Iter went over 100."); + } + #[test] + fn pixels_range_circle() { + let mut iter = StampPixels::new( + CircleRaster(2.0), + Vector2::default(), + 1.0, + Matrix2::identity(), + ); + let mut rect = Rect::new(0, 0, 0, 0); + let mut points: Vec> = Vec::default(); + for _ in 0..100 { + if let Some(BrushPixel { position, .. }) = iter.next() { + points.push(Vector2::new(position.x, position.y)); + rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0)); + } else { + break; + } + } + assert_eq!( + rect, + Rect::new(-2, -2, 4, 4), + "{:?} {:?}", + iter.bounds(), + points + ); + } + #[test] + fn pixels_range_rect() { + let mut iter = StampPixels::new( + RectRaster(2.0, 2.0), + Vector2::default(), + 1.0, + Matrix2::identity(), + ); + let mut rect = Rect::new(0, 0, 0, 0); + let mut points: Vec> = Vec::default(); + for _ in 0..100 { + if let Some(BrushPixel { position, .. }) = iter.next() { + points.push(Vector2::new(position.x, position.y)); + rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0)); + } else { + break; + } + } + assert_eq!( + rect, + Rect::new(-2, -2, 4, 4), + "{:?} {:?}", + iter.bounds(), + points + ); + } + fn find_strength_at(iter: &StampPixels, (x, y): (i32, i32)) -> f32 { + let p = Vector2::new(x, y); + let pix = iter.clone().find(|x| x.position == p); + pix.map(|p| p.strength).unwrap_or(0.0) + } + #[test] + fn simple_rect() { + let iter = StampPixels::new( + RectRaster(1.0, 1.0), + Vector2::new(1.0, 1.0), + 1.0, + Matrix2::identity(), + ); + assert_eq!(find_strength_at(&iter, (1, 1)), 1.0); + assert_eq!(find_strength_at(&iter, (0, 1)), 1.0); + assert_eq!(find_strength_at(&iter, (0, 2)), 1.0); + assert_eq!(find_strength_at(&iter, (0, 0)), 1.0); + assert_eq!(find_strength_at(&iter, (-1, 1)), 0.0); + } + #[test] + fn distant_rect() { + let iter = StampPixels::new( + RectRaster(1001.0, 2501.0), + Vector2::new(1.0, 1.0), + 1.0, + Matrix2::identity(), + ); + assert_eq!(find_strength_at(&iter, (1001, 2501)), 1.0); + assert_eq!(find_strength_at(&iter, (1000, 2501)), 1.0); + assert_eq!(find_strength_at(&iter, (1000, 2502)), 1.0); + assert_eq!(find_strength_at(&iter, (1000, 2500)), 1.0); + assert_eq!(find_strength_at(&iter, (999, 2501)), 0.0); + } +} diff --git a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs new file mode 100644 index 000000000..c995428a4 --- /dev/null +++ b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs @@ -0,0 +1,931 @@ +//! The brushstroke module contains tools for modifying terrain textures. +//! It uses a triple-buffer system to separate the UI mouse movements +//! from the update of the data within the actual textures. +//! 1. The first buffer is a [std::sync::mpsc::channel] that is used to send +//! messages to control a thread that processes brush strokes. +//! These messages are [BrushThreadMessage]. Some of the messages +//! are processed as soon as they are received, but [BrushThreadMessage::Pixel] +//! messages are sent to the next buffer. +//! 2. The pixel message buffer holds a limited number of pixel messages. +//! It serves to merge redundent pixel messages to spare the thread from +//! repeating work. It is expected that brush operations will paint multiple +//! times to the same pixel in quick succession. +//! Once the new value for a pixel has been calculated, the value is stored +//! in the third buffer. +//! 3. The [StrokeData] buffer stores every change that a particular brush stroke +//! has made to the texture. Because modifying a texture is a nontrivial operation, +//! modified pixels are allowed to accumulate to some quantity before the new pixel +//! values are actually written to the textures of the terrain. +//! [StrokeChunks] is used to keep track of which pixels are waiting to be written +//! to which terrain chunks. +use super::Chunk; +use crate::asset::ResourceDataRef; +use crate::core::{ + algebra::{Matrix2, Vector2}, + log::Log, + math::Rect, + pool::Handle, + reflect::prelude::*, +}; +use crate::fxhash::FxHashMap; +use crate::resource::texture::{Texture, TextureResource}; +use crate::scene::node::Node; +use fyrox_core::uuid_provider; +use std::collections::VecDeque; +use std::sync::mpsc::{Receiver, SendError, Sender}; + +mod brushraster; +use brushraster::*; +mod strokechunks; +use strokechunks::*; + +/// The number of pixel messages we can accept at once before we must start processing them. +/// Often later messages will cause earlier messages to be unnecessary, so it can be more efficient +/// to let some messages accumulate rather than process each message one-at-a-time. +const MESSAGE_BUFFER_SIZE: usize = 40; +/// The number of processed pixels we can hold before we must write the pixels to the targetted textures. +/// Modifying a texture is expensive, so it is important to do it in batches of multiple pixels. +const PIXEL_BUFFER_SIZE: usize = 40; + +#[inline] +fn mask_raise(original: u8, amount: f32) -> u8 { + (original as f32 + amount * 255.0).clamp(0.0, 255.0) as u8 +} + +#[inline] +fn mask_lerp(original: u8, value: f32, t: f32) -> u8 { + let original = original as f32; + let value = value * 255.0; + (original * (1.0 - t) + value * t).clamp(0.0, 255.0) as u8 +} + +/// A value that is stored in a terrain and can be edited by a brush. +pub trait BrushValue { + /// Increase the value by the given amount, or decrease it if the amount is negative. + fn raise(self, amount: f32) -> Self; + /// Linear interpolations between two values. + fn lerp(self, other: Self, t: f32) -> Self; + /// Linear interpolation toward an `f32` value. + fn lerp_f32(self, value: f32, t: f32) -> Self; + /// Create a value based upon a float representation. + fn from_f32(value: f32) -> Self; + /// Create an f32 representation of this value. + fn into_f32(self) -> f32; +} + +impl BrushValue for f32 { + #[inline] + fn raise(self, amount: f32) -> Self { + self + amount + } + #[inline] + fn lerp(self, value: Self, t: f32) -> Self { + self * (1.0 - t) + value * t + } + #[inline] + fn lerp_f32(self, value: f32, t: f32) -> Self { + self * (1.0 - t) + value * t + } + #[inline] + fn from_f32(value: f32) -> Self { + value + } + #[inline] + fn into_f32(self) -> f32 { + self + } +} + +impl BrushValue for u8 { + #[inline] + fn raise(self, amount: f32) -> Self { + (self as f32 + amount * 255.0).clamp(0.0, 255.0) as Self + } + #[inline] + fn lerp(self, other: Self, t: f32) -> Self { + (self as f32 * (1.0 - t) + other as f32 * t) as Self + } + #[inline] + fn lerp_f32(self, value: f32, t: f32) -> Self { + (self as f32 * (1.0 - t) + value * 255.0 * t).clamp(0.0, 255.0) as Self + } + #[inline] + fn from_f32(value: f32) -> Self { + (value * 255.0).clamp(0.0, 255.0) as Self + } + #[inline] + fn into_f32(self) -> f32 { + self as f32 / 255.0 + } +} + +/// A message that can be sent to the terrain painting thread to control the painting. +#[derive(Debug, Clone)] +pub enum BrushThreadMessage { + /// Set the brush that will be used for future pixels and the textures that will be modified. + StartStroke(Brush, Handle, TerrainTextureData), + /// No futher pixels will be sent for the current stroke. + EndStroke, + /// Paint the given pixel as part of the current stroke. + Pixel(BrushPixelMessage), +} + +/// A message that can be sent to indicate that the pixel at the given coordinates +/// should be painted with the given alpha. +#[derive(Debug, Clone)] +pub struct BrushPixelMessage { + /// The coordinates of the pixel to paint. + pub position: Vector2, + /// The transparency of the brush, from 0.0 for transparent to 1.0 for opaque. + pub alpha: f32, + /// A value whose meaning depends on the brush. + /// For flatten brushes, this is the target height. + pub value: f32, +} + +/// A queue that stores pixels that are waiting to be drawn by the brush. +pub struct PixelMessageBuffer { + data: VecDeque, + max_size: usize, +} + +impl PixelMessageBuffer { + /// Create a new buffer with the given size. + #[inline] + pub fn new(max_size: usize) -> Self { + Self { + data: VecDeque::with_capacity(max_size), + max_size, + } + } + /// True if the buffer has reached its maximum size. + #[inline] + pub fn is_full(&self) -> bool { + self.max_size == self.data.len() + } + /// True if there is nothing to pop from the queue. + #[inline] + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + /// Remove the message from the front of the queue and return it, if the queue is not empty. + #[inline] + pub fn pop(&mut self) -> Option { + self.data.pop_front() + } + /// Push a message onto the back of the queue, or panic of the queue is full. + pub fn push(&mut self, message: BrushPixelMessage) { + assert!(self.data.len() < self.max_size); + if let Some(m) = self + .data + .iter_mut() + .find(|m| m.position == message.position) + { + if message.alpha > m.alpha { + m.alpha = message.alpha; + m.value = message.value; + } + } else { + self.data.push_back(message); + } + } +} + +/// Object to send to painting thread to control which textures are modified. +#[derive(Debug, Clone)] +pub struct TerrainTextureData { + /// The height and width of the texture in pixels. + pub chunk_size: Vector2, + /// The kind of texture. + pub kind: TerrainTextureKind, + /// The texture resources, organized by chunk grid position. + pub resources: FxHashMap, TextureResource>, +} + +/// Terrain textures come in multiple kinds. +/// Height textures contain f32 values for each vertex of the terrain. +/// Mask textures contain u8 values indicating transparency. +/// Coordinates are interpreted differently between the two kinds of texture +/// because the data in height textures overlaps with the data in neighboring chunks, +/// so the pixels along each edge are duplicated and must be kept in sync +/// so that the chunks do not disconnect. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub enum TerrainTextureKind { + #[default] + /// Height texture with f32 height values and overlapping edges between chunks. + Height, + /// Mask texture with u8 oppacity values. + Mask, +} + +/// Sender with methods for sending the messages which control a brush painting thread. +pub struct BrushSender(Sender); + +impl BrushSender { + /// Create a new BrushSender using the given Sender. + pub fn new(sender: Sender) -> Self { + Self(sender) + } + /// Begin a new stroke using the given brush. + pub fn start_stroke(&self, brush: Brush, node: Handle, data: TerrainTextureData) { + self.0 + .send(BrushThreadMessage::StartStroke(brush, node, data)) + .unwrap_or_else(on_send_failure); + } + /// End the current stroke. + pub fn end_stroke(&self) { + self.0 + .send(BrushThreadMessage::EndStroke) + .unwrap_or_else(on_send_failure); + } + /// Draw a pixel using the brush that was set in the most recent call to [BrushSender::start_stroke]. + #[inline] + pub fn draw_pixel(&self, position: Vector2, alpha: f32, value: f32) { + if alpha == 0.0 { + return; + } + self.0 + .send(BrushThreadMessage::Pixel(BrushPixelMessage { + position, + alpha, + value, + })) + .unwrap_or_else(on_send_failure); + } +} + +fn on_send_failure(_error: SendError) { + /* + Log::err(format!( + "A brush painting message was not sent. {:?}", + error + )); + */ +} + +/// Type for a callback that delivers the original data of textures that have been modified +/// by the brush so that changes might be undone. +pub type UndoChunkHandler = dyn FnMut(UndoData) + Send; + +/// A record of original data data for chunks that have been modified by a brushstroke. +pub struct UndoData { + /// The handle of the terrain being edited + pub node: Handle, + /// The data of the chunks as they were before the brushstroke. + pub chunks: Vec, + /// The kind of data within the terrain that is being edited. + pub target: BrushTarget, +} + +#[derive(Default)] +/// Data for an in-progress terrain painting operation +pub struct BrushStroke { + /// The brush that is currently being used. This determines how the terrain textures are edited. + brush: Brush, + /// The textures for the terrain that is currently being edited. + textures: FxHashMap, TextureResource>, + /// The node of the terrain being edted + node: Handle, + /// Callback to handle the saved original chunk data after each stroke. + /// This is called when [BrushThreadMessage::EndStroke] is received. + undo_chunk_handler: Option>, + /// A record of which pixels have been modified in each chunk since the last UpdateTextures. + /// This is cleared after [BrushThreadMessage::UpdateTextures] is received, + /// when the pixel data is transferred into the textures. + chunks: StrokeChunks, + /// Data copied from chunks that have been edited by the current brush stroke. + /// This preserves the textures as they were before the stroke began, so that + /// an undo command can be created at the end of the stroke. + undo_chunks: Vec, + /// A record of every pixel of the stroke, including the strength of the brush at that pixel, + /// the original value before the stroke began, and the current value. + height_pixels: StrokeData, + /// A record of every pixel of the stroke, including the strength of the brush at that pixel, + /// the original value before the stroke began, and the current value. + mask_pixels: StrokeData, +} + +/// Stores pixels that have been modified by a brush during a stroke. +/// It remembers the strength of the brush, the value of the painted pixel, +/// and the value of the original pixel before the brushstroke. +/// This should be cleared after each stroke using [StrokeData::clear]. +/// +/// `V` is the type of data stored in the pixel being edited. +#[derive(Debug, Default)] +pub struct StrokeData(FxHashMap, StrokeElement>); + +/// A single pixel data of a brush stroke +#[derive(Debug, Copy, Clone)] +pub struct StrokeElement { + /// The intensity of the brush stroke, with 0.0 indicating a pixel that brush has not touched + /// and 1.0 indicates a pixel fully covered by the brush. + pub strength: f32, + /// The value of the pixel before the stroke began. + pub original_value: V, + /// The current value of the pixel. + pub latest_value: V, +} + +impl BrushStroke { + /// Create a BrushStroke with the given handler for saving undo data for chunks. + pub fn with_chunk_handler(undo_chunk_handler: Box) -> Self { + Self { + undo_chunk_handler: Some(undo_chunk_handler), + ..Default::default() + } + } + /// Prepare this object for a new brushstroke. + pub fn clear(&mut self) { + self.height_pixels.clear(); + self.mask_pixels.clear(); + self.chunks.clear(); + } + /// Access the data in the textures to find the value for the pixel at the given position. + pub fn data_pixel(&self, position: Vector2) -> Option + where + V: Clone, + { + // Determine which texture holds the data for the position. + let grid_pos = self.chunks.pixel_position_to_grid_position(position); + // Determine which pixel within the texture corresponds to the given position. + let origin = self.chunks.chunk_to_origin(grid_pos); + let p = position - origin; + // Access the texture and extract the data. + let texture = self.textures.get(&grid_pos)?; + let index = self.chunks.pixel_index(p); + let data = texture.data_ref(); + Some(data.data_of_type::().unwrap()[index].clone()) + } + /// Block on the given receiver until its messages are exhausted and perform the painting + /// operations according to the messages. + /// It does not return until the receiver's channel no longer has senders. + pub fn accept_messages(&mut self, receiver: Receiver) { + let mut message_buffer = PixelMessageBuffer::new(MESSAGE_BUFFER_SIZE); + loop { + // Collect all waiting messages, until the buffer is full. + while !message_buffer.is_full() { + if let Ok(message) = receiver.try_recv() { + // Act on the message, potentially adding it to the message buffer. + self.handle_message(message, &mut message_buffer); + } else { + break; + } + } + if let Some(pixel) = message_buffer.pop() { + // Perform the drawing operation for the current pixel message. + self.handle_pixel_message(pixel); + } else if self.chunks.count() > 0 { + // We have run out of pixels to process, so before we block to wait for more, + // write the currently processed pixels to the terrain textures. + self.flush(); + } else { + // If the message buffer is empty, we cannot proceed, so block until a message is available. + // Block until either a message arrives or the channel is closed. + if let Ok(message) = receiver.recv() { + // Act on the message, potentially adding it to the message buffer. + self.handle_message(message, &mut message_buffer); + } else { + // The message buffer is empty and the channel is closed, so we're finished. + // Flush pixels to the textures. + self.end_stroke(); + return; + } + } + } + } + fn handle_message( + &mut self, + message: BrushThreadMessage, + message_buffer: &mut PixelMessageBuffer, + ) { + match message { + BrushThreadMessage::StartStroke(brush, node, textures) => { + self.brush = brush; + self.node = node; + self.textures = textures.resources; + self.chunks.set_layout(textures.kind, textures.chunk_size); + } + BrushThreadMessage::EndStroke => { + // The stroke has ended, so finish processing all buffered pixel messages. + while let Some(p) = message_buffer.pop() { + self.handle_pixel_message(p); + } + // Apply buffered pixels to the terrain textures. + self.end_stroke(); + + // Prepare to process the next stroke. + self.clear(); + } + BrushThreadMessage::Pixel(pixel) => { + message_buffer.push(pixel); + } + } + } + fn end_stroke(&mut self) { + if let Some(handler) = &mut self.undo_chunk_handler { + // Copy the textures that are about to be modified so that the modifications can be undone. + self.chunks + .copy_texture_data(&self.textures, &mut self.undo_chunks); + // Send the saved textures to the handler so that an undo command might be created. + handler(UndoData { + node: self.node, + chunks: std::mem::take(&mut self.undo_chunks), + target: self.brush.target, + }); + } + // Flush pixels to the terrain textures + self.apply(); + } + fn handle_pixel_message(&mut self, pixel: BrushPixelMessage) { + let position = pixel.position; + match self.chunks.kind() { + TerrainTextureKind::Height => self.accept_pixel_height(pixel), + TerrainTextureKind::Mask => self.accept_pixel_mask(pixel), + } + self.chunks.write(position); + if self.chunks.count() >= PIXEL_BUFFER_SIZE { + self.flush(); + } + } + fn smooth_height( + &self, + position: Vector2, + kernel_radius: u32, + original: f32, + alpha: f32, + ) -> f32 { + let radius = kernel_radius as i32; + let diameter = kernel_radius * 2 + 1; + let area = (diameter * diameter - 1) as f32; + let mut total = 0.0; + for x in -radius..=radius { + for y in -radius..=radius { + if x == 0 && y == 0 { + continue; + } + let pos = position + Vector2::new(x, y); + let value = self + .height_pixels + .original_pixel_value(pos) + .copied() + .or_else(|| self.data_pixel(position)) + .unwrap_or_default(); + total += value; + } + } + let smoothed = total / area; + original * (1.0 - alpha) + smoothed * alpha + } + fn smooth_mask( + &self, + position: Vector2, + kernel_radius: u32, + original: u8, + alpha: f32, + ) -> u8 { + let radius = kernel_radius as i32; + let diameter = kernel_radius as u64 * 2 + 1; + let area = diameter * diameter - 1; + let mut total: u64 = 0; + for x in -radius..=radius { + for y in -radius..=radius { + if x == 0 && y == 0 { + continue; + } + let pos = position + Vector2::new(x, y); + let value = self + .mask_pixels + .original_pixel_value(pos) + .copied() + .or_else(|| self.data_pixel(position)) + .unwrap_or_default(); + total += value as u64; + } + } + let smoothed = total / area; + (original as f32 * (1.0 - alpha) + smoothed as f32 * alpha).clamp(0.0, 255.0) as u8 + } + fn accept_pixel_height(&mut self, pixel: BrushPixelMessage) { + let position = pixel.position; + let mut pixels = std::mem::take(&mut self.height_pixels); + let original = pixels.update_strength(position, pixel.alpha, || self.data_pixel(position)); + self.height_pixels = pixels; + let Some(original) = original else { + return; + }; + let alpha = self.brush.alpha * pixel.alpha; + let result: f32 = match self.brush.mode { + BrushMode::Raise { amount } => original + amount * alpha, + BrushMode::Flatten => original * (1.0 - alpha) + pixel.value * alpha, + BrushMode::Assign { value } => original * (1.0 - alpha) + value * alpha, + BrushMode::Smooth { kernel_radius } => { + self.smooth_height(position, kernel_radius, original, alpha) + } + }; + self.height_pixels.set_latest(position, result); + } + fn accept_pixel_mask(&mut self, pixel: BrushPixelMessage) { + let position = pixel.position; + let mut pixels = std::mem::take(&mut self.mask_pixels); + let original = pixels.update_strength(position, pixel.alpha, || self.data_pixel(position)); + self.mask_pixels = pixels; + let Some(original) = original else { + return; + }; + let alpha = self.brush.alpha * pixel.alpha; + let result: u8 = match self.brush.mode { + BrushMode::Raise { amount } => mask_raise(original, amount * alpha), + BrushMode::Flatten => mask_lerp(original, pixel.value, alpha), + BrushMode::Assign { value } => mask_lerp(original, value, alpha), + BrushMode::Smooth { kernel_radius } => { + self.smooth_mask(position, kernel_radius, original, alpha) + } + }; + self.mask_pixels.set_latest(position, result); + } + fn flush(&mut self) { + if self.undo_chunk_handler.is_some() { + // Copy the textures that are about to be modified so that the modifications can be undone. + self.chunks + .copy_texture_data(&self.textures, &mut self.undo_chunks); + } + self.apply(); + self.chunks.clear(); + } + fn apply(&self) { + match self.chunks.kind() { + TerrainTextureKind::Height => { + self.chunks.apply(&self.height_pixels, &self.textures); + } + TerrainTextureKind::Mask => { + self.chunks.apply(&self.mask_pixels, &self.textures); + } + } + } +} + +impl StrokeData { + /// Reset the brush stroke so it is ready to begin a new stroke. + #[inline] + pub fn clear(&mut self) { + self.0.clear() + } + /// For every pixel that is modified by the stroke, the original values + /// is stored as it was before the stroke began. + #[inline] + pub fn original_pixel_value(&self, position: Vector2) -> Option<&V> { + self.0.get(&position).map(|x| &x.original_value) + } + /// The updated pixel value based on whatever editing operation the stroke is performing. + #[inline] + pub fn latest_pixel_value(&self, position: Vector2) -> Option<&V> { + self.0.get(&position).map(|x| &x.latest_value) + } + /// Update the stroke with a new value at the given pixel position. + /// This must only be called after calling [StrokeData::update_strength] + /// to ensure that this stroke contains data for the position. + /// Otherwise this method may panic. + #[inline] + pub fn set_latest(&mut self, position: Vector2, value: V) { + if let Some(el) = self.0.get_mut(&position) { + el.latest_value = value; + } else { + unreachable!("Setting latest value of missing element"); + } + } + /// Stores or modifies the StrokeElement at the given position. + /// If the element is updated, return the original pixel value of the element. + /// - `position`: The position of the data to modify within the terrain. + /// - `strength`: The strength of the brush at the position, from 0.0 to 1.0. + /// The element is updated if the stored strength is less than the given strength. + /// If there is no stored strength, that is treated as a strength of 0.0. + /// - `pixel_value`: The current value of the data. + /// This may be stored in the StrokeData if no pixel value is currently recorded for the given position. + /// Otherwise, this value is ignored. + #[inline] + pub fn update_strength( + &mut self, + position: Vector2, + strength: f32, + pixel_value: F, + ) -> Option + where + V: Clone, + F: FnOnce() -> Option, + { + if strength == 0.0 { + None + } else if let Some(element) = self.0.get_mut(&position) { + if element.strength < strength { + element.strength = strength; + Some(element.original_value.clone()) + } else { + None + } + } else { + let value = pixel_value()?; + let element = StrokeElement { + strength, + latest_value: value.clone(), + original_value: value.clone(), + }; + self.0.insert(position, element); + Some(value) + } + } +} + +/// Shape of a brush. +#[derive(Copy, Clone, Reflect, Debug)] +pub enum BrushShape { + /// Circle with given radius. + Circle { + /// Radius of the circle. + radius: f32, + }, + /// Rectangle with given width and height. + Rectangle { + /// Width of the rectangle. + width: f32, + /// Length of the rectangle. + length: f32, + }, +} + +uuid_provider!(BrushShape = "a4dbfba0-077c-4658-9972-38384a8432f9"); + +impl Default for BrushShape { + fn default() -> Self { + BrushShape::Circle { radius: 1.0 } + } +} + +impl BrushShape { + /// Return true if the given point is within the shape when positioned at the given center point. + pub fn contains(&self, brush_center: Vector2, pixel_position: Vector2) -> bool { + match *self { + BrushShape::Circle { radius } => (brush_center - pixel_position).norm() < radius, + BrushShape::Rectangle { width, length } => Rect::new( + brush_center.x - width * 0.5, + brush_center.y - length * 0.5, + width, + length, + ) + .contains(pixel_position), + } + } +} + +/// Paint mode of a brush. It defines operation that will be performed on the terrain. +#[derive(Clone, PartialEq, PartialOrd, Reflect, Debug)] +pub enum BrushMode { + /// Raise or lower the value + Raise { + /// An offset to change the value by + amount: f32, + }, + /// Flattens value of the terrain data + Flatten, + /// Assigns a particular value to anywhere the brush touches. + Assign { + /// Fixed value to paint into the data + value: f32, + }, + /// Reduce sharp changes in the data. + Smooth { + /// Determines the size of each pixel's neighborhood in terms of + /// distance from the pixel. + /// 0 means no smoothing at all. + /// 1 means taking the mean of the 3x3 square of pixels surrounding each smoothed pixel. + /// 2 means using a 5x5 square of pixels. And so on. + kernel_radius: u32, + }, +} + +uuid_provider!(BrushMode = "48ad4cac-05f3-485a-b2a3-66812713841f"); + +impl Default for BrushMode { + fn default() -> Self { + BrushMode::Raise { amount: 1.0 } + } +} + +/// Paint target of a brush. It defines the data that the brush will operate on. +#[derive(Copy, Default, Clone, Reflect, Debug, PartialEq, Eq)] +pub enum BrushTarget { + #[default] + /// Modifies the height map + HeightMap, + /// Draws on a given layer + LayerMask { + /// The number of the layer to modify + layer: usize, + }, +} + +uuid_provider!(BrushTarget = "461c1be7-189e-44ee-b8fd-00b8fdbc668f"); + +/// Brush is used to modify terrain. It supports multiple shapes and modes. +#[derive(Clone, Default, Reflect, Debug)] +pub struct Brush { + /// Shape of the brush. + pub shape: BrushShape, + /// Paint mode of the brush. + pub mode: BrushMode, + /// The data to modify with the brush + pub target: BrushTarget, + /// Transform that can modify the shape of the brush + pub transform: Matrix2, + /// The softness of the edges of the brush. + /// 0.0 means that the brush fades very gradually from opaque to transparent. + /// 1.0 means that the edges of the brush do not fade. + pub hardness: f32, + /// The transparency of the brush, allowing the values beneath the brushstroke to show through. + /// 0.0 means the brush is fully transparent and does not draw. + /// 1.0 means the brush is fully opaque. + pub alpha: f32, +} + +impl Brush { + /// Send the pixels for this brush to the brush thread. + /// - `position`: The position of the brush in texture pixels. + /// - `scale`: The size of each pixel in local 2D space. This is used + /// to convert the brush's radius from local 2D to pixels. + /// - `value`: The brush's value. The meaning of this number depends on the brush. + /// - `sender`: The sender that will transmit the pixels. + pub fn stamp( + &self, + position: Vector2, + scale: Vector2, + value: f32, + sender: &BrushSender, + ) { + let mut transform = self.transform; + let x_factor = scale.y / scale.x; + transform.m11 *= x_factor; + transform.m12 *= x_factor; + match self.shape { + BrushShape::Circle { radius } => { + for BrushPixel { position, strength } in StampPixels::new( + CircleRaster(radius / scale.y), + position, + self.hardness, + transform, + ) { + sender.draw_pixel(position, strength, value); + } + } + BrushShape::Rectangle { width, length } => { + for BrushPixel { position, strength } in StampPixels::new( + RectRaster(width * 0.5 / scale.y, length * 0.5 / scale.y), + position, + self.hardness, + transform, + ) { + sender.draw_pixel(position, strength, value); + } + } + } + } + /// Send the pixels for this brush to the brush thread. + /// - `start`: The position of the brush when it started the smear in texture pixels. + /// - `end`: The current position of the brush in texture pixels. + /// - `scale`: The size of each pixel in local 2D space. This is used + /// to convert the brush's radius from local 2D to pixels. + /// - `value`: The brush's value. The meaning of this number depends on the brush. + /// - `sender`: The sender that will transmit the pixels. + pub fn smear( + &self, + start: Vector2, + end: Vector2, + scale: Vector2, + value: f32, + sender: &BrushSender, + ) { + let mut transform = self.transform; + let x_factor = scale.y / scale.x; + transform.m11 *= x_factor; + transform.m12 *= x_factor; + match self.shape { + BrushShape::Circle { radius } => { + for BrushPixel { position, strength } in SmearPixels::new( + CircleRaster(radius / scale.y), + start, + end, + self.hardness, + transform, + ) { + sender.draw_pixel(position, strength, value); + } + } + BrushShape::Rectangle { width, length } => { + for BrushPixel { position, strength } in SmearPixels::new( + RectRaster(width * 0.5 / scale.y, length * 0.5 / scale.y), + start, + end, + self.hardness, + transform, + ) { + sender.draw_pixel(position, strength, value); + } + } + } + } +} + +/// A copy of a layer of data from a chunk. +/// It can be height data or mask data, since the type is erased. +/// The layer that this data represents must be remembered externally. +pub struct ChunkData { + /// The grid position of the original chunk. + pub grid_position: Vector2, + /// The size of the original chunk, to confirm that the chunk's size has not changed since the data was copied. + pub size: Vector2, + /// The type-erased data from either the height or one of the layers of the chunk. + pub content: Box<[u8]>, +} + +impl std::fmt::Debug for ChunkData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ChunkData") + .field("grid_position", &self.grid_position) + .field("content", &format!("[..](len: {})", &self.content.len())) + .finish() + } +} + +fn size_from_texture(texture: &ResourceDataRef<'_, Texture>) -> Vector2 { + match texture.kind() { + crate::resource::texture::TextureKind::Rectangle { width, height } => { + Vector2::new(width, height) + } + _ => unreachable!("Terrain texture was not rectangle."), + } +} + +impl ChunkData { + /// Extract the size from the given texture and return true if that size matches + /// the size required by this data. Log an error message and return false otherwise. + fn verify_texture_size(&self, texture: &ResourceDataRef<'_, Texture>) -> bool { + let size = size_from_texture(texture); + if size != self.size { + Log::err("Command swap failed due to texture size mismatch"); + false + } else { + true + } + } + /// Create a ChunkData for the given texture at the given position. + pub fn from_texture(grid_position: Vector2, texture: &TextureResource) -> Self { + let data_ref = texture.data_ref(); + let size = size_from_texture(&data_ref); + let data = Box::<[u8]>::from(data_ref.data()); + Self { + grid_position, + size, + content: data, + } + } + /// Swap the content of this data with the content of the given chunk's height map. + pub fn swap_height(&mut self, chunk: &mut Chunk) { + let mut data_ref = chunk.heightmap().data_ref(); + if !self.verify_texture_size(&data_ref) { + return; + } + let mut modify = data_ref.modify(); + for (a, b) in modify.data_mut().iter_mut().zip(self.content.iter_mut()) { + std::mem::swap(a, b); + } + } + /// Swap the content of this data with the content of the given chunk's mask layer. + pub fn swap_layer_mask(&mut self, chunk: &mut Chunk, layer: usize) { + let mut data_ref = chunk.layer_masks[layer].data_ref(); + if !self.verify_texture_size(&data_ref) { + return; + } + let mut modify = data_ref.modify(); + for (a, b) in modify.data_mut().iter_mut().zip(self.content.iter_mut()) { + std::mem::swap(a, b); + } + } + /// Swap the height data of the a chunk from the list with the height data in this object. + /// The given list of chunks will be searched to find the chunk that matches `grid_position`. + pub fn swap_height_from_list(&mut self, chunks: &mut [Chunk]) { + for c in chunks { + if c.grid_position == self.grid_position { + self.swap_height(c); + break; + } + } + } + /// Swap the layer mask data of a particular layer of a chunk from the list with the data in this object. + /// The given list of chunks will be searched to find the chunk that matches `grid_position`. + pub fn swap_layer_mask_from_list(&mut self, chunks: &mut [Chunk], layer: usize) { + for c in chunks { + if c.grid_position == self.grid_position { + self.swap_layer_mask(c, layer); + break; + } + } + } +} diff --git a/fyrox-impl/src/scene/terrain/brushstroke/strokechunks.rs b/fyrox-impl/src/scene/terrain/brushstroke/strokechunks.rs new file mode 100644 index 000000000..75c61aef0 --- /dev/null +++ b/fyrox-impl/src/scene/terrain/brushstroke/strokechunks.rs @@ -0,0 +1,326 @@ +use super::{ChunkData, StrokeData, TerrainTextureKind}; +use crate::core::algebra::Vector2; +use crate::fxhash::{FxHashMap, FxHashSet}; +use crate::resource::texture::TextureResource; +use crate::scene::terrain::pixel_position_to_grid_position; + +/// The pixels for a stroke for one chunk, generalized over the type of data being edited. +#[derive(Debug, Default)] +pub struct StrokeChunks { + /// The size of each chunk as measured by distance from one chunk origin to the next. + /// This does not include the overlap pixel around the edges of height textures, + /// because that overlap does not contribute to the distance between the origins of the textuers. + chunk_size: Vector2, + kind: TerrainTextureKind, + /// The position of each written pixel within each chunk. + written_pixels: FxHashMap, FxHashSet>>, + /// The number of pixels written to this object. + count: usize, + unused_chunks: Vec>>, +} + +impl StrokeChunks { + #[inline] + pub fn count(&self) -> usize { + self.count + } + #[inline] + pub fn kind(&self) -> TerrainTextureKind { + self.kind + } + /// Erase the currently stored pixel data and prepare for a new set of pixels. + pub fn clear(&mut self) { + self.count = 0; + // Move and clear the no-longer needed chunk pixel sets into the unused list. + for mut c in self.written_pixels.drain().map(|(_, v)| v) { + c.clear(); + self.unused_chunks.push(c); + } + } + /// Update the texture kind and the texture size. + pub fn set_layout(&mut self, kind: TerrainTextureKind, size: Vector2) { + self.kind = kind; + // If the texture is a height texture, then its edges overlap with the neigboring chunks, + // so the size we need is one less than the actual size in each dimension. + self.chunk_size = match kind { + TerrainTextureKind::Height => size.map(|x| x - 1), + TerrainTextureKind::Mask => size, + }; + } + /// For every chunk that has been written in this object since the last clear, copy the texture data from the given textures + /// and store it in the given list of texture data, if the list does not already contain data for those coordinates. + /// + /// The purpose of this is to save a backup copy of chunks that are modified by the current brushstroke so that an undo command + /// can be created. This should be called immediately before [StrokeChunks::apply] so that the copied textures are unmodified. + /// If saved chunk data already exists for some chunk, nothing is done since it is presumed that existing data is the original + /// data that we are trying to preserve. + /// + /// - `textures`: The source of texture data. + /// - `saved_chunk_data`: The list of chunk data that may be modified if it does not already contain a copy of chunk data for each + /// written chunk coordinates. + pub fn copy_texture_data( + &self, + textures: &FxHashMap, TextureResource>, + saved_chunk_data: &mut Vec, + ) { + for (c, _) in self.written_pixels.iter() { + if saved_chunk_data.iter().any(|x| x.grid_position == *c) { + continue; + } + let Some(texture) = textures.get(c) else { + continue; + }; + saved_chunk_data.push(ChunkData::from_texture(*c, texture)); + } + } + /// Use the pixels stored in this object to modify the given textures with + /// pixel data from the given StrokeData. + /// Once the textures have been modified using this method [StrokeChunks::clear] + /// should be called, since the data in this object has served its purpose. + pub fn apply( + &self, + stroke: &StrokeData, + textures: &FxHashMap, TextureResource>, + ) where + V: Clone, + { + for (c, pxs) in self.written_pixels.iter() { + let Some(texture) = textures.get(c) else { + continue; + }; + let mut texture_data = texture.data_ref(); + let mut modify = texture_data.modify(); + let Some(data) = modify.data_mut_of_type::() else { + continue; + }; + let origin = self.chunk_to_origin(*c); + let row_size = self.row_size(); + for p in pxs.iter() { + let Some(value) = stroke.latest_pixel_value(origin + p.map(|x| x as i32)) else { + continue; + }; + let index = p.x as usize + p.y as usize * row_size; + data[index].clone_from(value); + } + } + } + #[inline] + pub fn pixel_position_to_grid_position(&self, position: Vector2) -> Vector2 { + pixel_position_to_grid_position(position, self.chunk_size) + } + pub fn chunk_to_origin(&self, grid_position: Vector2) -> Vector2 { + Vector2::new( + grid_position.x * self.chunk_size.x as i32, + grid_position.y * self.chunk_size.y as i32, + ) + } + /// The width of the texture in pixels. + pub fn row_size(&self) -> usize { + match self.kind { + TerrainTextureKind::Height => (self.chunk_size.x + 1) as usize, + TerrainTextureKind::Mask => self.chunk_size.x as usize, + } + } + /// Calculate the index of a pixel at the given position within texture data, + /// based on the row size. The given position is relative to the origin of the texture + /// and must be within the bounds of the texture. + pub fn pixel_index(&self, position: Vector2) -> usize { + if position.x < 0 + || position.x >= self.chunk_size.x as i32 + || position.y < 0 + || position.y >= self.chunk_size.y as i32 + { + panic!( + "Invalid pixel position: ({}, {}) within ({}, {})", + position.x, position.y, self.chunk_size.x, self.chunk_size.y + ); + } + let p = position.map(|x| x as usize); + p.x + p.y * self.row_size() + } + /// Insert the the pixel at the given position into this data. + /// This method determines which chunks have a pixel at that position + /// and marks each of those chunks as being modified. + pub fn write(&mut self, position: Vector2) { + let grid_pos = self.pixel_position_to_grid_position(position); + let origin = self.chunk_to_origin(grid_pos); + let pos = (position - origin).map(|x| x as u32); + self.count += 1; + self.write_to_chunk(grid_pos, pos); + if self.kind == TerrainTextureKind::Height { + if pos.x == 0 { + self.write_to_chunk( + Vector2::new(grid_pos.x - 1, grid_pos.y), + Vector2::new(self.chunk_size.x, pos.y), + ); + } + if pos.y == 0 { + self.write_to_chunk( + Vector2::new(grid_pos.x, grid_pos.y - 1), + Vector2::new(pos.x, self.chunk_size.y), + ); + } + if pos.x == 0 && pos.y == 0 { + self.write_to_chunk( + Vector2::new(grid_pos.x - 1, grid_pos.y - 1), + self.chunk_size, + ); + } + } + } + fn write_to_chunk(&mut self, grid_pos: Vector2, position: Vector2) { + let mut unused = std::mem::take(&mut self.unused_chunks); + self.written_pixels + .entry(grid_pos) + .or_insert_with(|| unused.pop().unwrap_or_default()) + .insert(position); + self.unused_chunks = unused; + } +} + +#[cfg(test)] +mod tests { + use super::*; + const RANDOM_POINTS: &[(i32, i32)] = &[ + (0, 0), + (1, 1), + (2, 2), + (-1, -1), + (20, -123), + (-11, 22), + (42, 285), + (360, -180), + (123, -456), + (54, 32), + (-2, -3), + ]; + #[test] + fn chunk_to_origin() { + let mut chunks = StrokeChunks::default(); + chunks.set_layout(TerrainTextureKind::Height, Vector2::new(5, 5)); + assert_eq!( + chunks.chunk_to_origin(Vector2::new(0, 0)), + Vector2::new(0, 0) + ); + assert_eq!( + chunks.chunk_to_origin(Vector2::new(1, 0)), + Vector2::new(4, 0) + ); + assert_eq!( + chunks.chunk_to_origin(Vector2::new(-2, -1)), + Vector2::new(-8, -4) + ); + } + #[test] + fn pixel_position_to_grid_position() { + let mut chunks = StrokeChunks::default(); + chunks.set_layout(TerrainTextureKind::Height, Vector2::new(5, 5)); + assert_eq!( + chunks.pixel_position_to_grid_position(Vector2::new(0, 0)), + Vector2::new(0, 0) + ); + assert_eq!( + chunks.pixel_position_to_grid_position(Vector2::new(2, 3)), + Vector2::new(0, 0) + ); + assert_eq!( + chunks.pixel_position_to_grid_position(Vector2::new(-1, -1)), + Vector2::new(-1, -1) + ); + assert_eq!( + chunks.pixel_position_to_grid_position(Vector2::new(4, -4)), + Vector2::new(1, -1) + ); + } + fn test_points_height(size: Vector2) { + let mut chunks = StrokeChunks::default(); + chunks.set_layout(TerrainTextureKind::Height, size); + for p in RANDOM_POINTS.iter() { + let p = Vector2::new(p.0, p.1); + let grid_pos = chunks.pixel_position_to_grid_position(p); + let origin = chunks.chunk_to_origin(grid_pos); + let pixel = p - origin; + test_point(p, pixel, size.map(|x| x - 1)); + } + let s = size.map(|x| x as i32); + for x in -s.x..=s.x * 2 { + for y in -s.y..=s.y * 2 { + let p = Vector2::new(x, y); + let grid_pos = chunks.pixel_position_to_grid_position(p); + let origin = chunks.chunk_to_origin(grid_pos); + let pixel = p - origin; + test_point(p, pixel, size.map(|x| x - 1)); + } + } + } + fn test_points_mask(size: Vector2) { + let mut chunks = StrokeChunks::default(); + chunks.set_layout(TerrainTextureKind::Mask, size); + for p in RANDOM_POINTS.iter() { + let p = Vector2::new(p.0, p.1); + let grid_pos = chunks.pixel_position_to_grid_position(p); + let origin = chunks.chunk_to_origin(grid_pos); + let pixel = p - origin; + test_point(p, pixel, size); + } + let s = size.map(|x| x as i32); + for x in -s.x..=s.x * 2 { + for y in -s.y..=s.y * 2 { + let p = Vector2::new(x, y); + let grid_pos = chunks.pixel_position_to_grid_position(p); + let origin = chunks.chunk_to_origin(grid_pos); + let pixel = p - origin; + test_point(p, pixel, size); + } + } + } + fn test_point(p: Vector2, pixel: Vector2, size: Vector2) { + assert!( + pixel.x >= 0, + "({}, {}) -> ({}, {})", + p.x, + p.y, + pixel.x, + pixel.y + ); + assert!( + pixel.y >= 0, + "({}, {}) -> ({}, {})", + p.x, + p.y, + pixel.x, + pixel.y + ); + assert!( + pixel.x < size.x as i32, + "({}, {}) -> ({}, {})", + p.x, + p.y, + pixel.x, + pixel.y + ); + assert!( + pixel.y < size.y as i32, + "({}, {}) -> ({}, {})", + p.x, + p.y, + pixel.x, + pixel.y + ); + } + #[test] + fn random_points_5x5() { + test_points_height(Vector2::new(5, 5)); + test_points_mask(Vector2::new(5, 5)); + } + #[test] + fn random_points_10x10() { + test_points_height(Vector2::new(10, 10)); + test_points_mask(Vector2::new(10, 10)); + } + #[test] + fn random_points_257x257() { + test_points_height(Vector2::new(257, 257)); + test_points_mask(Vector2::new(257, 257)); + } +} diff --git a/fyrox-impl/src/scene/terrain/mod.rs b/fyrox-impl/src/scene/terrain/mod.rs index d8a9f70bc..bd9148f18 100644 --- a/fyrox-impl/src/scene/terrain/mod.rs +++ b/fyrox-impl/src/scene/terrain/mod.rs @@ -6,7 +6,7 @@ use crate::scene::node::RdcControlFlow; use crate::{ asset::Resource, core::{ - algebra::{Matrix2, Matrix4, Point3, Vector2, Vector3, Vector4}, + algebra::{Matrix4, Point3, Vector2, Vector3, Vector4}, arrayvec::ArrayVec, log::Log, math::{aabb::AxisAlignedBoundingBox, ray::Ray, ray_rect_intersection, Rect}, @@ -49,11 +49,11 @@ use std::{ ops::{Deref, DerefMut, Range}, }; -mod brushstroke; +pub mod brushstroke; mod geometry; mod quadtree; -pub use brushstroke::*; +use brushstroke::*; /// Current implementation version marker. pub const VERSION: u8 = 1; @@ -81,123 +81,6 @@ impl TerrainRect { } } -fn push_height_data(chunks: &mut Vec, new_chunk: &Chunk) { - if chunks - .iter() - .all(|c| c.grid_position != new_chunk.grid_position) - { - chunks.push(ChunkData::from_height(new_chunk)); - } -} - -fn push_layer_mask_data(chunks: &mut Vec, new_chunk: &Chunk, layer: usize) { - if chunks - .iter() - .all(|c| c.grid_position != new_chunk.grid_position) - { - chunks.push(ChunkData::from_layer_mask(new_chunk, layer)); - } -} - -/// Abstract access to terrain height data for use by brushes. -pub struct BrushableHeight<'a, 'b> { - /// The terrain to be edited - pub terrain: &'a mut Terrain, - /// The deta of the in-progress brushstroke. - pub stroke: &'b mut StrokeData, - /// Copies of the chunks that have been edited by the brushstroke, - /// as they were before being touched by the brush. - pub chunks: Vec, -} - -/// Abstract access to terrain layer mask data for use by brushes. -pub struct BrushableLayerMask<'a, 'b> { - /// The terrain to be edited - pub terrain: &'a mut Terrain, - /// The deta of the in-progress brushstroke. - pub stroke: &'b mut StrokeData, - /// The layer to edit. - pub layer: usize, - /// Copies of the chunks that have been edited by the brushstroke, - /// as they were before being touched by the brush. - pub chunks: Vec, -} - -impl<'a, 'b> BrushableTerrainData for BrushableHeight<'a, 'b> { - fn get_value(&self, position: Vector2) -> f32 { - self.stroke - .get(position) - .map(|x| x.original_value) - .or_else(|| self.terrain.get_height(position)) - .unwrap_or(0.0) - } - - fn update(&mut self, position: Vector2, strength: f32, func: F) - where - F: FnOnce(&Self, f32) -> f32, - { - if let Some(pixel_value) = self.terrain.get_height(position) { - if let Some(pixel_value) = self.stroke.update_pixel(position, strength, pixel_value) { - let value = func(self, pixel_value); - self.terrain.update_height_pixel( - position, - |_| value, - |c| push_height_data(&mut self.chunks, c), - ); - } - } - } - - fn sum_kernel(&self, position: Vector2, kernel_radius: u32) -> f32 { - let r = kernel_radius as i32; - let mut sum = 0.0; - for x in position.x - r..=position.x + r { - for y in position.y - r..=position.y + r { - sum += self.get_value(Vector2::new(x, y)); - } - } - sum - } -} - -impl<'a, 'b> BrushableTerrainData for BrushableLayerMask<'a, 'b> { - fn get_value(&self, position: Vector2) -> u8 { - self.stroke - .get(position) - .map(|x| x.original_value) - .or_else(|| self.terrain.get_layer_mask(position, self.layer)) - .unwrap_or(0) - } - - fn update(&mut self, position: Vector2, strength: f32, func: F) - where - F: FnOnce(&Self, u8) -> u8, - { - if let Some(pixel_value) = self.terrain.get_layer_mask(position, self.layer) { - if let Some(pixel_value) = self.stroke.update_pixel(position, strength, pixel_value) { - let value = func(self, pixel_value); - self.terrain - .update_mask_pixel(position, self.layer, |_| value); - let chunk_pos = self.terrain.chunk_containing_mask_pos(position); - if let Some(chunk) = self.terrain.find_chunk(chunk_pos) { - push_layer_mask_data(&mut self.chunks, chunk, self.layer); - } - } - } - } - - fn sum_kernel(&self, position: Vector2, kernel_radius: u32) -> f32 { - let r = kernel_radius as i32; - let mut sum: u32 = 0; - for x in position.x - r..=position.x + r { - for y in position.y - r..=position.y + r { - sum += self.get_value(Vector2::new(x, y)) as u32; - } - } - sum as f32 - } -} - /// Layers is a material Terrain can have as many layers as you want, but each layer slightly decreases /// performance, so keep amount of layers on reasonable level (1 - 5 should be enough for most /// cases). @@ -273,81 +156,6 @@ fn make_height_map_texture(height_map: Vec, size: Vector2) -> TextureR make_height_map_texture_internal(height_map, size).unwrap() } -/// A copy of a layer of data from a chunk. -/// It can be height data or mask data, since the type is erased. -/// The layer that this data represents must be remembered externally. -pub struct ChunkData { - /// The grid position of the original chunk. - pub grid_position: Vector2, - /// The type-erased data from either the height or one of the layers of the chunk. - pub content: Box<[u8]>, -} - -impl std::fmt::Debug for ChunkData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ChunkData") - .field("grid_position", &self.grid_position) - .field("content", &format!("[..](len: {})", &self.content.len())) - .finish() - } -} - -impl ChunkData { - /// Create a ChunkData object using data from the height map of the given chunk. - pub fn from_height(chunk: &Chunk) -> ChunkData { - let data = Box::<[u8]>::from(chunk.heightmap().data_ref().data()); - ChunkData { - grid_position: chunk.grid_position, - content: data, - } - } - /// Create a ChunkData object using data from a layer mask of the given chunk. - pub fn from_layer_mask(chunk: &Chunk, layer: usize) -> ChunkData { - let resource = &chunk.layer_masks[layer]; - let data = Box::<[u8]>::from(resource.data_ref().data()); - ChunkData { - grid_position: chunk.grid_position, - content: data, - } - } - /// Swap the content of this data with the content of the given chunk's height map. - pub fn swap_height(&mut self, chunk: &mut Chunk) { - let mut state = chunk.heightmap().state(); - let mut modify = state.data().unwrap().modify(); - for (a, b) in modify.data_mut().iter_mut().zip(self.content.iter_mut()) { - std::mem::swap(a, b); - } - } - /// Swap the content of this data with the content of the given chunk's mask layer. - pub fn swap_layer_mask(&mut self, chunk: &mut Chunk, layer: usize) { - let mut state = chunk.layer_masks[layer].state(); - let mut modify = state.data().unwrap().modify(); - for (a, b) in modify.data_mut().iter_mut().zip(self.content.iter_mut()) { - std::mem::swap(a, b); - } - } - /// Swap the height data of the a chunk from the list with the height data in this object. - /// The given list of chunks will be searched to find the chunk that matches `grid_position`. - pub fn swap_height_from_list(&mut self, chunks: &mut [Chunk]) { - for c in chunks { - if c.grid_position == self.grid_position { - self.swap_height(c); - break; - } - } - } - /// Swap the layer mask data of a particular layer of a chunk from the list with the data in this object. - /// The given list of chunks will be searched to find the chunk that matches `grid_position`. - pub fn swap_layer_mask_from_list(&mut self, chunks: &mut [Chunk], layer: usize) { - for c in chunks { - if c.grid_position == self.grid_position { - self.swap_layer_mask(c, layer); - break; - } - } - } -} - /// Chunk is smaller block of a terrain. Terrain can have as many chunks as you need, which always arranged in a /// grid. You can add chunks from any side of a terrain. Chunks could be considered as a "sub-terrain", which could /// use its own set of materials for layers. This could be useful for different biomes, to prevent high amount of @@ -500,6 +308,12 @@ impl Chunk { ) } + /// The 2D position of the chunk within the chunk array. + #[inline] + pub fn grid_position(&self) -> Vector2 { + self.grid_position + } + /// Returns a reference to height map. pub fn heightmap(&self) -> &TextureResource { self.heightmap.as_ref().unwrap() @@ -841,13 +655,13 @@ pub struct TerrainRayCastResult { /// - **Grid Position:** These are the 2D `i32` coordinates that represent a chunk's position within the regular grid of /// chunks that make up a terrain. The *local 2D* position of a chunk can be calculated from its *grid position* by /// multiplying its x and y coordinates by the x and y of [Terrain::chunk_size]. -/// - **Height Pixel Position:** These are the 2D `i32` coordinates that measure position across the x and z axes of +/// - **Height Pixel Position:** These are the 2D coordinates that measure position across the x and z axes of /// the terrain using pixels in the height data of each chunk. (0,0) is the position of the Terrain node. /// The *height pixel position* of a chunk can be calculated from its *grid position* by /// multiplying its x and y coordinates by (x - 1) and (y - 1) of [Terrain::height_map_size]. /// Subtracting 1 from each dimension is necessary because the height map data of chunks overlaps by one pixel /// on each edge, so the distance between the origins of two adjacent chunks is one less than height_map_size. -/// - **Mask Pixel Position:** These are the 2D `i32` coordinates that measure position across the x and z axes of +/// - **Mask Pixel Position:** These are the 2D coordinates that measure position across the x and z axes of /// the terrain using pixels of the mask data of each chunk. (0,0) is the position of the (0,0) pixel of the /// mask texture of the (0,0) chunk. /// This means that (0,0) is offset from the position of the Terrain node by a half-pixel in the x direction @@ -1120,6 +934,29 @@ fn project(global_transform: Matrix4, p: Vector3) -> Option, + chunk_size: Vector2, +) -> Vector2 { + let chunk_size = chunk_size.map(|x| x as i32); + let x = position.x / chunk_size.x; + let y = position.y / chunk_size.y; + // Correct for the possibility of x or y being negative. + let x = if position.x < 0 && position.x % chunk_size.x != 0 { + x - 1 + } else { + x + }; + let y = if position.y < 0 && position.y % chunk_size.y != 0 { + y - 1 + } else { + y + }; + Vector2::new(x, y) +} + impl TypeUuidProvider for Terrain { fn type_uuid() -> Uuid { uuid!("4b0a7927-bcd8-41a3-949a-dd10fba8e16a") @@ -1247,7 +1084,11 @@ impl Terrain { let heightmap = vec![0.0; (self.height_map_size.x * self.height_map_size.y) as usize]; let new_chunk = Chunk { - quad_tree: QuadTree::new(&heightmap, *self.block_size, *self.block_size), + quad_tree: QuadTree::new( + &heightmap, + *self.height_map_size, + *self.block_size, + ), heightmap: Some(make_height_map_texture(heightmap, self.height_map_size())), position: Vector3::new( x as f32 * self.chunk_size.x, @@ -1333,6 +1174,20 @@ impl Terrain { project(self.global_transform(), p) } + /// Convert from local 2D to height pixel position. + pub fn local_to_height_pixel(&self, p: Vector2) -> Vector2 { + let scale = self.height_grid_scale(); + Vector2::new(p.x / scale.x, p.y / scale.y) + } + + /// Convert from local 2D to mask pixel position. + pub fn local_to_mask_pixel(&self, p: Vector2) -> Vector2 { + let scale = self.mask_grid_scale(); + let half = scale * 0.5; + let p = p - half; + Vector2::new(p.x / scale.x, p.y / scale.y) + } + /// The size of each cell of the height grid in local 2D units. pub fn height_grid_scale(&self) -> Vector2 { let cell_width = self.chunk_size.x / (self.height_map_size.x - 1) as f32; @@ -1495,21 +1350,8 @@ impl Terrain { /// in 4 chunks. pub fn chunk_containing_height_pos(&self, position: Vector2) -> Vector2 { // Subtract 1 from x and y to exclude the overlapping pixel along both axes from the chunk size. - let chunk_size = self.height_map_size.map(|x| x as i32 - 1); - let x = position.x / chunk_size.x; - let y = position.y / chunk_size.y; - // Correct for the possibility of x or y being negative. - let x = if position.x < 0 && position.x % chunk_size.x != 0 { - x - 1 - } else { - x - }; - let y = if position.y < 0 && position.y % chunk_size.y != 0 { - y - 1 - } else { - y - }; - Vector2::new(x, y) + let chunk_size = self.height_map_size.map(|x| x - 1); + pixel_position_to_grid_position(position, chunk_size) } /// Determines the position of the (0,0) coordinate of the given chunk @@ -1526,22 +1368,7 @@ impl Terrain { /// This method makes no guarantee that there is actually a chunk at the returned coordinates. /// It returns the grid_position that the chunk would have if it existed. pub fn chunk_containing_mask_pos(&self, position: Vector2) -> Vector2 { - // Subtract 1 from x and y to exclude the overlapping pixel along both axes from the chunk size. - let chunk_size = self.mask_size.map(|x| x as i32); - let x = position.x / chunk_size.x; - let y = position.y / chunk_size.y; - // Correct for the possibility of x or y being negative. - let x = if position.x < 0 && position.x % chunk_size.x != 0 { - x - 1 - } else { - x - }; - let y = if position.y < 0 && position.y % chunk_size.y != 0 { - y - 1 - } else { - y - }; - Vector2::new(x, y) + pixel_position_to_grid_position(position, *self.mask_size) } /// Determines the position of the (0,0) coordinate of the given chunk @@ -1692,85 +1519,6 @@ impl Terrain { self.bounding_box_dirty.set(true); } - fn draw_data( - center: Vector2, - scale: Vector2, - brush: &Brush, - value: f32, - data: &mut D, - ) where - D: BrushableTerrainData, - V: BrushValue, - { - let scale_matrix = Matrix2::new(1.0 / scale.x, 0.0, 0.0, 1.0 / scale.y); - let transform = brush.transform * scale_matrix; - match brush.shape { - BrushShape::Circle { radius } => brush.mode.draw( - CircleBrushPixels::new(scale_matrix * center, radius, brush.hardness, transform), - data, - value, - brush.alpha, - ), - BrushShape::Rectangle { width, length } => brush.mode.draw( - RectBrushPixels::new( - scale_matrix * center, - Vector2::new(width, length), - brush.hardness, - transform, - ), - data, - value, - brush.alpha, - ), - } - } - - /// Multi-functional drawing method. It uses given brush to modify terrain, see [`Brush`] docs for - /// more info. - /// - `position`: The position of the brush in global space. - /// - `brush`: The Brush structure defines how the terrain will be modified around the given position. - /// - `stroke`: The BrushStroke structure remembers information about a brushstroke across multiple calls to `draw`. - /// - `chunks`: The vector in which to save copies of the data from modified chunks for the purpose of creating an undo command. - /// This vector will be returned. - pub fn draw( - &mut self, - position: Vector3, - brush: &Brush, - stroke: &mut BrushStroke, - chunks: Vec, - ) -> Vec { - let center = project(self.global_transform(), position).unwrap(); - match brush.target { - BrushTarget::HeightMap => { - let scale = self.height_grid_scale(); - let value = stroke.value; - let mut data = BrushableHeight { - stroke: &mut stroke.height_pixels, - terrain: self, - chunks, - }; - Terrain::draw_data(center, scale, brush, value, &mut data); - let chunks = data.chunks; - self.update_quad_trees(); - chunks - } - BrushTarget::LayerMask { layer } => { - let scale = self.mask_grid_scale(); - let value = stroke.value; - let mut data = BrushableLayerMask { - stroke: &mut stroke.mask_pixels, - terrain: self, - layer, - chunks, - }; - Terrain::draw_data(center, scale, brush, value, &mut data); - let chunks = data.chunks; - self.update_quad_trees(); - chunks - } - } - } - /// Casts a ray and looks for intersections with the terrain. This method collects all results in /// given array with optional sorting by the time-of-impact. /// @@ -1992,7 +1740,9 @@ impl Terrain { } fn resize_height_maps(&mut self, mut new_size: Vector2) { - new_size = new_size.sup(&Vector2::repeat(2)); + // Height maps should be a 1 + a multiple of 2 and they should be at least + // 3x3, since a 1x1 height map would be just a single vertex with no faces. + new_size = new_size.sup(&Vector2::repeat(3)); for chunk in self.chunks.iter_mut() { let texture = chunk.heightmap.as_ref().unwrap().data_ref(); @@ -2035,6 +1785,7 @@ impl Terrain { chunk.height_map_size = new_size; chunk.heightmap = Some(make_height_map_texture(resampled_heightmap, new_size)); + chunk.update_quad_tree(); } self.height_map_size.set_value_and_mark_modified(new_size); diff --git a/fyrox-math/src/lib.rs b/fyrox-math/src/lib.rs index a9eb6ba88..3c14cee13 100644 --- a/fyrox-math/src/lib.rs +++ b/fyrox-math/src/lib.rs @@ -8,6 +8,7 @@ pub mod octree; pub mod plane; pub mod ray; pub mod triangulator; +pub mod segment; use crate::ray::IntersectionResult; use nalgebra::{ diff --git a/fyrox-math/src/segment.rs b/fyrox-math/src/segment.rs new file mode 100644 index 000000000..cb1fdec3a --- /dev/null +++ b/fyrox-math/src/segment.rs @@ -0,0 +1,269 @@ +use nalgebra::{ + allocator::Allocator, ArrayStorage, ClosedAdd, ClosedMul, ClosedSub, DefaultAllocator, Dim, + OVector, RealField, Scalar, Storage, Vector, Vector2, U2, U3, +}; +use num_traits::{One, Signed, Zero}; +use rectutils::{Number, Rect}; + +/// Line segment in two dimensions +pub type LineSegment2 = LineSegment; +/// Line segment in three dimensions +pub type LineSegment3 = LineSegment; + +/// Line segment in any number of dimensions +#[derive(Clone, Debug)] +pub struct LineSegment +where + DefaultAllocator: Allocator, + D: Dim, +{ + /// One end of the line segment, the point returned when interpolating at t = 0.0 + pub start: OVector, + /// One end of the line segment, the point returned when interpolating at t = 1.0 + pub end: OVector, +} + +impl LineSegment +where + T: Zero + One + Scalar + ClosedAdd + ClosedSub + ClosedMul + RealField, + D: Dim, + DefaultAllocator: Allocator, +{ + /// Create a new line segment with the given points. + pub fn new(start: &Vector, end: &Vector) -> Self + where + S1: Storage, + S2: Storage, + { + Self { + start: start.clone_owned(), + end: end.clone_owned(), + } + } + /// Creates a reversed line segment by swapping `start` and `end`. + pub fn swapped(&self) -> Self { + Self::new(&self.end, &self.start) + } + /// The two end-points of the line segment are equal. + pub fn is_degenerate(&self) -> bool { + self.start == self.end + } + /// Create a point somewhere between `start` and `end`. + /// When t = 0.0, `start` is returned. + /// When t = 1.0, `end` is returned. + /// The result is `(1.0 - t) * start + t * end`, which may produce points off the line segment, + /// if t < 0.0 or t > 1.0. + pub fn interpolate(&self, t: T) -> OVector { + self.start.lerp(&self.end, t) + } + /// Create a point somewhere between `start` and `end`. + /// This is just like [LineSegment::interpolate] except that t is clamped to between 0.0 and 1.0, + /// so points off the line segment can never be returned. + pub fn interpolate_clamped(&self, t: T) -> OVector { + self.interpolate(t.clamp(::zero(), ::one())) + } + /// The vector from `start` to `end` + pub fn vector(&self) -> OVector { + self.end.clone() - self.start.clone() + } + /// The distance between `start` and `end` + pub fn length(&self) -> T { + self.vector().norm() + } + /// The square of the distance between `start` and `end` + pub fn length_squared(&self) -> T { + self.vector().norm_squared() + } + /// The interpolation parameter of the point on this segment that is closest to the given point. + /// + /// https://math.stackexchange.com/questions/2193720/find-a-point-on-a-line-segment-which-is-the-closest-to-other-point-not-on-the-li + pub fn nearest_t(&self, point: &Vector) -> T + where + S: Storage, + { + let v = self.vector(); + let u = self.start.clone() - point; + let n2 = v.norm_squared(); + if n2.is_zero() { + return T::zero(); + } + -v.dot(&u) / n2 + } + /// The point on this segment that is closest to the given point. + pub fn nearest_point(&self, point: &Vector) -> OVector + where + S: Storage, + { + self.interpolate_clamped(self.nearest_t(point)) + } + /// The squared distance between the given point and the nearest point on this line segment. + pub fn distance_squared(&self, point: &Vector) -> T + where + S: Storage, + { + (point - self.nearest_point(point)).norm_squared() + } + /// The distance between the given point and the nearest point on this line segment. + pub fn distance(&self, point: &Vector) -> T + where + S: Storage, + { + (point - self.nearest_point(point)).norm() + } +} + +impl LineSegment2 +where + T: Zero + One + Scalar + ClosedAdd + ClosedSub + ClosedMul + RealField, + DefaultAllocator: Allocator>, +{ + /// AABB for a 2D line segment + pub fn bounds(&self) -> Rect + where + T: Number, + { + Rect::from_points(self.start, self.end) + } + /// Test whether a point is collinear with this segment. + /// * 0.0 means collinear. Near to 0.0 means near to collinear. + /// * Negative means that the point is to the counter-clockwise of `end` as viewed from `start`. + /// * Positive means that the point is to the clockwise of `end` as viewed from `start`. + pub fn collinearity(&self, point: &Vector2) -> T { + let v = self.vector(); + let u = self.start.clone() - point; + v.x.clone() * u.y.clone() - u.x.clone() * v.y.clone() + } + /// True if this segment intersects the given segment based on collinearity. + pub fn intersects(&self, other: &LineSegment2) -> bool { + fn pos(t: &T) -> bool + where + T: Zero + Signed, + { + t.is_positive() && !t.is_zero() + } + fn neg(t: &T) -> bool + where + T: Zero + Signed, + { + t.is_negative() && !t.is_zero() + } + let o1 = self.collinearity(&other.start); + let o2 = self.collinearity(&other.end); + let s1 = other.collinearity(&self.start); + let s2 = other.collinearity(&self.end); + // If both points of self are left of `other` or both points are right of `other`... + if neg(&s1) && neg(&s2) || pos(&s1) && pos(&s2) { + return false; + } + // If both points of `other` are left of self or both points are right of self... + if neg(&o1) && neg(&o2) || pos(&o1) && pos(&o2) { + return false; + } + true + } +} + +#[cfg(test)] +mod test { + use super::*; + use nalgebra::Vector2; + #[test] + fn nearest_at_start() { + let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0)); + assert_eq!(segment.nearest_t(&Vector2::new(-1.0, -1.0)).max(0.0), 0.0); + assert_eq!( + segment.nearest_point(&Vector2::new(-1.0, -1.0)), + Vector2::new(0.0, 0.0) + ); + assert_eq!(segment.distance_squared(&Vector2::new(-1.0, -1.0)), 2.0); + assert_eq!(segment.distance(&Vector2::new(-1.0, 0.0)), 1.0); + } + #[test] + fn nearest_at_end() { + let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0)); + assert_eq!(segment.nearest_t(&Vector2::new(2.0, 2.0)).min(1.0), 1.0); + assert_eq!( + segment.nearest_point(&Vector2::new(2.0, 2.0)), + Vector2::new(1.0, 2.0) + ); + assert_eq!(segment.distance_squared(&Vector2::new(3.0, 2.0)), 4.0); + assert_eq!(segment.distance(&Vector2::new(3.0, 2.0)), 2.0); + } + #[test] + fn nearest_in_middle() { + let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0)); + assert_eq!(segment.nearest_t(&Vector2::new(2.5, 0.0)), 0.5); + assert_eq!( + segment.nearest_point(&Vector2::new(2.5, 0.0)), + Vector2::new(0.5, 1.0) + ); + assert_eq!(segment.distance_squared(&Vector2::new(2.5, 0.0)), 5.0); + } + #[test] + fn length() { + let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(4.0, 3.0)); + assert_eq!(segment.length_squared(), 25.0); + assert_eq!(segment.length(), 5.0); + } + #[test] + fn degenerate() { + let segment = LineSegment2::new(&Vector2::new(1.0, 2.0), &Vector2::new(1.0, 2.0)); + assert!(segment.is_degenerate()); + assert_eq!(segment.length_squared(), 0.0); + assert_eq!(segment.length(), 0.0); + } + #[test] + fn collinear() { + let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0)); + assert_eq!(segment.collinearity(&Vector2::new(2.0, 4.0)), 0.0); + assert_eq!(segment.collinearity(&Vector2::new(0.0, 0.0)), 0.0); + assert_eq!(segment.collinearity(&Vector2::new(1.0, 2.0)), 0.0); + assert!( + segment.collinearity(&Vector2::new(1.0, 5.0)) < 0.0, + "{} >= 0.0", + segment.collinearity(&Vector2::new(1.0, 5.0)) + ); + assert!( + segment.collinearity(&Vector2::new(1.0, 3.0)) < 0.0, + "{} >= 0.0", + segment.collinearity(&Vector2::new(1.0, 3.0)) + ); + assert!( + segment.collinearity(&Vector2::new(1.0, 1.0)) > 0.0, + "{} <= 0.0", + segment.collinearity(&Vector2::new(1.0, 1.0)) + ); + assert!( + segment.collinearity(&Vector2::new(-1.0, -5.0)) > 0.0, + "{} <= 0.0", + segment.collinearity(&Vector2::new(-1.0, -5.0)) + ); + } + #[test] + fn intersects() { + let a = LineSegment::new(&Vector2::new(1.0, 2.0), &Vector2::new(3.0, 1.0)); + let b = LineSegment::new(&Vector2::new(2.0, 0.0), &Vector2::new(2.5, 3.0)); + let c = LineSegment::new(&Vector2::new(1.0, 2.0), &Vector2::new(-3.0, 1.0)); + assert!(a.intersects(&b)); + assert!(a.intersects(&c)); + assert!(b.intersects(&a)); + assert!(c.intersects(&a)); + assert!(a.swapped().intersects(&b)); + assert!(a.swapped().intersects(&c)); + } + #[test] + fn not_intersects() { + let a = LineSegment::new(&Vector2::new(1.0, 2.0), &Vector2::new(3.0, 1.0)); + let b = LineSegment::new(&Vector2::new(0.0, 0.0), &Vector2::new(-1.0, 6.0)); + let c = LineSegment::new(&Vector2::new(2.0, 0.0), &Vector2::new(2.0, -1.0)); + assert!(!a.intersects(&b)); + assert!(!b.intersects(&c)); + assert!(!c.intersects(&a)); + assert!(!b.intersects(&a)); + assert!(!c.intersects(&b)); + assert!(!a.intersects(&c)); + assert!(!a.swapped().intersects(&b)); + assert!(!b.swapped().intersects(&c)); + assert!(!c.swapped().intersects(&a)); + } +} From 4f16559e4db546fa5ef4c4b67b36d3e0ce5fd93d Mon Sep 17 00:00:00 2001 From: b-guild Date: Sun, 30 Jun 2024 11:58:25 -0700 Subject: [PATCH 03/10] Terrain brush gizmo size fix and comment tweaks. --- editor/src/interaction/terrain.rs | 4 +++- .../scene/terrain/brushstroke/brushraster.rs | 18 +++++++++++++----- .../src/scene/terrain/brushstroke/mod.rs | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/editor/src/interaction/terrain.rs b/editor/src/interaction/terrain.rs index 5ff77a45e..01ff9ef79 100644 --- a/editor/src/interaction/terrain.rs +++ b/editor/src/interaction/terrain.rs @@ -369,7 +369,9 @@ impl InteractionMode for TerrainInteractionMode { } let scale = match self.brush.shape { - BrushShape::Circle { radius } => Vector3::new(radius, radius, 1.0), + BrushShape::Circle { radius } => { + Vector3::new(radius * 2.0, radius * 2.0, 1.0) + } BrushShape::Rectangle { width, length } => { Vector3::new(width, length, 1.0) } diff --git a/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs b/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs index 5e5778da1..d370479e4 100644 --- a/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs +++ b/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs @@ -12,6 +12,7 @@ use crate::core::{ math::{OptionRect, Rect}, }; +/// Adjust the strength of a brush pixel based on the hardness of the brush. fn apply_hardness(hardness: f32, strength: f32) -> f32 { if strength == 0.0 { return 0.0; @@ -33,6 +34,9 @@ pub trait BrushRaster { /// An AABB that contains all the pixels of the brush, with (0.0, 0.0) being /// at the center of the brush. fn bounds(&self) -> Rect; + /// An AABB that contains all the pixels of the brush after it has been transformed + /// and translated. First the brush is multiplied by `transform` and then it is + /// translated to `center`, and then an AABB it calculated for the resulting brush. fn transformed_bounds(&self, center: Vector2, transform: &Matrix2) -> Rect { let mut bounds = OptionRect::::default(); let rect = self.bounds(); @@ -148,6 +152,9 @@ impl Iterator for RectIter { } } +/// An iterator over the pixels of a [BrushRaster] object. +/// For each pixel, it produces a [BrushPixel]. +/// The pixels produced can include pixels with zero strength. #[derive(Debug, Clone)] pub struct StampPixels { brush_raster: R, @@ -161,11 +168,11 @@ impl StampPixels where R: BrushRaster, { + /// An AABB containing all the pixels that this iterator produces. pub fn bounds(&self) -> Rect { self.bounds_iter.bounds() } - /// Construct a new pixel iterator for a round brush at the given position, radius, - /// and 2x2 transform matrix. + /// Construct a new pixel iterator for a stamp at the given location. pub fn new( brush_raster: R, center: Vector2, @@ -205,7 +212,9 @@ where } } -/// An iterator of the pixels of a round brush. +/// An iterator of the pixels that are painted when a brush +/// is smeared from a start point to an end point. +/// It works just like [StampPixels] but across a line segment instead of at a single point. #[derive(Debug, Clone)] pub struct SmearPixels { brush_raster: R, @@ -221,8 +230,7 @@ impl SmearPixels { pub fn bounds(&self) -> Rect { self.bounds_iter.bounds() } - /// Construct a new pixel iterator for a brush at the given position, radius, - /// and 2x2 transform matrix. + /// Construct a new pixel iterator for a smear with the given start and end points. pub fn new( brush_raster: R, start: Vector2, diff --git a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs index c995428a4..00d7d9d58 100644 --- a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs +++ b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs @@ -34,7 +34,7 @@ use fyrox_core::uuid_provider; use std::collections::VecDeque; use std::sync::mpsc::{Receiver, SendError, Sender}; -mod brushraster; +pub mod brushraster; use brushraster::*; mod strokechunks; use strokechunks::*; From 5e62c2c3f607278a194957ad582d11ebc3daf7f0 Mon Sep 17 00:00:00 2001 From: b-guild Date: Sun, 30 Jun 2024 12:50:59 -0700 Subject: [PATCH 04/10] Format tweaks to fyrox-math and the matrix2 property editor. --- fyrox-math/src/lib.rs | 2 +- fyrox-ui/src/inspector/editors/matrix2.rs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/fyrox-math/src/lib.rs b/fyrox-math/src/lib.rs index 3c14cee13..c88efcda4 100644 --- a/fyrox-math/src/lib.rs +++ b/fyrox-math/src/lib.rs @@ -7,8 +7,8 @@ pub mod frustum; pub mod octree; pub mod plane; pub mod ray; -pub mod triangulator; pub mod segment; +pub mod triangulator; use crate::ray::IntersectionResult; use nalgebra::{ diff --git a/fyrox-ui/src/inspector/editors/matrix2.rs b/fyrox-ui/src/inspector/editors/matrix2.rs index 9c9791684..f5f3c5897 100644 --- a/fyrox-ui/src/inspector/editors/matrix2.rs +++ b/fyrox-ui/src/inspector/editors/matrix2.rs @@ -7,9 +7,9 @@ use crate::{ }, FieldKind, InspectorError, PropertyChanged, }, + matrix2::{Matrix2EditorBuilder, Matrix2EditorMessage}, message::{MessageDirection, UiMessage}, numeric::NumericType, - matrix2::{Matrix2EditorBuilder, Matrix2EditorMessage}, widget::WidgetBuilder, Thickness, }; @@ -28,9 +28,7 @@ impl Default for Matrix2PropertyEditorDefinition { } } -impl PropertyEditorDefinition - for Matrix2PropertyEditorDefinition -{ +impl PropertyEditorDefinition for Matrix2PropertyEditorDefinition { fn value_type_id(&self) -> TypeId { TypeId::of::>() } From 936e7a159e944051087607a2ebdb5bb7191683c5 Mon Sep 17 00:00:00 2001 From: b-guild Date: Sun, 30 Jun 2024 13:37:36 -0700 Subject: [PATCH 05/10] Fixing bug in brushraster tests. --- .../src/scene/terrain/brushstroke/brushraster.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs b/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs index d370479e4..c7f065d0a 100644 --- a/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs +++ b/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs @@ -285,10 +285,6 @@ mod tests { #[test] fn rect_extend_to_contain_f64() { let mut rect = Rect::new(0.0, 0.0, 1.0, 1.0); - - //rect.extend_to_contain(Rect::new(1.0, 1.0, 1.0, 1.0)); - //assert_eq!(rect, Rect::new(0.0, 0.0, 2.0, 2.0)); - rect.extend_to_contain(Rect::new(-1.0, -1.0, 1.0, 1.0)); assert_eq!(rect, Rect::new(-1.0, -1.0, 2.0, 2.0)); } @@ -414,11 +410,13 @@ mod tests { #[test] fn simple_rect() { let iter = StampPixels::new( - RectRaster(1.0, 1.0), + RectRaster(1.1, 1.1), Vector2::new(1.0, 1.0), 1.0, Matrix2::identity(), ); + assert_eq!(iter.bounds().w(), 4, "w != 4: {:?}", iter.bounds()); + assert_eq!(iter.bounds().h(), 4, "h != 4: {:?}", iter.bounds()); assert_eq!(find_strength_at(&iter, (1, 1)), 1.0); assert_eq!(find_strength_at(&iter, (0, 1)), 1.0); assert_eq!(find_strength_at(&iter, (0, 2)), 1.0); @@ -428,11 +426,13 @@ mod tests { #[test] fn distant_rect() { let iter = StampPixels::new( - RectRaster(1001.0, 2501.0), - Vector2::new(1.0, 1.0), + RectRaster(1.1, 1.1), + Vector2::new(1001.0, 2501.0), 1.0, Matrix2::identity(), ); + assert_eq!(iter.bounds().w(), 4, "w != 4: {:?}", iter.bounds()); + assert_eq!(iter.bounds().h(), 4, "h != 4: {:?}", iter.bounds()); assert_eq!(find_strength_at(&iter, (1001, 2501)), 1.0); assert_eq!(find_strength_at(&iter, (1000, 2501)), 1.0); assert_eq!(find_strength_at(&iter, (1000, 2502)), 1.0); From 80314e408298ac38b165ae668ef75a2ed3fe11a9 Mon Sep 17 00:00:00 2001 From: b-guild Date: Sun, 30 Jun 2024 13:57:39 -0700 Subject: [PATCH 06/10] Fixing Rect tests in fyrox-math. --- fyrox-math/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fyrox-math/src/lib.rs b/fyrox-math/src/lib.rs index c88efcda4..04fae1cfe 100644 --- a/fyrox-math/src/lib.rs +++ b/fyrox-math/src/lib.rs @@ -959,13 +959,13 @@ mod test { let mut rect = Rect::new(10, 10, 11, 11); rect.push(Vector2::new(0, 0)); - assert_eq!(rect, Rect::new(0, 0, 11, 11)); + assert_eq!(rect, Rect::new(0, 0, 21, 21)); rect.push(Vector2::new(0, 20)); - assert_eq!(rect, Rect::new(0, 0, 11, 20)); + assert_eq!(rect, Rect::new(0, 0, 21, 21)); rect.push(Vector2::new(20, 20)); - assert_eq!(rect, Rect::new(0, 0, 20, 20)); + assert_eq!(rect, Rect::new(0, 0, 21, 21)); rect.push(Vector2::new(30, 30)); assert_eq!(rect, Rect::new(0, 0, 30, 30)); @@ -1030,7 +1030,7 @@ mod test { assert_eq!(rect, Rect::new(0.0, 0.0, 2.0, 2.0)); rect.extend_to_contain(Rect::new(-1.0, -1.0, 1.0, 1.0)); - assert_eq!(rect, Rect::new(-1.0, -1.0, 2.0, 2.0)); + assert_eq!(rect, Rect::new(-1.0, -1.0, 3.0, 3.0)); } #[test] From 28963aecf1fa87a60455e79ecc2414215f85d796 Mon Sep 17 00:00:00 2001 From: b-guild Date: Sun, 30 Jun 2024 14:17:24 -0700 Subject: [PATCH 07/10] Reformatting URL as link to satisfy comment checker. --- fyrox-math/src/segment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fyrox-math/src/segment.rs b/fyrox-math/src/segment.rs index cb1fdec3a..4183f692e 100644 --- a/fyrox-math/src/segment.rs +++ b/fyrox-math/src/segment.rs @@ -76,7 +76,7 @@ where } /// The interpolation parameter of the point on this segment that is closest to the given point. /// - /// https://math.stackexchange.com/questions/2193720/find-a-point-on-a-line-segment-which-is-the-closest-to-other-point-not-on-the-li + /// [Stack Exchange question: Find a point on a line segment which is the closest to other point not on the line segment](https://math.stackexchange.com/questions/2193720/find-a-point-on-a-line-segment-which-is-the-closest-to-other-point-not-on-the-li) pub fn nearest_t(&self, point: &Vector) -> T where S: Storage, From 07d5b44e706694235d52b09fde481a3c01edaaca Mon Sep 17 00:00:00 2001 From: b-guild Date: Sun, 30 Jun 2024 15:58:48 -0700 Subject: [PATCH 08/10] Terrain comment improvements --- fyrox-impl/src/scene/terrain/brushstroke/mod.rs | 2 +- .../src/scene/terrain/brushstroke/strokechunks.rs | 10 +++++++++- fyrox-impl/src/scene/terrain/mod.rs | 5 +++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs index 00d7d9d58..435e34bb5 100644 --- a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs +++ b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs @@ -36,7 +36,7 @@ use std::sync::mpsc::{Receiver, SendError, Sender}; pub mod brushraster; use brushraster::*; -mod strokechunks; +pub mod strokechunks; use strokechunks::*; /// The number of pixel messages we can accept at once before we must start processing them. diff --git a/fyrox-impl/src/scene/terrain/brushstroke/strokechunks.rs b/fyrox-impl/src/scene/terrain/brushstroke/strokechunks.rs index 75c61aef0..fb249a885 100644 --- a/fyrox-impl/src/scene/terrain/brushstroke/strokechunks.rs +++ b/fyrox-impl/src/scene/terrain/brushstroke/strokechunks.rs @@ -1,10 +1,13 @@ +//! This module manages the record of which pixels have been recently edited by a brushstroke. +//! It stores the modified chunks and the pixels within each chunk since the last time +//! the changes were written to the terrain's textures. use super::{ChunkData, StrokeData, TerrainTextureKind}; use crate::core::algebra::Vector2; use crate::fxhash::{FxHashMap, FxHashSet}; use crate::resource::texture::TextureResource; use crate::scene::terrain::pixel_position_to_grid_position; -/// The pixels for a stroke for one chunk, generalized over the type of data being edited. +/// The list of modified pixels in each chunk. #[derive(Debug, Default)] pub struct StrokeChunks { /// The size of each chunk as measured by distance from one chunk origin to the next. @@ -16,14 +19,17 @@ pub struct StrokeChunks { written_pixels: FxHashMap, FxHashSet>>, /// The number of pixels written to this object. count: usize, + /// Pixel hash sets that are allocated but not currently in use unused_chunks: Vec>>, } impl StrokeChunks { + /// The number of modified pixels that this object is currently tracking #[inline] pub fn count(&self) -> usize { self.count } + /// The kind of texture being edited #[inline] pub fn kind(&self) -> TerrainTextureKind { self.kind @@ -104,10 +110,12 @@ impl StrokeChunks { } } } + /// Calculates which chunk contains the given pixel position. #[inline] pub fn pixel_position_to_grid_position(&self, position: Vector2) -> Vector2 { pixel_position_to_grid_position(position, self.chunk_size) } + /// Calculates the origin pixel position of the given chunk. pub fn chunk_to_origin(&self, grid_position: Vector2) -> Vector2 { Vector2::new( grid_position.x * self.chunk_size.x as i32, diff --git a/fyrox-impl/src/scene/terrain/mod.rs b/fyrox-impl/src/scene/terrain/mod.rs index bd9148f18..ef492976d 100644 --- a/fyrox-impl/src/scene/terrain/mod.rs +++ b/fyrox-impl/src/scene/terrain/mod.rs @@ -625,8 +625,9 @@ pub struct TerrainRayCastResult { /// /// ## Painting /// -/// Terrain has a single method for "painting" - [`Terrain::draw`], it accepts a brush with specific parameters, -/// which can either alternate height map or a layer mask. See method's documentation for more info. +/// Painting involves constructing a [BrushStroke] and calling its [BrushStroke::accept_messages] method with +/// a channel receiver, and sending a series of pixel messages into that channel. The BrushStroke will translate +/// those messages into modifications to the Terrain's textures. /// /// ## Ray casting /// From 03f059aa1049e7f8e734ae49ecd5efb6c1269a59 Mon Sep 17 00:00:00 2001 From: b-guild Date: Mon, 1 Jul 2024 15:37:07 -0700 Subject: [PATCH 09/10] Added sanity check for brush operations to protect editor from being overloaded by huge brushes. --- .../src/scene/terrain/brushstroke/mod.rs | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs index 435e34bb5..b2fa58022 100644 --- a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs +++ b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs @@ -46,6 +46,10 @@ const MESSAGE_BUFFER_SIZE: usize = 40; /// The number of processed pixels we can hold before we must write the pixels to the targetted textures. /// Modifying a texture is expensive, so it is important to do it in batches of multiple pixels. const PIXEL_BUFFER_SIZE: usize = 40; +/// The maximum number of pixels that are allowed to be involved in a single step of a brushstroke. +/// This limit is arbitrarily chosen, but there should be some limit to prevent the editor +/// from freezing as a result of an excessively large brush. +const BRUSH_PIXEL_SANITY_LIMIT: i32 = 1000000; #[inline] fn mask_raise(original: u8, amount: f32) -> u8 { @@ -746,6 +750,22 @@ pub struct Brush { pub alpha: f32, } +/// Verify that the brush operation is not so big that it could cause the editor to freeze. +/// The user can type in any size of brush they please, even disastrous sizes, and +/// this check prevents the editor from breaking. +fn within_size_limit(bounds: &Rect) -> bool { + let size = bounds.size; + let area = size.x * size.y; + let accepted = area <= BRUSH_PIXEL_SANITY_LIMIT; + if !accepted { + Log::warn(format!( + "Terrain brush operation dropped due to sanity limit: {}", + area + )) + } + accepted +} + impl Brush { /// Send the pixels for this brush to the brush thread. /// - `position`: The position of the brush in texture pixels. @@ -766,22 +786,30 @@ impl Brush { transform.m12 *= x_factor; match self.shape { BrushShape::Circle { radius } => { - for BrushPixel { position, strength } in StampPixels::new( + let iter = StampPixels::new( CircleRaster(radius / scale.y), position, self.hardness, transform, - ) { + ); + if !within_size_limit(&iter.bounds()) { + return; + } + for BrushPixel { position, strength } in iter { sender.draw_pixel(position, strength, value); } } BrushShape::Rectangle { width, length } => { - for BrushPixel { position, strength } in StampPixels::new( + let iter = StampPixels::new( RectRaster(width * 0.5 / scale.y, length * 0.5 / scale.y), position, self.hardness, transform, - ) { + ); + if !within_size_limit(&iter.bounds()) { + return; + } + for BrushPixel { position, strength } in iter { sender.draw_pixel(position, strength, value); } } @@ -808,24 +836,32 @@ impl Brush { transform.m12 *= x_factor; match self.shape { BrushShape::Circle { radius } => { - for BrushPixel { position, strength } in SmearPixels::new( + let iter = SmearPixels::new( CircleRaster(radius / scale.y), start, end, self.hardness, transform, - ) { + ); + if !within_size_limit(&iter.bounds()) { + return; + } + for BrushPixel { position, strength } in iter { sender.draw_pixel(position, strength, value); } } BrushShape::Rectangle { width, length } => { - for BrushPixel { position, strength } in SmearPixels::new( + let iter = SmearPixels::new( RectRaster(width * 0.5 / scale.y, length * 0.5 / scale.y), start, end, self.hardness, transform, - ) { + ); + if !within_size_limit(&iter.bounds()) { + return; + } + for BrushPixel { position, strength } in iter { sender.draw_pixel(position, strength, value); } } From aa395d5bc4c29ccbf486d828f105960aa96c34d9 Mon Sep 17 00:00:00 2001 From: b-guild Date: Wed, 3 Jul 2024 01:03:15 -0700 Subject: [PATCH 10/10] Cleaning up forgotten comments. --- fyrox-impl/src/scene/terrain/brushstroke/mod.rs | 4 +--- fyrox-impl/src/scene/terrain/mod.rs | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs index b2fa58022..d571368f5 100644 --- a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs +++ b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs @@ -258,13 +258,11 @@ impl BrushSender { } } -fn on_send_failure(_error: SendError) { - /* +fn on_send_failure(error: SendError) { Log::err(format!( "A brush painting message was not sent. {:?}", error )); - */ } /// Type for a callback that delivers the original data of textures that have been modified diff --git a/fyrox-impl/src/scene/terrain/mod.rs b/fyrox-impl/src/scene/terrain/mod.rs index ef492976d..802eb0335 100644 --- a/fyrox-impl/src/scene/terrain/mod.rs +++ b/fyrox-impl/src/scene/terrain/mod.rs @@ -2049,12 +2049,12 @@ impl TerrainBuilder { pub fn new(base_builder: BaseBuilder) -> Self { Self { base_builder, - chunk_size: Vector2::new(16.0, 16.0), // Vector2::new(16.0, 16.0) + chunk_size: Vector2::new(16.0, 16.0), width_chunks: 0..2, length_chunks: 0..2, - mask_size: Vector2::new(256, 256), // Vector2::new(256, 256) - height_map_size: Vector2::new(257, 257), // Vector2::new(257, 257) - block_size: Vector2::new(33, 33), // Vector2::new(33,33) + mask_size: Vector2::new(256, 256), + height_map_size: Vector2::new(257, 257), + block_size: Vector2::new(33, 33), layers: Default::default(), decal_layer_index: 0, }