惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
Cloudbric
Cloudbric
云风的 BLOG
云风的 BLOG
A
About on SuperTechFans
The GitHub Blog
The GitHub Blog
IT之家
IT之家
F
Full Disclosure
B
Blog RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
B
Blog
H
Help Net Security
The Cloudflare Blog
Recorded Future
Recorded Future
P
Proofpoint News Feed
P
Proofpoint News Feed
C
Cisco Blogs
T
Tailwind CSS Blog
P
Palo Alto Networks Blog
D
Docker
爱范儿
爱范儿
Know Your Adversary
Know Your Adversary
博客园 - 聂微东
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Y
Y Combinator Blog
雷峰网
雷峰网
AWS News Blog
AWS News Blog
D
DataBreaches.Net
博客园 - 司徒正美
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - Franky
C
Cybersecurity and Infrastructure Security Agency CISA
Blog — PlanetScale
Blog — PlanetScale
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Latest news
Latest news
Google DeepMind News
Google DeepMind News
Martin Fowler
Martin Fowler
MongoDB | Blog
MongoDB | Blog
C
CERT Recently Published Vulnerability Notes
阮一峰的网络日志
阮一峰的网络日志
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
CXSECURITY Database RSS Feed - CXSecurity.com
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Cyber Attacks, Cyber Crime and Cyber Security
腾讯CDC
小众软件
小众软件
G
Google Developers Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Scott Helme
Scott Helme
O
OpenAI News

博客园 - dalgleish

C# Avalonia 22- SoundAndVideo- SpeechVoiceTest C# Avalonia 22- SoundAndVideo- SpeechRecognition C# Avalonia 22- SoundAndVideo- SpeechSynthesis C# Avalonia 22- SoundAndVideo- CodePlayback C# Avalonia 22- SoundAndVideo- SoundPlayerTest C# Avalonia 21- MenusAndToolbars- RibbonTest C# Avalonia 21- MenusAndToolbars- ProportionalStatusBar C# Avalonia 21- MenusAndToolbars- BasicToolbar C# Avalonia 21- MenusAndToolbars- ToolBar/Ribbon开源项目介绍和修复 C# Avalonia 21- MenusAndToolbars- SidebarMenu C# Avalonia 21- MenusAndToolbars- MixedMenus C# Avalonia 20 - WindowsMenu- 魔改Hyperlink - 使用例子 C# Avalonia 20 - WindowsMenu- 魔改Hyperlink C# Avalonia 20 - WindowsMenu- WebViewTest C# Avalonia 20 - WindowsMenu- WebView C# Avalonia 20 - WindowsMenu- ModernWindowTest C# Avalonia 20 - WindowsMenu- ModernWindow C# Avalonia 20 - WindowsMenu- TransparentWithShapes C# Avalonia 20 - WindowsMenu- TransparentBackground C# Avalonia 20 - WindowsMenu- OpenFileTest C# Avalonia 20 - WindowsMenu- SavePostion C# Avalonia 20 - WindowsMenu- CenterScreen C# Avalonia 19- DataBinding- DataGridGrouping C# Avalonia 19- DataBinding- DirectoryTreeView C# Avalonia 19- DataBinding- BoundTreeView C# Avalonia 19- DataBinding- CustomListViewTest
C# Avalonia 23- OpenGL- 例子通用类
dalgleish · 2026-07-20 · via 博客园 - dalgleish
using Avalonia.Controls;
using Avalonia.Platform;
using LibVLCSharp.Shared;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;

namespace AvaloniaUI.Demos.Book._23.Tools;

public sealed class Frame3D : IDisposable
{
    private static readonly Lazy<LibVLC?> shared = new(() =>
    {
        if (IsDesignerProcess)
            return null;

        Core.Initialize();
        return new LibVLC("--no-plugins-cache");
    });

    private readonly LibVLC? libvlc;

    private MediaPlayer? player;
    private Media? media;
    private Stream? stream;

    private Uri? source;
    private bool disposed;

    private nint nativeBuffer;
    private int nativeSize;

    private byte[] bgra = [];
    private byte[] rgba = [];

    private int width;
    private int height;
    private int stride;

    private long frameId;
    private readonly object sync = new();

    private int loopRestartGate;

    public bool Loop { get; set; }

