.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | The open-source VTuber toolkit.
16 |
17 |
18 | ## Getting Started
19 |
20 | ### Building
21 | Please make sure you meet the following prerequisites:
22 | - A desktop platform with .NET 7 SDK or above installed.
23 | - Access to [GitHub Packages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry).
24 |
25 | ## License
26 | Vignette is Copyright © 2023 Cosyne, licensed under GNU General Public License v3.0 with SDK exception. For the full license text please see the [LICENSE](./LICENSE) file in this repository.
--------------------------------------------------------------------------------
/Vignette.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31903.59
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "source", "source", "{287C23D6-960D-4ACA-8C4C-5833A62D9AEC}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vignette", "source\Vignette\Vignette.csproj", "{25C6A684-ABFE-4A8F-8DCD-B936E92A4FC7}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vignette.Desktop", "source\Vignette.Desktop\Vignette.Desktop.csproj", "{48F80885-59E2-4289-B65A-CB2F50EB981A}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(SolutionProperties) = preSolution
18 | HideSolutionNode = FALSE
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {25C6A684-ABFE-4A8F-8DCD-B936E92A4FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {25C6A684-ABFE-4A8F-8DCD-B936E92A4FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {25C6A684-ABFE-4A8F-8DCD-B936E92A4FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
24 | {25C6A684-ABFE-4A8F-8DCD-B936E92A4FC7}.Release|Any CPU.Build.0 = Release|Any CPU
25 | {48F80885-59E2-4289-B65A-CB2F50EB981A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {48F80885-59E2-4289-B65A-CB2F50EB981A}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {48F80885-59E2-4289-B65A-CB2F50EB981A}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {48F80885-59E2-4289-B65A-CB2F50EB981A}.Release|Any CPU.Build.0 = Release|Any CPU
29 | EndGlobalSection
30 | GlobalSection(NestedProjects) = preSolution
31 | {25C6A684-ABFE-4A8F-8DCD-B936E92A4FC7} = {287C23D6-960D-4ACA-8C4C-5833A62D9AEC}
32 | {48F80885-59E2-4289-B65A-CB2F50EB981A} = {287C23D6-960D-4ACA-8C4C-5833A62D9AEC}
33 | EndGlobalSection
34 | EndGlobal
35 |
--------------------------------------------------------------------------------
/analysis/BannedSymbols.txt:
--------------------------------------------------------------------------------
1 | M:System.Object.Equals(System.Object,System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable or EqualityComparer.Default instead.
2 | M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable or EqualityComparer.Default instead.
3 | M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable or EqualityComparer.Default instead.
4 | T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
5 | M:System.Guid.#ctor;You probably meant to use Guid.NewGuid() instead. If you actually want empty, use Guid.Empty.
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vignetteapp/vignette/70eb27079a7f928e329d4a7fdc1ecbc9486c8aa8/assets/logo.png
--------------------------------------------------------------------------------
/source/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | Vignette
4 | The open source VTuber software.
5 | Cosyne
6 | Copyright (c) 2023 Cosyne
7 | README.md
8 | LICENSE
9 | https://github.com/vignetteapp/vignette
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/source/Vignette.Desktop/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using Sekai;
5 | using Vignette;
6 |
7 | Host.Run(new HostOptions { Name = "Vignette" });
8 |
--------------------------------------------------------------------------------
/source/Vignette.Desktop/Vignette.Desktop.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | WinExe
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/source/Vignette/Allocation/IObjectPool.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | namespace Vignette.Allocation;
5 |
6 | ///
7 | /// Defines a mechanism for objects that can pool .
8 | ///
9 | /// The type of object being pooled.
10 | public interface IObjectPool
11 | {
12 | ///
13 | /// Gets one from the pool.
14 | ///
15 | T Get();
16 |
17 | ///
18 | /// Returns back to the pool.
19 | ///
20 | /// The to return.
21 | /// if the item has been returned. Otherwise, .
22 | bool Return(T item);
23 | }
24 |
--------------------------------------------------------------------------------
/source/Vignette/Audio/AudioManager.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections.Concurrent;
6 | using System.Collections.Generic;
7 | using Sekai.Audio;
8 | using Vignette.Allocation;
9 |
10 | namespace Vignette.Audio;
11 |
12 | public sealed class AudioManager : IObjectPool
13 | {
14 | private const int max_buffer_size = 8192;
15 | private const int max_buffer_count = 500;
16 | private readonly AudioDevice device;
17 | private readonly ConcurrentBag bufferPool = new();
18 | private readonly List controllers = new();
19 |
20 | internal AudioManager(AudioDevice device)
21 | {
22 | this.device = device;
23 | }
24 |
25 | ///
26 | /// Creates a new for a .
27 | ///
28 | /// The audio stream to attach to the controller.
29 | /// An audio controller.
30 | public IAudioController GetController(AudioStream stream)
31 | {
32 | return new StreamingAudioController(device.CreateSource(), stream, this);
33 | }
34 |
35 | internal void Update()
36 | {
37 | for (int i = 0; i < controllers.Count; i++)
38 | {
39 | controllers[i].Update();
40 | }
41 | }
42 |
43 | ///
44 | /// Returns an back to the .
45 | ///
46 | /// The controller to return.
47 | public void Return(IAudioController controller)
48 | {
49 | if (controller is not StreamingAudioController streaming)
50 | {
51 | return;
52 | }
53 |
54 | if (!controllers.Remove(streaming))
55 | {
56 | return;
57 | }
58 |
59 | streaming.Dispose();
60 | }
61 |
62 | AudioBuffer IObjectPool.Get()
63 | {
64 | if (!bufferPool.TryTake(out var buffer))
65 | {
66 | buffer = device.CreateBuffer();
67 | }
68 |
69 | return buffer;
70 | }
71 |
72 | bool IObjectPool.Return(AudioBuffer item)
73 | {
74 | if (bufferPool.Count >= max_buffer_count)
75 | {
76 | item.Dispose();
77 | return false;
78 | }
79 |
80 | bufferPool.Add(item);
81 | return true;
82 | }
83 |
84 | private sealed class StreamingAudioController : IAudioController, IDisposable
85 | {
86 | public bool Loop { get; set; }
87 |
88 | public TimeSpan Position
89 | {
90 | get => getTimeFromByteCount((int)stream.Position, stream.Format, stream.SampleRate);
91 | set => seek(getByteCountFromTime(value, stream.Format, stream.SampleRate));
92 | }
93 |
94 | public TimeSpan Duration => getTimeFromByteCount((int)stream.Length, stream.Format, stream.SampleRate);
95 |
96 | public TimeSpan Buffered => getTimeFromByteCount(buffered, stream.Format, stream.SampleRate);
97 |
98 | public AudioSourceState State => source.State;
99 |
100 | private int buffered;
101 | private bool isDisposed;
102 | private const int max_buffer_stream = 4;
103 | private readonly AudioSource source;
104 | private readonly AudioStream stream;
105 | private readonly IObjectPool bufferPool;
106 |
107 | public StreamingAudioController(AudioSource source, AudioStream stream, IObjectPool bufferPool)
108 | {
109 | this.source = source;
110 | this.stream = stream;
111 | this.bufferPool = bufferPool;
112 | }
113 |
114 | public void Play()
115 | {
116 | if (State != AudioSourceState.Paused)
117 | {
118 | seek(0);
119 |
120 | for (int i = 0; i < max_buffer_stream; i++)
121 | {
122 | var buffer = bufferPool.Get();
123 |
124 | if (!allocate(buffer))
125 | {
126 | break;
127 | }
128 |
129 | source.Enqueue(buffer);
130 | }
131 | }
132 |
133 | source.Play();
134 | }
135 |
136 | public void Stop()
137 | {
138 | seek(0);
139 | }
140 |
141 | public void Pause()
142 | {
143 | source.Pause();
144 | }
145 |
146 | public void Update()
147 | {
148 | while (source.TryDequeue(out var buffer))
149 | {
150 | if (!allocate(buffer))
151 | {
152 | source.Loop = Loop;
153 | break;
154 | }
155 |
156 | source.Enqueue(buffer);
157 | }
158 | }
159 |
160 | public void Dispose()
161 | {
162 | if (isDisposed)
163 | {
164 | return;
165 | }
166 |
167 | source.Stop();
168 |
169 | while(source.TryDequeue(out var buffer))
170 | {
171 | bufferPool.Return(buffer);
172 | }
173 |
174 | source.Dispose();
175 |
176 | isDisposed = false;
177 | }
178 |
179 | private void seek(int position)
180 | {
181 | source.Stop();
182 | source.Clear();
183 | stream.Position = buffered = position;
184 | }
185 |
186 | private bool allocate(AudioBuffer buffer)
187 | {
188 | Span data = stackalloc byte[max_buffer_size];
189 | int read = stream.Read(data);
190 |
191 | if (read <= 0)
192 | {
193 | return false;
194 | }
195 |
196 | buffer.SetData(data[..read], stream.Format, stream.SampleRate);
197 | buffered += read;
198 |
199 | return true;
200 | }
201 | }
202 |
203 | private static int getChannelCount(AudioFormat format)
204 | {
205 | return format is AudioFormat.Stereo8 or AudioFormat.Stereo16 ? 2 : 1;
206 | }
207 |
208 | private static int getSamplesCount(AudioFormat format)
209 | {
210 | return format is AudioFormat.Stereo8 or AudioFormat.Mono8 ? 8 : 16;
211 | }
212 |
213 | private static int getByteCountFromTime(TimeSpan time, AudioFormat format, int sampleRate)
214 | {
215 | return (int)time.TotalSeconds * sampleRate * getChannelCount(format) * (getSamplesCount(format) / 8);
216 | }
217 |
218 | private static TimeSpan getTimeFromByteCount(int count, AudioFormat format, int sampleRate)
219 | {
220 | return TimeSpan.FromSeconds(count / (sampleRate * getChannelCount(format) * (getSamplesCount(format) / 8)));
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/source/Vignette/Audio/AudioStream.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System.IO;
5 | using Sekai.Audio;
6 |
7 | namespace Vignette.Audio;
8 |
9 | ///
10 | /// A containing pulse code modulation (PCM) audio data.
11 | ///
12 | public class AudioStream : Stream
13 | {
14 | ///
15 | /// The audio stream's sample rate
16 | ///
17 | public int SampleRate { get; }
18 |
19 | ///
20 | /// The audio stream's format.
21 | ///
22 | public AudioFormat Format { get; }
23 |
24 | public override bool CanRead => stream.CanRead;
25 |
26 | public override bool CanSeek => stream.CanSeek;
27 |
28 | public override bool CanWrite => stream.CanWrite;
29 |
30 | public override long Length => stream.Length;
31 |
32 | public override long Position
33 | {
34 | get => stream.Position;
35 | set => stream.Position = value;
36 | }
37 |
38 | private bool isDisposed;
39 | private readonly MemoryStream stream;
40 |
41 | public AudioStream(byte[] buffer, AudioFormat format, int sampleRate)
42 | : this(buffer, true, format, sampleRate)
43 | {
44 | }
45 |
46 | public AudioStream(byte[] buffer, bool writable, AudioFormat format, int sampleRate)
47 | {
48 | Format = format;
49 | stream = new MemoryStream(buffer, writable);
50 | SampleRate = sampleRate;
51 | }
52 |
53 | public AudioStream(byte[] buffer, int index, int count, AudioFormat format, int sampleRate)
54 | : this(buffer, index, count, true, format, sampleRate)
55 | {
56 | }
57 |
58 | public AudioStream(byte[] buffer, int index, int count, bool writable, AudioFormat format, int sampleRate)
59 | {
60 | Format = format;
61 | stream = new MemoryStream(buffer, index, count, writable);
62 | SampleRate = sampleRate;
63 | }
64 |
65 | public AudioStream(int capacity, AudioFormat format, int sampleRate)
66 | {
67 | Format = format;
68 | stream = new MemoryStream(capacity);
69 | SampleRate = sampleRate;
70 | }
71 |
72 | public AudioStream(AudioFormat format, int sampleRate)
73 | : this(0, format, sampleRate)
74 | {
75 | }
76 |
77 | public override void Flush()
78 | {
79 | stream.Flush();
80 | }
81 |
82 | public override int Read(byte[] buffer, int offset, int count)
83 | {
84 | return stream.Read(buffer, offset, count);
85 | }
86 |
87 | public override long Seek(long offset, SeekOrigin origin)
88 | {
89 | return stream.Seek(offset, origin);
90 | }
91 |
92 | public override void SetLength(long value)
93 | {
94 | stream.SetLength(value);
95 | }
96 |
97 | public override void Write(byte[] buffer, int offset, int count)
98 | {
99 | stream.Write(buffer, offset, count);
100 | }
101 |
102 | protected override void Dispose(bool disposing)
103 | {
104 | if (isDisposed)
105 | {
106 | return;
107 | }
108 |
109 | stream.Dispose();
110 |
111 | isDisposed = true;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/source/Vignette/Audio/IAudioController.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using Sekai.Audio;
6 |
7 | namespace Vignette.Audio;
8 |
9 | ///
10 | /// Provides access to audio playback controls.
11 | ///
12 | public interface IAudioController
13 | {
14 | ///
15 | /// Gets or sets whether audio playback should loop.
16 | ///
17 | bool Loop { get; set; }
18 |
19 | ///
20 | /// Gets or seeks the current playback position.
21 | ///
22 | TimeSpan Position { get; set; }
23 |
24 | ///
25 | /// Gets total playable duration.
26 | ///
27 | TimeSpan Duration { get; }
28 |
29 | ///
30 | /// Gets the duration of the buffered data.
31 | ///
32 | TimeSpan Buffered { get; }
33 |
34 | ///
35 | /// Gets the state of this audio controller.
36 | ///
37 | AudioSourceState State { get; }
38 |
39 | ///
40 | /// Starts audio playback.
41 | ///
42 | void Play();
43 |
44 | ///
45 | /// Stops audio playback.
46 | ///
47 | void Stop();
48 |
49 | ///
50 | /// Pauses audio playback.
51 | ///
52 | void Pause();
53 | }
54 |
--------------------------------------------------------------------------------
/source/Vignette/Behavior.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 |
6 | namespace Vignette;
7 |
8 | ///
9 | /// A that processes itself per-frame.
10 | ///
11 | public abstract class Behavior : Node, IComparable
12 | {
13 | ///
14 | /// The processing order for this .
15 | ///
16 | public int Order
17 | {
18 | get => order;
19 | set
20 | {
21 | if (order.Equals(value))
22 | {
23 | return;
24 | }
25 |
26 | order = value;
27 | OrderChanged?.Invoke(this, EventArgs.Empty);
28 | }
29 | }
30 |
31 | ///
32 | /// Whether this should be enabled or not affecting calls.
33 | ///
34 | public bool Enabled
35 | {
36 | get => enabled;
37 | set
38 | {
39 | if (enabled.Equals(value))
40 | {
41 | return;
42 | }
43 |
44 | enabled = value;
45 | EnabledChanged?.Invoke(this, EventArgs.Empty);
46 | }
47 | }
48 |
49 | ///
50 | /// Called when has been changed.
51 | ///
52 | public event EventHandler? OrderChanged;
53 |
54 | ///
55 | /// Called when has been changed.
56 | ///
57 | public event EventHandler? EnabledChanged;
58 |
59 | private int order;
60 | private bool enabled = true;
61 |
62 | ///
63 | /// Called once in the update loop after the has entered the node graph.
64 | ///
65 | public virtual void Load()
66 | {
67 | }
68 |
69 | ///
70 | /// Called every frame to perform updates on this .
71 | ///
72 | /// The time elapsed between frames.
73 | public virtual void Update(TimeSpan elapsed)
74 | {
75 | }
76 |
77 | ///
78 | /// Called once in the update loop before the exits the node graph.
79 | ///
80 | public virtual void Unload()
81 | {
82 | }
83 |
84 | public int CompareTo(Behavior? other)
85 | {
86 | if (other is null)
87 | {
88 | return -1;
89 | }
90 |
91 | int value = Depth.CompareTo(other.Depth);
92 |
93 | if (value != 0)
94 | {
95 | return value;
96 | }
97 |
98 | return Order.CompareTo(other.Order);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/source/Vignette/Camera.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Numerics;
6 | using Sekai.Mathematics;
7 | using Vignette.Graphics;
8 |
9 | namespace Vignette;
10 |
11 | public class Camera : Node, IProjector
12 | {
13 | ///
14 | /// The near plane distance.
15 | ///
16 | public float NearPlane = 0.1f;
17 |
18 | ///
19 | /// The far plane distance.
20 | ///
21 | public float FarPlane = 1000f;
22 |
23 | ///
24 | /// The camera's aspect ratio.
25 | ///
26 | /// Used when is .
27 | public float AspectRatio = 16.0f / 9.0f;
28 |
29 | ///
30 | /// The camera's field of view.
31 | ///
32 | public float FieldOfView = 60.0f;
33 |
34 | ///
35 | /// The camera's view size.
36 | ///
37 | public SizeF ViewSize = SizeF.Zero;
38 |
39 | ///
40 | /// The camera's view scale.
41 | ///
42 | public Vector2 ViewScale = Vector2.One;
43 |
44 | ///
45 | /// The camera's top left position.
46 | ///
47 | public Vector2 ViewTopLeft = Vector2.Zero;
48 |
49 | ///
50 | /// The camera projection mode.
51 | ///
52 | public CameraProjectionMode ProjectionMode = CameraProjectionMode.OrthographicOffCenter;
53 |
54 | ///
55 | /// The camera's rendering groups.
56 | ///
57 | public RenderGroup Groups { get; set; } = RenderGroup.Default;
58 |
59 | ///
60 | /// The camera's view frustum.
61 | ///
62 | public BoundingFrustum Frustum => BoundingFrustum.FromMatrix(((IProjector)this).ProjMatrix);
63 |
64 | Matrix4x4 IProjector.ViewMatrix => Matrix4x4.CreateLookAt(Position, Position + Vector3.Transform(-Vector3.UnitZ, Matrix4x4.CreateFromYawPitchRoll(Rotation.Y, Rotation.X, Rotation.Z)), Vector3.UnitY);
65 |
66 | Matrix4x4 IProjector.ProjMatrix => ProjectionMode switch
67 | {
68 | CameraProjectionMode.Perspective => Matrix4x4.CreatePerspective(ViewSize.Width * ViewScale.X, ViewSize.Height * ViewScale.Y, NearPlane, FarPlane),
69 | CameraProjectionMode.PerspectiveOffCenter => Matrix4x4.CreatePerspectiveOffCenter(ViewTopLeft.X, ViewSize.Width * ViewScale.X, ViewSize.Height * ViewScale.Y, ViewTopLeft.Y, NearPlane, FarPlane),
70 | CameraProjectionMode.PerspectiveFieldOfView => Matrix4x4.CreatePerspectiveFieldOfView(FieldOfView, AspectRatio, NearPlane, FarPlane),
71 | CameraProjectionMode.Orthographic => Matrix4x4.CreateOrthographic(ViewSize.Width * ViewScale.X, ViewSize.Height * ViewScale.Y, NearPlane, FarPlane),
72 | CameraProjectionMode.OrthographicOffCenter => Matrix4x4.CreateOrthographicOffCenter(ViewTopLeft.X, ViewSize.Width * ViewScale.X, ViewSize.Height * ViewScale.Y, ViewTopLeft.Y, NearPlane, FarPlane),
73 | _ => throw new InvalidOperationException($"Unknown {nameof(ProjectionMode)} {ProjectionMode}."),
74 | };
75 | }
76 |
77 | ///
78 | /// An enumeration of camera projection modes.
79 | ///
80 | public enum CameraProjectionMode
81 | {
82 | ///
83 | /// Orthographic projection.
84 | ///
85 | Orthographic,
86 |
87 | ///
88 | /// Custom orthographic projection.
89 | ///
90 | OrthographicOffCenter,
91 |
92 | ///
93 | /// Perspective projection.
94 | ///
95 | Perspective,
96 |
97 | ///
98 | /// Custom perspective projection.
99 | ///
100 | PerspectiveOffCenter,
101 |
102 | ///
103 | /// Perspective field of view projection.
104 | ///
105 | PerspectiveFieldOfView,
106 | }
107 |
--------------------------------------------------------------------------------
/source/Vignette/Collections/SortedFilteredCollection.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections;
6 | using System.Collections.Generic;
7 |
8 | namespace Vignette.Collections;
9 |
10 | ///
11 | /// A collection whose items are sorted and filtered.
12 | ///
13 | /// The type this collection will contain.
14 | public class SortedFilteredCollection : ICollection
15 | {
16 | public int Count => items.Count;
17 |
18 | private bool shouldRebuildCache;
19 | private readonly List items = new();
20 | private readonly List cache = new();
21 | private readonly IComparer sorter;
22 | private readonly Predicate filter;
23 | private readonly Action sorterChangedSubscriber;
24 | private readonly Action sorterChangedUnsubscriber;
25 | private readonly Action filterChangedSubscriber;
26 | private readonly Action filterChangedUnsubscriber;
27 |
28 | public SortedFilteredCollection(
29 | IComparer sorter,
30 | Predicate filter,
31 | Action sorterChangedSubscriber,
32 | Action sorterChangedUnsubscriber,
33 | Action filterChangedSubscriber,
34 | Action filterChangedUnsubscriber
35 | )
36 | {
37 | this.sorter = sorter;
38 | this.filter = filter;
39 | this.sorterChangedSubscriber = sorterChangedSubscriber;
40 | this.sorterChangedUnsubscriber = sorterChangedUnsubscriber;
41 | this.filterChangedSubscriber = filterChangedSubscriber;
42 | this.filterChangedUnsubscriber = filterChangedUnsubscriber;
43 | }
44 |
45 | public void Add(T item)
46 | {
47 | items.Add(item);
48 | invalidate();
49 | }
50 |
51 | public bool Remove(T item)
52 | {
53 | if (!items.Remove(item))
54 | {
55 | return false;
56 | }
57 |
58 | invalidate();
59 | unsubscribeFromEvents(item);
60 |
61 | return true;
62 | }
63 |
64 | public void Clear()
65 | {
66 | for (int i = 0; i < items.Count; i++)
67 | {
68 | unsubscribeFromEvents(items[i]);
69 | }
70 |
71 | items.Clear();
72 | invalidate();
73 | }
74 |
75 | public bool Contains(T item)
76 | {
77 | return items.Contains(item);
78 | }
79 |
80 | public IEnumerator GetEnumerator()
81 | {
82 | if (shouldRebuildCache)
83 | {
84 | cache.Clear();
85 |
86 | for (int i = 0; i < items.Count; i++)
87 | {
88 | var item = items[i];
89 |
90 | if (filter(item))
91 | {
92 | cache.Add(item);
93 | subscribeToEvents(item);
94 | }
95 | }
96 |
97 | if (cache.Count > 0)
98 | {
99 | cache.Sort(sorter);
100 | }
101 |
102 | shouldRebuildCache = false;
103 | }
104 |
105 | return cache.GetEnumerator();
106 | }
107 |
108 | private void invalidate()
109 | {
110 | shouldRebuildCache = true;
111 | }
112 |
113 | private void subscribeToEvents(T item)
114 | {
115 | sorterChangedSubscriber(item, handleSorterChanged);
116 | filterChangedSubscriber(item, handleFilterChanged);
117 | }
118 |
119 | private void unsubscribeFromEvents(T item)
120 | {
121 | sorterChangedUnsubscriber(item, handleSorterChanged);
122 | filterChangedUnsubscriber(item, handleFilterChanged);
123 | }
124 |
125 | private void handleSorterChanged(object? sender, EventArgs args)
126 | {
127 | unsubscribeFromEvents((T)sender!);
128 | invalidate();
129 | }
130 |
131 | private void handleFilterChanged(object? sender, EventArgs args)
132 | {
133 | invalidate();
134 | }
135 |
136 | IEnumerator IEnumerable.GetEnumerator()
137 | {
138 | return GetEnumerator();
139 | }
140 |
141 | bool ICollection.IsReadOnly => false;
142 |
143 | void ICollection.CopyTo(T[] array, int arrayIndex) => items.CopyTo(array, arrayIndex);
144 | }
145 |
--------------------------------------------------------------------------------
/source/Vignette/Content/ContentManager.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Diagnostics.CodeAnalysis;
7 | using System.IO;
8 | using Sekai.Storages;
9 |
10 | namespace Vignette.Content;
11 |
12 | ///
13 | /// Manages content.
14 | ///
15 | public sealed class ContentManager
16 | {
17 | private readonly Storage storage;
18 | private readonly HashSet extensions = new();
19 | private readonly HashSet loaders = new();
20 | private readonly Dictionary content = new();
21 |
22 | internal ContentManager(Storage storage)
23 | {
24 | this.storage = storage;
25 | }
26 |
27 | ///
28 | /// Reads a file from and loads it as .
29 | ///
30 | /// The type of content to load.
31 | /// The path to the content.
32 | /// The loaded content.
33 | /// Thrown when invalid path has been passed.
34 | public T Load([StringSyntax(StringSyntaxAttribute.Uri)] string path)
35 | where T : class
36 | {
37 | string ext = Path.GetExtension(path);
38 |
39 | if (string.IsNullOrEmpty(ext))
40 | {
41 | throw new ArgumentException($"Failed to determine file extension.", nameof(path));
42 | }
43 |
44 | if (!extensions.Contains(ext))
45 | {
46 | throw new ArgumentException($"Cannot load unsupported file extension \"{ext}\".");
47 | }
48 |
49 | var key = new ContentKey(typeof(T), path);
50 |
51 | if (!content.TryGetValue(key, out var weak))
52 | {
53 | weak = new WeakReference(null);
54 | content.Add(key, weak);
55 | }
56 |
57 | if (!weak.IsAlive)
58 | {
59 | weak.Target = Load(storage.Open(path, FileMode.Open, FileAccess.Read));
60 | }
61 |
62 | return (T)weak.Target!;
63 | }
64 |
65 | ///
66 | /// Loads a as .
67 | ///
68 | /// The type of content to load.
69 | /// The stream to be read.
70 | /// The loaded content.
71 | /// Thrown when the stream can't be read.
72 | public T Load(Stream stream)
73 | where T : class
74 | {
75 | Span buffer = stackalloc byte[(int)stream.Length];
76 |
77 | if (stream.Read(buffer) <= 0)
78 | {
79 | throw new InvalidOperationException("Failed to read stream.");
80 | }
81 |
82 | return Load((ReadOnlySpan)buffer);
83 | }
84 |
85 | ///
86 | /// Loads a as .
87 | ///
88 | /// The type of content to load.
89 | /// The byte data to be read.
90 | /// The loaded content.
91 | public T Load(byte[] bytes)
92 | where T : class
93 | {
94 | return Load(bytes);
95 | }
96 |
97 | ///
98 | /// Loads a as .
99 | ///
100 | /// The type of content to load.
101 | /// The byte data to be read.
102 | /// The loaded content.
103 | /// Thrown when no loader was able to load the content.
104 | public T Load(ReadOnlySpan bytes)
105 | where T : class
106 | {
107 | var result = default(T);
108 |
109 | foreach (var loader in loaders)
110 | {
111 | if (loader is not IContentLoader typedLoader)
112 | {
113 | continue;
114 | }
115 |
116 | try
117 | {
118 | result = typedLoader.Load(bytes);
119 | break;
120 | }
121 | catch
122 | {
123 | }
124 | }
125 |
126 | if (result is null)
127 | {
128 | throw new InvalidOperationException("Failed to load content.");
129 | }
130 |
131 | return result;
132 | }
133 |
134 | ///
135 | /// Adds a content loader to the content manager.
136 | ///
137 | /// The content loader to add.
138 | /// The file extensions supported by this loader.
139 | /// Thrown when
140 | internal void Add(IContentLoader loader, params string[] extensions)
141 | {
142 | foreach (string extension in extensions)
143 | {
144 | string ext = extension.StartsWith(ext_separator) ? extension : ext_separator + extension;
145 | this.loaders.Add(loader);
146 | this.extensions.Add(ext);
147 | }
148 | }
149 |
150 | private const char ext_separator = '.';
151 |
152 | private readonly record struct ContentKey(Type Type, string Path);
153 | }
154 |
--------------------------------------------------------------------------------
/source/Vignette/Content/IContentLoader.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 |
6 | namespace Vignette.Content;
7 |
8 | ///
9 | /// Defines a mechanism for loading content.
10 | ///
11 | public interface IContentLoader
12 | {
13 | }
14 |
15 | ///
16 | /// Defines a mechanism for loading .
17 | ///
18 | /// The type of content it loads.
19 | public interface IContentLoader : IContentLoader
20 | where T : class
21 | {
22 | ///
23 | /// Loads a as .
24 | ///
25 | /// The byte data to be read.
26 | /// The loaded content.
27 | T Load(ReadOnlySpan bytes);
28 | }
29 |
--------------------------------------------------------------------------------
/source/Vignette/Content/ShaderLoader.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Text;
6 | using Vignette.Graphics;
7 |
8 | namespace Vignette.Content;
9 |
10 | internal sealed class ShaderLoader : IContentLoader
11 | {
12 | public ShaderMaterial Load(ReadOnlySpan bytes)
13 | {
14 | return ShaderMaterial.Create(Encoding.UTF8.GetString(bytes));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/source/Vignette/Content/TextureLoader.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using Sekai.Graphics;
6 | using StbiSharp;
7 |
8 | namespace Vignette.Content;
9 |
10 | internal sealed class TextureLoader : IContentLoader
11 | {
12 | private readonly GraphicsDevice device;
13 |
14 | public TextureLoader(GraphicsDevice device)
15 | {
16 | this.device = device;
17 | }
18 |
19 | public Texture Load(ReadOnlySpan bytes)
20 | {
21 | var image = Stbi.LoadFromMemory(bytes, 4);
22 |
23 | var texture = device.CreateTexture(new TextureDescription
24 | (
25 | image.Width,
26 | image.Height,
27 | PixelFormat.R8G8B8A8_UNorm,
28 | 1,
29 | 1,
30 | TextureUsage.Resource
31 | ));
32 |
33 | texture.SetData(image.Data, 0, 0, 0, 0, 0, image.Width, image.Height, 0);
34 |
35 | return texture;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/source/Vignette/Content/WaveAudioLoader.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Text;
6 | using Sekai.Audio;
7 | using Vignette.Audio;
8 |
9 | namespace Vignette.Content;
10 |
11 | internal sealed class WaveAudioLoader : IContentLoader
12 | {
13 | public AudioStream Load(ReadOnlySpan bytes)
14 | {
15 | if (!MemoryExtensions.SequenceEqual(bytes[0..4], header_riff))
16 | throw new ArgumentException(@"Failed to find ""RIFF"" header at position 0.", nameof(bytes));
17 |
18 | if (!MemoryExtensions.SequenceEqual(bytes[8..12], header_wave))
19 | throw new ArgumentException(@"Failed to find ""WAVE"" header at position 8.", nameof(bytes));
20 |
21 | if (!MemoryExtensions.SequenceEqual(bytes[12..16], header_fmt))
22 | throw new ArgumentException(@"Failed to find ""fmt "" header at position 12.", nameof(bytes));
23 |
24 | if (!MemoryExtensions.SequenceEqual(bytes[36..40], header_data))
25 | throw new ArgumentException(@"Failed to find ""data"" header at position 36.", nameof(bytes));
26 |
27 | short contentType = BitConverter.ToInt16(bytes[20..22]);
28 |
29 | if (contentType != 1)
30 | {
31 | throw new ArgumentException(@"Content is not PCM data.", nameof(bytes));
32 | }
33 |
34 | short numChannels = BitConverter.ToInt16(bytes[22..24]);
35 | short bitsPerSamp = BitConverter.ToInt16(bytes[34..36]);
36 |
37 | var format = numChannels == 2
38 | ? bitsPerSamp == 8 ? AudioFormat.Stereo8 : AudioFormat.Stereo16
39 | : bitsPerSamp == 8 ? AudioFormat.Mono8 : AudioFormat.Mono16;
40 |
41 | int rate = BitConverter.ToInt32(bytes[24..28]);
42 | int size = BitConverter.ToInt32(bytes[40..44]);
43 |
44 | var stream = new AudioStream(size, format, rate);
45 | stream.Write(bytes[44..size]);
46 |
47 | return stream;
48 | }
49 |
50 | private static readonly byte[] header_riff = Encoding.ASCII.GetBytes("RIFF");
51 | private static readonly byte[] header_wave = Encoding.ASCII.GetBytes("WAVE");
52 | private static readonly byte[] header_data = Encoding.ASCII.GetBytes("data");
53 | private static readonly byte[] header_fmt = Encoding.ASCII.GetBytes("fmt ");
54 | }
55 |
--------------------------------------------------------------------------------
/source/Vignette/Drawable.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using Vignette.Graphics;
6 |
7 | namespace Vignette;
8 |
9 | ///
10 | /// A that is capable of drawing.
11 | ///
12 | public abstract class Drawable : Behavior
13 | {
14 | ///
15 | /// Whether this should be drawn or not.
16 | ///
17 | public bool Visible
18 | {
19 | get => visible;
20 | set
21 | {
22 | if (visible.Equals(value))
23 | {
24 | return;
25 | }
26 |
27 | visible = value;
28 | VisibleChanged?.Invoke(this, EventArgs.Empty);
29 | }
30 | }
31 |
32 | ///
33 | /// Called when has been changed.
34 | ///
35 | public event EventHandler? VisibleChanged;
36 |
37 | private bool visible = true;
38 |
39 | ///
40 | /// Called every frame to perform drawing operations on this .
41 | ///
42 | /// The drawable rendering context.
43 | public virtual void Draw(RenderContext context)
44 | {
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/Effect.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections;
6 | using System.Collections.Generic;
7 | using System.Collections.Immutable;
8 | using System.Diagnostics.CodeAnalysis;
9 | using Sekai.Graphics;
10 |
11 | namespace Vignette.Graphics;
12 |
13 | public readonly struct Effect : IEquatable
14 | {
15 | private readonly ShaderCode[] shaderCodes;
16 |
17 | private Effect(params ShaderCode[] shaderCodes)
18 | {
19 | this.shaderCodes = shaderCodes;
20 | }
21 |
22 | public readonly bool Equals(Effect other)
23 | {
24 | return ((IStructuralEquatable)shaderCodes).Equals(other.shaderCodes, EqualityComparer.Default);
25 | }
26 |
27 | public override readonly bool Equals([NotNullWhen(true)] object? obj)
28 | {
29 | return obj is Effect effect && Equals(effect);
30 | }
31 |
32 | public override readonly int GetHashCode()
33 | {
34 | HashCode hash = default;
35 |
36 | for (int i = 0; i < shaderCodes.Length; i++)
37 | {
38 | hash.Add(shaderCodes[i]);
39 | }
40 |
41 | return hash.ToHashCode();
42 | }
43 |
44 | ///
45 | /// Creates a new from HLSL shader code.
46 | ///
47 | /// The HLSL shader code.
48 | /// The reflected input layout.
49 | /// The reflected shader properties.
50 | /// A new .
51 | internal static Effect From(string code, out InputLayoutDescription layout, out IProperty[] properties)
52 | {
53 | code = sh_common + code;
54 |
55 | var shVert = ShaderCode.From(code, ShaderStage.Vertex, sh_vert, ShaderLanguage.HLSL);
56 | var shFrag = ShaderCode.From(code, ShaderStage.Fragment, sh_frag, ShaderLanguage.HLSL);
57 |
58 | var shVertReflect = shVert.Reflect();
59 | var shFragReflect = shFrag.Reflect();
60 |
61 | if (shVertReflect.Inputs is not null)
62 | {
63 | var format = new InputLayoutMember[shVertReflect.Inputs.Length];
64 |
65 | for (int i = 0; i < shVertReflect.Inputs.Length; i++)
66 | {
67 | format[i] = format_members[shVertReflect.Inputs[i].Type];
68 | }
69 |
70 | layout = new(format);
71 | }
72 | else
73 | {
74 | layout = new();
75 | }
76 |
77 | var textures = new List();
78 | var uniforms = new List();
79 |
80 | if (shVertReflect.Uniforms is not null)
81 | {
82 | uniforms.AddRange(shVertReflect.Uniforms);
83 | }
84 |
85 | if (shVertReflect.Textures is not null)
86 | {
87 | textures.AddRange(shVertReflect.Textures);
88 | }
89 |
90 | if (shFragReflect.Uniforms is not null)
91 | {
92 | uniforms.AddRange(shFragReflect.Uniforms);
93 | }
94 |
95 | if (shFragReflect.Textures is not null)
96 | {
97 | textures.AddRange(shFragReflect.Textures);
98 | }
99 |
100 | var props = new List();
101 |
102 | foreach (var uniform in uniforms)
103 | {
104 | if (uniform.Name.StartsWith(sh_global))
105 | {
106 | continue;
107 | }
108 |
109 | props.Add(new UniformProperty(uniform.Name, uniform.Binding));
110 | }
111 |
112 | foreach (var texture in textures)
113 | {
114 | if (texture.Name.StartsWith(sh_global))
115 | {
116 | continue;
117 | }
118 |
119 | props.Add(new TextureProperty(texture.Name, texture.Binding));
120 | }
121 |
122 | properties = props.ToArray();
123 |
124 | return new Effect(shVert, shFrag);
125 | }
126 |
127 | public static bool operator ==(Effect left, Effect right)
128 | {
129 | return left.Equals(right);
130 | }
131 |
132 | public static bool operator !=(Effect left, Effect right)
133 | {
134 | return !(left == right);
135 | }
136 |
137 | public static explicit operator ShaderCode[](Effect effect) => effect.shaderCodes;
138 |
139 | public const int GLOBAL_TRANSFORM_ID = 89;
140 |
141 | private const string sh_vert = "Vertex";
142 | private const string sh_frag = "Pixel";
143 | private const string sh_global = "g_internal_";
144 |
145 | private static readonly string sh_common =
146 | @$"
147 | #define P_MATRIX g_internal_ProjMatrix
148 | #define V_MATRIX g_internal_ViewMatrix
149 | #define M_MATRIX g_internal_ModelMatrix
150 | #define OBJECT_TO_CLIP(a) mul(mul(V_MATRIX, M_MATRIX), a)
151 | #define OBJECT_TO_VIEW(a) mul(P_MATRIX, OBJECT_TO_CLIP(a))
152 |
153 | cbuffer g_internal_Transform : register(b{GLOBAL_TRANSFORM_ID})
154 | {{
155 | float4x4 g_internal_ProjMatrix;
156 | float4x4 g_internal_ViewMatrix;
157 | float4x4 g_internal_ModelMatrix;
158 | }};
159 | ";
160 |
161 | private static readonly IReadOnlyDictionary format_members = ImmutableDictionary.CreateRange
162 | (
163 | new KeyValuePair[]
164 | {
165 | KeyValuePair.Create("int", new(1, false, InputLayoutFormat.Int)),
166 | KeyValuePair.Create("ivec2", new(2, false, InputLayoutFormat.Int)),
167 | KeyValuePair.Create("ivec3", new(3, false, InputLayoutFormat.Int)),
168 | KeyValuePair.Create("ivec4", new(4, false, InputLayoutFormat.Int)),
169 | KeyValuePair.Create("imat2", new(4, false, InputLayoutFormat.Int)),
170 | KeyValuePair.Create("imat2x3", new(6, false, InputLayoutFormat.Int)),
171 | KeyValuePair.Create("imat2x4", new(8, false, InputLayoutFormat.Int)),
172 | KeyValuePair.Create("imat3", new(9, false, InputLayoutFormat.Int)),
173 | KeyValuePair.Create("imat3x2", new(6, false, InputLayoutFormat.Int)),
174 | KeyValuePair.Create("imat3x4", new(12, false, InputLayoutFormat.Int)),
175 | KeyValuePair.Create("imat4", new(16, false, InputLayoutFormat.Int)),
176 | KeyValuePair.Create("imat4x2", new(8, false, InputLayoutFormat.Int)),
177 | KeyValuePair.Create("imat4x3", new(12, false, InputLayoutFormat.Int)),
178 | KeyValuePair.Create("uint", new(1, false, InputLayoutFormat.UnsignedInt)),
179 | KeyValuePair.Create("uvec2", new(2, false, InputLayoutFormat.UnsignedInt)),
180 | KeyValuePair.Create("uvec3", new(3, false, InputLayoutFormat.UnsignedInt)),
181 | KeyValuePair.Create("uvec4", new(4, false, InputLayoutFormat.UnsignedInt)),
182 | KeyValuePair.Create("umat2", new(4, false, InputLayoutFormat.UnsignedInt)),
183 | KeyValuePair.Create("umat2x3", new(6, false, InputLayoutFormat.UnsignedInt)),
184 | KeyValuePair.Create("umat2x4", new(8, false, InputLayoutFormat.UnsignedInt)),
185 | KeyValuePair.Create("umat3", new(9, false, InputLayoutFormat.UnsignedInt)),
186 | KeyValuePair.Create("umat3x2", new(6, false, InputLayoutFormat.UnsignedInt)),
187 | KeyValuePair.Create("umat3x4", new(12, false, InputLayoutFormat.UnsignedInt)),
188 | KeyValuePair.Create("umat4", new(16, false, InputLayoutFormat.UnsignedInt)),
189 | KeyValuePair.Create("umat4x2", new(8, false, InputLayoutFormat.UnsignedInt)),
190 | KeyValuePair.Create("umat4x3", new(12, false, InputLayoutFormat.UnsignedInt)),
191 | KeyValuePair.Create("bool", new(1, false, InputLayoutFormat.Int)),
192 | KeyValuePair.Create("bvec2", new(2, false, InputLayoutFormat.Int)),
193 | KeyValuePair.Create("bvec3", new(3, false, InputLayoutFormat.Int)),
194 | KeyValuePair.Create("bvec4", new(4, false, InputLayoutFormat.Int)),
195 | KeyValuePair.Create("bmat2", new(4, false, InputLayoutFormat.Int)),
196 | KeyValuePair.Create("bmat2x3", new(6, false, InputLayoutFormat.Int)),
197 | KeyValuePair.Create("bmat2x4", new(8, false, InputLayoutFormat.Int)),
198 | KeyValuePair.Create("bmat3", new(9, false, InputLayoutFormat.Int)),
199 | KeyValuePair.Create("bmat3x2", new(6, false, InputLayoutFormat.Int)),
200 | KeyValuePair.Create("bmat3x4", new(12, false, InputLayoutFormat.Int)),
201 | KeyValuePair.Create("bmat4", new(16, false, InputLayoutFormat.Int)),
202 | KeyValuePair.Create("bmat4x2", new(8, false, InputLayoutFormat.Int)),
203 | KeyValuePair.Create("bmat4x3", new(12, false, InputLayoutFormat.Int)),
204 | KeyValuePair.Create("float", new(1, false, InputLayoutFormat.Float)),
205 | KeyValuePair.Create("vec2", new(2, false, InputLayoutFormat.Float)),
206 | KeyValuePair.Create("vec3", new(3, false, InputLayoutFormat.Float)),
207 | KeyValuePair.Create("vec4", new(4, false, InputLayoutFormat.Float)),
208 | KeyValuePair.Create("mat2", new(4, false, InputLayoutFormat.Float)),
209 | KeyValuePair.Create("mat2x3", new(6, false, InputLayoutFormat.Float)),
210 | KeyValuePair.Create("mat2x4", new(8, false, InputLayoutFormat.Float)),
211 | KeyValuePair.Create("mat3", new(9, false, InputLayoutFormat.Float)),
212 | KeyValuePair.Create("mat3x2", new(6, false, InputLayoutFormat.Float)),
213 | KeyValuePair.Create("mat3x4", new(12, false, InputLayoutFormat.Float)),
214 | KeyValuePair.Create("mat4", new(16, false, InputLayoutFormat.Float)),
215 | KeyValuePair.Create("mat4x2", new(8, false, InputLayoutFormat.Float)),
216 | KeyValuePair.Create("mat4x3", new(12, false, InputLayoutFormat.Float)),
217 | KeyValuePair.Create("double", new(1, false, InputLayoutFormat.Double)),
218 | KeyValuePair.Create("dvec2", new(2, false, InputLayoutFormat.Double)),
219 | KeyValuePair.Create("dvec3", new(3, false, InputLayoutFormat.Double)),
220 | KeyValuePair.Create("dvec4", new(4, false, InputLayoutFormat.Double)),
221 | KeyValuePair.Create("dmat2", new(4, false, InputLayoutFormat.Double)),
222 | KeyValuePair.Create("dmat2x3", new(6, false, InputLayoutFormat.Double)),
223 | KeyValuePair.Create("dmat2x4", new(8, false, InputLayoutFormat.Double)),
224 | KeyValuePair.Create("dmat3", new(9, false, InputLayoutFormat.Double)),
225 | KeyValuePair.Create("dmat3x2", new(6, false, InputLayoutFormat.Double)),
226 | KeyValuePair.Create("dmat3x4", new(12, false, InputLayoutFormat.Double)),
227 | KeyValuePair.Create("dmat4", new(16, false, InputLayoutFormat.Double)),
228 | KeyValuePair.Create("dmat4x2", new(8, false, InputLayoutFormat.Double)),
229 | KeyValuePair.Create("dmat4x3", new(12, false, InputLayoutFormat.Double)),
230 | }
231 | );
232 | }
233 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/IMaterial.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using Sekai.Graphics;
7 |
8 | namespace Vignette.Graphics;
9 |
10 | ///
11 | /// Defines properties of a surface and how it should be drawn.
12 | ///
13 | public interface IMaterial
14 | {
15 | ///
16 | /// The stencil reference.
17 | ///
18 | int Stencil { get; }
19 |
20 | ///
21 | /// The material effect.
22 | ///
23 | Effect Effect { get; }
24 |
25 | ///
26 | /// The primitive type.
27 | ///
28 | PrimitiveType Primitives { get; }
29 |
30 | ///
31 | /// The layout descriptor.
32 | ///
33 | InputLayoutDescription Layout { get; }
34 |
35 | ///
36 | /// The blend descriptor.
37 | ///
38 | BlendStateDescription Blend { get; }
39 |
40 | ///
41 | /// The rasterizer descriptor.
42 | ///
43 | RasterizerStateDescription Rasterizer { get; }
44 |
45 | ///
46 | /// The depth stencil descriptor.
47 | ///
48 | DepthStencilStateDescription DepthStencil { get; }
49 |
50 | ///
51 | /// The material properties.
52 | ///
53 | IEnumerable Properties { get; }
54 |
55 | ///
56 | /// Gets a unique ID for this .
57 | ///
58 | int GetMaterialID()
59 | {
60 | return HashCode.Combine(Stencil, Blend, Rasterizer, DepthStencil, Effect, Layout);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/IProjector.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System.Numerics;
5 | using Sekai.Mathematics;
6 |
7 | namespace Vignette.Graphics;
8 |
9 | ///
10 | /// An interface for objects providing clip space info.
11 | ///
12 | public interface IProjector
13 | {
14 | ///
15 | /// The projector's position.
16 | ///
17 | Vector3 Position { get; }
18 |
19 | ///
20 | /// The projector's rotation.
21 | ///
22 | Vector3 Rotation { get; }
23 |
24 | ///
25 | /// The projector's view matrix.
26 | ///
27 | Matrix4x4 ViewMatrix { get; }
28 |
29 | ///
30 | /// The projector's projection matrix.
31 | ///
32 | Matrix4x4 ProjMatrix { get; }
33 |
34 | ///
35 | /// The projector's render groups.
36 | ///
37 | RenderGroup Groups { get; }
38 |
39 | ///
40 | /// The projector's bounding frustum.
41 | ///
42 | BoundingFrustum Frustum { get; }
43 | }
44 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/IProperty.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using Sekai.Graphics;
5 |
6 | namespace Vignette.Graphics;
7 |
8 | ///
9 | /// Defines a property.
10 | ///
11 | public interface IProperty
12 | {
13 | ///
14 | /// The property name.
15 | ///
16 | string Name { get; }
17 |
18 | ///
19 | /// The property slot.
20 | ///
21 | int Slot { get; }
22 | }
23 |
24 | ///
25 | /// Defines a property that contains a as its value.
26 | ///
27 | /// The property name.
28 | /// The property slot.
29 | /// The buffer.
30 | public record struct UniformProperty(string Name, int Slot, GraphicsBuffer? Uniform = null) : IProperty;
31 |
32 | ///
33 | /// Defines a property that contains a and pair as its value.
34 | ///
35 | /// The property name.
36 | /// The property slot.
37 | /// The texture.
38 | /// The sampler.
39 | public record struct TextureProperty(string Name, int Slot, Texture? Texture = null, Sampler? Sampler = null) : IProperty;
40 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/IWorld.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System.Numerics;
5 |
6 | namespace Vignette.Graphics;
7 |
8 | ///
9 | /// An interface for objects providing world space info.
10 | ///
11 | public interface IWorld
12 | {
13 | ///
14 | /// The world's position.
15 | ///
16 | Vector3 Position { get; }
17 |
18 | ///
19 | /// The world's rotation.
20 | ///
21 | Vector3 Rotation { get; }
22 |
23 | ///
24 | /// The world's scaling.
25 | ///
26 | Vector3 Scale { get; }
27 |
28 | ///
29 | /// The world's shearing.
30 | ///
31 | Vector3 Shear { get; }
32 |
33 | ///
34 | /// The world's local matrix.
35 | ///
36 | Matrix4x4 LocalMatrix { get; }
37 |
38 | ///
39 | /// The world's world matrix.
40 | ///
41 | Matrix4x4 WorldMatrix { get; }
42 | }
43 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/RenderContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | namespace Vignette.Graphics;
5 |
6 | ///
7 | /// A rendering context for a given .
8 | ///
9 | public readonly struct RenderContext
10 | {
11 | private readonly IWorld world;
12 | private readonly RenderQueue queue;
13 | private readonly IProjector projector;
14 |
15 | internal RenderContext(RenderQueue queue, IProjector projector, IWorld world)
16 | {
17 | this.queue = queue;
18 | this.world = world;
19 | this.projector = projector;
20 | }
21 |
22 | ///
23 | /// Draws a render object.
24 | ///
25 | /// The to draw.
26 | public void Draw(RenderObject renderObject)
27 | {
28 | queue.Enqueue(projector, world, renderObject);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/RenderData.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | namespace Vignette.Graphics;
5 |
6 | ///
7 | /// A fully realized renderable that can be drawn.
8 | ///
9 | public readonly struct RenderData
10 | {
11 | ///
12 | /// The world.
13 | ///
14 | public IWorld World { get; }
15 |
16 | ///
17 | /// The projector.
18 | ///
19 | public IProjector Projector { get; }
20 |
21 | ///
22 | /// The renderable.
23 | ///
24 | public RenderObject Renderable { get; }
25 |
26 | public RenderData(IProjector projector, IWorld world, RenderObject renderable)
27 | {
28 | World = world;
29 | Projector = projector;
30 | Renderable = renderable;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/RenderGroup.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 |
6 | namespace Vignette.Graphics;
7 |
8 | ///
9 | /// Render Group Flags.
10 | ///
11 | [Flags]
12 | public enum RenderGroup : uint
13 | {
14 | ///
15 | /// Render Group Default
16 | ///
17 | Default = 0,
18 |
19 | ///
20 | /// Render Group 1
21 | ///
22 | Group1 = 1 << 0,
23 |
24 | ///
25 | /// Render Group 2
26 | ///
27 | Group2 = 1 << 1,
28 |
29 | ///
30 | /// Render Group 3
31 | ///
32 | Group3 = 1 << 2,
33 |
34 | ///
35 | /// Render Group 4
36 | ///
37 | Group4 = 1 << 3,
38 |
39 | ///
40 | /// Render Group 5
41 | ///
42 | Group5 = 1 << 4,
43 |
44 | ///
45 | /// Render Group 6
46 | ///
47 | Group6 = 1 << 5,
48 |
49 | ///
50 | /// Render Group 7
51 | ///
52 | Group7 = 1 << 6,
53 |
54 | ///
55 | /// Render Group 8
56 | ///
57 | Group8 = 1 << 7,
58 |
59 | ///
60 | /// Render Group 9
61 | ///
62 | Group9 = 1 << 8,
63 |
64 | ///
65 | /// Render Group 10
66 | ///
67 | Group10 = 1 << 9,
68 |
69 | ///
70 | /// Render Group 11
71 | ///
72 | Group11 = 1 << 10,
73 |
74 | ///
75 | /// Render Group 12
76 | ///
77 | Group12 = 1 << 11,
78 |
79 | ///
80 | /// Render Group 13
81 | ///
82 | Group13 = 1 << 12,
83 |
84 | ///
85 | /// Render Group 14
86 | ///
87 | Group14 = 1 << 13,
88 |
89 | ///
90 | /// Render Group 15
91 | ///
92 | Group15 = 1 << 14,
93 |
94 | ///
95 | /// Render Group 16
96 | ///
97 | Group16 = 1 << 15,
98 | }
99 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/RenderObject.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using Sekai.Graphics;
5 | using Sekai.Mathematics;
6 |
7 | namespace Vignette.Graphics;
8 |
9 | ///
10 | /// An object that can be drawn.
11 | ///
12 | public class RenderObject
13 | {
14 | ///
15 | /// The bounding box of this .
16 | ///
17 | public BoundingBox Bounds { get; set; } = BoundingBox.Empty;
18 |
19 | ///
20 | /// The rendering groups this is visible to.
21 | ///
22 | public RenderGroup Groups { get; set; } = RenderGroup.Default;
23 |
24 | ///
25 | /// The material this uses.
26 | ///
27 | public IMaterial Material { get; set; } = UnlitMaterial.Default;
28 |
29 | ///
30 | /// The number of indices to be drawn.
31 | ///
32 | public int IndexCount { get; set; }
33 |
34 | ///
35 | /// The type of indices being to be interpreted in the .
36 | ///
37 | public IndexType IndexType { get; set; } = IndexType.UnsignedShort;
38 |
39 | ///
40 | /// The index buffer for this render object.
41 | ///
42 | public GraphicsBuffer? IndexBuffer { get; set; }
43 |
44 | ///
45 | /// The vertex buffer for this render object.
46 | ///
47 | public GraphicsBuffer? VertexBuffer { get; set; }
48 | }
49 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/RenderQueue.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections;
6 | using System.Collections.Generic;
7 | using System.Diagnostics.CodeAnalysis;
8 | using System.Numerics;
9 | using Sekai.Mathematics;
10 |
11 | namespace Vignette.Graphics;
12 |
13 | ///
14 | /// A priority queue that is sorted by the distance between a projector and a model.
15 | ///
16 | public sealed class RenderQueue : IReadOnlyCollection
17 | {
18 | ///
19 | /// Gets the number of s queued.
20 | ///
21 | public int Count => renderables.Count;
22 |
23 | private readonly List renderables = new();
24 | private readonly List renderOrders = new();
25 |
26 | ///
27 | /// Creates a new render queue.
28 | ///
29 | public RenderQueue()
30 | {
31 | }
32 |
33 | ///
34 | /// Enqueues a to this queue.
35 | ///
36 | /// The projector used.
37 | /// The model used.
38 | /// The render object to be enqueued.
39 | public void Enqueue(IProjector projector, IWorld world, RenderObject renderObject)
40 | {
41 | if ((projector.Groups & renderObject.Groups) != 0)
42 | {
43 | return;
44 | }
45 |
46 | if (!renderObject.Bounds.Equals(BoundingBox.Empty))
47 | {
48 | if (BoundingFrustum.Contains(projector.Frustum, renderObject.Bounds) == Containment.Disjoint)
49 | {
50 | return;
51 | }
52 | }
53 |
54 | int renderable = renderables.Count;
55 | int materialID = renderObject.Material.GetMaterialID();
56 | float distance = Vector3.Distance((renderObject.Bounds.Center * world.Scale) + world.Position, projector.Position);
57 |
58 | renderables.Add(new RenderData(projector, world, renderObject));
59 | renderOrders.Add(new(renderable, materialID, distance));
60 | }
61 |
62 | ///
63 | /// Clears the queue.
64 | ///
65 | public void Clear()
66 | {
67 | renderables.Clear();
68 | renderOrders.Clear();
69 | }
70 |
71 | ///
72 | /// Returns an enumerator that iterates through this queue in order.
73 | ///
74 | /// An that iterates through this queue in order.
75 | public IEnumerator GetEnumerator()
76 | {
77 | renderOrders.Sort();
78 | return new Enumerator(renderOrders, renderables);
79 | }
80 |
81 | IEnumerator IEnumerable.GetEnumerator()
82 | {
83 | return GetEnumerator();
84 | }
85 |
86 | private struct Enumerator : IEnumerator
87 | {
88 | public RenderData Current { get; private set; }
89 |
90 | private int index;
91 | private readonly IReadOnlyList renderables;
92 | private readonly IReadOnlyList renderOrders;
93 |
94 | public Enumerator(IReadOnlyList renderOrders, IReadOnlyList renderables)
95 | {
96 | this.renderables = renderables;
97 | this.renderOrders = renderOrders;
98 | }
99 |
100 | public bool MoveNext()
101 | {
102 | if (index >= renderOrders.Count)
103 | {
104 | Current = default;
105 | return false;
106 | }
107 | else
108 | {
109 | Current = renderables[renderOrders[index].Renderable];
110 | index += 1;
111 | return true;
112 | }
113 | }
114 |
115 | public void Reset()
116 | {
117 | index = 0;
118 | Current = default;
119 | }
120 |
121 | public readonly void Dispose()
122 | {
123 | }
124 |
125 | readonly object IEnumerator.Current => Current;
126 | }
127 |
128 | private readonly struct RenderOrder : IEquatable, IComparable
129 | {
130 | public int Renderable { get; }
131 | public int MaterialID { get; }
132 | public float Distance { get; }
133 |
134 | public RenderOrder(int renderable, int materialID, float distance)
135 | {
136 | Distance = distance;
137 | Renderable = renderable;
138 | MaterialID = materialID;
139 | }
140 |
141 | public readonly int CompareTo(RenderOrder other)
142 | {
143 | if (Equals(other))
144 | {
145 | return 0;
146 | }
147 |
148 | int value = Distance.CompareTo(other.Distance);
149 |
150 | if (value != 0)
151 | {
152 | return value;
153 | }
154 |
155 | return MaterialID.CompareTo(other.MaterialID);
156 | }
157 |
158 | public readonly bool Equals(RenderOrder other)
159 | {
160 | return Renderable.Equals(other.Renderable) && MaterialID.Equals(other.MaterialID) && Distance.Equals(other.Distance);
161 | }
162 |
163 | public override readonly bool Equals([NotNullWhen(true)] object? obj)
164 | {
165 | return obj is RenderOrder order && Equals(order);
166 | }
167 |
168 | public override readonly int GetHashCode()
169 | {
170 | return HashCode.Combine(Renderable, MaterialID, Distance);
171 | }
172 |
173 | public static bool operator ==(RenderOrder left, RenderOrder right)
174 | {
175 | return left.Equals(right);
176 | }
177 |
178 | public static bool operator !=(RenderOrder left, RenderOrder right)
179 | {
180 | return !(left == right);
181 | }
182 |
183 | public static bool operator <(RenderOrder left, RenderOrder right)
184 | {
185 | return left.CompareTo(right) < 0;
186 | }
187 |
188 | public static bool operator <=(RenderOrder left, RenderOrder right)
189 | {
190 | return left.CompareTo(right) <= 0;
191 | }
192 |
193 | public static bool operator >(RenderOrder left, RenderOrder right)
194 | {
195 | return left.CompareTo(right) > 0;
196 | }
197 |
198 | public static bool operator >=(RenderOrder left, RenderOrder right)
199 | {
200 | return left.CompareTo(right) >= 0;
201 | }
202 | }
203 | }
204 |
205 | internal static class RenderQueueExtensions
206 | {
207 | ///
208 | /// Begins a rendering context.
209 | ///
210 | /// The render queue.
211 | /// The projector used.
212 | /// The model used.
213 | /// A new render context.
214 | internal static RenderContext Begin(this RenderQueue queue, IProjector projector, IWorld world)
215 | {
216 | return new RenderContext(queue, projector, world);
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/RenderTarget.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using Sekai.Graphics;
6 |
7 | namespace Vignette.Graphics;
8 |
9 | ///
10 | /// An output target for rendering.
11 | ///
12 | public sealed class RenderTarget : IDisposable
13 | {
14 | ///
15 | /// The render target's width.
16 | ///
17 | public int Width { get; }
18 |
19 | ///
20 | /// The render target's height.
21 | ///
22 | public int Height { get; }
23 |
24 | private bool isDisposed;
25 | private readonly Texture? color;
26 | private readonly Texture? depth;
27 | private readonly Framebuffer framebuffer;
28 |
29 | private RenderTarget(int width, int height, Texture? color, Texture? depth, Framebuffer framebuffer)
30 | {
31 | this.color = color;
32 | this.depth = depth;
33 | this.Width = width;
34 | this.Height = height;
35 | this.framebuffer = framebuffer;
36 | }
37 |
38 | ///
39 | /// Creates a new .
40 | ///
41 | /// The graphics device used in creating the render target.
42 | /// The width of the render target.
43 | /// The height of the render target.
44 | /// The layer count of the render target.
45 | /// The color texture pixel format.
46 | /// The depth texture pixel format.
47 | /// A new .
48 | /// Thrown when is less than or equal to zero.
49 | /// Thrown when both and are .
50 | /// Thrown when either or are passed with an invalid format.
51 | public static RenderTarget Create(GraphicsDevice device, int width, int height, int layers = 1, PixelFormat? colorFormat = null, PixelFormat? depthFormat = null)
52 | {
53 | if (layers <= 0)
54 | {
55 | throw new ArgumentOutOfRangeException(nameof(layers), "Layer count must be greater than zero.");
56 | }
57 |
58 | if (!depthFormat.HasValue && !colorFormat.HasValue)
59 | {
60 | throw new InvalidOperationException("There must be a defined format for either or both the color and depth textures.");
61 | }
62 |
63 | if (colorFormat.HasValue && colorFormat.Value.IsDepthStencil())
64 | {
65 | throw new ArgumentException("Invalid color format.", nameof(colorFormat));
66 | }
67 |
68 | if (depthFormat.HasValue && !depthFormat.Value.IsDepthStencil())
69 | {
70 | throw new ArgumentException("Invalid depth format.", nameof(depthFormat));
71 | }
72 |
73 | var color = default(Texture);
74 | var depth = default(Texture);
75 |
76 | FramebufferAttachment? depthAttachment = null;
77 | FramebufferAttachment[] colorAttachments;
78 |
79 | if (colorFormat.HasValue)
80 | {
81 | color = device.CreateTexture(new TextureDescription
82 | (
83 | TextureType.Texture2D,
84 | width,
85 | height,
86 | 1,
87 | colorFormat.Value,
88 | 1,
89 | layers,
90 | TextureUsage.RenderTarget | TextureUsage.Resource
91 | ));
92 |
93 | colorAttachments = new FramebufferAttachment[layers];
94 |
95 | for (int i = 0; i < layers; i++)
96 | {
97 | colorAttachments[i] = new FramebufferAttachment(color, i, 0);
98 | }
99 | }
100 | else
101 | {
102 | colorAttachments = Array.Empty();
103 | }
104 |
105 | if (depthFormat.HasValue)
106 | {
107 | depth = device.CreateTexture(new TextureDescription
108 | (
109 | TextureType.Texture2D,
110 | width,
111 | height,
112 | 1,
113 | depthFormat.Value,
114 | 1,
115 | 1,
116 | TextureUsage.RenderTarget | TextureUsage.Resource
117 | ));
118 |
119 | depthAttachment = new FramebufferAttachment(depth, 0, 0);
120 | }
121 |
122 | return new RenderTarget(width, height, color, depth, device.CreateFramebuffer(depthAttachment, colorAttachments));
123 | }
124 |
125 | public void Dispose()
126 | {
127 | if (isDisposed)
128 | {
129 | return;
130 | }
131 |
132 | color?.Dispose();
133 | depth?.Dispose();
134 | framebuffer.Dispose();
135 |
136 | isDisposed = true;
137 | }
138 |
139 | public static explicit operator Framebuffer(RenderTarget target) => target.framebuffer;
140 | }
141 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/Renderer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections;
6 | using System.Collections.Generic;
7 | using System.Numerics;
8 | using Sekai.Graphics;
9 | using Sekai.Mathematics;
10 |
11 | namespace Vignette.Graphics;
12 |
13 | public sealed class Renderer
14 | {
15 | ///
16 | /// A white pixel texture.
17 | ///
18 | public Texture WhitePixel { get; }
19 |
20 | ///
21 | /// A point sampler.
22 | ///
23 | public Sampler SamplerPoint { get; }
24 |
25 | ///
26 | /// A linear sampler.
27 | ///
28 | public Sampler SamplerLinear { get; }
29 |
30 | ///
31 | /// A 4x anisotropic sampler.
32 | ///
33 | public Sampler SamplerAniso4x { get; }
34 |
35 | private readonly GraphicsBuffer ubo;
36 | private readonly GraphicsDevice device;
37 | private readonly Dictionary caches = new();
38 |
39 | internal Renderer(GraphicsDevice device)
40 | {
41 | ubo = device.CreateBuffer(BufferType.Uniform, 3, true);
42 |
43 | Span whitePixel = stackalloc byte[] { 255, 255, 255, 255 };
44 |
45 | WhitePixel = device.CreateTexture(new(1, 1, PixelFormat.R8G8B8A8_UNorm, 1, 1, TextureUsage.Resource));
46 | WhitePixel.SetData((ReadOnlySpan)whitePixel, 0, 0, 0, 0, 0, 1, 1, 0);
47 |
48 | SamplerPoint = device.CreateSampler(new(TextureFilter.MinMagMipPoint, TextureAddress.Repeat, TextureAddress.Repeat, TextureAddress.Repeat, 0, Color.White, 0, 0, 0));
49 | SamplerLinear = device.CreateSampler(new(TextureFilter.MinMagMipLinear, TextureAddress.Repeat, TextureAddress.Repeat, TextureAddress.Repeat, 0, Color.White, 0, 0, 0));
50 | SamplerAniso4x = device.CreateSampler(new(TextureFilter.Anisotropic, TextureAddress.Repeat, TextureAddress.Repeat, TextureAddress.Repeat, 4, Color.White, 0, 0, 0));
51 |
52 | this.device = device;
53 | }
54 |
55 | ///
56 | /// Draws a single to .
57 | ///
58 | /// The renderable to draw.
59 | /// The target to draw to. A value of to draw to the backbuffer.
60 | public void Draw(RenderData renderable, RenderTarget? target = null)
61 | {
62 | Draw(new[] { renderable }, target);
63 | }
64 |
65 | ///
66 | /// Draws to .
67 | ///
68 | /// The renderables to draw.
69 | /// The target to draw to. A value of to draw to the backbuffer.
70 | public void Draw(IEnumerable renderables, RenderTarget? target = null)
71 | {
72 | var currentLayout = default(InputLayout);
73 | int currentMaterialID = -1;
74 |
75 | foreach (var data in renderables)
76 | {
77 | using (var mvp = ubo.Map(MapMode.Write))
78 | {
79 | mvp[0] = data.Projector.ProjMatrix;
80 | mvp[1] = data.Projector.ViewMatrix;
81 | mvp[2] = data.World.WorldMatrix;
82 | }
83 |
84 | device.SetUniformBuffer(ubo, Effect.GLOBAL_TRANSFORM_ID);
85 |
86 | if (target is not null)
87 | {
88 | device.SetFramebuffer((Framebuffer)target);
89 | }
90 | else
91 | {
92 | device.SetFramebuffer(null);
93 | }
94 |
95 | draw(data.Renderable, ref currentMaterialID, ref currentLayout!);
96 | }
97 | }
98 |
99 | private void draw(RenderObject renderObject, ref int currentMaterialID, ref InputLayout currentLayout)
100 | {
101 | if (renderObject.IndexCount <= 0 || renderObject.VertexBuffer is null || renderObject.IndexBuffer is null)
102 | {
103 | return;
104 | }
105 |
106 | int materialID = renderObject.Material.GetMaterialID();
107 |
108 | if (currentMaterialID != materialID)
109 | {
110 | apply(renderObject.Material, ref currentLayout);
111 | currentMaterialID = materialID;
112 | }
113 |
114 | foreach (var property in renderObject.Material.Properties)
115 | {
116 | if (property is UniformProperty uniform)
117 | {
118 | if (uniform.Uniform is not null)
119 | {
120 | device.SetUniformBuffer(uniform.Uniform, (uint)uniform.Slot);
121 | }
122 | }
123 |
124 | if (property is TextureProperty texture)
125 | {
126 | device.SetTexture(texture.Texture ?? WhitePixel, texture.Sampler ?? SamplerPoint, (uint)texture.Slot);
127 | }
128 | }
129 |
130 | device.SetVertexBuffer(renderObject.VertexBuffer, currentLayout);
131 | device.SetIndexBuffer(renderObject.IndexBuffer, renderObject.IndexType);
132 | device.DrawIndexed(renderObject.Material.Primitives, (uint)renderObject.IndexCount);
133 | }
134 |
135 | private void apply(IMaterial material, ref InputLayout layout)
136 | {
137 | var blendCache = getCache();
138 |
139 | if (!blendCache.TryGetValue(material.Blend, out var blendState))
140 | {
141 | blendState = device.CreateBlendState(material.Blend);
142 | blendCache.Add(material.Blend, blendState);
143 | }
144 |
145 | device.SetBlendState(blendState);
146 |
147 | var shaderCache = getCache();
148 |
149 | if (!shaderCache.TryGetValue(material.Effect, out var shader))
150 | {
151 | shader = device.CreateShader((ShaderCode[])material.Effect);
152 | shaderCache.Add(material.Effect, shader);
153 | }
154 |
155 | device.SetShader(shader);
156 |
157 | var layoutCache = getCache();
158 |
159 | if (!layoutCache.TryGetValue(material.Layout, out var layoutState))
160 | {
161 | layoutState = device.CreateInputLayout(material.Layout);
162 | layoutCache.Add(material.Layout, layoutState);
163 | }
164 |
165 | layout = layoutState;
166 |
167 | var rasterizerCache = getCache();
168 |
169 | if (!rasterizerCache.TryGetValue(material.Rasterizer, out var rasterizerState))
170 | {
171 | rasterizerState = device.CreateRasterizerState(material.Rasterizer);
172 | rasterizerCache.Add(material.Rasterizer, rasterizerState);
173 | }
174 |
175 | device.SetRasterizerState(rasterizerState);
176 |
177 | var depthStencilCache = getCache();
178 |
179 | if (!depthStencilCache.TryGetValue(material.DepthStencil, out var depthStencilState))
180 | {
181 | depthStencilState = device.CreateDepthStencilState(material.DepthStencil);
182 | depthStencilCache.Add(material.DepthStencil, depthStencilState);
183 | }
184 |
185 | device.SetDepthStencilState(depthStencilState, material.Stencil);
186 | }
187 |
188 | private IDictionary getCache()
189 | where T : struct, IEquatable
190 | where U : notnull
191 | {
192 | if (!caches.TryGetValue(typeof(T), out var cache))
193 | {
194 | cache = new Dictionary();
195 | caches.Add(typeof(T), cache);
196 | }
197 |
198 | return (IDictionary)cache;
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/ShaderMaterial.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using Sekai.Graphics;
8 |
9 | namespace Vignette.Graphics;
10 |
11 | ///
12 | /// A composable material that created from shader code.
13 | ///
14 | public sealed class ShaderMaterial : IMaterial, ICloneable
15 | {
16 | private int stencil;
17 | private PrimitiveType primitives;
18 | private BlendStateDescription blend;
19 | private RasterizerStateDescription rasterizer;
20 | private DepthStencilStateDescription depthStencil;
21 | private readonly Effect effect;
22 | private readonly InputLayoutDescription layout;
23 | private readonly IProperty[] properties;
24 |
25 | private ShaderMaterial(InputLayoutDescription layout, Effect effect, IProperty[] properties)
26 | {
27 | this.layout = layout;
28 | this.effect = effect;
29 | this.properties = properties;
30 | }
31 |
32 | private ShaderMaterial(PrimitiveType primitives, BlendStateDescription blend, RasterizerStateDescription rasterizer, DepthStencilStateDescription depthStencil, InputLayoutDescription layout, Effect effect, IProperty[] properties)
33 | {
34 | this.blend = blend;
35 | this.layout = layout;
36 | this.effect = effect;
37 | this.properties = properties;
38 | this.primitives = primitives;
39 | this.rasterizer = rasterizer;
40 | this.depthStencil = depthStencil;
41 | }
42 |
43 | ///
44 | /// Sets the primitive type for this .
45 | ///
46 | /// The primitive type.
47 | public ShaderMaterial SetPrimitives(PrimitiveType primitives)
48 | {
49 | this.primitives = primitives;
50 | return this;
51 | }
52 |
53 | ///
54 | /// Sets the face culling mode for this .
55 | ///
56 | /// The face culling mode.
57 | public ShaderMaterial SetFaceCulling(FaceCulling culling)
58 | {
59 | rasterizer.Culling = culling;
60 | return this;
61 | }
62 |
63 | ///
64 | /// Sets the face winding mode for this .
65 | ///
66 | /// The face winding mode.
67 | public ShaderMaterial SetFaceWinding(FaceWinding winding)
68 | {
69 | rasterizer.Winding = winding;
70 | return this;
71 | }
72 |
73 | ///
74 | /// Sets the polygon fill mode for this .
75 | ///
76 | /// The polygon fill mode.
77 | public ShaderMaterial SetFillMode(FillMode mode)
78 | {
79 | rasterizer.Mode = mode;
80 | return this;
81 | }
82 |
83 | ///
84 | /// Sets custom stencil parameters for this .
85 | ///
86 | /// The stencil reference.
87 | /// The operation performed for the passing front face stencil test.
88 | /// The operation performed for the failing front face stencil test.
89 | /// The operation performed for the failing front face depth test.
90 | /// The comparison performed for the front face.
91 | /// The operation performed for the passing back face stencil test.
92 | /// The operation performed for the failing back face stencil test.
93 | /// The operation performed for the failing back face depth test.
94 | /// The comparison performed for the back face.
95 | public ShaderMaterial SetStencil(int reference, StencilOperation frontStencilPass, StencilOperation frontStencilFail, StencilOperation frontDepthFail, ComparisonKind frontComparison, StencilOperation backStencilPass, StencilOperation backStencilFail, StencilOperation backDepthFail, ComparisonKind backComparison)
96 | {
97 | stencil = reference;
98 | depthStencil.Front.StencilPass = frontStencilPass;
99 | depthStencil.Front.StencilFail = frontStencilFail;
100 | depthStencil.Front.DepthFail = frontDepthFail;
101 | depthStencil.Front.Comparison = frontComparison;
102 | depthStencil.Back.StencilPass = backStencilPass;
103 | depthStencil.Back.StencilFail = backStencilFail;
104 | depthStencil.Back.DepthFail = backDepthFail;
105 | depthStencil.Back.Comparison = backComparison;
106 | return this;
107 | }
108 |
109 | ///
110 | /// Sets custom stencil parameters for both the front and back faces for this .
111 | ///
112 | /// The stencil reference.
113 | /// The operation performed for passing the stencil test.
114 | /// The operation performed for failing the stencil test.
115 | /// The operation performed for failing the depth test.
116 | /// The comparison performed.
117 | public ShaderMaterial SetStencil(int reference, StencilOperation pass, StencilOperation fail, StencilOperation depthFail, ComparisonKind comparison)
118 | {
119 | return SetStencil(reference, pass, fail, depthFail, comparison, pass, fail, depthFail, comparison);
120 | }
121 |
122 | ///
123 | /// Sets custom blending parameters for this .
124 | ///
125 | /// Whether to enable or disable blending.
126 | /// The source color blending.
127 | /// The source alpha blending.
128 | /// The destination color blending.
129 | /// The destination alpha blending.
130 | /// The operation performed between and .
131 | /// The operation performed between and .
132 | public ShaderMaterial SetBlend(bool enabled, BlendType srcColor, BlendType srcAlpha, BlendType dstColor, BlendType dstAlpha, BlendOperation colorOperation, BlendOperation alphaOperation)
133 | {
134 | blend.Enabled = enabled;
135 | blend.SourceColor = srcColor;
136 | blend.SourceAlpha = srcAlpha;
137 | blend.DestinationColor = dstColor;
138 | blend.DestinationAlpha = dstAlpha;
139 | blend.ColorOperation = colorOperation;
140 | blend.AlphaOperation = alphaOperation;
141 | return this;
142 | }
143 |
144 | ///
145 | /// Sets individual blending parameters for this .
146 | ///
147 | /// The source color blending.
148 | /// The source alpha blending.
149 | /// The destination color blending.
150 | /// The destination alpha blending.
151 | public ShaderMaterial SetBlend(BlendType srcColor, BlendType srcAlpha, BlendType dstColor, BlendType dstAlpha)
152 | {
153 | return SetBlend(true, srcColor, srcAlpha, dstColor, dstAlpha, BlendOperation.Add, BlendOperation.Add);
154 | }
155 |
156 | ///
157 | /// Sets blending parameters for the source and destination colors for this .
158 | ///
159 | /// The source blending.
160 | /// The destination blending.
161 | public ShaderMaterial SetBlend(BlendType source, BlendType destination)
162 | {
163 | return SetBlend(source, source, destination, destination);
164 | }
165 |
166 | ///
167 | /// Sets the color write mask.
168 | ///
169 | /// The color write mask.
170 | public ShaderMaterial SetColorMask(ColorWriteMask mask)
171 | {
172 | blend.WriteMask = mask;
173 | return this;
174 | }
175 |
176 | ///
177 | /// Sets a for this .
178 | ///
179 | /// The property name.
180 | /// The to set. Setting will use the default texture.
181 | /// The to set. Setting will use the default sampler.
182 | /// Thrown when the is not usable as a resource.
183 | public ShaderMaterial SetProperty(string name, Texture? texture = null, Sampler? sampler = null)
184 | {
185 | var prop = getProperty(name);
186 |
187 | if (texture is not null && (texture.Usage & TextureUsage.Resource) == 0)
188 | {
189 | throw new ArgumentException($"The texture must have the {nameof(TextureUsage.Resource)} flag to be used on materials.", nameof(texture));
190 | }
191 |
192 | prop.Texture = texture;
193 | prop.Sampler = sampler;
194 |
195 | return this;
196 | }
197 |
198 | ///
199 | /// Sets a for this .
200 | ///
201 | /// The property name.
202 | /// The to set. Setting will not bind this property during rendering.
203 | /// Thrown when the is not usable as a uniform.
204 | public ShaderMaterial SetProperty(string name, GraphicsBuffer? buffer = null)
205 | {
206 | var prop = getProperty(name);
207 |
208 | if (buffer is not null && buffer.Type is not BufferType.Uniform)
209 | {
210 | throw new ArgumentException($"The buffer must be a {nameof(BufferType.Uniform)} to be used on materials.", nameof(buffer));
211 | }
212 |
213 | prop.Uniform = buffer;
214 |
215 | return this;
216 | }
217 |
218 | ///
219 | /// Gets whether a property exists.
220 | ///
221 | /// The property name.
222 | /// if the property exists or if it does not.
223 | public bool HasProperty(string name)
224 | {
225 | foreach (var prop in properties)
226 | {
227 | if (prop.Name == name)
228 | {
229 | return true;
230 | }
231 | }
232 |
233 | return false;
234 | }
235 |
236 | ///
237 | /// Creates a shallow copy of this .
238 | ///
239 | /// A new .
240 | public ShaderMaterial Clone() => new
241 | (
242 | primitives,
243 | blend,
244 | rasterizer,
245 | depthStencil,
246 | layout,
247 | effect,
248 | properties.ToArray()
249 | );
250 |
251 | private T getProperty(string name)
252 | where T : IProperty
253 | {
254 | var prop = default(IProperty);
255 |
256 | foreach (var p in properties)
257 | {
258 | if (p.Name == name)
259 | {
260 | prop = p;
261 | break;
262 | }
263 | }
264 |
265 | if (prop is null)
266 | {
267 | throw new KeyNotFoundException($"There is no property named \"{name}\" on this material.");
268 | }
269 |
270 | if (prop is not T typedProp)
271 | {
272 | throw new InvalidCastException($"Property \"{name}\" is not compatible with the type {typeof(T)}.");
273 | }
274 |
275 | return typedProp;
276 | }
277 |
278 | int IMaterial.Stencil => stencil;
279 | Effect IMaterial.Effect => effect;
280 | PrimitiveType IMaterial.Primitives => primitives;
281 | InputLayoutDescription IMaterial.Layout => layout;
282 | BlendStateDescription IMaterial.Blend => blend;
283 | RasterizerStateDescription IMaterial.Rasterizer => rasterizer;
284 | DepthStencilStateDescription IMaterial.DepthStencil => depthStencil;
285 | IEnumerable IMaterial.Properties => properties;
286 | object ICloneable.Clone() => Clone();
287 |
288 | ///
289 | /// Creates a new from HLSL shader code.
290 | ///
291 | /// The shader code to use.
292 | /// A new .
293 | public static ShaderMaterial Create(string code)
294 | {
295 | var effect = Effect.From(code, out var layout, out var properties);
296 | return new(layout, effect, properties);
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/source/Vignette/Graphics/UnlitMaterial.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using Sekai.Graphics;
7 |
8 | namespace Vignette.Graphics;
9 |
10 | ///
11 | /// A material that is unlit.
12 | ///
13 | public sealed class UnlitMaterial : IMaterial
14 | {
15 | ///
16 | /// The default unlit material.
17 | ///
18 | public static readonly IMaterial Default = new UnlitMaterial(true);
19 |
20 | ///
21 | /// The material's texture.
22 | ///
23 | public Texture? Texture
24 | {
25 | get => ((TextureProperty)properties[0]).Texture;
26 | set
27 | {
28 | if (isDefault)
29 | {
30 | throw new InvalidOperationException("Cannot modify the default instance.");
31 | }
32 |
33 | var texture = (TextureProperty)properties[0];
34 | texture.Texture = value;
35 | }
36 | }
37 |
38 | ///
39 | /// The material's sampler.
40 | ///
41 | public Sampler? Sampler
42 | {
43 | get => ((TextureProperty)properties[0]).Sampler;
44 | set
45 | {
46 | if (isDefault)
47 | {
48 | throw new InvalidOperationException("Cannot modify the default instance.");
49 | }
50 |
51 | var texture = (TextureProperty)properties[0];
52 | texture.Sampler = value;
53 | }
54 | }
55 |
56 | ///
57 | /// The material's primitive type.
58 | ///
59 | public PrimitiveType Primitives { get; set; } = PrimitiveType.TriangleList;
60 |
61 | private readonly bool isDefault;
62 | private readonly Effect effect;
63 | private readonly IProperty[] properties;
64 | private readonly InputLayoutDescription layout;
65 | private readonly RasterizerStateDescription rasterizer;
66 | private readonly DepthStencilStateDescription depthStencil;
67 |
68 | public UnlitMaterial()
69 | : this(false)
70 | {
71 | }
72 |
73 | public UnlitMaterial(Texture texture)
74 | : this(false)
75 | {
76 | Texture = texture;
77 | }
78 |
79 | public UnlitMaterial(Texture texture, Sampler sampler)
80 | : this(false)
81 | {
82 | Texture = texture;
83 | Sampler = sampler;
84 | }
85 |
86 | private UnlitMaterial(bool isDefault)
87 | {
88 | effect = Effect.From(shader, out layout, out properties);
89 |
90 | rasterizer = new RasterizerStateDescription
91 | (
92 | FaceCulling.None,
93 | FaceWinding.CounterClockwise,
94 | FillMode.Solid,
95 | false
96 | );
97 |
98 | depthStencil = new DepthStencilStateDescription
99 | (
100 | false,
101 | false,
102 | ComparisonKind.Always
103 | );
104 |
105 | this.isDefault = isDefault;
106 | }
107 |
108 | int IMaterial.Stencil => 0;
109 | Effect IMaterial.Effect => effect;
110 | InputLayoutDescription IMaterial.Layout => layout;
111 | BlendStateDescription IMaterial.Blend => BlendStateDescription.NonPremultiplied;
112 | RasterizerStateDescription IMaterial.Rasterizer => rasterizer;
113 | DepthStencilStateDescription IMaterial.DepthStencil => depthStencil;
114 | IEnumerable IMaterial.Properties => properties;
115 |
116 | private const string shader =
117 | @"
118 | struct VSInput
119 | {
120 | float3 Position : POSITION;
121 | float2 TexCoord : TEXCOORD;
122 | float4 Color : COLOR;
123 | };
124 |
125 | struct PSInput
126 | {
127 | float4 Position : SV_POSITION;
128 | float2 TexCoord : TEXCOORD;
129 | float4 Color : COLOR;
130 | };
131 |
132 | Texture2D AlbedoTexture : register(t0);
133 | SamplerState AlbedoSampler : register(s0);
134 |
135 | PSInput Vertex(in VSInput input)
136 | {
137 | PSInput output;
138 |
139 | output.Color = input.Color;
140 | output.Position = OBJECT_TO_VIEW(float4(input.Position, 1.0));
141 | output.TexCoord = input.TexCoord;
142 |
143 | return output;
144 | }
145 |
146 | float4 Pixel(in PSInput input) : SV_TARGET
147 | {
148 | return AlbedoTexture.Sample(AlbedoSampler, input.TexCoord) * input.Color;
149 | }
150 | ";
151 | }
152 |
--------------------------------------------------------------------------------
/source/Vignette/Light.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System.Numerics;
5 | using Sekai.Mathematics;
6 | using Vignette.Graphics;
7 |
8 | namespace Vignette;
9 |
10 | ///
11 | /// Represents a light source.
12 | ///
13 | public abstract class Light : Node, IProjector
14 | {
15 | ///
16 | /// The light source's view matrix.
17 | ///
18 | protected abstract Matrix4x4 ViewMatrix { get; }
19 |
20 | ///
21 | /// The light source's projection matrix.
22 | ///
23 | protected abstract Matrix4x4 ProjMatrix { get; }
24 |
25 | ///
26 | /// The light source's bounding frustum.
27 | ///
28 | public BoundingFrustum Frustum => BoundingFrustum.FromMatrix(ProjMatrix);
29 |
30 | Matrix4x4 IProjector.ViewMatrix => ViewMatrix;
31 | Matrix4x4 IProjector.ProjMatrix => ProjMatrix;
32 | RenderGroup IProjector.Groups => RenderGroup.Default;
33 | }
34 |
--------------------------------------------------------------------------------
/source/Vignette/Node.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections;
6 | using System.Collections.Generic;
7 | using System.Collections.Specialized;
8 | using System.Diagnostics.CodeAnalysis;
9 | using System.IO;
10 | using System.Linq;
11 | using System.Numerics;
12 | using Vignette.Graphics;
13 |
14 | namespace Vignette;
15 |
16 | ///
17 | /// The base class of everything that resides inside the node graph. It can be a child of
18 | /// another and can contain its own children s.
19 | ///
20 | public class Node : IWorld, INotifyCollectionChanged, ICollection, IEquatable
21 | {
22 | ///
23 | /// The 's unique identifier.
24 | ///
25 | public Guid Id { get; }
26 |
27 | ///
28 | /// The 's name.
29 | ///
30 | public string Name { get; }
31 |
32 | ///
33 | /// The depth of this relative to the root.
34 | ///
35 | public int Depth { get; private set; }
36 |
37 | ///
38 | /// The number of children this contains.
39 | ///
40 | public int Count => nodes.Count;
41 |
42 | ///
43 | /// The 's services.
44 | ///
45 | public virtual IServiceLocator Services => Parent is not null ? Parent.Services : throw new InvalidOperationException("Services are unavailable");
46 |
47 | ///
48 | /// The parent .
49 | ///
50 | public Node? Parent { get; private set; }
51 |
52 | ///
53 | /// The node's position.
54 | ///
55 | public Vector3 Position { get; set; }
56 |
57 | ///
58 | /// The node's rotation.
59 | ///
60 | public Vector3 Rotation { get; set; }
61 |
62 | ///
63 | /// The node's scaling.
64 | ///
65 | public Vector3 Scale { get; set; } = Vector3.One;
66 |
67 | ///
68 | /// The node's shearing.
69 | ///
70 | public Vector3 Shear
71 | {
72 | get => new(shear[0, 1], shear[0, 2], shear[1, 2]);
73 | set
74 | {
75 | shear[0, 1] = value.X;
76 | shear[0, 2] = value.Y;
77 | shear[1, 2] = value.Z;
78 | }
79 | }
80 |
81 | ///
82 | /// The node's local matrix.
83 | ///
84 | protected virtual Matrix4x4 LocalMatrix => shear * Matrix4x4.CreateScale(Scale) * Matrix4x4.CreateFromYawPitchRoll(Rotation.Y, Rotation.X, Rotation.Z) * Matrix4x4.CreateTranslation(Position);
85 |
86 | ///
87 | /// The node's world matrix.
88 | ///
89 | protected virtual Matrix4x4 WorldMatrix => Parent is not IWorld provider ? LocalMatrix : provider.LocalMatrix * LocalMatrix;
90 |
91 | ///
92 | /// Called when the 's children has been changed.
93 | ///
94 | public event NotifyCollectionChangedEventHandler? CollectionChanged;
95 |
96 | private Matrix4x4 shear = Matrix4x4.Identity;
97 | private readonly Dictionary nodes = new();
98 |
99 | ///
100 | /// Creates a new .
101 | ///
102 | /// The optional name for this .
103 | public Node(string? name = null)
104 | : this(Guid.NewGuid(), name)
105 | {
106 | }
107 |
108 | private Node(Guid id, string? name = null)
109 | {
110 | Id = id;
111 | Name = name ?? id.ToString();
112 | }
113 |
114 | ///
115 | /// Called when the has entered the node graph.
116 | ///
117 | protected virtual void Enter()
118 | {
119 | }
120 |
121 | ///
122 | /// Called when the is leaving the node graph.
123 | ///
124 | protected virtual void Leave()
125 | {
126 | }
127 |
128 | ///
129 | /// Adds a child .
130 | ///
131 | /// The to add.
132 | public void Add(Node node)
133 | {
134 | add(node);
135 | raiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, node));
136 | }
137 |
138 | ///
139 | /// Adds a range of children.
140 | ///
141 | /// The children to add.
142 | public void AddRange(IEnumerable nodes)
143 | {
144 | foreach (var node in nodes)
145 | {
146 | add(node);
147 | }
148 |
149 | raiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, nodes.ToArray()));
150 | }
151 |
152 | ///
153 | /// Removes a child .
154 | ///
155 | /// The to remove.
156 | /// if the has been removed. Otherwise, returns .
157 | public bool Remove(Node node)
158 | {
159 | if (!remove(node))
160 | {
161 | return false;
162 | }
163 |
164 | raiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, node));
165 |
166 | return true;
167 | }
168 |
169 | ///
170 | /// Removes a range of children based on a given .
171 | ///
172 | /// The predicate used to select the children.
173 | /// The number of removed children.
174 | public int RemoveRange(Predicate predicate)
175 | {
176 | var selected = nodes.Values.Where(n => predicate(n)).ToArray();
177 |
178 | foreach (var node in selected)
179 | {
180 | remove(node);
181 | }
182 |
183 | raiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, selected));
184 |
185 | return selected.Length;
186 | }
187 |
188 | ///
189 | /// Removes a range of children.
190 | ///
191 | /// The children to remove.
192 | /// The number of removed children.
193 | public int RemoveRange(IEnumerable nodes)
194 | {
195 | var removed = new List();
196 |
197 | foreach (var node in nodes)
198 | {
199 | if (remove(node))
200 | {
201 | removed.Add(node);
202 | }
203 | }
204 |
205 | raiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
206 |
207 | return removed.Count;
208 | }
209 |
210 | ///
211 | /// Removes all children from this .
212 | ///
213 | public void Clear()
214 | {
215 | var copy = nodes.Values.ToArray();
216 |
217 | foreach (var node in copy)
218 | {
219 | remove(node);
220 | }
221 |
222 | raiseCollectionChanged(reset_args);
223 | }
224 |
225 | ///
226 | /// Determines whether a given is a child of this node.
227 | ///
228 | /// The node to test.
229 | /// if the is a child of this node or if not.
230 | public bool Contains(Node node)
231 | {
232 | return nodes.ContainsValue(node);
233 | }
234 |
235 | ///
236 | /// Gets the root node.
237 | ///
238 | /// The root node.
239 | public Node GetRoot()
240 | {
241 | var current = this;
242 |
243 | while (current.Parent is not null)
244 | {
245 | current = current.Parent;
246 | }
247 |
248 | return current;
249 | }
250 |
251 | ///
252 | /// Gets the nearest node.
253 | ///
254 | /// The node type to search for.
255 | /// The nearest node.
256 | public T? GetNearest()
257 | where T : Node
258 | {
259 | var current = this;
260 |
261 | while (current.Parent is not null)
262 | {
263 | current = current.Parent;
264 |
265 | if (current is T)
266 | {
267 | break;
268 | }
269 | }
270 |
271 | return current as T;
272 | }
273 |
274 | ///
275 | /// Gets the node from the given path.
276 | ///
277 | /// The relative or absolute path.
278 | /// The node on the given path.
279 | /// Thrown when is invalid.
280 | /// Thrown when a part of the path is not found.
281 | public Node GetNode(string path)
282 | {
283 | if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri))
284 | {
285 | throw new ArgumentException("Provided path is not a URI.", nameof(path));
286 | }
287 |
288 | if (uri.IsAbsoluteUri && uri.Scheme != node_scheme)
289 | {
290 | throw new ArgumentException("Absolute paths must start with the node scheme.", nameof(path));
291 | }
292 |
293 | var current = uri.IsAbsoluteUri ? GetRoot() : this;
294 | string[] cm = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped).Split(Path.AltDirectorySeparatorChar, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
295 |
296 | foreach (string part in cm)
297 | {
298 | if (!current.nodes.ContainsKey(part))
299 | {
300 | throw new KeyNotFoundException($"The node \"{part}\" was not found.");
301 | }
302 |
303 | current = current.nodes[part];
304 | }
305 |
306 | return current;
307 | }
308 |
309 | ///
310 | /// Gets the node from the given path.
311 | ///
312 | /// The type to cast the node as.
313 | /// The relative or absolute path.
314 | /// The node on the given path.
315 | /// Thrown when the returned node cannot be casted to .
316 | public T GetNode(string path)
317 | where T : Node
318 | {
319 | var node = GetNode(path);
320 |
321 | if (node is not T typed)
322 | {
323 | throw new InvalidCastException($"Cannot cast {typeof(T)} to the found node.");
324 | }
325 |
326 | return (T)node;
327 | }
328 |
329 | ///
330 | /// Gets an enumeration of the nodes of type .
331 | ///
332 | /// The type to filter the enumeration.
333 | /// An enumerable of nodes of type .
334 | public IEnumerable GetNodes()
335 | where T : Node
336 | {
337 | return this.OfType();
338 | }
339 |
340 | public IEnumerator GetEnumerator()
341 | {
342 | return nodes.Values.GetEnumerator();
343 | }
344 |
345 | public bool Equals(Node? node)
346 | {
347 | if (node is null)
348 | {
349 | return false;
350 | }
351 |
352 | if (node.Id.Equals(Id))
353 | {
354 | return true;
355 | }
356 |
357 | if (ReferenceEquals(this, node))
358 | {
359 | return true;
360 | }
361 |
362 | return false;
363 | }
364 |
365 | public override bool Equals([NotNullWhen(true)] object? obj)
366 | {
367 | return obj is Node node && Equals(node);
368 | }
369 |
370 | public override int GetHashCode()
371 | {
372 | return HashCode.Combine(Id);
373 | }
374 |
375 | private void raiseCollectionChanged(NotifyCollectionChangedEventArgs args)
376 | {
377 | CollectionChanged?.Invoke(this, args);
378 | }
379 |
380 | private void add(Node node)
381 | {
382 | if (Equals(node))
383 | {
384 | throw new ArgumentException("Cannot add self as a child.", nameof(node));
385 | }
386 |
387 | if (node.Parent is not null)
388 | {
389 | throw new ArgumentException("Cannot add a node that already has a parent.", nameof(node));
390 | }
391 |
392 | if (nodes.ContainsKey(node.Name))
393 | {
394 | throw new ArgumentException($"There is already a child with the name \"{node.Name}\".", nameof(node));
395 | }
396 |
397 | node.Depth = Depth + 1;
398 | node.Parent = this;
399 |
400 | nodes.Add(node.Name, node);
401 |
402 | node.Enter();
403 | }
404 |
405 | private bool remove(Node node)
406 | {
407 | if (!nodes.ContainsKey(node.Name))
408 | {
409 | return false;
410 | }
411 |
412 | node.Leave();
413 |
414 | node.Depth = 0;
415 | node.Parent = null;
416 |
417 | nodes.Remove(node.Name);
418 |
419 | return true;
420 | }
421 |
422 | void ICollection.CopyTo(Node[] array, int arrayIndex)
423 | {
424 | nodes.Values.CopyTo(array, arrayIndex);
425 | }
426 |
427 | IEnumerator IEnumerable.GetEnumerator()
428 | {
429 | return GetEnumerator();
430 | }
431 |
432 | bool ICollection.IsReadOnly => false;
433 | Matrix4x4 IWorld.LocalMatrix => LocalMatrix;
434 | Matrix4x4 IWorld.WorldMatrix => WorldMatrix;
435 |
436 | private const string node_scheme = "node";
437 | private static readonly NotifyCollectionChangedEventArgs reset_args = new(NotifyCollectionChangedAction.Reset);
438 | }
439 |
--------------------------------------------------------------------------------
/source/Vignette/ServiceLocator.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Diagnostics.CodeAnalysis;
7 | using System.Runtime.CompilerServices;
8 |
9 | namespace Vignette;
10 |
11 | ///
12 | /// A collection of services.
13 | ///
14 | public sealed class ServiceLocator : IServiceLocator, IServiceProvider
15 | {
16 | private readonly Dictionary services = new();
17 |
18 | ///
19 | /// Adds a service to this locator.
20 | ///
21 | /// The type of service.
22 | /// The service instance.
23 | /// Thrown when is already added to this locator.
24 | /// Thrown when cannot be assigned to .
25 | public void Add(Type type, object instance)
26 | {
27 | if (services.ContainsKey(type))
28 | {
29 | throw new ArgumentException($"{type} already exists in this locator.", nameof(type));
30 | }
31 |
32 | if (!instance.GetType().IsAssignableTo(type))
33 | {
34 | throw new InvalidCastException($"The {nameof(instance)} cannot be casted to {type}.");
35 | }
36 |
37 | services.Add(type, instance);
38 | }
39 |
40 | ///
41 | /// Adds a service to this locator.
42 | ///
43 | /// The service instance.
44 | /// The type of service.
45 | /// Thrown when is already added to this locator.
46 | /// Thrown when cannot be assigned to .
47 | public void Add(T instance)
48 | where T : class
49 | {
50 | Add(typeof(T), instance);
51 | }
52 |
53 | ///
54 | /// Removes a service from this locator.
55 | ///
56 | /// The service type to remove.
57 | /// true when the service is removed. false otherwise.
58 | public bool Remove(Type type)
59 | {
60 | return services.Remove(type);
61 | }
62 |
63 | ///
64 | /// Removes a service from this locator.
65 | ///
66 | /// The service type to remove.
67 | /// true when the service is removed. false otherwise.
68 | public bool Remove()
69 | where T : class
70 | {
71 | return Remove(typeof(T));
72 | }
73 |
74 | public object? Get(Type type, [DoesNotReturnIf(true)] bool required = true)
75 | {
76 | if (!services.TryGetValue(type, out object? instance) && required)
77 | {
78 | throw new ServiceNotFoundException(type);
79 | }
80 |
81 | return instance;
82 | }
83 |
84 | public T? Get([DoesNotReturnIf(true)] bool required = true)
85 | where T : class
86 | {
87 | return Unsafe.As(Get(typeof(T), required));
88 | }
89 |
90 | object? IServiceProvider.GetService(Type type) => Get(type, false);
91 | }
92 |
93 | ///
94 | /// An interface for objects capable of locating services.
95 | ///
96 | public interface IServiceLocator
97 | {
98 | ///
99 | /// Gets the service of a given type.
100 | ///
101 | /// The object type to resolve.
102 | /// Whether the service is required or not.
103 | /// The service object of the given type or when is false and the service is not found.
104 | /// Thrown when is true and the service is not found.
105 | object? Get(Type type, [DoesNotReturnIf(true)] bool required = true);
106 |
107 | ///
108 | /// Gets the service of a given type.
109 | ///
110 | /// The object type to resolve.
111 | /// Whether the service is required or not.
112 | /// The service object of type or when is false and the service is not found.
113 | /// Thrown when is true and the service is not found.
114 | T? Get([DoesNotReturnIf(true)] bool required = true) where T : class;
115 | }
116 |
117 | ///
118 | /// Exception thrown when fails to locate a required service of a given type.
119 | ///
120 | public sealed class ServiceNotFoundException : Exception
121 | {
122 | internal ServiceNotFoundException(Type type)
123 | : base($"Failed to locate service of type {type}.")
124 | {
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/source/Vignette/Vignette.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | true
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/source/Vignette/VignetteGame.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using Sekai;
6 | using Vignette.Audio;
7 | using Vignette.Content;
8 | using Vignette.Graphics;
9 |
10 | namespace Vignette;
11 |
12 | public sealed class VignetteGame : Game
13 | {
14 | private Window root = null!;
15 | private Camera camera = null!;
16 | private Renderer renderer = null!;
17 | private AudioManager audio = null!;
18 | private ContentManager content = null!;
19 | private ServiceLocator services = null!;
20 |
21 | public override void Load()
22 | {
23 | audio = new(Audio);
24 | content = new(Storage);
25 | content.Add(new ShaderLoader(), ".hlsl");
26 | content.Add(new TextureLoader(Graphics), ".png", ".jpg", ".jpeg", ".bmp", ".gif");
27 |
28 | renderer = new(Graphics);
29 |
30 | services = new();
31 | services.Add(audio);
32 | services.Add(content);
33 |
34 | root = new(services)
35 | {
36 | (camera = new Camera { ProjectionMode = CameraProjectionMode.OrthographicOffCenter })
37 | };
38 | }
39 |
40 | public override void Draw()
41 | {
42 | root.Draw(renderer);
43 | }
44 |
45 | public override void Update(TimeSpan elapsed)
46 | {
47 | camera.ViewSize = Window.Size;
48 | audio.Update();
49 | root.Update(elapsed);
50 | }
51 |
52 | public override void Unload()
53 | {
54 | root.Clear();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/source/Vignette/Window.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | namespace Vignette;
5 |
6 | ///
7 | /// The root of .
8 | ///
9 | public sealed class Window : World
10 | {
11 | public override IServiceLocator Services { get; }
12 |
13 | internal Window(IServiceLocator services)
14 | {
15 | Services = services;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/source/Vignette/World.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Cosyne
2 | // Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Collections.Specialized;
7 | using System.Linq;
8 | using System.Numerics;
9 | using Sekai.Mathematics;
10 | using Vignette.Collections;
11 | using Vignette.Graphics;
12 |
13 | namespace Vignette;
14 |
15 | ///
16 | /// A that presents and processes its children.
17 | ///
18 | public class World : Behavior
19 | {
20 | protected override Matrix4x4 WorldMatrix => LocalMatrix;
21 |
22 | private readonly SortedFilteredCollection behaviors = new
23 | (
24 | Comparer.Default,
25 | (node) => node.Enabled,
26 | (node, handler) => node.OrderChanged += handler,
27 | (node, handler) => node.OrderChanged -= handler,
28 | (node, handler) => node.EnabledChanged += handler,
29 | (node, handler) => node.EnabledChanged -= handler
30 | );
31 |
32 | private readonly SortedFilteredCollection drawables = new
33 | (
34 | Comparer.Default,
35 | (node) => node.Visible,
36 | (node, handler) => node.OrderChanged += handler,
37 | (node, handler) => node.OrderChanged -= handler,
38 | (node, handler) => node.VisibleChanged += handler,
39 | (node, handler) => node.VisibleChanged -= handler
40 | );
41 |
42 | private readonly SortedFilteredCollection worlds = new
43 | (
44 | Comparer.Default,
45 | (node) => node.Enabled,
46 | (node, handler) => node.OrderChanged += handler,
47 | (node, handler) => node.OrderChanged -= handler,
48 | (node, handler) => node.EnabledChanged += handler,
49 | (node, handler) => node.EnabledChanged -= handler
50 | );
51 |
52 | private readonly List lights = new();
53 | private readonly List cameras = new();
54 |
55 | private readonly RenderQueue renderQueue = new();
56 | private readonly Queue behaviorLoadQueue = new();
57 | private readonly Queue behaviorUnloadQueue = new();
58 |
59 | public World()
60 | {
61 | CollectionChanged += handleCollectionChanged;
62 | }
63 |
64 | public override void Update(TimeSpan elapsed)
65 | {
66 | while (behaviorLoadQueue.TryDequeue(out var node))
67 | {
68 | node.Load();
69 | }
70 |
71 | while (behaviorUnloadQueue.TryDequeue(out var node))
72 | {
73 | node.Unload();
74 | }
75 |
76 | foreach (var behavior in behaviors)
77 | {
78 | behavior.Update(elapsed);
79 | }
80 | }
81 |
82 | public void Draw(Renderer renderer)
83 | {
84 | foreach (var world in worlds)
85 | {
86 | world.Draw(renderer);
87 | }
88 |
89 | // Shadow Map Pass
90 |
91 | foreach (var camera in cameras)
92 | {
93 | foreach (var light in lights)
94 | {
95 | if (BoundingFrustum.Contains(camera.Frustum, light.Frustum) == Containment.Disjoint)
96 | {
97 | continue;
98 | }
99 |
100 | renderQueue.Clear();
101 |
102 | foreach (var drawable in drawables)
103 | {
104 | drawable.Draw(renderQueue.Begin(light, drawable));
105 | }
106 |
107 | renderer.Draw(renderQueue);
108 | }
109 | }
110 |
111 | // Lighting Pass
112 |
113 | foreach (var camera in cameras)
114 | {
115 | renderQueue.Clear();
116 |
117 | foreach (var drawable in drawables)
118 | {
119 | drawable.Draw(renderQueue.Begin(camera, drawable));
120 | }
121 |
122 | renderer.Draw(renderQueue);
123 | }
124 | }
125 |
126 | private void handleCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args)
127 | {
128 | if (args.Action == NotifyCollectionChangedAction.Add)
129 | {
130 | foreach (var node in args.NewItems!.OfType())
131 | {
132 | load(node);
133 | }
134 | }
135 |
136 | if (args.Action == NotifyCollectionChangedAction.Remove)
137 | {
138 | foreach (var node in args.OldItems!.OfType())
139 | {
140 | unload(node);
141 | }
142 | }
143 |
144 | if (args.Action == NotifyCollectionChangedAction.Reset)
145 | {
146 | foreach (var node in this)
147 | {
148 | unload(node);
149 | }
150 | }
151 | }
152 |
153 | private void load(Node node)
154 | {
155 | foreach (var child in node.GetNodes())
156 | {
157 | load(child);
158 | }
159 |
160 | if (node is Behavior behavior)
161 | {
162 | behaviors.Add(behavior);
163 | behaviorLoadQueue.Enqueue(behavior);
164 | }
165 |
166 | if (node is Drawable drawable)
167 | {
168 | drawables.Add(drawable);
169 | }
170 |
171 | if (node is Light light)
172 | {
173 | lights.Add(light);
174 | }
175 |
176 | if (node is Camera camera)
177 | {
178 | cameras.Add(camera);
179 | }
180 |
181 | if (node is World world)
182 | {
183 | worlds.Add(world);
184 | }
185 | else
186 | {
187 | node.CollectionChanged += handleCollectionChanged;
188 | }
189 | }
190 |
191 | private void unload(Node node)
192 | {
193 | foreach (var child in node.GetNodes())
194 | {
195 | unload(child);
196 | }
197 |
198 | if (node is Behavior behavior)
199 | {
200 | behaviors.Remove(behavior);
201 | behaviorUnloadQueue.Enqueue(behavior);
202 | }
203 |
204 | if (node is Drawable drawable)
205 | {
206 | drawables.Remove(drawable);
207 | }
208 |
209 | if (node is Light light)
210 | {
211 | lights.Remove(light);
212 | }
213 |
214 | if (node is Camera camera)
215 | {
216 | cameras.Remove(camera);
217 | }
218 |
219 | if (node is World world)
220 | {
221 | worlds.Add(world);
222 | }
223 | else
224 | {
225 | node.CollectionChanged -= handleCollectionChanged;
226 | }
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/tests/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 | true
5 |
6 |
7 |
8 | runtime; build; native; contentfiles; analyzers; buildtransitive
9 | all
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------