演示下OpenGL + Avalonia下的点击测试。之后基于Helix C++引擎,我们会出一个开源的UI,以后C++ 用户不用担心内存泄漏,也能享受C# Avalonia的所有功能,打算支持OpenGL, WebView等。
HitTestTorus.axaml代码
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="365.6" Width="429.6" x:Class="AvaloniaUI.HitTesting" xmlns:local="using:AvaloniaUI" Title="HitTesting"> <Grid> <local:HitTestTorus/> </Grid> </Window>
HitTestTorus.axaml.cs代码
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Controls;
using Avalonia.VisualTree;
using AvaloniaUI.Demos.Book._23.Tools;
using OpenTK;
using OpenTK.Graphics.OpenGL;
using System;
using System.Diagnostics;
using System.Numerics;
namespace AvaloniaUI;
public sealed class HitTestTorus : OpenGlControlBase
{
public static readonly StyledProperty<float> MajorRadiusProperty =
AvaloniaProperty.Register<HitTestTorus, float>(nameof(MajorRadius), 0.65f);
public static readonly StyledProperty<float> MinorRadiusProperty =
AvaloniaProperty.Register<HitTestTorus, float>(nameof(MinorRadius), 0.18f);
public static readonly StyledProperty<int> MajorSegmentsProperty =
AvaloniaProperty.Register<HitTestTorus, int>(nameof(MajorSegments), 64);
public static readonly StyledProperty<int> MinorSegmentsProperty =
AvaloniaProperty.Register<HitTestTorus, int>(nameof(MinorSegments), 24);
public float MajorRadius
{
get => GetValue(MajorRadiusProperty);
set => SetValue(MajorRadiusProperty, value);
}
public float MinorRadius
{
get => GetValue(MinorRadiusProperty);
set => SetValue(MinorRadiusProperty, value);
}
public int MajorSegments
{
get => GetValue(MajorSegmentsProperty);
set => SetValue(MajorSegmentsProperty, value);
}
public int MinorSegments
{
get => GetValue(MinorSegmentsProperty);
set => SetValue(MinorSegmentsProperty, value);
}
private int program;
private int vao;
private int vbo;
private int ibo;
private int indexCount;
private int uMvpLocation;
private int uModelLocation;
private int uLightDirLocation;
private Mesh? torusMesh;
private readonly Stopwatch stopwatch = new();
private float currentAngleDeg;
private float targetAngleDeg;
private bool returning;
private bool animating;
private readonly Vector3 eye = new(0f, 0.75f, 2.35f);
private readonly Vector3 lookTarget = new(0f, 0.18f, 0f);
private readonly Vector3 up = Vector3.UnitY;
private TopLevel? topLevel;
public HitTestTorus()
{
Focusable = true;
IsHitTestVisible = true;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
topLevel = TopLevel.GetTopLevel(this);
if (topLevel is null)
return;
topLevel.AddHandler(PointerPressedEvent, OnTopLevelPointerPressed, RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (topLevel is not null)
topLevel.RemoveHandler(PointerPressedEvent, OnTopLevelPointerPressed);
topLevel = null;
}
private void OnTopLevelPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (topLevel is null)
return;
var currentPoint = e.GetCurrentPoint(topLevel);
if (!currentPoint.Properties.IsLeftButtonPressed)
return;
var point = e.GetPosition(this);
if (point.X < 0 || point.Y < 0 || point.X > Bounds.Width || point.Y > Bounds.Height)
return;
Focus();
var model = CreateModelMatrix();
if (!TryHitTorus(point, model))
return;
StartRotateFromPoint(point);
e.Handled = true;
}
protected override void OnOpenGlInit(GlInterface gl)
{
GL.LoadBindings(new BindingsContext(gl));
CreateResources();
stopwatch.Restart();
}
protected override void OnOpenGlDeinit(GlInterface gl)
{
if (program != 0) GL.DeleteProgram(program);
if (vbo != 0) GL.DeleteBuffer(vbo);
if (ibo != 0) GL.DeleteBuffer(ibo);
if (vao != 0) GL.DeleteVertexArray(vao);
program = 0;
vbo = 0;
ibo = 0;
vao = 0;
indexCount = 0;
uMvpLocation = -1;
uModelLocation = -1;
uLightDirLocation = -1;
torusMesh = null;
}
protected override void OnOpenGlRender(GlInterface gl, int fb)
{
RequestNextFrameRendering();
GL.BindFramebuffer(FramebufferTarget.Framebuffer, fb);
var width = Math.Max(1, (int)Bounds.Width);
var height = Math.Max(1, (int)Bounds.Height);
GL.Viewport(0, 0, width, height);
GL.ClearColor(0.12f, 0.12f, 0.12f, 1f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
GL.Enable(EnableCap.DepthTest);
GL.DepthFunc(DepthFunction.Lequal);
GL.DepthMask(true);
var aspect = width / (float)height;
var projection = CreatePerspectiveOpenGl(MathF.PI / 3f, aspect, 0.1f, 50f);
var view = CreateLookAt(eye, lookTarget, up);
var vp = view * projection;
TickAnimation();
var model = CreateModelMatrix();
var mvp = model * vp;
GL.UseProgram(program);
GL.BindVertexArray(vao);
GL.UniformMatrix4(uMvpLocation, 1, true, ToFloat16(mvp));
GL.UniformMatrix4(uModelLocation, 1, true, ToFloat16(model));
GL.Uniform3(uLightDirLocation, 0.5f, 0.9f, 0.2f);
GL.DrawElements(PrimitiveType.Triangles, indexCount, DrawElementsType.UnsignedShort, IntPtr.Zero);
}
private void CreateResources()
{
program = CreateProgram(VertexShaderSource, FragmentShaderSource);
uMvpLocation = GL.GetUniformLocation(program, "uMvp");
uModelLocation = GL.GetUniformLocation(program, "uModel");
uLightDirLocation = GL.GetUniformLocation(program, "uLightDir");
torusMesh = Mesh.CreateTorus(MajorRadius, MinorRadius, MajorSegments, MinorSegments);
indexCount = torusMesh.indexCount;
vao = GL.GenVertexArray();
vbo = GL.GenBuffer();
ibo = GL.GenBuffer();
GL.BindVertexArray(vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
GL.BufferData(BufferTarget.ArrayBuffer, torusMesh.vertices.Length * sizeof(float), torusMesh.vertices, BufferUsageHint.StaticDraw);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, ibo);
GL.BufferData(BufferTarget.ElementArrayBuffer, torusMesh.indices.Length * sizeof(ushort), torusMesh.indices, BufferUsageHint.StaticDraw);
var stride = 8 * sizeof(float);
GL.EnableVertexAttribArray(0);
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, 0);
GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, 3 * sizeof(float));
}
private void StartRotateFromPoint(Point point)
{
var width = Math.Max(1.0, Bounds.Width);
var dx = (float)((point.X / width) * 2.0 - 1.0);
targetAngleDeg = 25f * (dx >= 0 ? -1f : 1f);
returning = false;
animating = true;
}
private void TickAnimation()
{
var dt = (float)stopwatch.Elapsed.TotalSeconds;
stopwatch.Restart();
if (!animating)
return;
const float speed = 18f;
var desired = returning ? 0f : targetAngleDeg;
currentAngleDeg = Damp(currentAngleDeg, desired, speed, dt);
if (!returning && MathF.Abs(currentAngleDeg - targetAngleDeg) < 0.2f)
returning = true;
if (returning && MathF.Abs(currentAngleDeg) < 0.1f)
{
currentAngleDeg = 0f;
targetAngleDeg = 0f;
returning = false;
animating = false;
}
}
private Matrix4x4 CreateModelMatrix()
{
var standRotation = Matrix4x4.CreateRotationZ(MathF.PI / 2f);
var viewRotation = Matrix4x4.CreateRotationY(-0.35f);
if (currentAngleDeg == 0f)
return standRotation * viewRotation;
var radians = currentAngleDeg * (MathF.PI / 180f);
var animRotation = Matrix4x4.CreateRotationY(radians);
return standRotation * viewRotation * animRotation;
}
private bool TryHitTorus(Point point, Matrix4x4 model)
{
if (!TryCreateWorldRay(point, out var rayOriginWorld, out var rayDirWorld))
return false;
if (!Matrix4x4.Invert(model, out var invModel))
return false;
var rayOriginModel = TransformPosition(rayOriginWorld, invModel);
var rayDirModel = Vector3.Normalize(TransformDirection(rayDirWorld, invModel));
var boundRadius = MajorRadius + MinorRadius;
if (!RayIntersectsSphere(rayOriginModel, rayDirModel, Vector3.Zero, boundRadius))
return false;
return RayMarchTorus(rayOriginModel, rayDirModel, MajorRadius, MinorRadius);
}
private bool TryCreateWorldRay(Point point, out Vector3 rayOriginWorld, out Vector3 rayDirWorld)
{
rayOriginWorld = default;
rayDirWorld = default;
var width = Math.Max(1, (int)Bounds.Width);
var height = Math.Max(1, (int)Bounds.Height);
var aspect = width / (float)height;
var projection = CreatePerspectiveOpenGl(MathF.PI / 3f, aspect, 0.1f, 50f);
var view = CreateLookAt(eye, lookTarget, up);
var vp = view * projection;
if (!Matrix4x4.Invert(vp, out var invVp))
return false;
var ndcX = (float)((point.X / width) * 2.0 - 1.0);
var ndcY = (float)(1.0 - (point.Y / height) * 2.0);
var nearClip = new Vector4(ndcX, ndcY, -1f, 1f);
var farClip = new Vector4(ndcX, ndcY, 1f, 1f);
var nearWorld4 = Vector4.Transform(nearClip, invVp);
var farWorld4 = Vector4.Transform(farClip, invVp);
if (MathF.Abs(nearWorld4.W) < 1e-6f || MathF.Abs(farWorld4.W) < 1e-6f)
return false;
var nearWorld = new Vector3(nearWorld4.X, nearWorld4.Y, nearWorld4.Z) / nearWorld4.W;
var farWorld = new Vector3(farWorld4.X, farWorld4.Y, farWorld4.Z) / farWorld4.W;
rayOriginWorld = nearWorld;
rayDirWorld = Vector3.Normalize(farWorld - nearWorld);
return true;
}
private static bool RayMarchTorus(Vector3 ro, Vector3 rd, float majorRadius, float minorRadius)
{
const int maxSteps = 128;
const float hitEpsilon = 0.004f;
const float maxDistance = 12f;
var t = 0f;
for (var i = 0; i < maxSteps; i++)
{
var p = ro + rd * t;
var d = SdTorus(p, majorRadius, minorRadius);
if (d < hitEpsilon)
return true;
t += MathF.Max(d, 0.003f);
if (t > maxDistance)
return false;
}
return false;
}
private static float SdTorus(Vector3 p, float majorRadius, float minorRadius)
{
var qx = new Vector2(p.X, p.Z).Length() - majorRadius;
var qy = p.Y;
return new Vector2(qx, qy).Length() - minorRadius;
}
private static bool RayIntersectsSphere(Vector3 ro, Vector3 rd, Vector3 center, float radius)
{
var oc = ro - center;
var b = Vector3.Dot(oc, rd);
var c = Vector3.Dot(oc, oc) - radius * radius;
var h = b * b - c;
return h >= 0;
}
private static Vector3 TransformPosition(Vector3 p, Matrix4x4 m)
{
var v4 = Vector4.Transform(new Vector4(p, 1f), m);
if (MathF.Abs(v4.W) < 1e-6f)
return new Vector3(v4.X, v4.Y, v4.Z);
return new Vector3(v4.X, v4.Y, v4.Z) / v4.W;
}
private static Vector3 TransformDirection(Vector3 d, Matrix4x4 m)
{
var v4 = Vector4.Transform(new Vector4(d, 0f), m);
return new Vector3(v4.X, v4.Y, v4.Z);
}
private static float Damp(float current, float target, float speed, float dt)
{
var k = 1f - MathF.Exp(-speed * dt);
return current + (target - current) * k;
}
private static Matrix4x4 CreatePerspectiveOpenGl(float fovYRadians, float aspect, float zNear, float zFar)
{
var f = 1f / MathF.Tan(fovYRadians * 0.5f);
var m33 = (zFar + zNear) / (zNear - zFar);
var m34 = -1f;
var m43 = (2f * zFar * zNear) / (zNear - zFar);
return new Matrix4x4(
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, m33, m34,
0, 0, m43, 0);
}
private static Matrix4x4 CreateLookAt(Vector3 eye, Vector3 target, Vector3 up)
{
var z = Vector3.Normalize(eye - target);
var x = Vector3.Normalize(Vector3.Cross(up, z));
var y = Vector3.Cross(z, x);
var tx = -Vector3.Dot(x, eye);
var ty = -Vector3.Dot(y, eye);
var tz = -Vector3.Dot(z, eye);
return new Matrix4x4(
x.X, y.X, z.X, 0,
x.Y, y.Y, z.Y, 0,
x.Z, y.Z, z.Z, 0,
tx, ty, tz, 1);
}
private static float[] ToFloat16(Matrix4x4 m)
=>
[
m.M11, m.M12, m.M13, m.M14,
m.M21, m.M22, m.M23, m.M24,
m.M31, m.M32, m.M33, m.M34,
m.M41, m.M42, m.M43, m.M44,
];
private static int CreateProgram(string vsSource, string fsSource)
{
var vs = CompileShader(ShaderType.VertexShader, vsSource);
var fs = CompileShader(ShaderType.FragmentShader, fsSource);
var p = GL.CreateProgram();
GL.AttachShader(p, vs);
GL.AttachShader(p, fs);
GL.LinkProgram(p);
GL.GetProgram(p, GetProgramParameterName.LinkStatus, out var linked);
if (linked == 0)
{
var log = GL.GetProgramInfoLog(p);
throw new InvalidOperationException($"Link failed: {log}");
}
GL.DetachShader(p, vs);
GL.DetachShader(p, fs);
GL.DeleteShader(vs);
GL.DeleteShader(fs);
return p;
}
private static int CompileShader(ShaderType type, string source)
{
source = source.TrimStart('\uFEFF');
var s = GL.CreateShader(type);
GL.ShaderSource(s, source);
GL.CompileShader(s);
GL.GetShader(s, ShaderParameter.CompileStatus, out var ok);
if (ok == 0)
{
var log = GL.GetShaderInfoLog(s);
GL.DeleteShader(s);
throw new InvalidOperationException($"{type} compile failed: {log}");
}
return s;
}
private const string VertexShaderSource =
"""
#version 300 es
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aNormal;
uniform mat4 uMvp;
uniform mat4 uModel;
out vec3 vNormal;
void main()
{
gl_Position = uMvp * vec4(aPos, 1.0);
vNormal = mat3(uModel) * aNormal;
}
""";
private const string FragmentShaderSource =
"""
#version 300 es
precision mediump float;
in vec3 vNormal;
uniform vec3 uLightDir;
out vec4 FragColor;
void main()
{
vec3 n = normalize(vNormal);
vec3 l = normalize(uLightDir);
float ndotl = max(dot(n, l), 0.0);
vec3 baseColor = vec3(0.10, 0.35, 0.85);
vec3 color = baseColor * (0.25 + 0.75 * ndotl);
FragColor = vec4(color, 1.0);
}
""";
private sealed class BindingsContext(GlInterface gl) : IBindingsContext
{
public IntPtr GetProcAddress(string procName) => gl.GetProcAddress(procName);
}
}
public partial class HitTesting : Window
{
public HitTesting()
{
InitializeComponent();
}
}
运行效果

浙公网安备 33010602011771号