


























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;
}
}
}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。