    private readonly MediaPlayer.LibVLCVideoFormatCb videoFormatCb;
    private readonly MediaPlayer.LibVLCVideoCleanupCb videoCleanupCb;
    private readonly MediaPlayer.LibVLCVideoLockCb videoLockCb;
    private readonly MediaPlayer.LibVLCVideoUnlockCb videoUnlockCb;
    private readonly MediaPlayer.LibVLCVideoDisplayCb videoDisplayCb;

    private readonly EventHandler<EventArgs> endReachedHandler;

    public Frame3D() : this(shared.Value) { }

    public Frame3D(LibVLC? libvlc)
    {
        this.libvlc = libvlc;

        videoFormatCb = VideoFormat;
        videoCleanupCb = VideoCleanup;
        videoLockCb = VideoLock;
        videoUnlockCb = VideoUnlock;
        videoDisplayCb = VideoDisplay;

        endReachedHandler = OnEndReached;
    }

    public Uri? Source
    {
        get => source;
        set
        {
            if (Equals(source, value))
                return;

            source = value;
            Restart();
        }
    }

    public bool FlipY { get; set; } = true;

    public int Width => width;
    public int Height => height;
    public int Stride => stride;

    public long FrameId => Interlocked.Read(ref frameId);

    public bool HasFrame => width > 0 && height > 0 && rgba.Length == width * height * 4;

    public event EventHandler<FrameReadyEventArgs>? Ready;

    public void Play()
    {
        if (IsDesignerProcess)
            return;

        player?.Play();
    }

    public void Pause()
    {
        if (IsDesignerProcess)
            return;

        player?.Pause();
    }

    public void Stop()
    {
        if (IsDesignerProcess)
            return;

        player?.Stop();
    }

    public bool TryGetLatest(out ReadOnlyMemory<byte> data, out int w, out int h, out int pitch, out long id)
    {
        lock (sync)
        {
            if (!HasFrame)
            {
                data = default;
                w = 0;
                h = 0;
                pitch = 0;
                id = 0;
                return false;
            }

            data = rgba;
            w = width;
            h = height;
            pitch = stride;
            id = frameId;
            return true;
        }
    }

    private void Restart()
    {
        ThrowIfDisposed();

        if (IsDesignerProcess)
            return;

        if (libvlc is null)
            return;

        var src = source;
        if (src is null || !src.IsAbsoluteUri)
        {
            StopInternal();
            FreeNative();
            return;
        }

        if (!AssetLoader.Exists(src))
        {
            StopInternal();
            FreeNative();
            return;
        }

        StopInternal();
        FreeNative();

        Stream? newStream = null;
        MediaPlayer? newPlayer = null;
        Media? newMedia = null;

        try
        {
            newStream = AssetLoader.Open(src);

            newPlayer = new MediaPlayer(libvlc)
            {
                Mute = true
            };

            newPlayer.EndReached += endReachedHandler;

            newMedia = new Media(libvlc, new StreamMediaInput(newStream));
            newPlayer.Media = newMedia;

            newPlayer.SetVideoFormatCallbacks(videoFormatCb, videoCleanupCb);
            newPlayer.SetVideoCallbacks(videoLockCb, videoUnlockCb, videoDisplayCb);

            lock (sync)
            {
                if (disposed)
                    return;

                stream = newStream;
                media = newMedia;
                player = newPlayer;

                newStream = null;
                newMedia = null;
                newPlayer = null;

                loopRestartGate = 0;
            }

            player?.Play();
        }
        finally
        {
            if (newPlayer is not null)
            {
                try { newPlayer.EndReached -= endReachedHandler; } catch { }
                try { newPlayer.Dispose(); } catch { }
            }

            if (newMedia is not null)
            {
                try { newMedia.Dispose(); } catch { }
            }

            if (newStream is not null)
            {
                try { newStream.Dispose(); } catch { }
            }
        }
    }

    private void OnEndReached(object? sender, EventArgs e)
    {
        if (!Loop)
            return;

        if (IsDesignerProcess)
            return;

        if (Interlocked.Exchange(ref loopRestartGate, 1) != 0)
            return;

        ThreadPool.QueueUserWorkItem(_ =>
        {
            try
            {
                if (disposed)
                    return;

                Restart();
            }
            catch
            {
            }
        });
    }

