From 0d50bab050df645670ce9be6359f4eafc560474d Mon Sep 17 00:00:00 2001 From: b-guild Date: Sun, 7 Jul 2024 16:03:21 -0700 Subject: [PATCH] Fixing smear aspect issue and creating BrushContext to allow brushing from code. --- editor/src/interaction/terrain.rs | 64 ++---- .../scene/terrain/brushstroke/brushraster.rs | 31 ++- .../src/scene/terrain/brushstroke/mod.rs | 211 +++++++++++------- fyrox-impl/src/scene/terrain/mod.rs | 208 ++++++++++++++++- 4 files changed, 377 insertions(+), 137 deletions(-) diff --git a/editor/src/interaction/terrain.rs b/editor/src/interaction/terrain.rs index 01ff9ef79..db8da909d 100644 --- a/editor/src/interaction/terrain.rs +++ b/editor/src/interaction/terrain.rs @@ -1,8 +1,4 @@ -use fyrox::fxhash::FxHashMap; -use fyrox::resource::texture::TextureResource; -use fyrox::scene::terrain::brushstroke::{ - BrushSender, BrushThreadMessage, TerrainTextureData, TerrainTextureKind, UndoData, -}; +use fyrox::scene::terrain::brushstroke::{BrushSender, BrushThreadMessage, UndoData}; use crate::fyrox::core::uuid::{uuid, Uuid}; use crate::fyrox::core::TypeUuidProvider; @@ -157,37 +153,19 @@ impl TerrainInteractionMode { 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; + // Reverse the behavior of a brush when shift is held. + if shift { + match &mut brush.mode { + BrushMode::Raise { amount } => { + *amount *= -1.0; + } + BrushMode::Assign { value } => { + *value = 1.0 - *value; + } + _ => (), } } - 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, - }; + let data = terrain.texture_data(brush.target); if let Some(sender) = &self.brush_sender { sender.start_stroke(brush, handle, data); } else { @@ -208,10 +186,13 @@ impl TerrainInteractionMode { }; 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); + self.brush.smear(start, position, scale, |p, a| { + sender.draw_pixel(p, a, self.brush_value) + }); } else { - self.brush.stamp(position, scale, self.brush_value, sender); + self.brush.stamp(position, scale, |p, a| { + sender.draw_pixel(p, a, self.brush_value) + }); } self.prev_brush_position = Some(position); } @@ -298,9 +279,12 @@ impl InteractionMode for TerrainInteractionMode { { self.brush_value = closest.height; } else if let Some(closest) = first { - let p = closest.position; - self.brush_value = terrain - .interpolate_value(Vector2::new(p.x, p.z), self.brush.target); + let p = terrain.project(closest.position); + self.brush_value = if let Some(position) = p { + terrain.interpolate_value(position, self.brush.target) + } else { + 0.0 + }; } else { self.brush_value = 0.0; } diff --git a/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs b/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs index c7f065d0a..372d61f64 100644 --- a/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs +++ b/fyrox-impl/src/scene/terrain/brushstroke/brushraster.rs @@ -218,10 +218,11 @@ where #[derive(Debug, Clone)] pub struct SmearPixels { brush_raster: R, - start: Vector2, - end: Vector2, + segment: LineSegment2, + aspect_segment: LineSegment2, hardness: f32, inv_transform: Matrix2, + aspect_transform: Matrix2, bounds_iter: RectIter, } @@ -248,12 +249,22 @@ impl SmearPixels { .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)); + let segment = LineSegment2::new(&start, &end); + let aspect_bounds = brush_raster.bounds(); + let aspect_transform = + Matrix2::new(1.0 / aspect_bounds.w(), 0.0, 0.0, 1.0 / aspect_bounds.h()); + let aspect_transform = aspect_transform * inv_transform; + let aspect_segment = LineSegment2 { + start: aspect_transform * segment.start, + end: aspect_transform * segment.end, + }; Self { brush_raster, - start, - end, + segment, + aspect_segment, hardness, inv_transform, + aspect_transform, bounds_iter: RectIter::new(bounds.unwrap()), } } @@ -269,12 +280,14 @@ where 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 = Vector2::new(fx, fy); + let aspect_p = self.aspect_transform * p; + let t = self.aspect_segment.nearest_t(&aspect_p); + let center = self.segment.interpolate_clamped(t); + let p = p - center; let p = self.inv_transform * p; - let strength = self.brush_raster.strength_at(p); - let strength = apply_hardness(self.hardness, strength); + let s = self.brush_raster.strength_at(p); + let strength = apply_hardness(self.hardness, s); Some(BrushPixel { position, strength }) } } diff --git a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs index d571368f5..5f35ec635 100644 --- a/fyrox-impl/src/scene/terrain/brushstroke/mod.rs +++ b/fyrox-impl/src/scene/terrain/brushstroke/mod.rs @@ -63,66 +63,6 @@ fn mask_lerp(original: u8, value: f32, t: f32) -> u8 { (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 { @@ -336,6 +276,69 @@ impl BrushStroke { ..Default::default() } } + /// The brush that this stroke is using. This is immutable access only, because + /// the brush's target may only be changed through [BrushStroke::start_stroke] or + /// [BrushStroke::accept_messages]. + /// + /// Mutable access to the brush's other properties is available through + /// [BrushStroke::shape], [BrushStroke::mode], [BrushStroke::hardness], + /// and [BrushStroke::alpha]. + pub fn brush(&self) -> &Brush { + &self.brush + } + /// Mutable access to the brush's shape + pub fn shape(&mut self) -> &mut BrushShape { + &mut self.brush.shape + } + /// Mutable access to the brush's mode + pub fn mode(&mut self) -> &mut BrushMode { + &mut self.brush.mode + } + /// Mutable access to the brush's hardness. The hardness controls how the edges + /// of the brush are blended with the original value of the texture. + pub fn hardness(&mut self) -> &mut f32 { + &mut self.brush.hardness + } + /// Mutable access to the brush's alpha. The alpha value controls how + /// the operation's result is blended with the original value of the texture. + pub fn alpha(&mut self) -> &mut f32 { + &mut self.brush.alpha + } + /// Insert a stamp of the brush at the given position with the given texture scale and the given value. + /// - `position`: The center of the stamp. + /// - `scale`: The size of each pixel in 2D local space. This is used to convert the brush's shape from local space to texture space. + /// - `value`: A value that is a parameter for the brush operation. + pub fn stamp(&mut self, position: Vector2, scale: Vector2, value: f32) { + let brush = self.brush.clone(); + brush.stamp(position, scale, |position, alpha| { + self.draw_pixel(BrushPixelMessage { + position, + alpha, + value, + }) + }); + } + /// Insert a smear of the brush at the given position with the given texture scale and the given value. + /// - `start`: The center of the smear's start. + /// - `end`: The center of the smear's end. + /// - `scale`: The size of each pixel in 2D local space. This is used to convert the brush's shape from local space to texture space. + /// - `value`: A value that is a parameter for the brush operation. + pub fn smear( + &mut self, + start: Vector2, + end: Vector2, + scale: Vector2, + value: f32, + ) { + let brush = self.brush.clone(); + brush.smear(start, end, scale, |position, alpha| { + self.draw_pixel(BrushPixelMessage { + position, + alpha, + value, + }) + }); + } /// Prepare this object for a new brushstroke. pub fn clear(&mut self) { self.height_pixels.clear(); @@ -414,16 +417,26 @@ impl BrushStroke { } // 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) { + /// -`brush`: The brush to paint with + /// -`node`: The handle of the terrain being modified. It is not used except to pass to `undo_chunk_handler`, + /// so it can be safely [Handle::NONE] if `undo_chunk_handler` is None, or if `undo_chunk_handler` is prepared for NONE. + /// -`textures`: Hash map of texture resources that this stroke will edit. + pub fn start_stroke(&mut self, brush: Brush, node: Handle, textures: TerrainTextureData) { + self.brush = brush; + self.node = node; + self.chunks.set_layout(textures.kind, textures.chunk_size); + self.textures = textures.resources; + } + /// Send the textures that have been touched by the brush to the undo handler, + /// then write the current changes to the textures and clear the stroke to prepare + /// for starting a new stroke. + pub 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 @@ -437,14 +450,23 @@ impl BrushStroke { } // Flush pixels to the terrain textures self.apply(); + self.clear(); } - fn handle_pixel_message(&mut self, pixel: BrushPixelMessage) { + /// Insert a pixel with the given texture-space coordinates and strength. + pub fn draw_pixel(&mut self, pixel: BrushPixelMessage) { + let pixel = BrushPixelMessage { + alpha: 0.5 * (1.0 - (pixel.alpha * std::f32::consts::PI).cos()), + ..pixel + }; 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); + } + fn handle_pixel_message(&mut self, pixel: BrushPixelMessage) { + self.draw_pixel(pixel); if self.chunks.count() >= PIXEL_BUFFER_SIZE { self.flush(); } @@ -545,13 +567,20 @@ impl BrushStroke { }; self.mask_pixels.set_latest(position, result); } - fn flush(&mut self) { + /// Update the texture resources to match the current state of this stroke. + pub fn flush(&mut self) { + // chunks stores the pixels that we have modified but not yet written to the textures. + // If we have an undo handler, we must inform the handler of the textures we are writing to + // because we are about to forget that we have written to them. 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); } + // Do the actual texture modification. self.apply(); + // Erase our memory of having modified these pixels, since they have already been finalized + // by writing them to the textures. self.chunks.clear(); } fn apply(&self) { @@ -592,7 +621,7 @@ impl StrokeData { if let Some(el) = self.0.get_mut(&position) { el.latest_value = value; } else { - unreachable!("Setting latest value of missing element"); + panic!("Setting latest value of missing element"); } } /// Stores or modifies the StrokeElement at the given position. @@ -728,7 +757,7 @@ pub enum BrushTarget { 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)] +#[derive(Clone, Reflect, Debug)] pub struct Brush { /// Shape of the brush. pub shape: BrushShape, @@ -748,6 +777,19 @@ pub struct Brush { pub alpha: f32, } +impl Default for Brush { + fn default() -> Self { + Self { + transform: Matrix2::identity(), + hardness: 0.0, + alpha: 1.0, + shape: Default::default(), + mode: Default::default(), + target: Default::default(), + } + } +} + /// 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. @@ -770,14 +812,11 @@ impl Brush { /// - `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, - ) { + /// - `draw_pixel`: The function that will draw the pixels to the terrain. + pub fn stamp(&self, position: Vector2, scale: Vector2, mut draw_pixel: F) + where + F: FnMut(Vector2, f32), + { let mut transform = self.transform; let x_factor = scale.y / scale.x; transform.m11 *= x_factor; @@ -794,7 +833,7 @@ impl Brush { return; } for BrushPixel { position, strength } in iter { - sender.draw_pixel(position, strength, value); + draw_pixel(position, strength); } } BrushShape::Rectangle { width, length } => { @@ -808,7 +847,7 @@ impl Brush { return; } for BrushPixel { position, strength } in iter { - sender.draw_pixel(position, strength, value); + draw_pixel(position, strength); } } } @@ -818,16 +857,16 @@ impl Brush { /// - `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( + /// - `draw_pixel`: The function that will draw the pixels to the terrain. + pub fn smear( &self, start: Vector2, end: Vector2, scale: Vector2, - value: f32, - sender: &BrushSender, - ) { + mut draw_pixel: F, + ) where + F: FnMut(Vector2, f32), + { let mut transform = self.transform; let x_factor = scale.y / scale.x; transform.m11 *= x_factor; @@ -845,7 +884,7 @@ impl Brush { return; } for BrushPixel { position, strength } in iter { - sender.draw_pixel(position, strength, value); + draw_pixel(position, strength); } } BrushShape::Rectangle { width, length } => { @@ -860,7 +899,7 @@ impl Brush { return; } for BrushPixel { position, strength } in iter { - sender.draw_pixel(position, strength, value); + draw_pixel(position, strength); } } } diff --git a/fyrox-impl/src/scene/terrain/mod.rs b/fyrox-impl/src/scene/terrain/mod.rs index 802eb0335..75ce50a40 100644 --- a/fyrox-impl/src/scene/terrain/mod.rs +++ b/fyrox-impl/src/scene/terrain/mod.rs @@ -37,6 +37,7 @@ use crate::{ terrain::{geometry::TerrainGeometry, quadtree::QuadTree}, }, }; +use fxhash::FxHashMap; use fyrox_core::uuid_provider; use fyrox_graph::BaseSceneGraph; use fyrox_resource::untyped::ResourceKind; @@ -53,7 +54,7 @@ pub mod brushstroke; mod geometry; mod quadtree; -use brushstroke::*; +pub use brushstroke::*; /// Current implementation version marker. pub const VERSION: u8 = 1; @@ -585,6 +586,121 @@ pub struct TerrainRayCastResult { pub toi: f32, } +/// An object representing the state of a terrain brush being used from code. +/// It has methods for starting, stopping, stamping, and smearing. +/// +/// Each BrushContext requires some amount of heap allocation, so it may be preferable +/// to reuse a BrushContext for multiple strokes when possible. +/// +/// A single brush stroke can include multiple operations across multiple frames, but +/// the terrain's texture resources should not be replaced during a stroke because +/// the BrushContext holds references the the texture resources that the terrain +/// had when the stroke started, and any brush operations will be applied to those +/// textures regardless of replacing the textures in the terrain. +#[derive(Default)] +pub struct BrushContext { + /// Parameter value for the brush. For flattening, this is the target height. + /// For flattening, it starts as None and then is given a value based on the first + /// stamp or smear. + pub value: Option, + /// The pixel and brush data of the in-progress stroke. + pub stroke: BrushStroke, +} + +impl BrushContext { + /// The current brush. This is immutable access only, because + /// the brush's target may only be changed through [BrushContext::start_stroke]. + /// + /// Mutable access to the brush's other properties is available through + /// [BrushContext::shape], [BrushContext::mode], [BrushContext::hardness], + /// and [BrushContext::alpha]. + pub fn brush(&self) -> &Brush { + self.stroke.brush() + } + /// Mutable access to the brush's shape. This allows the shape of the brush + /// to change without starting a new stroke. + pub fn shape(&mut self) -> &mut BrushShape { + self.stroke.shape() + } + /// Mutable access to the brush's mode. This allows the mode of the brush + /// to change without starting a new stroke. + pub fn mode(&mut self) -> &mut BrushMode { + self.stroke.mode() + } + /// Mutable access to the brush's hardness. This allows the hardness of the brush + /// to change without starting a new stroke. + pub fn hardness(&mut self) -> &mut f32 { + self.stroke.hardness() + } + /// Mutable access to the brush's alpha. This allows the alpha of the brush + /// to change without starting a new stroke. + pub fn alpha(&mut self) -> &mut f32 { + self.stroke.alpha() + } + /// Modify the given BrushStroke so that it is using the given Brush and it is modifying the given terrain. + /// The BrushContext will now hold references to the textures of this terrain for the target of the given brush, + /// and so the stroke should not be used with other terrains until the stroke is finished. + /// - `terrain`: The terrain that this stroke will edit. + /// - `brush`: The Brush containing the brush shape and painting operation to perform. + pub fn start_stroke(&mut self, terrain: &Terrain, brush: Brush) { + self.value = None; + terrain.start_stroke(brush, &mut self.stroke); + } + /// Modify the brushstroke to include a stamp of the brush at the given position. + /// The location of the stamp relative to the textures is determined based on the global position + /// of the terrain and the size of each terrain pixel. + /// - `terrain`: The terrain that will be used to translate the given world-space coordinates into + /// texture-space coordinates. This should be the same terrain as was given to [BrushContext::start_stroke]. + /// - `position`: The position of the brush in world coordinates. + pub fn stamp(&mut self, terrain: &Terrain, position: Vector3) { + let value = if matches!(self.stroke.brush().mode, BrushMode::Flatten { .. }) { + self.interpolate_value(terrain, position) + } else { + 0.0 + }; + terrain.stamp(position, value, &mut self.stroke); + } + /// Modify the brushstroke to include a smear of the brush from `start` to `end`. + /// The location of the smear relative to the textures is determined based on the global position + /// of the terrain and the size of each terrain pixel. + /// - `terrain`: The terrain that will be used to translate the given world-space coordinates into + /// texture-space coordinates. This should be the same terrain as was given to [BrushContext::start_stroke]. + /// - `start`: The start of the brush in world coordinates. + /// - `end`: The end of the brush in world coordinates. + pub fn smear(&mut self, terrain: &Terrain, start: Vector3, end: Vector3) { + let value = if matches!(self.stroke.brush().mode, BrushMode::Flatten { .. }) { + self.interpolate_value(terrain, start) + } else { + 0.0 + }; + terrain.smear(start, end, value, &mut self.stroke); + } + /// Update the terrain's textures to include the latest pixel data without ending the stroke. + pub fn flush(&mut self) { + self.stroke.flush(); + } + /// Update the terrain's textures to include the latest data and clear this context of all pixel data + /// to prepare for starting another stroke. + pub fn end_stroke(&mut self) { + self.stroke.end_stroke(); + } +} + +impl BrushContext { + fn interpolate_value(&mut self, terrain: &Terrain, position: Vector3) -> f32 { + if let Some(v) = self.value { + return v; + } + let Some(position) = terrain.project(position) else { + return 0.0; + }; + let target = self.stroke.brush().target; + let v = terrain.interpolate_value(position, target); + self.value = Some(v); + v + } +} + /// Terrain is a height field where each point has fixed coordinates in XZ plane, but variable Y coordinate. /// It can be used to create landscapes. It supports multiple layers, where each layer has its own material /// and mask. @@ -771,7 +887,7 @@ impl Default for Terrain { width_chunks: Default::default(), length_chunks: Default::default(), height_map_size: Default::default(), - block_size: Vector2::new(32, 32).into(), + block_size: Vector2::new(33, 33).into(), mask_size: Default::default(), chunks: Default::default(), bounding_box_dirty: Cell::new(true), @@ -1797,6 +1913,94 @@ impl Terrain { pub fn geometry(&self) -> &TerrainGeometry { &self.geometry } + /// Create an object that specifies which TextureResources are being used by this terrain + /// to hold the data for the given BrushTarget. + pub fn texture_data(&self, target: BrushTarget) -> TerrainTextureData { + let chunk_size = match target { + BrushTarget::HeightMap => self.height_map_size(), + BrushTarget::LayerMask { .. } => self.mask_size(), + }; + let kind = match target { + BrushTarget::HeightMap => TerrainTextureKind::Height, + BrushTarget::LayerMask { .. } => TerrainTextureKind::Mask, + }; + let resources: FxHashMap, TextureResource> = match target { + BrushTarget::HeightMap => self + .chunks_ref() + .iter() + .map(|c| (c.grid_position(), c.heightmap().clone())) + .collect(), + BrushTarget::LayerMask { layer } => self + .chunks_ref() + .iter() + .map(|c| (c.grid_position(), c.layer_masks[layer].clone())) + .collect(), + }; + TerrainTextureData { + chunk_size, + kind, + resources, + } + } + /// Modify the given BrushStroke so that it is using the given Brush and it is modifying this terrain. + /// The BrushStroke will now hold references to the textures of this terrain for the target of the given brush, + /// and so the stroke should not be used with other terrains until the stroke is finished. + /// - `brush`: The Brush containing the brush shape and painting operation to perform. + /// - `stroke`: The BrushStroke object to be reset to start a new stroke. + fn start_stroke(&self, brush: Brush, stroke: &mut BrushStroke) { + let target = brush.target; + stroke.start_stroke(brush, self.self_handle, self.texture_data(target)) + } + /// Modify the given BrushStroke to include a stamp of its brush at the given position. + /// The location of the stamp relative to the textures is determined based on the global position + /// of the terrain and the size of each terrain pixel. + /// - `position`: The position of the brush in world coordinates. + /// - `value`: The value of the brush stroke, whose meaning depends on the brush operation. + /// For flatten brush operations, this is the target value to flatten toward. + /// - `stroke`: The BrushStroke object to be modified. + fn stamp(&self, position: Vector3, value: f32, stroke: &mut BrushStroke) { + let Some(position) = self.project(position) else { + return; + }; + let position = match stroke.brush().target { + BrushTarget::HeightMap => self.local_to_height_pixel(position), + BrushTarget::LayerMask { .. } => self.local_to_mask_pixel(position), + }; + let scale = match stroke.brush().target { + BrushTarget::HeightMap => self.height_grid_scale(), + BrushTarget::LayerMask { .. } => self.mask_grid_scale(), + }; + stroke.stamp(position, scale, value); + } + /// Modify the given BrushStroke to include a stamp of its brush at the given position. + /// The location of the stamp relative to the textures is determined based on the global position + /// of the terrain and the size of each terrain pixel. + /// - `start`: The start of the smear in world coordinates. + /// - `end`: The end of the smear in world coordinates. + /// - `value`: The value of the brush stroke, whose meaning depends on the brush operation. + /// For flatten brush operations, this is the target value to flatten toward. + /// - `stroke`: The BrushStroke object to be modified. + fn smear(&self, start: Vector3, end: Vector3, value: f32, stroke: &mut BrushStroke) { + let Some(start) = self.project(start) else { + return; + }; + let Some(end) = self.project(end) else { + return; + }; + let start = match stroke.brush().target { + BrushTarget::HeightMap => self.local_to_height_pixel(start), + BrushTarget::LayerMask { .. } => self.local_to_mask_pixel(start), + }; + let end = match stroke.brush().target { + BrushTarget::HeightMap => self.local_to_height_pixel(end), + BrushTarget::LayerMask { .. } => self.local_to_mask_pixel(end), + }; + let scale = match stroke.brush().target { + BrushTarget::HeightMap => self.height_grid_scale(), + BrushTarget::LayerMask { .. } => self.mask_grid_scale(), + }; + stroke.smear(start, end, scale, value); + } } impl NodeTrait for Terrain {