Skip to content

Commit ef46da2

Browse files
committed
Add 360 degree node viewer
1 parent 1a69da4 commit ef46da2

File tree

12 files changed

+758
-11
lines changed

12 files changed

+758
-11
lines changed

MystIVAssetExplorer/Extensions.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Linq;
5+
6+
namespace MystIVAssetExplorer;
7+
8+
internal static class Extensions
9+
{
10+
public static bool TrySingle<T>(this IEnumerable<T> source, [MaybeNullWhen(false)] out T single)
11+
{
12+
using var enumerator = source.GetEnumerator();
13+
14+
if (enumerator.MoveNext())
15+
{
16+
var current = enumerator.Current;
17+
if (!enumerator.MoveNext())
18+
{
19+
single = current;
20+
return true;
21+
}
22+
}
23+
24+
single = default;
25+
return false;
26+
}
27+
28+
public static bool TrySingle<T>(this IEnumerable<T> source, Func<T, bool> predicate, [MaybeNullWhen(false)] out T single)
29+
{
30+
return source.Where(predicate).TrySingle(out single);
31+
}
32+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using MystIVAssetExplorer.Memory;
2+
using System;
3+
4+
namespace MystIVAssetExplorer.Formats;
5+
6+
public sealed class ZapImage
7+
{
8+
public required int Width { get; init; }
9+
public required int Height { get; init; }
10+
public required ReadOnlyMemory<byte> RgbChannels { get; init; }
11+
public required ReadOnlyMemory<byte> AlphaChannel { get; init; }
12+
13+
public static ZapImage Parse(ReadOnlyMemory<byte> zapFileData)
14+
{
15+
var reader = new SpanReader(zapFileData.Span);
16+
if (reader.ReadUInt32LittleEndian() != 32) throw new NotImplementedException();
17+
if (reader.ReadUInt32LittleEndian() != 2) throw new NotImplementedException();
18+
if (reader.ReadUInt32LittleEndian() != 10) throw new NotImplementedException();
19+
if (reader.ReadUInt32LittleEndian() != 10) throw new NotImplementedException();
20+
var dataLength1 = reader.ReadInt32LittleEndian();
21+
var dataLength2 = reader.ReadInt32LittleEndian();
22+
var width = reader.ReadInt32LittleEndian();
23+
var height = reader.ReadInt32LittleEndian();
24+
25+
var position = zapFileData.Length - reader.Span.Length;
26+
var image1 = zapFileData.Slice(position, dataLength1);
27+
position += dataLength1;
28+
var image2 = zapFileData.Slice(position, dataLength2);
29+
position += dataLength2;
30+
if (position != zapFileData.Length)
31+
throw new NotImplementedException();
32+
33+
return new ZapImage { Width = width, Height = height, RgbChannels = image1, AlphaChannel = image2 };
34+
}
35+
}

MystIVAssetExplorer/ILease.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System;
2+
3+
namespace MystIVAssetExplorer;
4+
5+
public interface ILease<out T> : IDisposable
6+
{
7+
T LeasedInstance { get; }
8+
}

MystIVAssetExplorer/MystIVAssetExplorer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<PackageReference Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
1616
<PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
1717
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
18+
<PackageReference Include="Avalonia.Skia" Version="$(AvaloniaVersion)" />
1819
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.303" />
1920
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
2021
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
using System.Threading;
3+
4+
namespace MystIVAssetExplorer;
5+
6+
public sealed class ReferenceCountedDisposable<T> where T : IDisposable
7+
{
8+
private readonly object lockObject = new();
9+
private T? instance;
10+
private int referenceCount = 1;
11+
12+
public ReferenceCountedDisposable(T instance, out ILease<T> initialLease)
13+
{
14+
this.instance = instance;
15+
initialLease = new DecrementLease(this);
16+
}
17+
18+
public ILease<T>? TryLease()
19+
{
20+
lock (lockObject)
21+
{
22+
if (referenceCount == 0)
23+
return null;
24+
25+
referenceCount++;
26+
return new DecrementLease(this);
27+
}
28+
}
29+
30+
private void Decrement()
31+
{
32+
var shouldDispose = false;
33+
var instanceToDispose = default(T);
34+
35+
lock (lockObject)
36+
{
37+
if (referenceCount == 0)
38+
throw new InvalidOperationException("More increments than decrements");
39+
40+
referenceCount--;
41+
if (referenceCount == 0)
42+
{
43+
shouldDispose = true;
44+
instanceToDispose = instance;
45+
instance = default;
46+
}
47+
}
48+
49+
if (shouldDispose)
50+
instanceToDispose!.Dispose();
51+
}
52+
53+
private sealed class DecrementLease(ReferenceCountedDisposable<T> owner) : ILease<T>
54+
{
55+
private ReferenceCountedDisposable<T>? owner = owner;
56+
57+
public T LeasedInstance => (Volatile.Read(ref owner) ?? throw new ObjectDisposedException(nameof(ILease<>))).instance!;
58+
59+
public void Dispose() => Interlocked.Exchange(ref owner, null)?.Decrement();
60+
}
61+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
using Avalonia;
2+
using Avalonia.Media;
3+
using Avalonia.Platform;
4+
using Avalonia.Rendering.SceneGraph;
5+
using Avalonia.Skia;
6+
using SkiaSharp;
7+
using System;
8+
using System.Numerics;
9+
10+
namespace MystIVAssetExplorer.Skybox;
11+
12+
partial class SkyboxControl
13+
{
14+
private sealed class BoxModelDrawOperation(SkyboxControl owner, ILease<SkyboxModel> boxModelLease) : ICustomDrawOperation
15+
{
16+
public Rect Bounds => new(owner.Bounds.Size);
17+
18+
public void Dispose() => boxModelLease.Dispose();
19+
20+
public bool Equals(ICustomDrawOperation? other) => other == this;
21+
22+
public bool HitTest(Point p) => false;
23+
24+
public void Render(ImmediateDrawingContext context)
25+
{
26+
using var lease = context.TryGetFeature<ISkiaSharpApiLeaseFeature>()!.Lease();
27+
var canvas = lease.SkCanvas;
28+
29+
canvas.ClipRect(Bounds.ToSKRect());
30+
canvas.Translate((float)Bounds.Width / 2, (float)Bounds.Height / 2);
31+
DrawSkybox(canvas, boxModelLease.LeasedInstance, (float)owner.AngleX, (float)owner.AngleY, scale: (float)double.Min(Bounds.Width, Bounds.Height) / 2);
32+
}
33+
34+
private void DrawSkybox(SKCanvas canvas, SkyboxModel boxModel, float angleX, float angleY, float scale)
35+
{
36+
var cubeVertices = new Vector3[]
37+
{
38+
new(-1, 1, 1), // 0: left-top-front
39+
new( 1, 1, 1), // 1: right-top-front
40+
new(-1, -1, 1), // 2: left-bottom-front
41+
new( 1, -1, 1), // 3: right-bottom-front
42+
new(-1, 1, -1), // 4: left-top-back
43+
new( 1, 1, -1), // 5: right-top-back
44+
new(-1, -1, -1), // 6: left-bottom-back
45+
new( 1, -1, -1), // 7: right-bottom-back
46+
};
47+
48+
var rotation =
49+
Quaternion.CreateFromAxisAngle(Vector3.UnitX, angleY)
50+
* Quaternion.CreateFromAxisAngle(Vector3.UnitY, angleX);
51+
52+
var rotatedVertices = new Vector3[cubeVertices.Length];
53+
for (var i = 0; i < cubeVertices.Length; i++)
54+
rotatedVertices[i] = Vector3.Transform(cubeVertices[i], rotation);
55+
56+
var faces = new (SKBitmap? Texture, int[] Vertices)[]
57+
{
58+
(boxModel.Front, [0, 1, 2, 3]),
59+
(boxModel.Back, [5, 4, 7, 6]),
60+
(boxModel.Left, [4, 0, 6, 2]),
61+
(boxModel.Right, [1, 5, 3, 7]),
62+
(boxModel.Top, [4, 5, 0, 1]),
63+
(boxModel.Bottom, [2, 3, 6, 7]),
64+
};
65+
66+
using var paint = new SKPaint
67+
{
68+
IsAntialias = false,
69+
FilterQuality = SKFilterQuality.High,
70+
};
71+
72+
foreach (var face in faces)
73+
{
74+
if (face.Texture is null) continue;
75+
76+
var firstPointNotBehindCamera = Array.FindIndex(face.Vertices, i => rotatedVertices[i].Z > 0);
77+
if (firstPointNotBehindCamera == -1)
78+
continue;
79+
80+
var pts = new SKPoint[4];
81+
for (int i = 0; i < 4; i++)
82+
{
83+
var v = rotatedVertices[face.Vertices[i]];
84+
pts[i] = new SKPoint(v.X / v.Z * scale, v.Y / v.Z * -scale);
85+
}
86+
87+
canvas.Save();
88+
89+
// The point used as the matrix's TransX+Y must have Z > 0.
90+
if (firstPointNotBehindCamera == 0)
91+
{
92+
canvas.SetMatrix(canvas.TotalMatrix.PreConcat(MapUnitSquareToGivenPoints(pts[0], pts[1], pts[2], pts[3])));
93+
canvas.DrawBitmap(face.Texture, new SKRect(0, 0, 1, 1), paint);
94+
}
95+
else if (firstPointNotBehindCamera == 1)
96+
{
97+
canvas.SetMatrix(canvas.TotalMatrix.PreConcat(MapNegativeXSquareToGivenPoints(pts[0], pts[1], pts[2], pts[3])));
98+
canvas.DrawBitmap(face.Texture, new SKRect(-1, 0, 0, 1), paint);
99+
}
100+
else if (firstPointNotBehindCamera == 2)
101+
{
102+
canvas.SetMatrix(canvas.TotalMatrix.PreConcat(MapNegativeYSquareToGivenPoints(pts[0], pts[1], pts[2], pts[3])));
103+
canvas.DrawBitmap(face.Texture, new SKRect(0, -1, 1, 0), paint);
104+
}
105+
else
106+
{
107+
canvas.SetMatrix(canvas.TotalMatrix.PreConcat(MapNegativeXYSquareToGivenPoints(pts[0], pts[1], pts[2], pts[3])));
108+
canvas.DrawBitmap(face.Texture, new SKRect(-1, -1, 0, 0), paint);
109+
}
110+
111+
canvas.Restore();
112+
}
113+
}
114+
115+
private static SKMatrix MapUnitSquareToGivenPoints(SKPoint topLeft, SKPoint topRight, SKPoint bottomLeft, SKPoint bottomRight)
116+
{
117+
var rightDiff = bottomRight - topRight;
118+
var bottomDiff = bottomRight - bottomLeft;
119+
120+
var determinant = rightDiff.X * bottomDiff.Y - bottomDiff.X * rightDiff.Y;
121+
122+
var topDiff = topRight - topLeft;
123+
var leftDiff = bottomLeft - topLeft;
124+
125+
var persp0 = (topDiff.X * bottomDiff.Y - topDiff.Y * bottomDiff.X) / determinant;
126+
var persp1 = (rightDiff.X * leftDiff.Y - rightDiff.Y * leftDiff.X) / determinant;
127+
128+
return new SKMatrix
129+
{
130+
ScaleX = persp0 * topRight.X + topDiff.X,
131+
SkewX = persp1 * bottomLeft.X + leftDiff.X,
132+
TransX = topLeft.X,
133+
SkewY = persp0 * topRight.Y + topDiff.Y,
134+
ScaleY = persp1 * bottomLeft.Y + leftDiff.Y,
135+
TransY = topLeft.Y,
136+
Persp0 = persp0,
137+
Persp1 = persp1,
138+
Persp2 = 1,
139+
};
140+
}
141+
142+
private static SKMatrix MapNegativeXSquareToGivenPoints(SKPoint topLeft, SKPoint topRight, SKPoint bottomLeft, SKPoint bottomRight)
143+
{
144+
var leftDiff = bottomLeft - topLeft;
145+
var bottomDiff = bottomLeft - bottomRight;
146+
147+
var determinant = leftDiff.X * bottomDiff.Y - bottomDiff.X * leftDiff.Y;
148+
149+
var topDiff = topLeft - topRight;
150+
var rightDiff = bottomRight - topRight;
151+
152+
var persp0 = (bottomDiff.X * topDiff.Y - bottomDiff.Y * topDiff.X) / determinant;
153+
var persp1 = (leftDiff.X * rightDiff.Y - leftDiff.Y * rightDiff.X) / determinant;
154+
155+
return new SKMatrix
156+
{
157+
ScaleX = persp0 * topLeft.X - topDiff.X,
158+
SkewX = persp1 * bottomRight.X + rightDiff.X,
159+
TransX = topRight.X,
160+
SkewY = persp0 * topLeft.Y - topDiff.Y,
161+
ScaleY = persp1 * bottomRight.Y + rightDiff.Y,
162+
TransY = topRight.Y,
163+
Persp0 = persp0,
164+
Persp1 = persp1,
165+
Persp2 = 1,
166+
};
167+
}
168+
169+
private static SKMatrix MapNegativeYSquareToGivenPoints(SKPoint topLeft, SKPoint topRight, SKPoint bottomLeft, SKPoint bottomRight)
170+
{
171+
var rightDiff = topRight - bottomRight;
172+
var topDiff = topRight - topLeft;
173+
174+
var determinant = rightDiff.X * topDiff.Y - topDiff.X * rightDiff.Y;
175+
176+
var bottomDiff = bottomRight - bottomLeft;
177+
var leftDiff = topLeft - bottomLeft;
178+
179+
var persp0 = (bottomDiff.X * topDiff.Y - bottomDiff.Y * topDiff.X) / determinant;
180+
var persp1 = (rightDiff.Y * leftDiff.X - rightDiff.X * leftDiff.Y) / determinant;
181+
182+
return new SKMatrix
183+
{
184+
ScaleX = persp0 * bottomRight.X + bottomDiff.X,
185+
SkewX = persp1 * topLeft.X - leftDiff.X,
186+
TransX = bottomLeft.X,
187+
SkewY = persp0 * bottomRight.Y + bottomDiff.Y,
188+
ScaleY = persp1 * topLeft.Y - leftDiff.Y,
189+
TransY = bottomLeft.Y,
190+
Persp0 = persp0,
191+
Persp1 = persp1,
192+
Persp2 = 1,
193+
};
194+
}
195+
196+
private static SKMatrix MapNegativeXYSquareToGivenPoints(SKPoint topLeft, SKPoint topRight, SKPoint bottomLeft, SKPoint bottomRight)
197+
{
198+
var leftDiff = topLeft - bottomLeft;
199+
var topDiff = topLeft - topRight;
200+
201+
var determinant = leftDiff.X * topDiff.Y - topDiff.X * leftDiff.Y;
202+
203+
var bottomDiff = bottomLeft - bottomRight;
204+
var rightDiff = topRight - bottomRight;
205+
206+
var persp0 = (topDiff.X * bottomDiff.Y - topDiff.Y * bottomDiff.X) / determinant;
207+
var persp1 = (leftDiff.Y * rightDiff.X - leftDiff.X * rightDiff.Y) / determinant;
208+
209+
return new SKMatrix
210+
{
211+
ScaleX = persp0 * bottomLeft.X - bottomDiff.X,
212+
SkewX = persp1 * topRight.X - rightDiff.X,
213+
TransX = bottomRight.X,
214+
SkewY = persp0 * bottomLeft.Y - bottomDiff.Y,
215+
ScaleY = persp1 * topRight.Y - rightDiff.Y,
216+
TransY = bottomRight.Y,
217+
Persp0 = persp0,
218+
Persp1 = persp1,
219+
Persp2 = 1,
220+
};
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)