    private uint VideoFormat(ref nint _, nint chroma, ref uint w, ref uint h, ref uint pitches, ref uint lines)
    {
        var newW = (int)w;
        var newH = (int)h;

        if (newW <= 0 || newH <= 0)
            return 0;

        WriteFourCC(chroma, "RV32");

        var newStride = newW * 4;
        var bytes = newStride * newH;

        lock (sync)
        {
            if (disposed)
                return 0;

            width = newW;
            height = newH;
            stride = newStride;

            EnsureNative(bytes);
            EnsureManaged(bytes);
        }

        pitches = (uint)newStride;
        lines = (uint)newH;
        return 1;
    }

    private static void VideoCleanup(ref nint _) { }

    private nint VideoLock(nint _, nint planes)
    {
        Marshal.WriteIntPtr(planes, nativeBuffer);
        return 0;
    }

    private void VideoUnlock(nint _, nint __, nint ___) { }

    private void VideoDisplay(nint _, nint __)
    {
        byte[] rgbaLocal;
        byte[] bgraLocal;
        int w;
        int h;
        int pitch;
        bool flipY;
        long id;

        lock (sync)
        {
            if (disposed)
                return;

            if (nativeBuffer == 0 || width <= 0 || height <= 0 || stride <= 0)
                return;

            w = width;
            h = height;
            pitch = stride;
            flipY = FlipY;

            bgraLocal = bgra;
            rgbaLocal = rgba;

            if (bgraLocal.Length < pitch * h || rgbaLocal.Length < pitch * h)
                return;

            Marshal.Copy(nativeBuffer, bgraLocal, 0, pitch * h);
            ConvertBgraToRgba(bgraLocal, rgbaLocal, w, h, pitch, flipY);

            id = Interlocked.Increment(ref frameId);
        }

        Ready?.Invoke(this, new FrameReadyEventArgs(rgbaLocal, w, h, pitch, id));
    }

    private static void WriteFourCC(nint chroma, string fourcc)
    {
        if (fourcc.Length != 4)
            throw new ArgumentException("fourcc must be 4 chars.", nameof(fourcc));

        Marshal.WriteByte(chroma, 0, (byte)fourcc[0]);
        Marshal.WriteByte(chroma, 1, (byte)fourcc[1]);
        Marshal.WriteByte(chroma, 2, (byte)fourcc[2]);
        Marshal.WriteByte(chroma, 3, (byte)fourcc[3]);
    }

    private void EnsureNative(int size)
    {
        if (nativeBuffer != 0 && nativeSize == size)
            return;

        FreeNative();

        nativeBuffer = Marshal.AllocHGlobal(size);
        nativeSize = size;
    }

    private void EnsureManaged(int size)
    {
        if (bgra.Length != size)
            bgra = new byte[size];

        if (rgba.Length != size)
            rgba = new byte[size];
    }

    private void FreeNative()
    {
        if (nativeBuffer == 0)
            return;

        Marshal.FreeHGlobal(nativeBuffer);
        nativeBuffer = 0;
        nativeSize = 0;
    }

    private static void ConvertBgraToRgba(ReadOnlySpan<byte> bgra, Span<byte> rgba, int _, int h, int pitch, bool flipY)
    {
        if (!flipY)
        {
            for (var i = 0; i < pitch * h; i += 4)
            {
                rgba[i + 0] = bgra[i + 2];
                rgba[i + 1] = bgra[i + 1];
                rgba[i + 2] = bgra[i + 0];
                rgba[i + 3] = bgra[i + 3];
            }

            return;
        }

        for (var y = 0; y < h; y++)
        {
            var srcRow = y * pitch;
            var dstRow = (h - 1 - y) * pitch;

            for (var x = 0; x < pitch; x += 4)
            {
                var si = srcRow + x;
                var di = dstRow + x;

                rgba[di + 0] = bgra[si + 2];
                rgba[di + 1] = bgra[si + 1];
                rgba[di + 2] = bgra[si + 0];
                rgba[di + 3] = bgra[si + 3];
            }
        }
    }

