Add a method to convert cursor coords to pixel coords (#77)

* Add a method to convert cursor coords to pixel coords

* Add method `window_pos_to_pixel` to Pixels struct
   * Converts cursor / window physical coordinates to pixel coords

* Fix formatting

* Return result rather than clamping pixel coordinates

* Use transformation matrices to convert from cursor coord to pixel

* Adds a struct ScalingMatrix that manages the creation and usage
  of the transformation matrix used in the renderer.
* Added an inverse function on ScalingMatrix - 4x4 matrix inverse
  (This should probably use a library, but it doesn't seem worth
  adding a dependancy for one function)

* Optimize matrix multiplication for cursor position calculation

* Use ultraviolet for matrix and vector math

* Add suggested changes

This keeps the split between usize and isize
This also changes the input cursor position to f32, because it was
immediately cast to an f32 for the transformations.
This commit is contained in:
Aeledfyr 2020-05-20 23:37:29 -05:00 committed by GitHub
parent b43336d45d
commit c96e46d3d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 186 additions and 57 deletions

View file

@ -22,6 +22,7 @@ include = [
thiserror = "1.0.15"
wgpu = "0.5.0"
futures-executor = "0.3"
ultraviolet = "0.4.6"
[dev-dependencies]
pixels-mocks = { path = "pixels-mocks" }

View file

@ -66,13 +66,19 @@ fn main() -> Result<(), Error> {
let (dx, dy) = input.mouse_diff();
let prev_x = mx - dx;
let prev_y = my - dy;
let dpx = hidpi_factor as f32;
let (w, h) = (p_width as f32 / dpx, p_height as f32 / dpx);
let mx_i = ((mx / w) * (SCREEN_WIDTH as f32)).round() as isize;
let my_i = ((my / h) * (SCREEN_HEIGHT as f32)).round() as isize;
let px_i = ((prev_x / w) * (SCREEN_WIDTH as f32)).round() as isize;
let py_i = ((prev_y / h) * (SCREEN_HEIGHT as f32)).round() as isize;
((mx_i, my_i), (px_i, py_i))
let (mx_i, my_i) = pixels
.window_pos_to_pixel((mx, my))
.unwrap_or_else(|pos| pixels.clamp_pixel_pos(pos));
let (px_i, py_i) = pixels
.window_pos_to_pixel((prev_x, prev_y))
.unwrap_or_else(|pos| pixels.clamp_pixel_pos(pos));
(
(mx_i as isize, my_i as isize),
(px_i as isize, py_i as isize),
)
})
.unwrap_or_default();

View file

@ -56,6 +56,10 @@ pub struct Pixels {
texture_extent: wgpu::Extent3d,
texture_format_size: u32,
pixels: Vec<u8>,
// The inverse of the scaling matrix used by the renderer
// Used to convert physical coordinates back to pixel coordinates (for the mouse)
scaling_matrix_inverse: ultraviolet::Mat4,
}
/// A builder to help create customized pixel buffers.
@ -161,6 +165,17 @@ impl Pixels {
self.surface_texture.width = width;
self.surface_texture.height = height;
// Update ScalingMatrix for mouse transformation
self.scaling_matrix_inverse = renderers::ScalingMatrix::new(
(
self.texture_extent.width as f32,
self.texture_extent.height as f32,
),
(width as f32, height as f32),
)
.transform
.inversed();
// Recreate the swap chain
self.swap_chain = self.device.create_swap_chain(
&self.surface_texture.surface,
@ -263,6 +278,99 @@ impl Pixels {
pub fn get_frame(&mut self) -> &mut [u8] {
&mut self.pixels
}
/// Calculate the pixel location from a physical location on the window,
/// dealing with window resizing, scaling, and margins. Takes a physical
/// position (x, y) within the window, and returns a pixel position (x, y).
///
/// The location must be given in physical units (for example, winit's `PhysicalLocation`)
///
/// If the given physical position is outside of the drawing area, this
/// function returns an `Err` value with the pixel coordinates outside of
/// the screen, using isize instead of usize.
///
/// ```no_run
/// # use pixels::Pixels;
/// # let surface = wgpu::Surface::create(&pixels_mocks::RWH);
/// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, surface);
/// const WIDTH: u32 = 320;
/// const HEIGHT: u32 = 240;
///
/// let mut pixels = Pixels::new(WIDTH, HEIGHT, surface_texture)?;
///
/// // A cursor position in physical units
/// let cursor_position: (f32, f32) = winit::dpi::PhysicalPosition::new(0.0, 0.0).into();
///
/// // Convert it to a pixel location
/// let pixel_position: (usize, usize) = pixels.window_pos_to_pixel(cursor_position)
/// // Clamp the output to within the screen
/// .unwrap_or_else(|pos| pixels.clamp_pixel_pos(pos));
/// # Ok::<(), pixels::Error>(())
/// ```
pub fn window_pos_to_pixel(
&self,
physical_position: (f32, f32),
) -> Result<(usize, usize), (isize, isize)> {
let physical_width = self.surface_texture.width as f32;
let physical_height = self.surface_texture.height as f32;
let pixels_width = self.texture_extent.width as f32;
let pixels_height = self.texture_extent.height as f32;
let pos = ultraviolet::Vec4::new(
(physical_position.0 / physical_width - 0.5) * pixels_width,
(physical_position.1 / physical_height - 0.5) * pixels_height,
0.0,
1.0,
);
let pos = self.scaling_matrix_inverse * pos;
let pos = (
pos.x / pos.w + pixels_width / 2.0,
-pos.y / pos.w + pixels_height / 2.0,
);
let pixel_x = pos.0.floor() as isize;
let pixel_y = pos.1.floor() as isize;
if pixel_x < 0
|| pixel_x >= self.texture_extent.width as isize
|| pixel_y < 0
|| pixel_y >= self.texture_extent.height as isize
{
Err((pixel_x, pixel_y))
} else {
Ok((pixel_x as usize, pixel_y as usize))
}
}
/// Clamp a pixel position to the pixel buffer size.
///
/// This can be used to clamp the `Err` value returned by [`Pixels::window_pos_to_pixel`]
/// to a position clamped within the drawing area.
///
/// ```no_run
/// # use pixels::Pixels;
/// # let surface = wgpu::Surface::create(&pixels_mocks::RWH);
/// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, surface);
/// const WIDTH: u32 = 320;
/// const HEIGHT: u32 = 240;
///
/// let mut pixels = Pixels::new(WIDTH, HEIGHT, surface_texture)?;
///
/// let pixel_pos = pixels.clamp_pixel_pos((-19, 20));
/// assert_eq!(pixel_pos, (0, 20));
///
/// let pixel_pos = pixels.clamp_pixel_pos((11, 3000));
/// assert_eq!(pixel_pos, (11, 240));
/// # Ok::<(), pixels::Error>(())
/// ```
pub fn clamp_pixel_pos(&self, pos: (isize, isize)) -> (usize, usize) {
(
pos.0.max(0).min(self.texture_extent.width as isize - 1) as usize,
pos.1.max(0).min(self.texture_extent.height as isize - 1) as usize,
)
}
}
impl<'req> PixelsBuilder<'req> {
@ -508,6 +616,13 @@ impl<'req> PixelsBuilder<'req> {
},
);
let scaling_matrix_inverse = renderers::ScalingMatrix::new(
(width as f32, height as f32),
(surface_texture.width as f32, surface_texture.height as f32),
)
.transform
.inversed();
// Create a renderer that impls `RenderPass`
let mut renderers = vec![Renderer::factory(
device.clone(),
@ -538,6 +653,7 @@ impl<'req> PixelsBuilder<'req> {
texture_extent,
texture_format_size,
pixels,
scaling_matrix_inverse,
})
}
}

View file

@ -1,5 +1,6 @@
use std::fmt;
use std::rc::Rc;
use ultraviolet::Mat4;
use wgpu::{self, Extent3d, TextureView};
use crate::include_spv;
@ -41,25 +42,17 @@ impl Renderer {
});
// Create uniform buffer
#[rustfmt::skip]
let transform: [f32; 16] = [
1.0, 0.0, 0.0, 0.0,
0.0, -1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
];
let mut tranform_bytes: Vec<u8> = Vec::with_capacity(4 * transform.len());
transform
.iter()
.map(|e| e.to_bits().to_ne_bytes())
.for_each(|b| tranform_bytes.extend(b.iter()));
let mapped = device.create_buffer_mapped(&wgpu::BufferDescriptor {
label: None,
size: tranform_bytes.len() as u64,
usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
});
mapped.data.copy_from_slice(&tranform_bytes);
let uniform_buffer = mapped.finish();
// TODO: This should also have the width / height of the of the window surface,
// so that it won't break when the window is created with a different size.
let matrix = ScalingMatrix::new(
(texture_size.width as f32, texture_size.height as f32),
(texture_size.width as f32, texture_size.height as f32),
);
let transform_bytes = matrix.as_bytes();
let uniform_buffer = device.create_buffer_with_data(
&transform_bytes,
wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
);
// Create bind group
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@ -176,38 +169,12 @@ impl RenderPass for Renderer {
}
fn resize(&mut self, encoder: &mut wgpu::CommandEncoder, width: u32, height: u32) {
let width = width as f32;
let height = height as f32;
let matrix = ScalingMatrix::new((self.width, self.height), (width as f32, height as f32));
let transform_bytes = matrix.as_bytes();
// Get smallest scale size
let scale = (width / self.width)
.min(height / self.height)
.max(1.0)
.floor();
// Update transformation matrix
let sw = self.width * scale / width;
let sh = self.height * scale / height;
#[rustfmt::skip]
let transform: [f32; 16] = [
sw, 0.0, 0.0, 0.0,
0.0, -sh, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
];
let mut tranform_bytes: Vec<u8> = Vec::with_capacity(4 * transform.len());
transform
.iter()
.map(|e| e.to_bits().to_ne_bytes())
.for_each(|b| tranform_bytes.extend(b.iter()));
let mapped = self.device.create_buffer_mapped(&wgpu::BufferDescriptor {
label: None,
size: tranform_bytes.len() as u64,
usage: wgpu::BufferUsage::COPY_SRC,
});
mapped.data.copy_from_slice(&tranform_bytes);
let temp_buf = mapped.finish();
encoder.copy_buffer_to_buffer(&temp_buf, 0, &self.uniform_buffer, 0, 64);
let temp_buf = self
.device
.create_buffer_with_data(&transform_bytes, wgpu::BufferUsage::COPY_SRC);
encoder.copy_buffer_to_buffer(&temp_buf, 0, &self.uniform_buffer, 0, 64);
}
@ -219,3 +186,42 @@ impl RenderPass for Renderer {
write!(f, "{:?}", self)
}
}
#[derive(Debug)]
pub(crate) struct ScalingMatrix {
pub(crate) transform: Mat4,
}
impl ScalingMatrix {
// texture_size is the dimensions of the drawing texture
// screen_size is the dimensions of the surface being drawn to
pub(crate) fn new(texture_size: (f32, f32), screen_size: (f32, f32)) -> ScalingMatrix {
let (screen_width, screen_height) = screen_size;
let (texture_width, texture_height) = texture_size;
// Get smallest scale size
let scale = (screen_width / texture_width)
.min(screen_height / texture_height)
.max(1.0)
.floor();
// Update transformation matrix
let sw = texture_width * scale / screen_width;
let sh = texture_height * scale / screen_height;
#[rustfmt::skip]
let transform: [f32; 16] = [
sw, 0.0, 0.0, 0.0,
0.0, -sh, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
];
ScalingMatrix {
transform: Mat4::from(transform),
}
}
fn as_bytes(&self) -> &[u8] {
self.transform.as_byte_slice()
}
}