using System.Numerics; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Avalonia; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Controls; using Avalonia.OpenGL; using Avalonia.Threading; using Avalonia.Input; using GSS2.UI.Core.ViewModels; using Avalonia.Markup.Xaml; using GSS2.Core.UI.Extensions; namespace GSS2.UI.Core.Views; public partial class CameraView : OpenGlControlBase { private class OpenGLContext { [DllImport("libEGL.so.1")] public static extern IntPtr eglQueryString(IntPtr dpy, int name); [DllImport("libEGL.so.1")] public static extern IntPtr eglGetCurrentDisplay(); [DllImport("libEGL.so.1")] public static extern IntPtr eglGetProcAddress(string procname); [DllImport("libGL.so.1")] public static extern void glUniform1i(int location, int falue); [DllImport("libGL.so.1")] public static extern void glUniformMatrix3x2fv(int location, int count, bool transpose, IntPtr value); [DllImport("libGL.so.1")] public static extern void glUniformMatrix3fv(int location, int count, bool transpose, IntPtr value); public const uint DRM_FORMAT_BGRX8888 = 0x34325258; // XR24 XRGB8888 public const int GL_TRIANGLE_STRIP = 0x0005; public const int GL_TEXTURE_WRAP_S = 0x2802; public const int GL_TEXTURE_WRAP_T = 0x2803; public const int GL_CLAMP_TO_EDGE = 0x812F; public const int EGL_LINUX_DMA_BUF_EXT = 0x3270; public const int EGL_LINUX_DRM_FOURCC_EXT = 0x3271; public const int EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272; public const int EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273; public const int EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274; public delegate IntPtr GlEGLImageTargetTexture2DOESDelegate(int target, IntPtr image); public delegate IntPtr EglCreateImageKHRDelegate(IntPtr dpy, IntPtr ctx, int target, IntPtr buffer, int[] attribs); public delegate bool EglDestroyImageKHRDelegate(IntPtr dpy, IntPtr image); private const string VERTEX_SHADER = @" #version 300 es layout (location = 0) in vec2 aPos; layout (location = 1) in vec2 aUv; uniform mat4 uMvp; uniform mat3 uUv; out vec2 vUv; void main() { vec3 uv = uUv * vec3(aUv, 1.0); vUv = uv.xy; gl_Position = uMvp * vec4(aPos, 0.0, 1.0); } "; private const string FRAGMENT_SHADER = @" #version 300 es precision mediump float; in vec2 vUv; uniform sampler2D uTex; out vec4 fragColor; void main() { fragColor = texture(uTex, vUv); } "; private readonly float[] Vertices = [ -1f, -1f, 0f, 1f, 1f, -1f, 1f, 1f, -1f, 1f, 0f, 0f, 1f, 1f, 1f, 0f, ]; public IntPtr EglDisplay { get; } public int Texture { get; } public int Program { get; } public int UTexLocation { get; } public int UMvpLocation { get; } public int UUvLocation { get; } public int VAO { get; } public int VBO { get; } public EglCreateImageKHRDelegate EglCreateImageKHR { get; } public EglDestroyImageKHRDelegate EglDestroyImageKHR { get; } public GlEGLImageTargetTexture2DOESDelegate GlEGLImageTargetTexture2DOES { get; } public OpenGLContext(GlInterface gl) { var glExtensions = gl.GetString(GlConsts.GL_EXTENSIONS); if (glExtensions is null) throw new Exception("GL extensions query returned null string"); if (!glExtensions.Contains("GL_OES_EGL_image") && !glExtensions.Contains("GL_OES_EGL_image_external")) throw new Exception("Required GL extensions \"GL_OES_EGL_image_external\" or \"GL_OES_EGL_image\" not found"); EglDisplay = eglGetCurrentDisplay(); if (EglDisplay == IntPtr.Zero) throw new Exception("EGL display is nullptr"); var eglExtensions = Marshal.PtrToStringAnsi(eglQueryString(EglDisplay, EglConsts.EGL_EXTENSIONS)); if (eglExtensions is null) throw new Exception("EGL extensions query returned null"); if (!eglExtensions.Contains("EGL_EXT_image_dma_buf_import")) throw new Exception("Required EGL extension \"EGL_EXT_image_dma_buf_import\" not found"); var pEglCreateImageKHR = eglGetProcAddress("eglCreateImageKHR"); if (pEglCreateImageKHR == IntPtr.Zero) throw new Exception("Required EGL method \"eglCreateImageKHR\" not found"); EglCreateImageKHR = Marshal.GetDelegateForFunctionPointer(pEglCreateImageKHR); var pEglDestroyImageKHR = eglGetProcAddress("eglDestroyImageKHR"); if (pEglDestroyImageKHR == IntPtr.Zero) throw new Exception("Required EGL method \"eglDestroyImageKHR\" not found"); EglDestroyImageKHR = Marshal.GetDelegateForFunctionPointer(pEglDestroyImageKHR); var pGlEGLImageTargetTexture2DOES = eglGetProcAddress("glEGLImageTargetTexture2DOES"); if (pGlEGLImageTargetTexture2DOES == IntPtr.Zero) throw new Exception("Required EGL method \"glEGLImageTargetTexture2DOES\" not found"); GlEGLImageTargetTexture2DOES = Marshal.GetDelegateForFunctionPointer(pGlEGLImageTargetTexture2DOES); Texture = gl.GenTexture(); ThrowIfOpenGLError(gl); gl.BindTexture(GlConsts.GL_TEXTURE_2D, Texture); ThrowIfOpenGLError(gl); gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ThrowIfOpenGLError(gl); gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ThrowIfOpenGLError(gl); gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MIN_FILTER, GlConsts.GL_LINEAR); ThrowIfOpenGLError(gl); gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MAG_FILTER, GlConsts.GL_LINEAR); ThrowIfOpenGLError(gl); int vertexShader = gl.CreateShader(GlConsts.GL_VERTEX_SHADER); ThrowIfOpenGLError(gl); gl.ShaderSourceString(vertexShader, VERTEX_SHADER); ThrowIfOpenGLError(gl); gl.CompileShader(vertexShader); int fragmentShader = gl.CreateShader(GlConsts.GL_FRAGMENT_SHADER); ThrowIfOpenGLError(gl); gl.ShaderSourceString(fragmentShader, FRAGMENT_SHADER); ThrowIfOpenGLError(gl); gl.CompileShader(fragmentShader); ThrowIfOpenGLError(gl); Program = gl.CreateProgram(); ThrowIfOpenGLError(gl); gl.AttachShader(Program, vertexShader); ThrowIfOpenGLError(gl); gl.AttachShader(Program, fragmentShader); ThrowIfOpenGLError(gl); gl.LinkProgram(Program); ThrowIfOpenGLError(gl); gl.DeleteShader(vertexShader); ThrowIfOpenGLError(gl); gl.DeleteShader(fragmentShader); ThrowIfOpenGLError(gl); UTexLocation = gl.GetUniformLocationString(Program, "uTex"); ThrowIfOpenGLError(gl); UMvpLocation = gl.GetUniformLocationString(Program, "uMvp"); ThrowIfOpenGLError(gl); UUvLocation = gl.GetUniformLocationString(Program, "uUv"); ThrowIfOpenGLError(gl); VAO = gl.GenVertexArray(); ThrowIfOpenGLError(gl); VBO = gl.GenBuffer(); ThrowIfOpenGLError(gl); gl.BindVertexArray(VAO); ThrowIfOpenGLError(gl); gl.BindBuffer(GlConsts.GL_ARRAY_BUFFER, VBO); ThrowIfOpenGLError(gl); unsafe { fixed (float* pVertices = Vertices) gl.BufferData(GlConsts.GL_ARRAY_BUFFER, Vertices.Length * sizeof(float), new IntPtr(pVertices), GlConsts.GL_STATIC_DRAW); ThrowIfOpenGLError(gl); } gl.EnableVertexAttribArray(0); ThrowIfOpenGLError(gl); gl.VertexAttribPointer(0, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), IntPtr.Zero); ThrowIfOpenGLError(gl); gl.EnableVertexAttribArray(1); ThrowIfOpenGLError(gl); gl.VertexAttribPointer(1, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), 2 * sizeof(float)); ThrowIfOpenGLError(gl); gl.BindVertexArray(0); ThrowIfOpenGLError(gl); } private void ThrowIfOpenGLError(GlInterface gl) { var error = gl.GetError(); if (error != 0) throw new Exception($"OpenGL context initialization error: {error}"); } } private readonly ILogger _logger; private readonly Dictionary _importedDmaBuffers = new Dictionary(); private bool _isViewFinderStarted = false; private OpenGLContext? _openGLContext = null; private Point? _lastMousePosition = null; private Dictionary? _lastTouchPositions = null; private float _zoom = 1; private float _panX = 0; private float _panY = 0; public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), defaultValue: Colors.Transparent); public Color Background { get => GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); } public static readonly StyledProperty StretchModeProperty = AvaloniaProperty.Register(nameof(StretchMode), defaultValue: Stretch.Uniform); public Stretch StretchMode { get => GetValue(StretchModeProperty); set => SetValue(StretchModeProperty, value); } public static readonly StyledProperty TileModeProperty = AvaloniaProperty.Register(nameof(TileMode), defaultValue: TileMode.None); public TileMode TileMode { get => GetValue(TileModeProperty); set => SetValue(TileModeProperty, value); } public CameraView(ILogger logger) { _logger = logger; AvaloniaXamlLoader.Load(this); _logger.LogInformation("{} initialized", nameof(CameraView)); DoubleTapped += (_, _) => { _zoom = 1f; _panX = 0f; _panY = 0f; }; } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs ea) { if (DataContext is CameraViewModel viewModel) { try { viewModel.StartViewFinder(); viewModel.RenderRequested += RenderRequested; _isViewFinderStarted = true; } catch (Exception ex) { _logger.LogCritical(ex, "Exception while view finder start"); } } else _logger.LogCritical("Cannot start view finder, data context is null or not camera view model"); base.OnAttachedToVisualTree(ea); } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs ea) { if (_isViewFinderStarted && DataContext is CameraViewModel viewModel) { try { viewModel.StopViewFinder(); foreach (var (fd, image) in _importedDmaBuffers) _openGLContext?.EglDestroyImageKHR!(_openGLContext.EglDisplay, image); _isViewFinderStarted = false; } catch (Exception ex) { _logger.LogCritical(ex, "Exception while view finder stop"); } viewModel.RenderRequested -= RenderRequested; } else if (_isViewFinderStarted) _logger.LogCritical("Cannot stop view finder, data context is null or not camera view model"); _isViewFinderStarted = false; base.OnDetachedFromLogicalTree(ea); } protected override void OnPointerWheelChanged(PointerWheelEventArgs ea) { if (ea.Pointer.Type == PointerType.Mouse) { var position = ea.GetPosition(this); // Рассчитываем позицию в координатах изображения (до масштабирования) var imageX = (position.X - Bounds.Width / 2 - _panX) / _zoom; var imageY = (-position.Y + Bounds.Height / 2 - _panY) / _zoom; // Ось Y инвертирована // Применяем масштаб float zoomFactor = ea.Delta.Y > 0 ? 1.1f : 1 / 1.1f; _zoom *= zoomFactor; _zoom = Math.Clamp(_zoom, 1f, 10f); _zoom = (float)Math.Round(_zoom, 2); if (_zoom != 1f) { // Пересчитываем смещение так, чтобы указатель остался на месте _panX = (float)(position.X - Bounds.Width / 2 - imageX * _zoom); _panY = (float)(-position.Y + Bounds.Height / 2 - imageY * _zoom); // Ось Y инвертирована } else { // Если зум == 1, то возвращаем изображение к исходному смещению _panX = 0f; _panY = 0f; } } base.OnPointerWheelChanged(ea); } protected override void OnPointerPressed(PointerPressedEventArgs ea) { if (ea.Pointer.Type == PointerType.Mouse) { if (ea.Properties.IsLeftButtonPressed) { if (ea.ClickCount == 2) // По двойному клику возвращаем изображение к смещение и зум к стандартным значениям { _zoom = 1f; _panX = 0f; _panY = 0f; } else _lastMousePosition = ea.GetPosition(this); } } else if (ea.Pointer.Type == PointerType.Touch) { if (_lastTouchPositions is null) _lastTouchPositions = new Dictionary(); if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2) _lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this)); else if (_lastTouchPositions.ContainsKey(ea.Pointer.Id)) _lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this); } base.OnPointerPressed(ea); } protected override void OnPointerReleased(PointerReleasedEventArgs ea) { if (ea.Pointer.Type == PointerType.Mouse) { if (!ea.Properties.IsLeftButtonPressed) _lastMousePosition = null; } else if (ea.Pointer.Type == PointerType.Touch) { if (_lastTouchPositions is not null && _lastTouchPositions.ContainsKey(ea.Pointer.Id)) _lastTouchPositions.Remove(ea.Pointer.Id); } base.OnPointerReleased(ea); } protected override void OnPointerMoved(PointerEventArgs ea) { if (ea.Pointer.Type == PointerType.Mouse) { if (_lastMousePosition is null || _zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем { base.OnPointerMoved(ea); return; } var position = ea.GetPosition(this); _panX -= (float)(_lastMousePosition.Value.X - position.X); _panY += (float)(_lastMousePosition.Value.Y - position.Y); // Ось Y инвертирована _lastMousePosition = position; } else if (ea.Pointer.Type == PointerType.Touch) { if (_lastTouchPositions is null) { base.OnPointerMoved(ea); return; } if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2) { _lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this)); base.OnPointerMoved(ea); return; } if (_lastTouchPositions.Count() == 1 && _zoom != 1) // Если есть только одно прикосновение и зум != 1, то только перемещение { var position = ea.GetPosition(this); _panX -= (float)(_lastTouchPositions[ea.Pointer.Id].X - position.X); _panY += (float)(_lastTouchPositions[ea.Pointer.Id].Y - position.Y); // Ось Y инвертирована _lastTouchPositions[ea.Pointer.Id] = position; } else if (_lastTouchPositions.Count() == 2) // Если два прикосновения, то перемещение с приближением { var lastPosition1 = _lastTouchPositions.Skip(0).First().Value; var lastPosition2 = _lastTouchPositions.Skip(1).First().Value; var lastDistance = Point.Distance(lastPosition1, lastPosition2); var lastCenter = PointHelper.Center(lastPosition1, lastPosition2); _lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this); var newPosition1 = _lastTouchPositions.Skip(0).First().Value; var newPosition2 = _lastTouchPositions.Skip(1).First().Value; var newDistance = Point.Distance(newPosition1, newPosition2); var newCenter = PointHelper.Center(newPosition1, newPosition2); var zoomFactor = newDistance / lastDistance; var newZoom = (float)(_zoom * zoomFactor); newZoom = (float)Math.Clamp(newZoom, 1f, 10f); var imageX = (newCenter.X - Bounds.Width / 2 - _panX) / _zoom; var imageY = (-newCenter.Y + Bounds.Height / 2 - _panY) / _zoom; // Ось Y инвертирована _zoom = newZoom; if (_zoom > 1f) { _panX = (float)(newCenter.X - Bounds.Width / 2 - imageX * _zoom); _panY = (float)(-newCenter.Y + Bounds.Height / 2 - imageY * _zoom); // Ось Y инвертирована } else { _zoom = 1f; _panX = 0f; _panY = 0f; } } } base.OnPointerMoved(ea); } public override void Render(DrawingContext context) { context.FillRectangle(new SolidColorBrush(Background), new Rect(0, 0, Bounds.Width, Bounds.Height)); base.Render(context); } private void RenderRequested(object? sender, EventArgs ea) => Dispatcher.UIThread.Invoke(() => RequestNextFrameRendering()); protected override void OnOpenGlInit(GlInterface gl) { _logger.LogInformation("OpenGL initialized"); _logger.LogInformation("GL version: {}", gl.GetString(GlConsts.GL_VERSION)); _logger.LogInformation("GL vendor: {}", gl.GetString(GlConsts.GL_VENDOR)); _logger.LogInformation("GL renderer: {}", gl.GetString(GlConsts.GL_RENDERER)); try { _openGLContext = new OpenGLContext(gl); _logger.LogInformation("OpenGL context initialized"); } catch (Exception ex) { _logger.LogCritical(ex, "Error while OpenGL initialization"); } base.OnOpenGlInit(gl); } protected override void OnOpenGlRender(GlInterface gl, int fb) { if (_openGLContext is null) { _logger.LogError("OpenGL render called with uninitialized EGL"); return; } try { (LibCameraSharp.FrameBuffer, LibCameraSharp.StreamConfiguration)? frame = null; while (frame is null && (DataContext as CameraViewModel)?.CanGrabViewFinderFrame() == true) frame = (DataContext as CameraViewModel)?.GrabViewFinderFrame(); if (frame is null) return; var (frameBuffer, streamConfiguration) = frame.Value; // 0. Проверяем что полученный фрейм ожидаемый правильный формат var width = streamConfiguration.Size.Width; if (width <= 0) { _logger.LogError("Cannot render frame, stream configuration validation error, expected frame width >= 0, got {}", streamConfiguration.Size.Width); (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); return; } var height = streamConfiguration.Size.Height; if (height <= 0) { _logger.LogError("Cannot render frame, stream configuration validation error, expected frame height >= 0, got {}", streamConfiguration.Size.Height); (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); return; } if (streamConfiguration.PixelFormat.Fourcc != OpenGLContext.DRM_FORMAT_BGRX8888) { _logger.LogError("Cannot render frame, stream configuration validation error, expected fourcc: 0x{:X}, got 0x{:X}", OpenGLContext.DRM_FORMAT_BGRX8888, streamConfiguration.PixelFormat.Fourcc); (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); return; } if (streamConfiguration.PixelFormat.Modifier != 0) { _logger.LogError("Cannot render frame, stream configuration validation error, expected DRM modifier: 0x{:X}, got 0x{:X}", 0, streamConfiguration.PixelFormat.Modifier); (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); return; } if (frameBuffer.Planes.Count() != 1) { _logger.LogError("Cannot render frame, planes validation error, expected planes count: 0, got {}", frameBuffer.Planes.Count()); (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); return; } var plane = frameBuffer.Planes.First(); // if (plane.Length != width * height * 4) // { // _logger.LogError("Cannot render frame, plane validation error, expected plane size 0x{:X}, got 0x{:X}", width * height * 4, plane.Length); // (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); // return; // } var fd = plane.Fd.Get(); if (fd <= 0) { _logger.LogError("Cannot render frame, FD validation error, FD >= 0, got 0x{:X}", fd); (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); return; } var stretchMode = StretchMode; var tileMode = TileMode; // 1. Очищаем экран gl.Disable(GlConsts.GL_SCISSOR_TEST); gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); gl.ClearColor(0f, 0f, 0f, 0f); gl.Clear(GlConsts.GL_COLOR_BUFFER_BIT); // 2. Создаём EGL изображение из dma-buf IntPtr image; if (_importedDmaBuffers.ContainsKey(fd)) { // 2.1. Если EGL изображение для fd уже создано, то берём его из словаря image = _importedDmaBuffers[fd]; } else { // 2.2. Если EGL изображение для fd не создано, то создаём var attribs = new[] { EglConsts.EGL_WIDTH, (int)width, EglConsts.EGL_HEIGHT, (int)height, OpenGLContext.EGL_LINUX_DRM_FOURCC_EXT, unchecked((int)OpenGLContext.DRM_FORMAT_BGRX8888), OpenGLContext.EGL_DMA_BUF_PLANE0_FD_EXT, fd, OpenGLContext.EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, OpenGLContext.EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)(plane.Length / height), EglConsts.EGL_NONE }; image = _openGLContext.EglCreateImageKHR.Invoke(_openGLContext.EglDisplay, IntPtr.Zero, OpenGLContext.EGL_LINUX_DMA_BUF_EXT, IntPtr.Zero, attribs); _importedDmaBuffers.Add(fd, image); } // 3. Биндим текстуры текстуры gl.ActiveTexture(GlConsts.GL_TEXTURE0); gl.BindTexture(GlConsts.GL_TEXTURE_2D, _openGLContext.Texture); // 4. Привязываем EGL изображение к текстуре _openGLContext.GlEGLImageTargetTexture2DOES!(GlConsts.GL_TEXTURE_2D, image); // 5. Вычисляем матрицы трансформаций var mvp = ComputeMvp(stretchMode, width, height); var zoom = Matrix4x4.CreateScale(_zoom, _zoom, 1f); var translate = Matrix4x4.CreateTranslation((float)(_panX / Bounds.Width * 2), (float)(_panY / Bounds.Height * 2), 0); mvp *= zoom; mvp *= translate; var mvpArray = new[] { mvp.M11, mvp.M12, mvp.M13, mvp.M14, mvp.M21, mvp.M22, mvp.M23, mvp.M24, mvp.M31, mvp.M32, mvp.M33, mvp.M34, mvp.M41, mvp.M42, mvp.M43, mvp.M44, }; unsafe { fixed (float* pMvp = mvpArray) gl.UniformMatrix4fv(_openGLContext.UMvpLocation, 1, false, pMvp); } var uv = ComputeUv(tileMode); var uvArray = new[] { uv.M11, uv.M12, 0f, uv.M21, uv.M22, 0f, uv.M31, uv.M32, 1f }; unsafe { fixed (float* pUv = uvArray) OpenGLContext.glUniformMatrix3fv(_openGLContext.UUvLocation, 1, false, new IntPtr(pUv)); } // 6. Запускаем шейдер gl.UseProgram(_openGLContext.Program); OpenGLContext.glUniform1i(_openGLContext.UTexLocation, 0); // 7. Рисуем вершины на экране gl.BindVertexArray(_openGLContext.VAO); gl.DrawArrays(OpenGLContext.GL_TRIANGLE_STRIP, 0, 4); // 8. Уничтожаем EGL изображение // _openGLContext.EglDestroyImageKHR!(_openGLContext.EglDisplay, image); (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); } catch (Exception ex) { _logger.LogError(ex, "Exception while OpenGL render"); } } private Matrix4x4 ComputeMvp(Stretch stretch, float imageWidth, float imageHeight) { float viewWidth = (float)Bounds.Width; float viewHeight = (float)Bounds.Height; float sx = 1f; float sy = 1f; float viewAspect = viewWidth / viewHeight; float imgAspect = imageWidth / imageHeight; switch (stretch) { case Stretch.None: sx = imageWidth / viewWidth; sy = imageHeight / viewHeight; break; case Stretch.Fill: break; case Stretch.Uniform: if (imgAspect > viewAspect) sy = viewAspect / imgAspect; else sx = imgAspect / viewAspect; break; case Stretch.UniformToFill: if (imgAspect > viewAspect) sx = imgAspect / viewAspect; else sy = viewAspect / imgAspect; break; } return Matrix4x4.CreateScale(sx, sy, 1f); } private Matrix3x2 ComputeUv(TileMode mode) { float sx = 1f; float sy = 1f; float ox = 0f; float oy = 0f; switch (mode) { case TileMode.Tile: sx = sy = 2f; break; case TileMode.FlipX: sx = -1f; ox = 1f; break; case TileMode.FlipY: sy = -1f; oy = 1f; break; case TileMode.FlipXY: sx = sy = -1f; ox = oy = 1f; break; } return new Matrix3x2( sx, 0, 0, sy, ox, oy ); } private (float sx, float sy) ComputeScale(Stretch mode, float imageWidth, float imageHeight) { float viewWidth = (float)Bounds.Width; float viewHeight = (float)Bounds.Height; float scaleX = viewWidth / imageWidth; float scaleY = viewHeight / imageHeight; switch (mode) { case Stretch.None: return (imageWidth / viewWidth, imageHeight / viewHeight); case Stretch.Fill: return (1f, 1f); case Stretch.Uniform: var sMin = MathF.Min(scaleX, scaleY); return (imageWidth * sMin / viewWidth, imageHeight * sMin / viewHeight); case Stretch.UniformToFill: var sMax = MathF.Max(scaleX, scaleY); return (imageWidth * sMax / viewWidth, imageHeight * sMax / viewHeight); default: return (1f, 1f); } } }