    private void StopInternal()
    {
        MediaPlayer? p;
        Media? m;
        Stream? s;

        lock (sync)
        {
            p = player;
            m = media;
            s = stream;

            player = null;
            media = null;
            stream = null;

            width = 0;
            height = 0;
            stride = 0;

            bgra = [];
            rgba = [];

            loopRestartGate = 0;
        }

        if (p is not null)
        {
            try { p.EndReached -= endReachedHandler; } catch { }
            try { p.Stop(); } catch { }
            try { p.Dispose(); } catch { }
        }

        if (m is not null)
        {
            try { m.Dispose(); } catch { }
        }

        if (s is not null)
        {
            try { s.Dispose(); } catch { }
        }
    }

    public void Dispose()
    {
        if (disposed)
            return;

        disposed = true;

        StopInternal();
        FreeNative();
    }

    private void ThrowIfDisposed()
    {
        ObjectDisposedException.ThrowIf(disposed, this);
    }

    private static bool IsDesignerProcess
    {
        get
        {
            if (Design.IsDesignMode)
                return true;

            var v = Environment.GetEnvironmentVariable("AVALONIA_DESIGNER");
            if (string.Equals(v, "1", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(v, "true", StringComparison.OrdinalIgnoreCase))
                return true;

            try
            {
                var name = Process.GetCurrentProcess().ProcessName;
                if (name.Contains("Avalonia", StringComparison.OrdinalIgnoreCase) &&
                    name.Contains("Designer", StringComparison.OrdinalIgnoreCase))
                    return true;
            }
            catch
            {
            }

            return false;
        }
    }
}

public sealed class FrameReadyEventArgs(byte[] rgba, int width, int height, int stride, long frameId) : EventArgs
{
    public byte[] Rgba { get; } = rgba;
    public int Width { get; } = width;
    public int Height { get; } = height;
    public int Stride { get; } = stride;
    public long FrameId { get; } = frameId;
}
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using OpenTK.Graphics.OpenGL;
using System;
using System.Numerics;
using System.Runtime.InteropServices;

namespace AvaloniaUI.Demos.Book._23.Tools
{
    public sealed class AmbientMaterial
    {
        public Vector3 color = new(0.15f, 0.15f, 0.15f);
        public float intensity = 1f;
    }

    public sealed class DiffuseMaterial
    {
        public Vector3 color = new(0.05f, 0.2f, 0.8f);
        public float intensity = 1f;
    }

    public sealed class SpecularMaterial
    {
        public Vector3 color = new(0.8f, 0.8f, 0.8f);
        public float intensity = 1f;
        public float power = 24f;
    }

    public sealed class EmissiveMaterial
    {
        public Vector3 color = new(0.6f, 0.0f, 0.0f);
        public float intensity = 1f;
    }

    public sealed class ImageMaterial : IDisposable
    {
        public Uri? source;
        public bool flipY = true;

        public TextureMinFilter minFilter = TextureMinFilter.LinearMipmapLinear;
        public TextureMagFilter magFilter = TextureMagFilter.Linear;

        public TextureWrapMode wrapS = TextureWrapMode.Repeat;
        public TextureWrapMode wrapT = TextureWrapMode.Repeat;

        private int texture;
        private int width;
        private int height;

        private Uri? loadedSource;
        private bool isDisposed;

        public int TextureId => texture;
        public bool HasTexture => texture != 0;

