演示下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();
    }
}

运行效果

image

 

posted on 2026-08-24 08:16  dalgleish  阅读(10)  评论(0)    收藏  举报