diff --git a/crates/re_data_store/src/entity_properties.rs b/crates/re_data_store/src/entity_properties.rs index 6b2b746b58fc..ca7915c9bd70 100644 --- a/crates/re_data_store/src/entity_properties.rs +++ b/crates/re_data_store/src/entity_properties.rs @@ -140,7 +140,8 @@ impl ExtraQueryHistory { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum ColorMap { +pub enum Colormap { + /// Perceptually even Grayscale, #[default] Turbo, @@ -150,15 +151,15 @@ pub enum ColorMap { Inferno, } -impl std::fmt::Display for ColorMap { +impl std::fmt::Display for Colormap { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { - ColorMap::Grayscale => "Grayscale", - ColorMap::Turbo => "Turbo", - ColorMap::Viridis => "Viridis", - ColorMap::Plasma => "Plasma", - ColorMap::Magma => "Magma", - ColorMap::Inferno => "Inferno", + Colormap::Grayscale => "Grayscale", + Colormap::Turbo => "Turbo", + Colormap::Viridis => "Viridis", + Colormap::Plasma => "Plasma", + Colormap::Magma => "Magma", + Colormap::Inferno => "Inferno", }) } } @@ -167,7 +168,7 @@ impl std::fmt::Display for ColorMap { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ColorMapper { /// Use a well-known color map, pre-implemented as a wgsl module. - ColorMap(ColorMap), + Colormap(Colormap), // TODO(cmc): support textures. // TODO(cmc): support custom transfer functions. } @@ -175,7 +176,7 @@ pub enum ColorMapper { impl std::fmt::Display for ColorMapper { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ColorMapper::ColorMap(colormap) => colormap.fmt(f), + ColorMapper::Colormap(colormap) => colormap.fmt(f), } } } @@ -183,7 +184,7 @@ impl std::fmt::Display for ColorMapper { impl Default for ColorMapper { #[inline] fn default() -> Self { - Self::ColorMap(ColorMap::default()) + Self::Colormap(Colormap::default()) } } diff --git a/crates/re_log_types/src/component_types/tensor.rs b/crates/re_log_types/src/component_types/tensor.rs index 19118545eb52..3261183f83f6 100644 --- a/crates/re_log_types/src/component_types/tensor.rs +++ b/crates/re_log_types/src/component_types/tensor.rs @@ -369,6 +369,23 @@ impl Tensor { self.shape.len() } + /// If this tensor is shaped as an image, return the height, width, and channels/depth of it. + pub fn image_height_width_channels(&self) -> Option<[u64; 3]> { + if self.shape.len() == 2 { + Some([self.shape[0].size, self.shape[1].size, 1]) + } else if self.shape.len() == 3 { + let channels = self.shape[2].size; + // gray, rgb, rgba + if matches!(channels, 1 | 3 | 4) { + Some([self.shape[0].size, self.shape[1].size, channels]) + } else { + None + } + } else { + None + } + } + pub fn is_shaped_like_an_image(&self) -> bool { self.num_dim() == 2 || self.num_dim() == 3 && { diff --git a/crates/re_renderer/examples/2d.rs b/crates/re_renderer/examples/2d.rs index a9ea6e7e5638..52f9bafefd20 100644 --- a/crates/re_renderer/examples/2d.rs +++ b/crates/re_renderer/examples/2d.rs @@ -1,7 +1,8 @@ use ecolor::Hsva; use re_renderer::{ renderer::{ - LineStripFlags, RectangleDrawData, TextureFilterMag, TextureFilterMin, TexturedRect, + ColormappedTexture, LineStripFlags, RectangleDrawData, TextureFilterMag, TextureFilterMin, + TexturedRect, }, resource_managers::{GpuTexture2DHandle, Texture2DCreationDesc}, view_builder::{self, Projection, TargetConfiguration, ViewBuilder}, @@ -39,7 +40,7 @@ impl framework::Example for Render2D { &mut re_ctx.gpu_resources.textures, &Texture2DCreationDesc { label: "rerun logo".into(), - data: &image_data, + data: image_data.into(), format: wgpu::TextureFormat::Rgba8UnormSrgb, width: rerun_logo.width(), height: rerun_logo.height(), @@ -196,7 +197,9 @@ impl framework::Example for Render2D { top_left_corner_position: glam::vec3(500.0, 120.0, -0.05), extent_u: self.rerun_logo_texture_width as f32 * image_scale * glam::Vec3::X, extent_v: self.rerun_logo_texture_height as f32 * image_scale * glam::Vec3::Y, - texture: self.rerun_logo_texture.clone(), + colormapped_texture: ColormappedTexture::from_unorm_srgba( + self.rerun_logo_texture.clone(), + ), texture_filter_magnification: TextureFilterMag::Nearest, texture_filter_minification: TextureFilterMin::Linear, ..Default::default() @@ -210,7 +213,9 @@ impl framework::Example for Render2D { ), extent_u: self.rerun_logo_texture_width as f32 * image_scale * glam::Vec3::X, extent_v: self.rerun_logo_texture_height as f32 * image_scale * glam::Vec3::Y, - texture: self.rerun_logo_texture.clone(), + colormapped_texture: ColormappedTexture::from_unorm_srgba( + self.rerun_logo_texture.clone(), + ), texture_filter_magnification: TextureFilterMag::Linear, texture_filter_minification: TextureFilterMin::Linear, depth_offset: 1, diff --git a/crates/re_renderer/examples/depth_cloud.rs b/crates/re_renderer/examples/depth_cloud.rs index ce80f17594c5..eb2d6ffc3148 100644 --- a/crates/re_renderer/examples/depth_cloud.rs +++ b/crates/re_renderer/examples/depth_cloud.rs @@ -20,8 +20,8 @@ use itertools::Itertools; use macaw::IsoTransform; use re_renderer::{ renderer::{ - DepthCloud, DepthCloudDepthData, DepthCloudDrawData, DepthClouds, DrawData, - GenericSkyboxDrawData, RectangleDrawData, TexturedRect, + ColormappedTexture, DepthCloud, DepthCloudDepthData, DepthCloudDrawData, DepthClouds, + DrawData, GenericSkyboxDrawData, RectangleDrawData, TexturedRect, }, resource_managers::{GpuTexture2DHandle, Texture2DCreationDesc}, view_builder::{self, Projection, ViewBuilder}, @@ -181,7 +181,7 @@ impl RenderDepthClouds { max_depth_in_world: 5.0, depth_dimensions: depth.dimensions, depth_data: depth.data.clone(), - colormap: re_renderer::ColorMap::ColorMapTurbo, + colormap: re_renderer::Colormap::Turbo, outline_mask_id: Default::default(), }], radius_boost_in_ui_points_for_outlines: 2.5, @@ -243,7 +243,7 @@ impl framework::Example for RenderDepthClouds { &mut re_ctx.gpu_resources.textures, &Texture2DCreationDesc { label: "albedo".into(), - data: bytemuck::cast_slice(&albedo.rgba8), + data: bytemuck::cast_slice(&albedo.rgba8).into(), format: wgpu::TextureFormat::Rgba8UnormSrgb, width: albedo.dimensions.x, height: albedo.dimensions.y, @@ -329,7 +329,7 @@ impl framework::Example for RenderDepthClouds { .transform_point3(glam::Vec3::new(1.0, 1.0, 0.0)), extent_u: world_from_model.transform_vector3(-glam::Vec3::X), extent_v: world_from_model.transform_vector3(-glam::Vec3::Y), - texture: albedo_handle.clone(), + colormapped_texture: ColormappedTexture::from_unorm_srgba(albedo_handle.clone()), texture_filter_magnification: re_renderer::renderer::TextureFilterMag::Nearest, texture_filter_minification: re_renderer::renderer::TextureFilterMin::Linear, multiplicative_tint: Rgba::from_white_alpha(0.5), diff --git a/crates/re_renderer/shader/colormap.wgsl b/crates/re_renderer/shader/colormap.wgsl index 6c99dac97ebb..d12b43246dca 100644 --- a/crates/re_renderer/shader/colormap.wgsl +++ b/crates/re_renderer/shader/colormap.wgsl @@ -2,18 +2,20 @@ #import <./utils/srgb.wgsl> // NOTE: Keep in sync with `colormap.rs`! -const GRAYSCALE: u32 = 0u; -const COLORMAP_TURBO: u32 = 1u; -const COLORMAP_VIRIDIS: u32 = 2u; -const COLORMAP_PLASMA: u32 = 3u; -const COLORMAP_MAGMA: u32 = 4u; -const COLORMAP_INFERNO: u32 = 5u; +const COLORMAP_GRAYSCALE: u32 = 1u; +const COLORMAP_TURBO: u32 = 2u; +const COLORMAP_VIRIDIS: u32 = 3u; +const COLORMAP_PLASMA: u32 = 4u; +const COLORMAP_MAGMA: u32 = 5u; +const COLORMAP_INFERNO: u32 = 6u; /// Returns a gamma-space sRGB in 0-1 range. /// /// The input will be saturated to [0, 1] range. fn colormap_srgb(which: u32, t: f32) -> Vec3 { - if which == COLORMAP_TURBO { + if which == COLORMAP_GRAYSCALE { + return linear_from_srgb(Vec3(t)); + } else if which == COLORMAP_TURBO { return colormap_turbo_srgb(t); } else if which == COLORMAP_VIRIDIS { return colormap_viridis_srgb(t); @@ -23,8 +25,8 @@ fn colormap_srgb(which: u32, t: f32) -> Vec3 { return colormap_magma_srgb(t); } else if which == COLORMAP_INFERNO { return colormap_inferno_srgb(t); - } else { // assume grayscale - return linear_from_srgb(Vec3(t)); + } else { + return ERROR_RGBA.rgb; } } diff --git a/crates/re_renderer/shader/rectangle.wgsl b/crates/re_renderer/shader/rectangle.wgsl index daf506956108..57584df1abb1 100644 --- a/crates/re_renderer/shader/rectangle.wgsl +++ b/crates/re_renderer/shader/rectangle.wgsl @@ -1,14 +1,33 @@ #import <./types.wgsl> +#import <./colormap.wgsl> #import <./global_bindings.wgsl> #import <./utils/depth_offset.wgsl> +// Keep in sync with mirror in rectangle.rs + +// Which texture to read from? +const SAMPLE_TYPE_FLOAT = 1u; +const SAMPLE_TYPE_SINT = 2u; +const SAMPLE_TYPE_UINT = 3u; + +// How do we do colormapping? +const COLOR_MAPPER_OFF = 1u; +const COLOR_MAPPER_FUNCTION = 2u; +const COLOR_MAPPER_TEXTURE = 3u; + struct UniformBuffer { /// Top left corner position in world space. top_left_corner_position: Vec3, + /// Which colormap to use, if any + colormap_function: u32, + /// Vector that spans up the rectangle from its top left corner along the u axis of the texture. extent_u: Vec3, + /// Which texture sample to use + sample_type: u32, + /// Vector that spans up the rectangle from its top left corner along the v axis of the texture. extent_v: Vec3, @@ -18,16 +37,35 @@ struct UniformBuffer { multiplicative_tint: Vec4, outline_mask: UVec2, + + /// Range of the texture values. + /// Will be mapped to the [0, 1] range before we colormap. + range_min_max: Vec2, + + color_mapper: u32, + + /// Exponent to raise the normalized texture value. + /// Inverse brightness. + gamma: f32, }; @group(1) @binding(0) var rect_info: UniformBuffer; @group(1) @binding(1) -var texture: texture_2d; +var texture_sampler: sampler; @group(1) @binding(2) -var texture_sampler: sampler; +var texture_float: texture_2d; + +@group(1) @binding(3) +var texture_sint: texture_2d; + +@group(1) @binding(4) +var texture_uint: texture_2d; + +@group(1) @binding(5) +var colormap_texture: texture_2d; struct VertexOut { @@ -50,7 +88,46 @@ fn vs_main(@builtin(vertex_index) v_idx: u32) -> VertexOut { @fragment fn fs_main(in: VertexOut) -> @location(0) Vec4 { - let texture_color = textureSample(texture, texture_sampler, in.texcoord); + // Sample the main texture: + var sampled_value: Vec4; + if rect_info.sample_type == SAMPLE_TYPE_FLOAT { + sampled_value = textureSampleLevel(texture_float, texture_sampler, in.texcoord, 0.0); // TODO(emilk): support mipmaps + } else if rect_info.sample_type == SAMPLE_TYPE_SINT { + let icoords = IVec2(in.texcoord * Vec2(textureDimensions(texture_sint).xy)); + sampled_value = Vec4(textureLoad(texture_sint, icoords, 0)); + } else if rect_info.sample_type == SAMPLE_TYPE_UINT { + let icoords = IVec2(in.texcoord * Vec2(textureDimensions(texture_uint).xy)); + sampled_value = Vec4(textureLoad(texture_uint, icoords, 0)); + } else { + return ERROR_RGBA; // unknown sample type + } + + // Normalize the sample: + let range = rect_info.range_min_max; + var normalized_value: Vec4 = (sampled_value - range.x) / (range.y - range.x); + + // Apply gamma: + normalized_value = vec4(pow(normalized_value.rgb, vec3(rect_info.gamma)), normalized_value.a); // TODO(emilk): handle premultiplied alpha + + // Apply colormap, if any: + var texture_color: Vec4; + if rect_info.color_mapper == COLOR_MAPPER_OFF { + texture_color = normalized_value; + } else if rect_info.color_mapper == COLOR_MAPPER_FUNCTION { + let rgb = colormap_linear(rect_info.colormap_function, normalized_value.r); + texture_color = Vec4(rgb, 1.0); + } else if rect_info.color_mapper == COLOR_MAPPER_TEXTURE { + let colormap_size = textureDimensions(colormap_texture).xy; + let color_index = normalized_value.r * f32(colormap_size.x * colormap_size.y); + // TODO(emilk): interpolate between neighboring colors for non-integral color indices + let color_index_i32 = i32(color_index); + let x = color_index_i32 % colormap_size.x; + let y = color_index_i32 / colormap_size.x; + texture_color = textureLoad(colormap_texture, IVec2(x, y), 0); + } else { + return ERROR_RGBA; // unknown color mapper + } + return texture_color * rect_info.multiplicative_tint; } diff --git a/crates/re_renderer/shader/types.wgsl b/crates/re_renderer/shader/types.wgsl index 71552d38e7dd..3323c7a6cd1f 100644 --- a/crates/re_renderer/shader/types.wgsl +++ b/crates/re_renderer/shader/types.wgsl @@ -48,3 +48,7 @@ const ONE = Vec4(1.0, 1.0, 1.0, 1.0); // fn inf() -> f32 { // return 1.0 / 0.0; // } + + +/// The color to use when we encounter an error. +const ERROR_RGBA = Vec4(1.0, 0.0, 1.0, 1.0); diff --git a/crates/re_renderer/src/colormap.rs b/crates/re_renderer/src/colormap.rs index 4625b4939d62..c64d85dbb24b 100644 --- a/crates/re_renderer/src/colormap.rs +++ b/crates/re_renderer/src/colormap.rs @@ -5,25 +5,28 @@ use glam::{Vec2, Vec3A, Vec4, Vec4Swizzles}; // --- // NOTE: Keep in sync with `colormap.wgsl`! -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] #[repr(u32)] -pub enum ColorMap { - Grayscale = 0, - ColorMapTurbo = 1, - ColorMapViridis = 2, - ColorMapPlasma = 3, - ColorMapMagma = 4, - ColorMapInferno = 5, +pub enum Colormap { + // Reserve 0 for "disabled" + /// Perceptually even + #[default] + Grayscale = 1, + Turbo = 2, + Viridis = 3, + Plasma = 4, + Magma = 5, + Inferno = 6, } -pub fn colormap_srgb(which: ColorMap, t: f32) -> [u8; 4] { +pub fn colormap_srgb(which: Colormap, t: f32) -> [u8; 4] { match which { - ColorMap::Grayscale => grayscale_srgb(t), - ColorMap::ColorMapTurbo => colormap_turbo_srgb(t), - ColorMap::ColorMapViridis => colormap_viridis_srgb(t), - ColorMap::ColorMapPlasma => colormap_plasma_srgb(t), - ColorMap::ColorMapMagma => colormap_magma_srgb(t), - ColorMap::ColorMapInferno => colormap_inferno_srgb(t), + Colormap::Grayscale => grayscale_srgb(t), + Colormap::Turbo => colormap_turbo_srgb(t), + Colormap::Viridis => colormap_viridis_srgb(t), + Colormap::Plasma => colormap_plasma_srgb(t), + Colormap::Magma => colormap_magma_srgb(t), + Colormap::Inferno => colormap_inferno_srgb(t), } } diff --git a/crates/re_renderer/src/importer/gltf.rs b/crates/re_renderer/src/importer/gltf.rs index fff88b006cb9..9a129640b43a 100644 --- a/crates/re_renderer/src/importer/gltf.rs +++ b/crates/re_renderer/src/importer/gltf.rs @@ -62,7 +62,7 @@ pub fn load_gltf_from_buffer( format!("gltf image used by {texture_names} in {mesh_name}") } .into(), - data: &data, + data: data.into(), format, width: image.width, height: image.height, diff --git a/crates/re_renderer/src/lib.rs b/crates/re_renderer/src/lib.rs index 7f4f23db8db0..3b9d660be33e 100644 --- a/crates/re_renderer/src/lib.rs +++ b/crates/re_renderer/src/lib.rs @@ -33,7 +33,7 @@ pub use allocator::GpuReadbackIdentifier; pub use color::Rgba32Unmul; pub use colormap::{ colormap_inferno_srgb, colormap_magma_srgb, colormap_plasma_srgb, colormap_srgb, - colormap_turbo_srgb, colormap_viridis_srgb, grayscale_srgb, ColorMap, + colormap_turbo_srgb, colormap_viridis_srgb, grayscale_srgb, Colormap, }; pub use context::RenderContext; pub use debug_label::DebugLabel; diff --git a/crates/re_renderer/src/renderer/depth_cloud.rs b/crates/re_renderer/src/renderer/depth_cloud.rs index fc6c13a36539..6c56428ba1c4 100644 --- a/crates/re_renderer/src/renderer/depth_cloud.rs +++ b/crates/re_renderer/src/renderer/depth_cloud.rs @@ -24,7 +24,7 @@ use crate::{ GpuRenderPipelineHandle, GpuTexture, PipelineLayoutDesc, RenderPipelineDesc, Texture2DBufferInfo, TextureDesc, }, - ColorMap, OutlineMaskPreference, PickingLayerProcessor, + Colormap, OutlineMaskPreference, PickingLayerProcessor, }; use super::{ @@ -160,7 +160,7 @@ pub struct DepthCloud { pub depth_data: DepthCloudDepthData, /// Configures color mapping mode. - pub colormap: ColorMap, + pub colormap: Colormap, /// Option outline mask id preference. pub outline_mask_id: OutlineMaskPreference, diff --git a/crates/re_renderer/src/renderer/mod.rs b/crates/re_renderer/src/renderer/mod.rs index 7eb9714b7a2e..5936d59665a7 100644 --- a/crates/re_renderer/src/renderer/mod.rs +++ b/crates/re_renderer/src/renderer/mod.rs @@ -19,7 +19,10 @@ mod test_triangle; pub use test_triangle::TestTriangleDrawData; mod rectangles; -pub use rectangles::{RectangleDrawData, TextureFilterMag, TextureFilterMin, TexturedRect}; +pub use rectangles::{ + ColorMapper, ColormappedTexture, RectangleDrawData, TextureFilterMag, TextureFilterMin, + TexturedRect, +}; mod mesh_renderer; pub(crate) use mesh_renderer::MeshRenderer; diff --git a/crates/re_renderer/src/renderer/rectangles.rs b/crates/re_renderer/src/renderer/rectangles.rs index 4cbebb883412..f869f055570b 100644 --- a/crates/re_renderer/src/renderer/rectangles.rs +++ b/crates/re_renderer/src/renderer/rectangles.rs @@ -10,6 +10,7 @@ //! Since we're not allowed to bind many textures at once (no widespread bindless support!), //! we are forced to have individual bind groups per rectangle and thus a draw call per rectangle. +use itertools::{izip, Itertools as _}; use smallvec::smallvec; use crate::{ @@ -23,7 +24,7 @@ use crate::{ BindGroupDesc, BindGroupEntry, BindGroupLayoutDesc, GpuBindGroup, GpuBindGroupLayoutHandle, GpuRenderPipelineHandle, PipelineLayoutDesc, RenderPipelineDesc, SamplerDesc, }, - OutlineMaskPreference, PickingLayerProcessor, Rgba, + Colormap, OutlineMaskPreference, PickingLayerProcessor, Rgba, }; use super::{ @@ -31,24 +32,6 @@ use super::{ WgpuResourcePools, }; -mod gpu_data { - use crate::wgpu_buffer_types; - - // Keep in sync with mirror in rectangle.wgsl - #[repr(C, align(256))] - #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] - pub struct UniformBuffer { - pub top_left_corner_position: wgpu_buffer_types::Vec3RowPadded, - pub extent_u: wgpu_buffer_types::Vec3RowPadded, - pub extent_v: wgpu_buffer_types::Vec3Unpadded, - pub depth_offset: f32, - pub multiplicative_tint: crate::Rgba, - pub outline_mask: wgpu_buffer_types::UVec2RowPadded, - - pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 5], - } -} - /// Texture filter setting for magnification (a texel covers several pixels). #[derive(Debug)] pub enum TextureFilterMag { @@ -65,6 +48,65 @@ pub enum TextureFilterMin { // TODO(andreas): Offer mipmapping here? } +/// Describes a texture and how to map it to a color. +pub struct ColormappedTexture { + pub texture: GpuTexture2DHandle, + + /// Min/max range of the values in the texture. + /// Used to normalize the input values (squash them to the 0-1 range). + pub range: [f32; 2], + + /// Raise the normalized values to this power (before any color mapping). + /// Acts like an inverse brightness. + /// + /// Default: 1.0 + pub gamma: f32, + + /// For any one-component texture, you need to supply a color mapper, + /// which maps the normalized `.r` component to a color. + /// + /// Setting a color mapper for a four-component texture is an error. + /// Failure to set a color mapper for a one-component texture is an error. + pub color_mapper: Option, +} + +/// How to map the normalized `.r` component to a color. +pub enum ColorMapper { + /// Apply the given function. + Function(Colormap), + + /// Look up the color in this texture. + /// + /// The texture is indexed in a row-major fashion, so that the top left pixel + /// corresponds to the the normalized value of 0.0, and the + /// bottom right pixel is 1.0. + /// + /// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`]. + Texture(GpuTexture2DHandle), +} + +impl Default for ColormappedTexture { + fn default() -> Self { + Self { + texture: GpuTexture2DHandle::invalid(), + range: [0.0, 1.0], + gamma: 1.0, + color_mapper: None, + } + } +} + +impl ColormappedTexture { + pub fn from_unorm_srgba(texture: GpuTexture2DHandle) -> Self { + Self { + texture, + range: [0.0, 1.0], + gamma: 1.0, + color_mapper: None, + } + } +} + pub struct TexturedRect { /// Top left corner position in world space. pub top_left_corner_position: glam::Vec3, @@ -76,7 +118,7 @@ pub struct TexturedRect { pub extent_v: glam::Vec3, /// Texture that fills the rectangle - pub texture: GpuTexture2DHandle, + pub colormapped_texture: ColormappedTexture, pub texture_filter_magnification: TextureFilterMag, pub texture_filter_minification: TextureFilterMin, @@ -96,7 +138,7 @@ impl Default for TexturedRect { top_left_corner_position: glam::Vec3::ZERO, extent_u: glam::Vec3::ZERO, extent_v: glam::Vec3::ZERO, - texture: GpuTexture2DHandle::invalid(), + colormapped_texture: Default::default(), texture_filter_magnification: TextureFilterMag::Nearest, texture_filter_minification: TextureFilterMin::Linear, multiplicative_tint: Rgba::WHITE, @@ -106,6 +148,145 @@ impl Default for TexturedRect { } } +#[derive(thiserror::Error, Debug)] +pub enum RectangleError { + #[error(transparent)] + ResourceManagerError(#[from] ResourceManagerError), + + #[error("Texture required special features: {0:?}")] + SpecialFeatures(wgpu::Features), + + // There's really no need for users to be able to sample depth textures. + // We don't get filtering of depth textures any way. + #[error("Depth textures not supported - use float or integer textures instead.")] + DepthTexturesNotSupported, + + #[error("Color mapping is being applied to a four-component RGBA texture")] + ColormappingRgbaTexture, + + #[error("Only 1 and 4 component textures are supported, got {0} components")] + UnsupportedComponentCount(u8), + + #[error("No color mapper was supplied for this 1-component texture")] + MissingColorMapper, + + #[error("Invalid color map texture format: {0:?}")] + UnsupportedColormapTextureFormat(wgpu::TextureFormat), +} + +mod gpu_data { + use crate::wgpu_buffer_types; + + use super::{ColorMapper, RectangleError}; + + // Keep in sync with mirror in rectangle.wgsl + + // Which texture to read from? + const SAMPLE_TYPE_FLOAT: u32 = 1; + const SAMPLE_TYPE_SINT: u32 = 2; + const SAMPLE_TYPE_UINT: u32 = 3; + + // How do we do colormapping? + const COLOR_MAPPER_OFF: u32 = 1; + const COLOR_MAPPER_FUNCTION: u32 = 2; + const COLOR_MAPPER_TEXTURE: u32 = 3; + + #[repr(C, align(256))] + #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] + pub struct UniformBuffer { + top_left_corner_position: wgpu_buffer_types::Vec3Unpadded, + colormap_function: u32, + + extent_u: wgpu_buffer_types::Vec3Unpadded, + sample_type: u32, + + extent_v: wgpu_buffer_types::Vec3Unpadded, + depth_offset: f32, + + multiplicative_tint: crate::Rgba, + outline_mask: wgpu_buffer_types::UVec2, + + /// Range of the texture values. + /// Will be mapped to the [0, 1] range before we colormap. + range_min_max: wgpu_buffer_types::Vec2, + + color_mapper: u32, + gamma: f32, + _row_padding: [u32; 2], + + _end_padding: [wgpu_buffer_types::PaddingRow; 16 - 6], + } + + impl UniformBuffer { + pub fn from_textured_rect( + rectangle: &super::TexturedRect, + texture_format: &wgpu::TextureFormat, + ) -> Result { + let texture_info = texture_format.describe(); + + let super::ColormappedTexture { + texture: _, + range, + gamma, + color_mapper, + } = &rectangle.colormapped_texture; + + let sample_type = match texture_info.sample_type { + wgpu::TextureSampleType::Float { .. } => SAMPLE_TYPE_FLOAT, + wgpu::TextureSampleType::Depth => { + return Err(RectangleError::DepthTexturesNotSupported); + } + wgpu::TextureSampleType::Sint => SAMPLE_TYPE_SINT, + wgpu::TextureSampleType::Uint => SAMPLE_TYPE_UINT, + }; + + let mut colormap_function = 0; + let color_mapper_int; + + match texture_info.components { + 1 => match color_mapper { + Some(ColorMapper::Function(colormap)) => { + color_mapper_int = COLOR_MAPPER_FUNCTION; + colormap_function = *colormap as u32; + } + Some(ColorMapper::Texture(_)) => { + color_mapper_int = COLOR_MAPPER_TEXTURE; + } + None => { + return Err(RectangleError::MissingColorMapper); + } + }, + 4 => { + if color_mapper.is_some() { + return Err(RectangleError::ColormappingRgbaTexture); + } else { + color_mapper_int = COLOR_MAPPER_OFF; + } + } + num_components => { + return Err(RectangleError::UnsupportedComponentCount(num_components)) + } + } + + Ok(Self { + top_left_corner_position: rectangle.top_left_corner_position.into(), + colormap_function, + extent_u: rectangle.extent_u.into(), + sample_type, + extent_v: rectangle.extent_v.into(), + depth_offset: rectangle.depth_offset as f32, + multiplicative_tint: rectangle.multiplicative_tint, + outline_mask: rectangle.outline_mask.0.unwrap_or_default().into(), + range_min_max: (*range).into(), + color_mapper: color_mapper_int, + gamma: *gamma, + _row_padding: Default::default(), + _end_padding: Default::default(), + }) + } + } +} + #[derive(Clone)] struct RectangleInstance { bind_group: GpuBindGroup, @@ -125,7 +306,7 @@ impl RectangleDrawData { pub fn new( ctx: &mut RenderContext, rectangles: &[TexturedRect], - ) -> Result { + ) -> Result { crate::profile_function!(); let mut renderers = ctx.renderers.write(); @@ -142,26 +323,31 @@ impl RectangleDrawData { }); } + // TODO(emilk): continue on error (skipping just that rectangle)? + let textures: Vec<_> = rectangles + .iter() + .map(|rectangle| { + ctx.texture_manager_2d + .get(&rectangle.colormapped_texture.texture) + }) + .try_collect()?; + + let uniform_buffers: Vec<_> = izip!(rectangles, &textures) + .map(|(rect, texture)| { + gpu_data::UniformBuffer::from_textured_rect(rect, &texture.creation_desc.format) + }) + .try_collect()?; + let uniform_buffer_bindings = create_and_fill_uniform_buffer_batch( ctx, "rectangle uniform buffers".into(), - rectangles.iter().map(|rectangle| gpu_data::UniformBuffer { - top_left_corner_position: rectangle.top_left_corner_position.into(), - extent_u: rectangle.extent_u.into(), - extent_v: rectangle.extent_v.into(), - depth_offset: rectangle.depth_offset as f32, - multiplicative_tint: rectangle.multiplicative_tint, - outline_mask: rectangle.outline_mask.0.unwrap_or_default().into(), - end_padding: Default::default(), - }), + uniform_buffers.into_iter(), ); let mut instances = Vec::with_capacity(rectangles.len()); - for (rectangle, uniform_buffer) in - rectangles.iter().zip(uniform_buffer_bindings.into_iter()) + for (rectangle, uniform_buffer, texture) in + izip!(rectangles, uniform_buffer_bindings, textures) { - let texture = ctx.texture_manager_2d.get(&rectangle.texture)?; - let sampler = ctx.gpu_resources.samplers.get_or_create( &ctx.device, &SamplerDesc { @@ -184,6 +370,49 @@ impl RectangleDrawData { }, ); + let texture_format = texture.creation_desc.format; + let texture_description = texture_format.describe(); + if texture_description.required_features != Default::default() { + return Err(RectangleError::SpecialFeatures( + texture_description.required_features, + )); + } + + // We set up three texture sources, then instruct the shader to read from at most one of them. + let mut texture_float = ctx.texture_manager_2d.zeroed_texture_float().handle; + let mut texture_sint = ctx.texture_manager_2d.zeroed_texture_sint().handle; + let mut texture_uint = ctx.texture_manager_2d.zeroed_texture_uint().handle; + + match texture_description.sample_type { + wgpu::TextureSampleType::Float { .. } => { + texture_float = texture.handle; + } + wgpu::TextureSampleType::Depth => { + return Err(RectangleError::DepthTexturesNotSupported); + } + wgpu::TextureSampleType::Sint => { + texture_sint = texture.handle; + } + wgpu::TextureSampleType::Uint => { + texture_uint = texture.handle; + } + } + + // We also set up an optional colormap texture. + let colormap_texture = if let Some(ColorMapper::Texture(handle)) = + &rectangle.colormapped_texture.color_mapper + { + let colormap_texture = ctx.texture_manager_2d.get(handle)?; + if colormap_texture.creation_desc.format != wgpu::TextureFormat::Rgba8UnormSrgb { + return Err(RectangleError::UnsupportedColormapTextureFormat( + colormap_texture.creation_desc.format, + )); + } + colormap_texture.handle + } else { + ctx.texture_manager_2d.zeroed_texture_float().handle + }; + instances.push(RectangleInstance { bind_group: ctx.gpu_resources.bind_groups.alloc( &ctx.device, @@ -192,8 +421,11 @@ impl RectangleDrawData { label: "RectangleInstance::bind_group".into(), entries: smallvec![ uniform_buffer, - BindGroupEntry::DefaultTextureView(texture.handle), - BindGroupEntry::Sampler(sampler) + BindGroupEntry::Sampler(sampler), + BindGroupEntry::DefaultTextureView(texture_float), + BindGroupEntry::DefaultTextureView(texture_sint), + BindGroupEntry::DefaultTextureView(texture_uint), + BindGroupEntry::DefaultTextureView(colormap_texture), ], layout: rectangle_renderer.bind_group_layout, }, @@ -244,9 +476,17 @@ impl Renderer for RectangleRenderer { }, count: None, }, + // float sampler: wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // float texture: + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, @@ -254,10 +494,37 @@ impl Renderer for RectangleRenderer { }, count: None, }, + // sint texture: wgpu::BindGroupLayoutEntry { - binding: 2, + binding: 3, visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Sint, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + // uint texture: + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Uint, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + // colormap texture: + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, count: None, }, ], diff --git a/crates/re_renderer/src/resource_managers/texture_manager.rs b/crates/re_renderer/src/resource_managers/texture_manager.rs index bdfb82755b4f..b31f2e01f0dc 100644 --- a/crates/re_renderer/src/resource_managers/texture_manager.rs +++ b/crates/re_renderer/src/resource_managers/texture_manager.rs @@ -32,7 +32,7 @@ pub struct Texture2DCreationDesc<'a> { /// Data for the highest mipmap level. /// Must be padded according to wgpu rules and ready for upload. /// TODO(andreas): This should be a kind of factory function/builder instead which gets target memory passed in. - pub data: &'a [u8], + pub data: std::borrow::Cow<'a, [u8]>, pub format: wgpu::TextureFormat, pub width: u32, pub height: u32, @@ -60,6 +60,9 @@ pub struct TextureManager2D { // optimize for short lived textures as we do with buffer data. //manager: ResourceManager, white_texture_unorm: GpuTexture2DHandle, + zeroed_texture_float: GpuTexture2DHandle, + zeroed_texture_depth: GpuTexture2DHandle, + zeroed_texture_sint: GpuTexture2DHandle, zeroed_texture_uint: GpuTexture2DHandle, // For convenience to reduce amount of times we need to pass them around @@ -91,33 +94,27 @@ impl TextureManager2D { texture_pool, &Texture2DCreationDesc { label: "white pixel - unorm".into(), - data: &[255, 255, 255, 255], + data: vec![255, 255, 255, 255].into(), format: wgpu::TextureFormat::Rgba8Unorm, width: 1, height: 1, }, ); - // Wgpu zeros out new textures automatically - let zeroed_texture_uint = GpuTexture2DHandle(Some(texture_pool.alloc( - &device, - &TextureDesc { - label: "zeroed pixel - uint".into(), - format: wgpu::TextureFormat::Rgba8Uint, - size: wgpu::Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - usage: wgpu::TextureUsages::TEXTURE_BINDING, - }, - ))); + let zeroed_texture_float = + create_zero_texture(texture_pool, &device, wgpu::TextureFormat::Rgba8Unorm); + let zeroed_texture_depth = + create_zero_texture(texture_pool, &device, wgpu::TextureFormat::Depth16Unorm); + let zeroed_texture_sint = + create_zero_texture(texture_pool, &device, wgpu::TextureFormat::Rgba8Sint); + let zeroed_texture_uint = + create_zero_texture(texture_pool, &device, wgpu::TextureFormat::Rgba8Uint); Self { white_texture_unorm, + zeroed_texture_float, + zeroed_texture_depth, + zeroed_texture_sint, zeroed_texture_uint, device, queue, @@ -158,13 +155,45 @@ impl TextureManager2D { &mut self, key: u64, texture_pool: &mut GpuTexturePool, - creation_desc: &Texture2DCreationDesc<'_>, + texture_desc: Texture2DCreationDesc<'_>, ) -> GpuTexture2DHandle { - let texture_handle = self.texture_cache.entry(key).or_insert_with(|| { - Self::create_and_upload_texture(&self.device, &self.queue, texture_pool, creation_desc) - }); + enum Never {} + match self.get_or_create_with(key, texture_pool, || -> Result<_, Never> { + Ok(texture_desc) + }) { + Ok(tex_handle) => tex_handle, + Err(never) => match never {}, + } + } + + /// Creates a new 2D texture resource and schedules data upload to the GPU if a texture + /// wasn't already created using the same key. + pub fn get_or_create_with<'a, Err>( + &mut self, + key: u64, + texture_pool: &mut GpuTexturePool, + try_create_texture_desc: impl FnOnce() -> Result, Err>, + ) -> Result { + let texture_handle = match self.texture_cache.entry(key) { + std::collections::hash_map::Entry::Occupied(texture_handle) => { + texture_handle.get().clone() // already inserted + } + std::collections::hash_map::Entry::Vacant(entry) => { + // Run potentially expensive texture creation code: + let tex_creation_desc = try_create_texture_desc()?; + entry + .insert(Self::create_and_upload_texture( + &self.device, + &self.queue, + texture_pool, + &tex_creation_desc, + )) + .clone() + } + }; + self.accessed_textures.insert(key); - texture_handle.clone() + Ok(texture_handle) } /// Returns a single pixel white pixel with an rgba8unorm format. @@ -177,7 +206,22 @@ impl TextureManager2D { self.white_texture_unorm.0.as_ref().unwrap() } - /// Returns a single pixel white pixel with an rgba8unorm format. + /// Returns a single zero pixel with format [`wgpu::TextureFormat::Rgba8Unorm`]. + pub fn zeroed_texture_float(&self) -> &GpuTexture { + self.zeroed_texture_float.0.as_ref().unwrap() + } + + /// Returns a single zero pixel with format [`wgpu::TextureFormat::Depth16Unorm`]. + pub fn zeroed_texture_depth(&self) -> &GpuTexture { + self.zeroed_texture_depth.0.as_ref().unwrap() + } + + /// Returns a single zero pixel with format [`wgpu::TextureFormat::Rgba8Sint`]. + pub fn zeroed_texture_sint(&self) -> &GpuTexture { + self.zeroed_texture_sint.0.as_ref().unwrap() + } + + /// Returns a single zero pixel with format [`wgpu::TextureFormat::Rgba8Uint`]. pub fn zeroed_texture_uint(&self) -> &GpuTexture { self.zeroed_texture_uint.0.as_ref().unwrap() } @@ -213,7 +257,7 @@ impl TextureManager2D { // TODO(andreas): Once we have our own temp buffer for uploading, we can do the padding inplace // I.e. the only difference will be if we do one memcopy or one memcopy per row, making row padding a nuisance! - let data = creation_desc.data; + let data: &[u8] = creation_desc.data.as_ref(); // TODO(andreas): temp allocator for staging data? // We don't do any further validation of the buffer here as wgpu does so extensively. @@ -243,10 +287,7 @@ impl TextureManager2D { /// Retrieves gpu handle. #[allow(clippy::unused_self)] - pub(crate) fn get( - &self, - handle: &GpuTexture2DHandle, - ) -> Result { + pub fn get(&self, handle: &GpuTexture2DHandle) -> Result { handle .0 .as_ref() @@ -261,3 +302,27 @@ impl TextureManager2D { self.accessed_textures.clear(); } } + +fn create_zero_texture( + texture_pool: &mut GpuTexturePool, + device: &Arc, + format: wgpu::TextureFormat, +) -> GpuTexture2DHandle { + // Wgpu zeros out new textures automatically + GpuTexture2DHandle(Some(texture_pool.alloc( + device, + &TextureDesc { + label: format!("zeroed pixel {format:?}").into(), + format, + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: wgpu::TextureUsages::TEXTURE_BINDING, + }, + ))) +} diff --git a/crates/re_renderer/src/wgpu_buffer_types.rs b/crates/re_renderer/src/wgpu_buffer_types.rs index af60adac00c3..ee7276337b55 100644 --- a/crates/re_renderer/src/wgpu_buffer_types.rs +++ b/crates/re_renderer/src/wgpu_buffer_types.rs @@ -61,6 +61,13 @@ impl From for Vec2 { } } +impl From<[f32; 2]> for Vec2 { + #[inline] + fn from([x, y]: [f32; 2]) -> Self { + Vec2 { x, y } + } +} + #[repr(C, align(16))] #[derive(Clone, Copy, Zeroable, Pod)] pub struct Vec2RowPadded { @@ -98,10 +105,10 @@ impl From for UVec2 { impl From<[u8; 2]> for UVec2 { #[inline] - fn from(v: [u8; 2]) -> Self { + fn from([x, y]: [u8; 2]) -> Self { UVec2 { - x: v[0] as u32, - y: v[1] as u32, + x: x as u32, + y: y as u32, } } } @@ -129,10 +136,10 @@ impl From for UVec2RowPadded { impl From<[u8; 2]> for UVec2RowPadded { #[inline] - fn from(v: [u8; 2]) -> Self { + fn from([x, y]: [u8; 2]) -> Self { UVec2RowPadded { - x: v[0] as u32, - y: v[1] as u32, + x: x as u32, + y: y as u32, padding0: 0, padding1: 0, } diff --git a/crates/re_viewer/src/misc/caches/tensor_image_cache.rs b/crates/re_viewer/src/misc/caches/tensor_image_cache.rs index b96ba4ecd149..d8455f2bcaa9 100644 --- a/crates/re_viewer/src/misc/caches/tensor_image_cache.rs +++ b/crates/re_viewer/src/misc/caches/tensor_image_cache.rs @@ -7,10 +7,6 @@ use re_log_types::{ component_types::{self, ClassId, Tensor, TensorData, TensorDataMeaning}, RowId, }; -use re_renderer::{ - resource_managers::{GpuTexture2DHandle, Texture2DCreationDesc}, - RenderContext, -}; use crate::{ misc::caches::TensorStats, @@ -27,9 +23,6 @@ use crate::{ /// In the case of images that leverage a `ColorMapping` this includes conversion from /// the native Tensor type A -> Color32. pub struct ColoredTensorView<'store, 'cache> { - /// Key used to retrieve this cached view - key: ImageCacheKey, - /// Borrowed tensor from the data store pub tensor: &'store Tensor, @@ -45,30 +38,6 @@ pub struct ColoredTensorView<'store, 'cache> { } impl<'store, 'cache> ColoredTensorView<'store, 'cache> { - /// Try to get a [`GpuTexture2DHandle`] for the cached [`Tensor`]. - /// - /// Will return None if a valid [`ColorImage`] could not be derived from the [`Tensor`]. - pub fn texture_handle(&self, render_ctx: &mut RenderContext) -> Option { - crate::profile_function!(); - self.colored_image.map(|i| { - let texture_key = self.key.hash64(); - - let debug_name = format!("tensor {:?}", self.tensor.shape()); - // TODO(andreas): The renderer should ingest images with less conversion (e.g. keep luma as 8bit texture, don't flip bits on bgra etc.) - render_ctx.texture_manager_2d.get_or_create( - texture_key, - &mut render_ctx.gpu_resources.textures, - &Texture2DCreationDesc { - label: debug_name.into(), - data: bytemuck::cast_slice(&i.pixels), - format: wgpu::TextureFormat::Rgba8UnormSrgb, - width: i.width() as u32, - height: i.height() as u32, - }, - ) - }) - } - /// Try to get a [`DynamicImage`] for the the cached [`Tensor`]. /// /// Note: this is a `DynamicImage` created from the cached [`ColorImage`], not from the @@ -138,7 +107,6 @@ impl ImageCache { ci.last_use_generation = self.generation; ColoredTensorView::<'store, '_> { - key, tensor, annotations, colored_image: ci.colored_image.as_ref(), @@ -205,7 +173,7 @@ impl CachedImage { fn from_tensor(debug_name: &str, tensor: &Tensor, annotations: &Arc) -> Self { crate::profile_function!(); - match apply_color_map(tensor, annotations) { + match apply_colormap(tensor, annotations) { Ok(colored_image) => { let memory_used = colored_image.pixels.len() * std::mem::size_of::(); @@ -241,7 +209,7 @@ impl CachedImage { } } -fn apply_color_map(tensor: &Tensor, annotations: &Arc) -> anyhow::Result { +fn apply_colormap(tensor: &Tensor, annotations: &Arc) -> anyhow::Result { match tensor.meaning { TensorDataMeaning::Unknown => color_tensor_as_color_image(tensor), TensorDataMeaning::ClassId => class_id_tensor_as_color_image(tensor, annotations), diff --git a/crates/re_viewer/src/misc/mod.rs b/crates/re_viewer/src/misc/mod.rs index 7ba4a8e82f05..023aafc3d6e9 100644 --- a/crates/re_viewer/src/misc/mod.rs +++ b/crates/re_viewer/src/misc/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod mesh_loader; pub mod queries; mod selection_state; pub(crate) mod space_info; +pub mod tensor_to_gpu; pub(crate) mod time_control; pub(crate) mod time_control_ui; mod transform_cache; diff --git a/crates/re_viewer/src/misc/tensor_to_gpu.rs b/crates/re_viewer/src/misc/tensor_to_gpu.rs new file mode 100644 index 000000000000..5417a8799f71 --- /dev/null +++ b/crates/re_viewer/src/misc/tensor_to_gpu.rs @@ -0,0 +1,402 @@ +use std::borrow::Cow; + +use bytemuck::{allocation::pod_collect_to_vec, cast_slice, Pod}; +use egui::util::hash; +use wgpu::TextureFormat; + +use re_log_types::component_types::{Tensor, TensorData}; +use re_renderer::{ + renderer::{ColorMapper, ColormappedTexture}, + resource_managers::{GpuTexture2DHandle, Texture2DCreationDesc}, + RenderContext, +}; + +use super::caches::TensorStats; + +// ---------------------------------------------------------------------------- + +/// Set up tensor for rendering on the GPU. +/// +/// This will only upload the tensor if it isn't on the GPU already. +/// +/// `tensor_stats` is used for determining the range of the texture. +// TODO(emilk): allow user to specify the range in ui. +pub fn tensor_to_gpu( + render_ctx: &mut RenderContext, + debug_name: &str, + tensor: &Tensor, + tensor_stats: &TensorStats, + annotations: &crate::ui::Annotations, +) -> anyhow::Result { + crate::profile_function!(format!( + "meaning: {:?}, dtype: {}, shape: {:?}", + tensor.meaning, + tensor.dtype(), + tensor.shape() + )); + + use re_log_types::component_types::TensorDataMeaning; + + match tensor.meaning { + TensorDataMeaning::Unknown => { + color_tensor_to_gpu(render_ctx, debug_name, tensor, tensor_stats) + } + TensorDataMeaning::ClassId => { + class_id_tensor_to_gpu(render_ctx, debug_name, tensor, tensor_stats, annotations) + } + TensorDataMeaning::Depth => { + depth_tensor_to_gpu(render_ctx, debug_name, tensor, tensor_stats) + } + } +} + +// ---------------------------------------------------------------------------- +// Color textures: + +fn color_tensor_to_gpu( + render_ctx: &mut RenderContext, + debug_name: &str, + tensor: &Tensor, + tensor_stats: &TensorStats, +) -> anyhow::Result { + let texture_handle = get_or_create_texture(render_ctx, hash(tensor.id()), || { + let [height, width, depth] = height_width_depth(tensor)?; + let (data, format) = match (depth, &tensor.data) { + // Use R8Unorm and R8Snorm to get filtering on the GPU: + (1, TensorData::U8(buf)) => (cast_slice_to_cow(buf.as_slice()), TextureFormat::R8Unorm), + (1, TensorData::I8(buf)) => (cast_slice_to_cow(buf), TextureFormat::R8Snorm), + + // Special handling for sRGB(A) textures: + (3, TensorData::U8(buf)) => ( + pad_and_cast(buf.as_slice(), 255), + TextureFormat::Rgba8UnormSrgb, + ), + (4, TensorData::U8(buf)) => ( + // TODO(emilk): premultiply alpha + cast_slice_to_cow(buf.as_slice()), + TextureFormat::Rgba8UnormSrgb, + ), + + _ => { + // Fallback to general case: + return general_texture_creation_desc_from_tensor(debug_name, tensor); + } + }; + + Ok(Texture2DCreationDesc { + label: debug_name.into(), + data, + format, + width, + height, + }) + })?; + + let gpu_texture = render_ctx.texture_manager_2d.get(&texture_handle)?; + let texture_format = gpu_texture.creation_desc.format; + + // Special casing for normalized textures used above: + let range = if matches!( + texture_format, + TextureFormat::R8Unorm | TextureFormat::Rgba8UnormSrgb + ) { + [0.0, 1.0] + } else if texture_format == TextureFormat::R8Snorm { + [-1.0, 1.0] + } else { + // For instance: 16-bit images. + // TODO(emilk): consider assuming [0-1] range for all float tensors. + let (min, max) = tensor_stats + .range + .ok_or_else(|| anyhow::anyhow!("missing tensor range. compressed?"))?; + [min as f32, max as f32] + }; + + let color_mapper = if texture_format.describe().components == 1 { + // Single-channel images = luminance = grayscale + Some(ColorMapper::Function(re_renderer::Colormap::Grayscale)) + } else { + None + }; + + Ok(ColormappedTexture { + texture: texture_handle, + range, + gamma: 1.0, + color_mapper, + }) +} + +// ---------------------------------------------------------------------------- +// Textures with class_id annotations: + +fn class_id_tensor_to_gpu( + render_ctx: &mut RenderContext, + debug_name: &str, + tensor: &Tensor, + tensor_stats: &TensorStats, + annotations: &crate::ui::Annotations, +) -> anyhow::Result { + let [_height, _width, depth] = height_width_depth(tensor)?; + anyhow::ensure!( + depth == 1, + "Cannot apply annotations to tensor of shape {:?}", + tensor.shape + ); + anyhow::ensure!( + tensor.dtype().is_integer(), + "Only integer tensors can be annotated" + ); + + let (min, max) = tensor_stats + .range + .ok_or_else(|| anyhow::anyhow!("compressed_tensor!?"))?; + anyhow::ensure!(0.0 <= min, "Negative class id"); + + // create a lookup texture for the colors that's 256 wide, + // and as many rows as needed to fit all the classes. + anyhow::ensure!(max <= 65535.0, "Too many class ids"); + + // We pack the colormap into a 2D texture so we don't go over the max texture size. + // We only support u8 and u16 class ids, so 256^2 is the biggest texture we need. + let colormap_width = 256; + let colormap_height = (max as usize + colormap_width - 1) / colormap_width; + + let colormap_texture_handle = get_or_create_texture( + render_ctx, + hash(annotations.row_id), + || -> anyhow::Result<_> { + let data: Vec = (0..(colormap_width * colormap_height)) + .flat_map(|id| { + let color = annotations + .class_description(Some(re_log_types::component_types::ClassId(id as u16))) + .annotation_info() + .color(None, crate::ui::DefaultColor::TransparentBlack); + color.to_array() // premultiplied! + }) + .collect(); + + Ok(Texture2DCreationDesc { + label: "class_id_colormap".into(), + data: data.into(), + format: TextureFormat::Rgba8UnormSrgb, + width: colormap_width as u32, + height: colormap_height as u32, + }) + }, + )?; + + let main_texture_handle = get_or_create_texture(render_ctx, hash(tensor.id()), || { + general_texture_creation_desc_from_tensor(debug_name, tensor) + })?; + + Ok(ColormappedTexture { + texture: main_texture_handle, + range: [0.0, (colormap_width * colormap_height) as f32], + gamma: 1.0, + color_mapper: Some(ColorMapper::Texture(colormap_texture_handle)), + }) +} + +// ---------------------------------------------------------------------------- +// Depth textures: + +fn depth_tensor_to_gpu( + render_ctx: &mut RenderContext, + debug_name: &str, + tensor: &Tensor, + tensor_stats: &TensorStats, +) -> anyhow::Result { + let [_height, _width, depth] = height_width_depth(tensor)?; + anyhow::ensure!( + depth == 1, + "Depth tensor of weird shape: {:?}", + tensor.shape + ); + let (min, max) = depth_tensor_range(tensor, tensor_stats)?; + + let texture = get_or_create_texture(render_ctx, hash(tensor.id()), || { + general_texture_creation_desc_from_tensor(debug_name, tensor) + })?; + + Ok(ColormappedTexture { + texture, + range: [min as f32, max as f32], + gamma: 1.0, + color_mapper: Some(ColorMapper::Function(re_renderer::Colormap::Turbo)), + }) +} + +fn depth_tensor_range(tensor: &Tensor, tensor_stats: &TensorStats) -> anyhow::Result<(f64, f64)> { + let range = tensor_stats.range.ok_or(anyhow::anyhow!( + "Tensor has no range!? Was this compressed?" + ))?; + let (mut min, mut max) = range; + + anyhow::ensure!( + min.is_finite() && max.is_finite(), + "Tensor has non-finite values" + ); + + min = min.min(0.0); // Depth usually start at zero. + + if min == max { + // Uniform image. We can't remap it to a 0-1 range, so do whatever: + min = 0.0; + max = if tensor.dtype().is_float() { + 1.0 + } else { + tensor.dtype().max_value() + }; + } + + Ok((min, max)) +} + +// ---------------------------------------------------------------------------- + +/// Uploads the tensor to a texture in a format that closely resembled the input. +/// Uses no `Unorm/Snorm` formats. +fn general_texture_creation_desc_from_tensor<'a>( + debug_name: &str, + tensor: &'a Tensor, +) -> anyhow::Result> { + let [height, width, depth] = height_width_depth(tensor)?; + let (data, format) = match (depth, &tensor.data) { + (1, TensorData::U8(buf)) => (cast_slice_to_cow(buf.as_slice()), TextureFormat::R8Uint), + (1, TensorData::I8(buf)) => (cast_slice_to_cow(buf), TextureFormat::R8Sint), + (1, TensorData::U16(buf)) => (cast_slice_to_cow(buf), TextureFormat::R16Uint), + (1, TensorData::I16(buf)) => (cast_slice_to_cow(buf), TextureFormat::R16Sint), + (1, TensorData::U32(buf)) => (cast_slice_to_cow(buf), TextureFormat::R32Uint), + (1, TensorData::I32(buf)) => (cast_slice_to_cow(buf), TextureFormat::R32Sint), + // (1, TensorData::F16(buf)) => (cast_slice_to_cow(buf), TextureFormat::R16Float), TODO(#854) + (1, TensorData::F32(buf)) => (cast_slice_to_cow(buf), TextureFormat::R32Float), + (1, TensorData::F64(buf)) => (narrow_f64_to_f32s(buf), TextureFormat::R32Float), + + // NOTE: 2-channel images are not supported by the shader yet, but are included here for completeness: + (2, TensorData::U8(buf)) => (cast_slice_to_cow(buf.as_slice()), TextureFormat::Rg8Uint), + (2, TensorData::I8(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rg8Sint), + (2, TensorData::U16(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rg16Uint), + (2, TensorData::I16(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rg16Sint), + (2, TensorData::U32(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rg32Uint), + (2, TensorData::I32(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rg32Sint), + // (2, TensorData::F16(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rg16Float), TODO(#854) + (2, TensorData::F32(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rg32Float), + (2, TensorData::F64(buf)) => (narrow_f64_to_f32s(buf), TextureFormat::Rg32Float), + + // There are no 3-channel textures in wgpu, so we need to pad to 4 channels: + (3, TensorData::U8(buf)) => (pad_and_cast(buf.as_slice(), 0), TextureFormat::Rgba8Uint), + (3, TensorData::I8(buf)) => (pad_and_cast(buf, 0), TextureFormat::Rgba8Sint), + (3, TensorData::U16(buf)) => (pad_and_cast(buf, 0), TextureFormat::Rgba16Uint), + (3, TensorData::I16(buf)) => (pad_and_cast(buf, 0), TextureFormat::Rgba16Sint), + (3, TensorData::U32(buf)) => (pad_and_cast(buf, 0), TextureFormat::Rgba32Uint), + (3, TensorData::I32(buf)) => (pad_and_cast(buf, 0), TextureFormat::Rgba32Sint), + // (3, TensorData::F16(buf)) => (pad_and_cast(buf, 0.0), TextureFormat::Rgba16Float), TODO(#854) + (3, TensorData::F32(buf)) => (pad_and_cast(buf, 0.0), TextureFormat::Rgba32Float), + (3, TensorData::F64(buf)) => { + let pad = 0.0; + let floats: Vec = buf + .chunks_exact(3) + .flat_map(|chunk| [chunk[0] as f32, chunk[1] as f32, chunk[2] as f32, pad]) + .collect(); + ( + pod_collect_to_vec(&floats).into(), + TextureFormat::Rgba32Float, + ) + } + + // TODO(emilk): premultiply alpha + (4, TensorData::U8(buf)) => (cast_slice_to_cow(buf.as_slice()), TextureFormat::Rgba8Uint), + (4, TensorData::I8(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rgba8Sint), + (4, TensorData::U16(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rgba16Uint), + (4, TensorData::I16(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rgba16Sint), + (4, TensorData::U32(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rgba32Uint), + (4, TensorData::I32(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rgba32Sint), + // (4, TensorData::F16(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rgba16Float), TODO(#854) + (4, TensorData::F32(buf)) => (cast_slice_to_cow(buf), TextureFormat::Rgba32Float), + (4, TensorData::F64(buf)) => (narrow_f64_to_f32s(buf), TextureFormat::Rgba32Float), + + // TODO(emilk): U64/I64 + (_depth, dtype) => { + anyhow::bail!("Don't know how to turn a tensor of shape={:?} and dtype={dtype:?} into a color image", tensor.shape) + } + }; + + Ok(Texture2DCreationDesc { + label: debug_name.into(), + data, + format, + width, + height, + }) +} + +fn get_or_create_texture<'a, Err>( + render_ctx: &mut RenderContext, + texture_key: u64, + try_create_texture_desc: impl FnOnce() -> Result, Err>, +) -> Result { + render_ctx.texture_manager_2d.get_or_create_with( + texture_key, + &mut render_ctx.gpu_resources.textures, + try_create_texture_desc, + ) +} + +fn cast_slice_to_cow(slice: &[From]) -> Cow<'_, [u8]> { + cast_slice(slice).into() +} + +// wgpu doesn't support f64 textures, so we need to narrow to f32: +fn narrow_f64_to_f32s(slice: &[f64]) -> Cow<'static, [u8]> { + crate::profile_function!(); + let bytes: Vec = slice + .iter() + .flat_map(|&f| (f as f32).to_le_bytes()) + .collect(); + bytes.into() +} + +fn pad_to_four_elements(data: &[T], pad: T) -> Vec { + crate::profile_function!(); + data.chunks_exact(3) + .flat_map(|chunk| [chunk[0], chunk[1], chunk[2], pad]) + .collect::>() +} + +fn pad_and_cast(data: &[T], pad: T) -> Cow<'static, [u8]> { + crate::profile_function!(); + let padded: Vec = pad_to_four_elements(data, pad); + let bytes: Vec = pod_collect_to_vec(&padded); + bytes.into() +} + +// ----------------------------------------------------------------------------; + +fn height_width_depth(tensor: &Tensor) -> anyhow::Result<[u32; 3]> { + use anyhow::Context as _; + + let shape = &tensor.shape(); + + anyhow::ensure!( + shape.len() == 2 || shape.len() == 3, + "Expected a 2D or 3D tensor, got {shape:?}", + ); + + let [height, width] = [ + u32::try_from(shape[0].size).context("tensor too large")?, + u32::try_from(shape[1].size).context("tensor too large")?, + ]; + let depth = if shape.len() == 2 { 1 } else { shape[2].size }; + + anyhow::ensure!( + depth == 1 || depth == 3 || depth == 4, + "Expected depth of 1,3,4 (gray, RGB, RGBA), found {depth:?}. Tensor shape: {shape:?}" + ); + debug_assert!( + tensor.is_shaped_like_an_image(), + "We should make the same checks above, but with actual error messages" + ); + + Ok([height, width, depth as u32]) +} diff --git a/crates/re_viewer/src/ui/annotations.rs b/crates/re_viewer/src/ui/annotations.rs index 69e97cb25149..4f83873b7df2 100644 --- a/crates/re_viewer/src/ui/annotations.rs +++ b/crates/re_viewer/src/ui/annotations.rs @@ -21,6 +21,7 @@ pub struct Annotations { } impl Annotations { + #[inline] pub fn class_description(&self, class_id: Option) -> ResolvedClassDescription<'_> { ResolvedClassDescription { class_id, @@ -35,6 +36,7 @@ pub struct ResolvedClassDescription<'a> { } impl<'a> ResolvedClassDescription<'a> { + #[inline] pub fn annotation_info(&self) -> ResolvedAnnotationInfo { ResolvedAnnotationInfo { class_id: self.class_id, diff --git a/crates/re_viewer/src/ui/data_ui/image.rs b/crates/re_viewer/src/ui/data_ui/image.rs index dd55348e4c44..1e66aeb27fe5 100644 --- a/crates/re_viewer/src/ui/data_ui/image.rs +++ b/crates/re_viewer/src/ui/data_ui/image.rs @@ -231,17 +231,12 @@ fn show_zoomed_image_region_tooltip( .on_hover_ui_at_pointer(|ui| { ui.set_max_width(320.0); ui.horizontal(|ui| { - if tensor_view.tensor.is_shaped_like_an_image() { - let h = tensor_view.tensor.shape()[0].size as _; - let w = tensor_view.tensor.shape()[1].size as _; - - use egui::NumExt; + if let Some([h, w, _]) = tensor_view.tensor.image_height_width_channels() { + use egui::remap_clamp; let center = [ - (egui::remap(pointer_pos.x, image_rect.x_range(), 0.0..=w as f32) as isize) - .at_most(w), - (egui::remap(pointer_pos.y, image_rect.y_range(), 0.0..=h as f32) as isize) - .at_most(h), + (remap_clamp(pointer_pos.x, image_rect.x_range(), 0.0..=w as f32) as isize), + (remap_clamp(pointer_pos.y, image_rect.y_range(), 0.0..=h as f32) as isize), ]; show_zoomed_image_region_area_outline( parent_ui, @@ -264,11 +259,11 @@ pub fn show_zoomed_image_region_area_outline( [center_x, center_y]: [isize; 2], image_rect: egui::Rect, ) { - if tensor_view.tensor.is_shaped_like_an_image() { + if let Some([height, width, _]) = tensor_view.tensor.image_height_width_channels() { use egui::{pos2, remap, Color32, Rect}; - let h = tensor_view.tensor.shape()[0].size as _; - let w = tensor_view.tensor.shape()[1].size as _; + let width = width as f32; + let height = height as f32; // Show where on the original image the zoomed-in region is at: let left = (center_x - ZOOMED_IMAGE_TEXEL_RADIUS) as f32; @@ -276,10 +271,10 @@ pub fn show_zoomed_image_region_area_outline( let top = (center_y - ZOOMED_IMAGE_TEXEL_RADIUS) as f32; let bottom = (center_y + ZOOMED_IMAGE_TEXEL_RADIUS) as f32; - let left = remap(left, 0.0..=w, image_rect.x_range()); - let right = remap(right, 0.0..=w, image_rect.x_range()); - let top = remap(top, 0.0..=h, image_rect.y_range()); - let bottom = remap(bottom, 0.0..=h, image_rect.y_range()); + let left = remap(left, 0.0..=width, image_rect.x_range()); + let right = remap(right, 0.0..=width, image_rect.x_range()); + let top = remap(top, 0.0..=height, image_rect.y_range()); + let bottom = remap(bottom, 0.0..=height, image_rect.y_range()); let rect = Rect::from_min_max(pos2(left, top), pos2(right, bottom)); // TODO(emilk): use `parent_ui.painter()` and put it in a high Z layer, when https://github.com/emilk/egui/issues/1516 is done diff --git a/crates/re_viewer/src/ui/selection_panel.rs b/crates/re_viewer/src/ui/selection_panel.rs index c5fe9f25f450..0b7c4f703aee 100644 --- a/crates/re_viewer/src/ui/selection_panel.rs +++ b/crates/re_viewer/src/ui/selection_panel.rs @@ -1,6 +1,6 @@ use egui::NumExt as _; use re_data_store::{ - query_latest_single, ColorMap, ColorMapper, EditableAutoValue, EntityPath, EntityProperties, + query_latest_single, ColorMapper, Colormap, EditableAutoValue, EntityPath, EntityProperties, }; use re_log_types::{ component_types::{Tensor, TensorDataMeaning}, @@ -429,12 +429,12 @@ fn colormap_props_ui(ui: &mut egui::Ui, entity_props: &mut EntityProperties) { } }; - add_label(ColorMapper::ColorMap(ColorMap::Grayscale)); - add_label(ColorMapper::ColorMap(ColorMap::Turbo)); - add_label(ColorMapper::ColorMap(ColorMap::Viridis)); - add_label(ColorMapper::ColorMap(ColorMap::Plasma)); - add_label(ColorMapper::ColorMap(ColorMap::Magma)); - add_label(ColorMapper::ColorMap(ColorMap::Inferno)); + add_label(ColorMapper::Colormap(Colormap::Grayscale)); + add_label(ColorMapper::Colormap(Colormap::Turbo)); + add_label(ColorMapper::Colormap(Colormap::Viridis)); + add_label(ColorMapper::Colormap(Colormap::Plasma)); + add_label(ColorMapper::Colormap(Colormap::Magma)); + add_label(ColorMapper::Colormap(Colormap::Inferno)); }); ui.end_row(); diff --git a/crates/re_viewer/src/ui/view_spatial/scene/scene_part/images.rs b/crates/re_viewer/src/ui/view_spatial/scene/scene_part/images.rs index 62d7cde09458..e653cb28a2cf 100644 --- a/crates/re_viewer/src/ui/view_spatial/scene/scene_part/images.rs +++ b/crates/re_viewer/src/ui/view_spatial/scene/scene_part/images.rs @@ -12,7 +12,7 @@ use re_log_types::{ use re_query::{query_primary_with_history, EntityView, QueryError}; use re_renderer::{ renderer::{DepthCloud, DepthCloudDepthData}, - ColorMap, OutlineMaskPreference, + Colormap, OutlineMaskPreference, }; use crate::{ @@ -32,36 +32,47 @@ fn push_tensor_texture( ctx: &mut ViewerContext<'_>, annotations: &Arc, world_from_obj: glam::Mat4, + entity_path: &EntityPath, instance_path_hash: InstancePathHash, tensor: &Tensor, - tint: egui::Rgba, + multiplicative_tint: egui::Rgba, outline_mask: OutlineMaskPreference, ) { crate::profile_function!(); - let tensor_view = ctx.cache.image.get_colormapped_view(tensor, annotations); + let Some([height, width, _]) = tensor.image_height_width_channels() else { return; }; - if let Some(texture_handle) = tensor_view.texture_handle(ctx.render_ctx) { - let (h, w) = (tensor.shape()[0].size as f32, tensor.shape()[1].size as f32); - scene - .primitives - .textured_rectangles - .push(re_renderer::renderer::TexturedRect { + let debug_name = entity_path.to_string(); + let tensor_stats = ctx.cache.tensor_stats(tensor); + + match crate::misc::tensor_to_gpu::tensor_to_gpu( + ctx.render_ctx, + &debug_name, + tensor, + tensor_stats, + annotations, + ) { + Ok(colormapped_texture) => { + let textured_rect = re_renderer::renderer::TexturedRect { top_left_corner_position: world_from_obj.transform_point3(glam::Vec3::ZERO), - extent_u: world_from_obj.transform_vector3(glam::Vec3::X * w), - extent_v: world_from_obj.transform_vector3(glam::Vec3::Y * h), - texture: texture_handle, + extent_u: world_from_obj.transform_vector3(glam::Vec3::X * width as f32), + extent_v: world_from_obj.transform_vector3(glam::Vec3::Y * height as f32), + colormapped_texture, texture_filter_magnification: re_renderer::renderer::TextureFilterMag::Nearest, texture_filter_minification: re_renderer::renderer::TextureFilterMin::Linear, - multiplicative_tint: tint, - // Push to background. Mostly important for mouse picking order! - depth_offset: -1, + multiplicative_tint, + depth_offset: -1, // Push to background. Mostly important for mouse picking order! outline_mask, - }); - scene - .primitives - .textured_rectangles_ids - .push(instance_path_hash); + }; + scene.primitives.textured_rectangles.push(textured_rect); + scene + .primitives + .textured_rectangles_ids + .push(instance_path_hash); + } + Err(err) => { + re_log::error_once!("Failed to create texture from tensor for {debug_name:?}: {err}"); + } } } @@ -236,6 +247,7 @@ impl ImagesPart { ctx, &annotations, world_from_obj, + ent_path, instance_path_hash, &tensor, color.into(), @@ -311,13 +323,13 @@ impl ImagesPart { let dimensions = glam::UVec2::new(w as _, h as _); let colormap = match *properties.color_mapper.get() { - re_data_store::ColorMapper::ColorMap(colormap) => match colormap { - re_data_store::ColorMap::Grayscale => ColorMap::Grayscale, - re_data_store::ColorMap::Turbo => ColorMap::ColorMapTurbo, - re_data_store::ColorMap::Viridis => ColorMap::ColorMapViridis, - re_data_store::ColorMap::Plasma => ColorMap::ColorMapPlasma, - re_data_store::ColorMap::Magma => ColorMap::ColorMapMagma, - re_data_store::ColorMap::Inferno => ColorMap::ColorMapInferno, + re_data_store::ColorMapper::Colormap(colormap) => match colormap { + re_data_store::Colormap::Grayscale => Colormap::Grayscale, + re_data_store::Colormap::Turbo => Colormap::Turbo, + re_data_store::Colormap::Viridis => Colormap::Viridis, + re_data_store::Colormap::Plasma => Colormap::Plasma, + re_data_store::Colormap::Magma => Colormap::Magma, + re_data_store::Colormap::Inferno => Colormap::Inferno, }, }; diff --git a/crates/re_viewer/src/ui/view_tensor/ui.rs b/crates/re_viewer/src/ui/view_tensor/ui.rs index 57cfbcb413e8..12d57d6b4374 100644 --- a/crates/re_viewer/src/ui/view_tensor/ui.rs +++ b/crates/re_viewer/src/ui/view_tensor/ui.rs @@ -348,18 +348,18 @@ fn tensor_ui( // ---------------------------------------------------------------------------- #[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -enum ColorMap { +enum Colormap { Greyscale, Turbo, Virdis, } -impl std::fmt::Display for ColorMap { +impl std::fmt::Display for Colormap { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { - ColorMap::Greyscale => "Greyscale", - ColorMap::Turbo => "Turbo", - ColorMap::Virdis => "Viridis", + Colormap::Greyscale => "Greyscale", + Colormap::Turbo => "Turbo", + Colormap::Virdis => "Viridis", }) } } @@ -367,14 +367,14 @@ impl std::fmt::Display for ColorMap { /// How we map values to colors. #[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)] struct ColorMapping { - map: ColorMap, + map: Colormap, gamma: f32, } impl Default for ColorMapping { fn default() -> Self { Self { - map: ColorMap::Virdis, + map: Colormap::Virdis, gamma: 1.0, } } @@ -385,15 +385,15 @@ impl ColorMapping { let f = f.powf(self.gamma); match self.map { - ColorMap::Greyscale => { + Colormap::Greyscale => { let lum = (f * 255.0 + 0.5) as u8; Color32::from_gray(lum) } - ColorMap::Turbo => { + Colormap::Turbo => { let [r, g, b, _] = re_renderer::colormap_turbo_srgb(f); Color32::from_rgb(r, g, b) } - ColorMap::Virdis => { + Colormap::Virdis => { let [r, g, b, _] = re_renderer::colormap_viridis_srgb(f); Color32::from_rgb(r, g, b) } @@ -408,9 +408,9 @@ impl ColorMapping { .selected_text(map.to_string()) .show_ui(ui, |ui| { ui.style_mut().wrap = Some(false); - ui.selectable_value(map, ColorMap::Greyscale, ColorMap::Greyscale.to_string()); - ui.selectable_value(map, ColorMap::Virdis, ColorMap::Virdis.to_string()); - ui.selectable_value(map, ColorMap::Turbo, ColorMap::Turbo.to_string()); + ui.selectable_value(map, Colormap::Greyscale, Colormap::Greyscale.to_string()); + ui.selectable_value(map, Colormap::Virdis, Colormap::Virdis.to_string()); + ui.selectable_value(map, Colormap::Turbo, Colormap::Turbo.to_string()); }); ui.end_row();