        public void EnsureTexture()
        {
            ObjectDisposedException.ThrowIf(isDisposed, this);

            if (source is null)
            {
                if (texture != 0)
                {
                    DeleteTexture();
                    Console.WriteLine("ImageMaterial: source is null, texture deleted.");
                }

                return;
            }

            if (texture != 0 && loadedSource == source)
                return;

            DeleteTexture();

            var pixels = LoadAvaresRgba(source, out width, out height, flipY);
            if (pixels.Length == 0 || width <= 0 || height <= 0)
            {
                Console.WriteLine($"ImageMaterial: no pixels loaded. uri={source}");
                return;
            }

            texture = GL.GenTexture();
            GL.BindTexture(TextureTarget.Texture2D, texture);

            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)wrapS);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)wrapT);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)minFilter);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)magFilter);

            GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);

            GL.TexImage2D(
                TextureTarget.Texture2D,
                0,
                PixelInternalFormat.Rgba8,
                width,
                height,
                0,
                OpenTK.Graphics.OpenGL.PixelFormat.Rgba,
                PixelType.UnsignedByte,
                pixels);

            var err = GL.GetError();
            if (err != ErrorCode.NoError)
                Console.WriteLine($"ImageMaterial: TexImage2D error: {err}");

            if (minFilter is TextureMinFilter.LinearMipmapLinear or TextureMinFilter.LinearMipmapNearest or TextureMinFilter.NearestMipmapLinear or TextureMinFilter.NearestMipmapNearest)
            {
                GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);

                err = GL.GetError();
                if (err != ErrorCode.NoError)
                    Console.WriteLine($"ImageMaterial: GenerateMipmap error: {err}");
            }

            GL.BindTexture(TextureTarget.Texture2D, 0);

            loadedSource = source;

            //Console.WriteLine($"ImageMaterial: texture uploaded. id={texture} size={width}x{height} uri={source}");
        }

        public void Bind(TextureUnit unit)
        {
            if (texture == 0)
                return;

            GL.ActiveTexture(unit);
            GL.BindTexture(TextureTarget.Texture2D, texture);
        }

        public static void Unbind(TextureUnit unit)
        {
            GL.ActiveTexture(unit);
            GL.BindTexture(TextureTarget.Texture2D, 0);
        }

        public void Dispose()
        {
            if (isDisposed)
                return;

            isDisposed = true;
            DeleteTexture();
        }

        private void DeleteTexture()
        {
            if (texture == 0)
                return;

            GL.DeleteTexture(texture);
            texture = 0;
            width = 0;
            height = 0;
            loadedSource = null;
        }

        private static byte[] LoadAvaresRgba(Uri uri, out int width, out int height, bool flipY)
        {
            width = 0;
            height = 0;

            try
            {
                if (!AssetLoader.Exists(uri))
                {
                    Console.WriteLine($"ImageMaterial: asset not found: {uri}");
                    return [];
                }

                using var stream = AssetLoader.Open(uri);
                using var bitmap = new Bitmap(stream);

                width = bitmap.PixelSize.Width;
                height = bitmap.PixelSize.Height;

                if (width <= 0 || height <= 0)
                    return [];

                var bytesPerPixel = 4;
                var stride = width * bytesPerPixel;
                var size = stride * height;

                var tempPtr = Marshal.AllocHGlobal(size);
                try
                {
                    bitmap.CopyPixels(new PixelRect(0, 0, width, height), tempPtr, size, stride);

                    var bgra = new byte[size];
                    Marshal.Copy(tempPtr, bgra, 0, size);

                    var rgba = new byte[size];
                    var rowBytes = stride;

                    if (!flipY)
                    {
                        for (var i = 0; i < size; i += 4)
                        {
                            var b = bgra[i + 0];
                            var g = bgra[i + 1];
                            var r = bgra[i + 2];
                            var a = bgra[i + 3];

                            rgba[i + 0] = r;
                            rgba[i + 1] = g;
                            rgba[i + 2] = b;
                            rgba[i + 3] = a;
                        }

                        return rgba;
                    }

                    for (var y = 0; y < height; y++)
                    {
                        var srcRow = y * rowBytes;
                        var dstRow = (height - 1 - y) * rowBytes;

                        for (var x = 0; x < rowBytes; x += 4)
                        {
                            var si = srcRow + x;
                            var di = dstRow + x;

                            var b = bgra[si + 0];
                            var g = bgra[si + 1];
                            var r = bgra[si + 2];
                            var a = bgra[si + 3];

                            rgba[di + 0] = r;
                            rgba[di + 1] = g;
                            rgba[di + 2] = b;
                            rgba[di + 3] = a;
                        }
                    }

                    return rgba;
                }
                finally
                {
                    Marshal.FreeHGlobal(tempPtr);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"ImageMaterial: load failed: {uri}");
                Console.WriteLine(ex);
                return [];
            }
        }
    }
}
using System;

namespace AvaloniaUI.Demos.Book._23.Tools;

public sealed class Mesh
{
    // 顶点布局:pos(3) + normal(3) + uv(2) = 8 floats
    public float[] vertices { get; }
    public ushort[] indices { get; }

    public int vertexCount => vertices.Length / 8;
    public int indexCount => indices.Length;

    private Mesh(float[] vertices, ushort[] indices)
    {
        this.vertices = vertices;
        this.indices = indices;
    }

    // Torus
    public static Mesh CreateTorus(
        float majorRadius,
        float minorRadius,
        int majorSegments,
        int minorSegments)
    {
        majorSegments = Math.Max(3, majorSegments);
        minorSegments = Math.Max(3, minorSegments);

        var vertexCount = majorSegments * minorSegments;
        var vertices = new float[vertexCount * 8];

        var indexCount = majorSegments * minorSegments * 6;
        var indices = new ushort[indexCount];

        var vi = 0;

        for (var i = 0; i < majorSegments; i++)
        {
            var u = i / (float)majorSegments * MathF.Tau;
            var cu = MathF.Cos(u);
            var su = MathF.Sin(u);

            // UV: U 沿大圆方向 [0,1)
            var tu = i / (float)majorSegments;

            for (var j = 0; j < minorSegments; j++)
            {
                var v = j / (float)minorSegments * MathF.Tau;
                var cv = MathF.Cos(v);
                var sv = MathF.Sin(v);

                // UV: V 沿小圆方向 [0,1)
                var tv = j / (float)minorSegments;

                var x = (majorRadius + minorRadius * cv) * cu;
                var y = minorRadius * sv;
                var z = (majorRadius + minorRadius * cv) * su;

                var nx = cv * cu;
                var ny = sv;
                var nz = cv * su;

                vertices[vi++] = x;
                vertices[vi++] = y;
                vertices[vi++] = z;

                vertices[vi++] = nx;
                vertices[vi++] = ny;
                vertices[vi++] = nz;

                vertices[vi++] = tu;
                vertices[vi++] = tv;
            }
        }

        var ii = 0;

        for (var i = 0; i < majorSegments; i++)
        {
            var inext = (i + 1) % majorSegments;

            for (var j = 0; j < minorSegments; j++)
            {
                var jnext = (j + 1) % minorSegments;

                var a = (ushort)(i * minorSegments + j);
                var b = (ushort)(inext * minorSegments + j);
                var c = (ushort)(inext * minorSegments + jnext);
                var d = (ushort)(i * minorSegments + jnext);

                indices[ii++] = a;
                indices[ii++] = b;
                indices[ii++] = c;

                indices[ii++] = a;
                indices[ii++] = c;
                indices[ii++] = d;
            }
        }

        return new Mesh(vertices, indices);
    }

    // Cube(每面独立顶点,方便 UV)
    public static Mesh CreateCube(float size = 1f)
    {
        var h = size * 0.5f;

        // 每个面 4 个顶点,uv 统一 0..1
        var vertices = new float[]
        {
            // Front (+Z)
            -h,-h, h,  0,0,1,   0,1,
             h,-h, h,  0,0,1,   1,1,
             h, h, h,  0,0,1,   1,0,
            -h, h, h,  0,0,1,   0,0,

            // Back (-Z)
             h,-h,-h,  0,0,-1,  0,1,
            -h,-h,-h,  0,0,-1,  1,1,
            -h, h,-h,  0,0,-1,  1,0,
             h, h,-h,  0,0,-1,  0,0,

            // Left (-X)
            -h,-h,-h, -1,0,0,   0,1,
            -h,-h, h, -1,0,0,   1,1,
            -h, h, h, -1,0,0,   1,0,
            -h, h,-h, -1,0,0,   0,0,

            // Right (+X)
             h,-h, h,  1,0,0,   0,1,
             h,-h,-h,  1,0,0,   1,1,
             h, h,-h,  1,0,0,   1,0,
             h, h, h,  1,0,0,   0,0,

            // Top (+Y)
            -h, h, h,  0,1,0,   0,1,
             h, h, h,  0,1,0,   1,1,
             h, h,-h,  0,1,0,   1,0,
            -h, h,-h,  0,1,0,   0,0,

            // Bottom (-Y)
            -h,-h,-h,  0,-1,0,  0,1,
             h,-h,-h,  0,-1,0,  1,1,
             h,-h, h,  0,-1,0,  1,0,
            -h,-h, h,  0,-1,0,  0,0,
        };

        var indices = new ushort[]
        {
            0,1,2, 0,2,3,
            4,5,6, 4,6,7,
            8,9,10, 8,10,11,
            12,13,14, 12,14,15,
            16,17,18, 16,18,19,
            20,21,22, 20,22,23
        };

        return new Mesh(vertices, indices);
    }

    // Sphere (UV sphere)
    public static Mesh CreateSphere(float radius, int slices, int stacks)
    {
        slices = Math.Max(3, slices);
        stacks = Math.Max(2, stacks);

        var vertexCount = (slices + 1) * (stacks + 1);
        var vertices = new float[vertexCount * 8];

        var indexCount = slices * stacks * 6;
        var indices = new ushort[indexCount];

        var vi = 0;

        for (var stack = 0; stack <= stacks; stack++)
        {
            var v01 = stack / (float)stacks;
            var phi = v01 * MathF.PI;

            var y = MathF.Cos(phi);
            var r = MathF.Sin(phi);

            for (var slice = 0; slice <= slices; slice++)
            {
                var u01 = slice / (float)slices;
                var theta = u01 * MathF.Tau;

                var x = r * MathF.Cos(theta);
                var z = r * MathF.Sin(theta);

                vertices[vi++] = x * radius;
                vertices[vi++] = y * radius;
                vertices[vi++] = z * radius;

                vertices[vi++] = x;
                vertices[vi++] = y;
                vertices[vi++] = z;

                vertices[vi++] = u01;
                vertices[vi++] = 1f - v01; // 常见纹理坐标习惯(上为0)
            }
        }

        var ii = 0;

        for (var stack = 0; stack < stacks; stack++)
        {
            for (var slice = 0; slice < slices; slice++)
            {
                var a = (ushort)(stack * (slices + 1) + slice);
                var b = (ushort)(a + slices + 1);
                var c = (ushort)(b + 1);
                var d = (ushort)(a + 1);

                indices[ii++] = a;
                indices[ii++] = b;
                indices[ii++] = c;

                indices[ii++] = a;
                indices[ii++] = c;
                indices[ii++] = d;
            }
        }

        return new Mesh(vertices, indices);
    }

    // Plane
    public static Mesh CreatePlane(float size = 1f)
    {
        var h = size * 0.5f;

        var vertices = new float[]
        {
            -h,0,-h,  0,1,0,   0,1,
             h,0,-h,  0,1,0,   1,1,
             h,0, h,  0,1,0,   1,0,
            -h,0, h,  0,1,0,   0,0,
        };

        var indices = new ushort[]
        {
            0,1,2,
            0,2,3
        };

        return new Mesh(vertices, indices);
    }

    // Cylinder(侧面 UV)
    public static Mesh CreateCylinder(float radius, float height, int segments)
    {
        segments = Math.Max(3, segments);

        var half = height * 0.5f;

        // (segments+1)*2 顶点,8 floats
        var vertices = new float[(segments + 1) * 2 * 8];
        var indices = new ushort[segments * 6];

        var vi = 0;

        for (var i = 0; i <= segments; i++)
        {
            var u01 = i / (float)segments;
            var t = u01 * MathF.Tau;

            var x = MathF.Cos(t);
            var z = MathF.Sin(t);

            // bottom
            vertices[vi++] = x * radius;
            vertices[vi++] = -half;
            vertices[vi++] = z * radius;
            vertices[vi++] = x;
            vertices[vi++] = 0;
            vertices[vi++] = z;
            vertices[vi++] = u01;
            vertices[vi++] = 1f;

            // top
            vertices[vi++] = x * radius;
            vertices[vi++] = half;
            vertices[vi++] = z * radius;
            vertices[vi++] = x;
            vertices[vi++] = 0;
            vertices[vi++] = z;
            vertices[vi++] = u01;
            vertices[vi++] = 0f;
        }

        var ii = 0;

        for (var i = 0; i < segments; i++)
        {
            var a = (ushort)(i * 2);
            var b = (ushort)(a + 1);
            var c = (ushort)(a + 2);
            var d = (ushort)(a + 3);

            indices[ii++] = a;
            indices[ii++] = b;
            indices[ii++] = c;

            indices[ii++] = b;
            indices[ii++] = d;
            indices[ii++] = c;
        }

        return new Mesh(vertices, indices);
    }
}
using System;
using System.Collections.Generic;
using System.Numerics;

namespace AvaloniaUI.Demos.Book._23.Tools;

public sealed class ScreenSpaceLines3D
{
    public List<Vector3> points { get; } = [];

    // thickness 单位:像素
    public float thickness { get; set; } = 2f;

    public Vector4 color { get; set; } = new(1f, 0f, 0f, 1f);

    // 输出:clipPos(vec4) + color(vec4) = 8 floats
    public const int floatsPerVertex = 8;

    public float[] vertexData { get; private set; } = [];

    public int vertexCount { get; private set; }

    public void SetAxes(float extent)
    {
        points.Clear();

        points.Add(new Vector3(-extent, 0, 0));
        points.Add(new Vector3(+extent, 0, 0));

        points.Add(new Vector3(0, -extent, 0));
        points.Add(new Vector3(0, +extent, 0));

        points.Add(new Vector3(0, 0, -extent));
        points.Add(new Vector3(0, 0, +extent));
    }

    public void Rebuild(Matrix4x4 viewProjection, int viewportWidth, int viewportHeight)
    {
        if (viewportWidth <= 0 || viewportHeight <= 0 || points.Count < 2 || points.Count % 2 != 0)
        {
            vertexData = [];
            vertexCount = 0;
            return;
        }

        var segmentCount = points.Count / 2;

        var maxVertices = segmentCount * 6;
        var requiredFloats = maxVertices * floatsPerVertex;

        if (vertexData.Length != requiredFloats)
            vertexData = new float[requiredFloats];

        var half = thickness * 0.5f;

        var width = viewportWidth;
        var height = viewportHeight;

        var di = 0;
        var writtenVertices = 0;

        for (var i = 0; i < segmentCount; i++)
        {
            var a = points[i * 2 + 0];
            var b = points[i * 2 + 1];

            var clipA = Vector4.Transform(new Vector4(a, 1f), viewProjection);
            var clipB = Vector4.Transform(new Vector4(b, 1f), viewProjection);

            if (MathF.Abs(clipA.W) < 1e-6f || MathF.Abs(clipB.W) < 1e-6f)
                continue;

            var ndcA = new Vector2(clipA.X / clipA.W, clipA.Y / clipA.W);
            var ndcB = new Vector2(clipB.X / clipB.W, clipB.Y / clipB.W);

            var screenA = new Vector2((ndcA.X * 0.5f + 0.5f) * width, (1f - (ndcA.Y * 0.5f + 0.5f)) * height);
            var screenB = new Vector2((ndcB.X * 0.5f + 0.5f) * width, (1f - (ndcB.Y * 0.5f + 0.5f)) * height);

            var dir = screenB - screenA;
            var len = dir.Length();

            if (len <= 1e-4f)
                continue;

            dir /= len;

            var perpPixels = new Vector2(-dir.Y, dir.X) * half;

            // pixel -> NDC,Y 需要翻转(screen Y 向下,NDC Y 向上)
            var ndcOffset = new Vector2(perpPixels.X / width * 2f, -perpPixels.Y / height * 2f);

            static Vector4 ClipWithOffset(Vector4 clip, Vector2 ndc, Vector2 off)
                => new((ndc.X + off.X) * clip.W, (ndc.Y + off.Y) * clip.W, clip.Z, clip.W);

            var a1 = ClipWithOffset(clipA, ndcA, ndcOffset);
            var a2 = ClipWithOffset(clipA, ndcA, -ndcOffset);
            var b1 = ClipWithOffset(clipB, ndcB, ndcOffset);
            var b2 = ClipWithOffset(clipB, ndcB, -ndcOffset);

            WriteVertex(a1);
            WriteVertex(b1);
            WriteVertex(a2);

            WriteVertex(a2);
            WriteVertex(b1);
            WriteVertex(b2);

            writtenVertices += 6;
        }

        vertexCount = writtenVertices;

        var actualFloats = vertexCount * floatsPerVertex;
        if (actualFloats != vertexData.Length)
        {
            var trimmed = new float[actualFloats];
            Array.Copy(vertexData, trimmed, actualFloats);
            vertexData = trimmed;
        }

        void WriteVertex(Vector4 clipPos)
        {
            vertexData[di++] = clipPos.X;
            vertexData[di++] = clipPos.Y;
            vertexData[di++] = clipPos.Z;
            vertexData[di++] = clipPos.W;

            vertexData[di++] = color.X;
            vertexData[di++] = color.Y;
            vertexData[di++] = color.Z;
            vertexData[di++] = color.W;
        }
    }
}