forked from MoonsideGames/MoonWorks
Compare commits
111 Commits
samplecoun
...
main
Author | SHA1 | Date |
---|---|---|
cosmonaut | bb7e45b9a3 | |
cosmonaut | 4cedf768f7 | |
cosmonaut | d986b3013f | |
cosmonaut | 42e3ac91af | |
cosmonaut | 0df2944ccf | |
cosmonaut | e50fb472b1 | |
cosmonaut | df3f38a67b | |
Evan Hemsley | eaa9266521 | |
cosmonaut | 4dbd5a2cbe | |
cosmonaut | 2e890fd696 | |
cosmonaut | 385783a846 | |
cosmonaut | 450b08cbd8 | |
cosmonaut | 528fb7ac7c | |
cosmonaut | fcd08fe231 | |
cosmonaut | e961a18a83 | |
cosmonaut | 772a0378bb | |
cosmonaut | 40fb313d12 | |
cosmonaut | a736ed031d | |
cosmonaut | b2a0ca3515 | |
cosmonaut | 36a88afe52 | |
cosmonaut | 6c93350f7f | |
cosmonaut | 352bb34f82 | |
Evan Hemsley | de7d76c03d | |
cosmonaut | 18d92aeec8 | |
cosmonaut | e616b0fa62 | |
cosmonaut | 1d27a9e4a4 | |
cosmonaut | 514a0bed29 | |
cosmonaut | 78252d1f6c | |
cosmonaut | daae1a34b9 | |
cosmonaut | 2e5657789c | |
cosmonaut | 0c76c568a4 | |
cosmonaut | abdcac1608 | |
cosmonaut | d8064862bf | |
cosmonaut | b223c31c8b | |
cosmonaut | dd79090028 | |
cosmonaut | 653f90c29f | |
cosmonaut | 402c26131d | |
cosmonaut | b026b9e81f | |
cosmonaut | e0f05881b0 | |
cosmonaut | 7e18764942 | |
cosmonaut | 1bff459be6 | |
cosmonaut | be77e8bad1 | |
cosmonaut | 7f6b6a7bae | |
cosmonaut | 1de3c73bb7 | |
cosmonaut | e77c87c772 | |
cosmonaut | 088e7c4b6f | |
cosmonaut | f298a5ec11 | |
cosmonaut | 0cd2c799ee | |
cosmonaut | 81cd397013 | |
cosmonaut | e73c7ede55 | |
cosmonaut | 83f1cc24db | |
cosmonaut | 1d86d0c210 | |
cosmonaut | dbbd6540ab | |
cosmonaut | 74ae295036 | |
cosmonaut | 36ddb03d8f | |
cosmonaut | bf3ad0c8b0 | |
cosmonaut | f761d4f76e | |
cosmonaut | 4c731401ff | |
cosmonaut | 071518732e | |
cosmonaut | 1adb76d5c7 | |
cosmonaut | 5ff7da927a | |
cosmonaut | 0fd3365d1d | |
cosmonaut | affb592c15 | |
cosmonaut | 7e79e4a11d | |
cosmonaut | fc0937b2ff | |
cosmonaut | c83997609f | |
cosmonaut | b65d4e391c | |
cosmonaut | 56bab545ba | |
cosmonaut | 2ae116c72b | |
cosmonaut | bd3e70b096 | |
cosmonaut | b1fe7f96b2 | |
cosmonaut | 00366cc9d4 | |
cosmonaut | 3bc25bc3a1 | |
cosmonaut | 496eb670ab | |
cosmonaut | 00f4bfdeae | |
cosmonaut | adeba633e5 | |
cosmonaut | 300ef9f88e | |
cosmonaut | 76684eaa33 | |
cosmonaut | c037b4cb69 | |
cosmonaut | 5df08727c1 | |
cosmonaut | 537517afb9 | |
cosmonaut | bd405dfbf0 | |
cosmonaut | 2d7bb24b5c | |
cosmonaut | 0ea60a376b | |
cosmonaut | e3c2f0e119 | |
cosmonaut | 3584e670ee | |
cosmonaut | 5a2f7eadb8 | |
cosmonaut | 1e3f04235e | |
cosmonaut | dd06205399 | |
cosmonaut | 1cf04a7279 | |
cosmonaut | 3bd435746b | |
cosmonaut | 8134761e44 | |
cosmonaut | 8209051a3c | |
cosmonaut | 80f3711f4c | |
cosmonaut | 3a6b73e637 | |
cosmonaut | 12e7e6b9c1 | |
cosmonaut | 455f4048df | |
cosmonaut | 1f0e3b5040 | |
cosmonaut | f8b14ea94f | |
cosmonaut | 472da0edd2 | |
cosmonaut | bd825b6c91 | |
cosmonaut | 515c2ebbca | |
cosmonaut | 86322e9373 | |
cosmonaut | e9aacb44da | |
cosmonaut | 5baa1d7b40 | |
cosmonaut | f673803c37 | |
cosmonaut | 0f78cd1a0c | |
cosmonaut | 36ce74b58a | |
cosmonaut | 40d12357c0 | |
evan | e52fe60657 | |
TheSpydog | b39526ca90 |
|
@ -10,6 +10,6 @@
|
||||||
[submodule "lib/WellspringCS"]
|
[submodule "lib/WellspringCS"]
|
||||||
path = lib/WellspringCS
|
path = lib/WellspringCS
|
||||||
url = https://gitea.moonside.games/MoonsideGames/WellspringCS.git
|
url = https://gitea.moonside.games/MoonsideGames/WellspringCS.git
|
||||||
[submodule "lib/Theorafile"]
|
[submodule "lib/dav1dfile"]
|
||||||
path = lib/Theorafile
|
path = lib/dav1dfile
|
||||||
url = https://github.com/FNA-XNA/Theorafile.git
|
url = https://github.com/MoonsideGames/dav1dfile.git
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net7.0</TargetFrameworks>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Platforms>x64</Platforms>
|
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<LangVersion>11</LangVersion>
|
<LangVersion>11</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@ -15,13 +14,29 @@
|
||||||
<Compile Include="lib\FAudio\csharp\FAudio.cs" />
|
<Compile Include="lib\FAudio\csharp\FAudio.cs" />
|
||||||
<Compile Include="lib\RefreshCS\src\Refresh.cs" />
|
<Compile Include="lib\RefreshCS\src\Refresh.cs" />
|
||||||
<Compile Include="lib\SDL2-CS\src\SDL2.cs" />
|
<Compile Include="lib\SDL2-CS\src\SDL2.cs" />
|
||||||
<Compile Include="lib\Theorafile\csharp\Theorafile.cs" />
|
|
||||||
<Compile Include="lib\WellspringCS\WellspringCS.cs" />
|
<Compile Include="lib\WellspringCS\WellspringCS.cs" />
|
||||||
|
<Compile Include="lib\dav1dfile\csharp\dav1dfile.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="MoonWorks.dll.config">
|
<None Include="MoonWorks.dll.config">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\video_fullscreen.vert.refresh">
|
||||||
|
<LogicalName>MoonWorks.Graphics.StockShaders.VideoFullscreen.vert.refresh</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\video_yuv2rgba.frag.refresh">
|
||||||
|
<LogicalName>MoonWorks.Graphics.StockShaders.VideoYUV2RGBA.frag.refresh</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\text_transform.vert.refresh">
|
||||||
|
<LogicalName>MoonWorks.Graphics.StockShaders.TextTransform.vert.refresh</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\text_msdf.frag.refresh">
|
||||||
|
<LogicalName>MoonWorks.Graphics.StockShaders.TextMSDF.frag.refresh</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -5,18 +5,18 @@
|
||||||
<dllmap dll="SDL2" os="linux,freebsd,netbsd" target="libSDL2-2.0.so.0"/>
|
<dllmap dll="SDL2" os="linux,freebsd,netbsd" target="libSDL2-2.0.so.0"/>
|
||||||
|
|
||||||
<dllmap dll="Refresh" os="windows" target="Refresh.dll"/>
|
<dllmap dll="Refresh" os="windows" target="Refresh.dll"/>
|
||||||
<dllmap dll="Refresh" os="osx" target="libRefresh.0.dylib"/>
|
<dllmap dll="Refresh" os="osx" target="libRefresh.1.dylib"/>
|
||||||
<dllmap dll="Refresh" os="linux,freebsd,netbsd" target="libRefresh.so.0"/>
|
<dllmap dll="Refresh" os="linux,freebsd,netbsd" target="libRefresh.so.1"/>
|
||||||
|
|
||||||
<dllmap dll="FAudio" os="windows" target="FAudio.dll"/>
|
<dllmap dll="FAudio" os="windows" target="FAudio.dll"/>
|
||||||
<dllmap dll="FAudio" os="osx" target="libFAudio.0.dylib"/>
|
<dllmap dll="FAudio" os="osx" target="libFAudio.0.dylib"/>
|
||||||
<dllmap dll="FAudio" os="linux,freebsd,netbsd" target="libFAudio.so.0"/>
|
<dllmap dll="FAudio" os="linux,freebsd,netbsd" target="libFAudio.so.0"/>
|
||||||
|
|
||||||
<dllmap dll="Wellspring" os="windows" target="Wellspring.dll"/>
|
<dllmap dll="Wellspring" os="windows" target="Wellspring.dll"/>
|
||||||
<dllmap dll="Wellspring" os="osx" target="libWellspring.0.dylib"/>
|
<dllmap dll="Wellspring" os="osx" target="libWellspring.1.dylib"/>
|
||||||
<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.0"/>
|
<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.1"/>
|
||||||
|
|
||||||
<dllmap dll="Theorafile" os="windows" target="libtheorafile.dll"/>
|
<dllmap dll="dav1dfile" os="windows" target="dav1dfile.dll"/>
|
||||||
<dllmap dll="Theorafile" os="osx" target="libtheorafile.dylib"/>
|
<dllmap dll="dav1dfile" os="osx" target="libdav1dfile.1.dylib"/>
|
||||||
<dllmap dll="Theorafile" os="linux,freebsd,netbsd" target="libtheorafile.so"/>
|
<dllmap dll="dav1dfile" os="linux,freebsd,netbsd,openbsd" target="libdav1dfile.so.1"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
|
@ -12,9 +12,13 @@ MoonWorks uses strictly Free Open Source Software. It will never have any kind o
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
API Reference: https://moonside.games/docs/moonworksapi/
|
||||||
|
|
||||||
High-level documentation is provided here: https://moonside.games/docs/moonworks/
|
High-level documentation is provided here: https://moonside.games/docs/moonworks/
|
||||||
|
|
||||||
For an actual API reference, the source is documented in doc comments that your preferred IDE can read.
|
The source is documented in doc comments that your preferred IDE can read.
|
||||||
|
|
||||||
|
Join our Discord! https://discord.gg/ujhwdkHmhN
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
@ -22,7 +26,7 @@ For an actual API reference, the source is documented in doc comments that your
|
||||||
* [Refresh](https://gitea.moonside.games/MoonsideGames/Refresh) - Graphics
|
* [Refresh](https://gitea.moonside.games/MoonsideGames/Refresh) - Graphics
|
||||||
* [FAudio](https://github.com/FNA-XNA/FAudio) - Audio
|
* [FAudio](https://github.com/FNA-XNA/FAudio) - Audio
|
||||||
* [Wellspring](https://gitea.moonside.games/MoonsideGames/Wellspring) - Font Rendering
|
* [Wellspring](https://gitea.moonside.games/MoonsideGames/Wellspring) - Font Rendering
|
||||||
* [Theorafile](https://github.com/FNA-XNA/Theorafile) - Compressed Video
|
* [dav1dfile](https://github.com/MoonsideGames/dav1dfile) - Compressed Video
|
||||||
|
|
||||||
Prebuilt dependencies can be obtained here: https://moonside.games/files/moonlibs.tar.bz2
|
Prebuilt dependencies can be obtained here: https://moonside.games/files/moonlibs.tar.bz2
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 11ba6b37509a6c2fa2690f2643ee7bf5ce2ab4f2
|
Subproject commit 60480416bda930bf7544e6abe31b937f0daa0256
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1643061386177f62b516ccaad0ea04607cae2333
|
Subproject commit b5325e6d0329eeb35b074091a569a5f679852d28
|
|
@ -1 +1 @@
|
||||||
Subproject commit f8c6fc407fbb22072fdafcda918aec52b2102519
|
Subproject commit e4afbb848586fca530b6538320f799f81a18b941
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 8f9419ea856480e08294698e1d6be8752df3710b
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit f8872bae59e394b0f8a35224bb39ab8fd041af97
|
Subproject commit 074f2afc833b221906bb2468735041ce78f2cb89
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 5065e2cd4662dbe023b77a45ef967f975170dfff
|
|
@ -0,0 +1,79 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains raw audio data in a specified Format. <br/>
|
||||||
|
/// Submit this to a SourceVoice to play audio.
|
||||||
|
/// </summary>
|
||||||
|
public class AudioBuffer : AudioResource
|
||||||
|
{
|
||||||
|
IntPtr BufferDataPtr;
|
||||||
|
uint BufferDataLength;
|
||||||
|
private bool OwnsBufferData;
|
||||||
|
|
||||||
|
public Format Format { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new AudioBuffer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ownsBufferData">If true, the buffer data will be destroyed when this AudioBuffer is destroyed.</param>
|
||||||
|
public AudioBuffer(
|
||||||
|
AudioDevice device,
|
||||||
|
Format format,
|
||||||
|
IntPtr bufferPtr,
|
||||||
|
uint bufferLengthInBytes,
|
||||||
|
bool ownsBufferData) : base(device)
|
||||||
|
{
|
||||||
|
Format = format;
|
||||||
|
BufferDataPtr = bufferPtr;
|
||||||
|
BufferDataLength = bufferLengthInBytes;
|
||||||
|
OwnsBufferData = ownsBufferData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create another AudioBuffer from this audio buffer.
|
||||||
|
/// It will not own the buffer data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="offset">Offset in bytes from the top of the original buffer.</param>
|
||||||
|
/// <param name="length">Length in bytes of the new buffer.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public AudioBuffer Slice(int offset, uint length)
|
||||||
|
{
|
||||||
|
return new AudioBuffer(Device, Format, BufferDataPtr + offset, length, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an FAudioBuffer struct from this AudioBuffer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loop">Whether we should set the FAudioBuffer to loop.</param>
|
||||||
|
public FAudio.FAudioBuffer ToFAudioBuffer(bool loop = false)
|
||||||
|
{
|
||||||
|
return new FAudio.FAudioBuffer
|
||||||
|
{
|
||||||
|
Flags = FAudio.FAUDIO_END_OF_STREAM,
|
||||||
|
pContext = IntPtr.Zero,
|
||||||
|
pAudioData = BufferDataPtr,
|
||||||
|
AudioBytes = BufferDataLength,
|
||||||
|
PlayBegin = 0,
|
||||||
|
PlayLength = 0,
|
||||||
|
LoopBegin = 0,
|
||||||
|
LoopLength = 0,
|
||||||
|
LoopCount = loop ? FAudio.FAUDIO_LOOP_INFINITE : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
if (OwnsBufferData)
|
||||||
|
{
|
||||||
|
NativeMemory.Free((void*) BufferDataPtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Streamable audio in Ogg format.
|
||||||
|
/// </summary>
|
||||||
|
public class AudioDataOgg : AudioDataStreamable
|
||||||
|
{
|
||||||
|
private IntPtr FileDataPtr = IntPtr.Zero;
|
||||||
|
private IntPtr VorbisHandle = IntPtr.Zero;
|
||||||
|
|
||||||
|
private string FilePath;
|
||||||
|
|
||||||
|
public override bool Loaded => VorbisHandle != IntPtr.Zero;
|
||||||
|
public override uint DecodeBufferSize => 32768;
|
||||||
|
|
||||||
|
public AudioDataOgg(AudioDevice device, string filePath) : base(device)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
|
||||||
|
var handle = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
|
||||||
|
|
||||||
|
if (error != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Error loading file!");
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = FAudio.stb_vorbis_get_info(handle);
|
||||||
|
|
||||||
|
Format = new Format
|
||||||
|
{
|
||||||
|
Tag = FormatTag.IEEE_FLOAT,
|
||||||
|
BitsPerSample = 32,
|
||||||
|
Channels = (ushort) info.channels,
|
||||||
|
SampleRate = info.sample_rate
|
||||||
|
};
|
||||||
|
|
||||||
|
FAudio.stb_vorbis_close(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd)
|
||||||
|
{
|
||||||
|
var lengthInFloats = bufferLengthInBytes / sizeof(float);
|
||||||
|
|
||||||
|
/* NOTE: this function returns samples per channel, not total samples */
|
||||||
|
var samples = FAudio.stb_vorbis_get_samples_float_interleaved(
|
||||||
|
VorbisHandle,
|
||||||
|
Format.Channels,
|
||||||
|
(IntPtr) buffer,
|
||||||
|
lengthInFloats
|
||||||
|
);
|
||||||
|
|
||||||
|
var sampleCount = samples * Format.Channels;
|
||||||
|
reachedEnd = sampleCount < lengthInFloats;
|
||||||
|
filledLengthInBytes = sampleCount * sizeof(float);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares the Ogg data for streaming.
|
||||||
|
/// </summary>
|
||||||
|
public override unsafe void Load()
|
||||||
|
{
|
||||||
|
if (!Loaded)
|
||||||
|
{
|
||||||
|
var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
|
||||||
|
var fileDataSpan = new Span<byte>((void*) FileDataPtr, (int) fileStream.Length);
|
||||||
|
fileStream.ReadExactly(fileDataSpan);
|
||||||
|
fileStream.Close();
|
||||||
|
|
||||||
|
VorbisHandle = FAudio.stb_vorbis_open_memory(FileDataPtr, fileDataSpan.Length, out int error, IntPtr.Zero);
|
||||||
|
if (error != 0)
|
||||||
|
{
|
||||||
|
NativeMemory.Free((void*) FileDataPtr);
|
||||||
|
Logger.LogError("Error opening OGG file!");
|
||||||
|
Logger.LogError("Error: " + error);
|
||||||
|
throw new InvalidOperationException("Error opening OGG file!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Seek(uint sampleFrame)
|
||||||
|
{
|
||||||
|
FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unloads the Ogg data, freeing resources.
|
||||||
|
/// </summary>
|
||||||
|
public override unsafe void Unload()
|
||||||
|
{
|
||||||
|
if (Loaded)
|
||||||
|
{
|
||||||
|
FAudio.stb_vorbis_close(VorbisHandle);
|
||||||
|
NativeMemory.Free((void*) FileDataPtr);
|
||||||
|
|
||||||
|
VorbisHandle = IntPtr.Zero;
|
||||||
|
FileDataPtr = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads an entire ogg file into an AudioBuffer. Useful for static audio.
|
||||||
|
/// </summary>
|
||||||
|
public static unsafe AudioBuffer CreateBuffer(AudioDevice device, string filePath)
|
||||||
|
{
|
||||||
|
var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
|
||||||
|
|
||||||
|
if (error != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Error loading file!");
|
||||||
|
}
|
||||||
|
var info = FAudio.stb_vorbis_get_info(filePointer);
|
||||||
|
var lengthInFloats =
|
||||||
|
FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels;
|
||||||
|
var lengthInBytes = lengthInFloats * Marshal.SizeOf<float>();
|
||||||
|
var buffer = NativeMemory.Alloc((nuint) lengthInBytes);
|
||||||
|
|
||||||
|
FAudio.stb_vorbis_get_samples_float_interleaved(
|
||||||
|
filePointer,
|
||||||
|
info.channels,
|
||||||
|
(nint) buffer,
|
||||||
|
(int) lengthInFloats
|
||||||
|
);
|
||||||
|
|
||||||
|
FAudio.stb_vorbis_close(filePointer);
|
||||||
|
|
||||||
|
var format = new Format
|
||||||
|
{
|
||||||
|
Tag = FormatTag.IEEE_FLOAT,
|
||||||
|
BitsPerSample = 32,
|
||||||
|
Channels = (ushort) info.channels,
|
||||||
|
SampleRate = info.sample_rate
|
||||||
|
};
|
||||||
|
|
||||||
|
return new AudioBuffer(
|
||||||
|
device,
|
||||||
|
format,
|
||||||
|
(nint) buffer,
|
||||||
|
(uint) lengthInBytes,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Streamable audio in QOA format.
|
||||||
|
/// </summary>
|
||||||
|
public class AudioDataQoa : AudioDataStreamable
|
||||||
|
{
|
||||||
|
private IntPtr QoaHandle = IntPtr.Zero;
|
||||||
|
private IntPtr FileDataPtr = IntPtr.Zero;
|
||||||
|
|
||||||
|
private string FilePath;
|
||||||
|
|
||||||
|
private const uint QOA_MAGIC = 0x716f6166; /* 'qoaf' */
|
||||||
|
|
||||||
|
public override bool Loaded => QoaHandle != IntPtr.Zero;
|
||||||
|
|
||||||
|
private uint decodeBufferSize;
|
||||||
|
public override uint DecodeBufferSize => decodeBufferSize;
|
||||||
|
|
||||||
|
public AudioDataQoa(AudioDevice device, string filePath) : base(device)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
|
||||||
|
using var stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
using var reader = new BinaryReader(stream);
|
||||||
|
|
||||||
|
UInt64 fileHeader = ReverseEndianness(reader.ReadUInt64());
|
||||||
|
if ((fileHeader >> 32) != QOA_MAGIC)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Specified file is not a QOA file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint totalSamplesPerChannel = (uint) (fileHeader & (0xFFFFFFFF));
|
||||||
|
if (totalSamplesPerChannel == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Specified file is not a valid QOA file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
UInt64 frameHeader = ReverseEndianness(reader.ReadUInt64());
|
||||||
|
uint channels = (uint) ((frameHeader >> 56) & 0x0000FF);
|
||||||
|
uint samplerate = (uint) ((frameHeader >> 32) & 0xFFFFFF);
|
||||||
|
uint samplesPerChannelPerFrame = (uint) ((frameHeader >> 16) & 0x00FFFF);
|
||||||
|
|
||||||
|
Format = new Format
|
||||||
|
{
|
||||||
|
Tag = FormatTag.PCM,
|
||||||
|
BitsPerSample = 16,
|
||||||
|
Channels = (ushort) channels,
|
||||||
|
SampleRate = samplerate
|
||||||
|
};
|
||||||
|
|
||||||
|
decodeBufferSize = channels * samplesPerChannelPerFrame * sizeof(short);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd)
|
||||||
|
{
|
||||||
|
var lengthInShorts = bufferLengthInBytes / sizeof(short);
|
||||||
|
|
||||||
|
// NOTE: this function returns samples per channel!
|
||||||
|
var samples = FAudio.qoa_decode_next_frame(QoaHandle, (short*) buffer);
|
||||||
|
|
||||||
|
var sampleCount = samples * Format.Channels;
|
||||||
|
reachedEnd = sampleCount < lengthInShorts;
|
||||||
|
filledLengthInBytes = (int) (sampleCount * sizeof(short));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares qoa data for streaming.
|
||||||
|
/// </summary>
|
||||||
|
public override unsafe void Load()
|
||||||
|
{
|
||||||
|
if (!Loaded)
|
||||||
|
{
|
||||||
|
var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
|
||||||
|
var fileDataSpan = new Span<byte>((void*) FileDataPtr, (int) fileStream.Length);
|
||||||
|
fileStream.ReadExactly(fileDataSpan);
|
||||||
|
fileStream.Close();
|
||||||
|
|
||||||
|
QoaHandle = FAudio.qoa_open_from_memory((char*) FileDataPtr, (uint) fileDataSpan.Length, 0);
|
||||||
|
if (QoaHandle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
NativeMemory.Free((void*) FileDataPtr);
|
||||||
|
Logger.LogError("Error opening QOA file!");
|
||||||
|
throw new InvalidOperationException("Error opening QOA file!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Seek(uint sampleFrame)
|
||||||
|
{
|
||||||
|
FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unloads the qoa data, freeing resources.
|
||||||
|
/// </summary>
|
||||||
|
public override unsafe void Unload()
|
||||||
|
{
|
||||||
|
if (Loaded)
|
||||||
|
{
|
||||||
|
FAudio.qoa_close(QoaHandle);
|
||||||
|
NativeMemory.Free((void*) FileDataPtr);
|
||||||
|
|
||||||
|
QoaHandle = IntPtr.Zero;
|
||||||
|
FileDataPtr = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the entire qoa file into an AudioBuffer. Useful for static audio.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath)
|
||||||
|
{
|
||||||
|
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||||
|
var fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length);
|
||||||
|
var fileDataSpan = new Span<byte>(fileDataPtr, (int) fileStream.Length);
|
||||||
|
fileStream.ReadExactly(fileDataSpan);
|
||||||
|
fileStream.Close();
|
||||||
|
|
||||||
|
var qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0);
|
||||||
|
if (qoaHandle == 0)
|
||||||
|
{
|
||||||
|
NativeMemory.Free(fileDataPtr);
|
||||||
|
Logger.LogError("Error opening QOA file!");
|
||||||
|
throw new InvalidOperationException("Error opening QOA file!");
|
||||||
|
}
|
||||||
|
|
||||||
|
FAudio.qoa_attributes(qoaHandle, out var channels, out var samplerate, out var samples_per_channel_per_frame, out var total_samples_per_channel);
|
||||||
|
|
||||||
|
var bufferLengthInBytes = total_samples_per_channel * channels * sizeof(short);
|
||||||
|
var buffer = NativeMemory.Alloc(bufferLengthInBytes);
|
||||||
|
FAudio.qoa_decode_entire(qoaHandle, (short*) buffer);
|
||||||
|
|
||||||
|
FAudio.qoa_close(qoaHandle);
|
||||||
|
NativeMemory.Free(fileDataPtr);
|
||||||
|
|
||||||
|
var format = new Format
|
||||||
|
{
|
||||||
|
Tag = FormatTag.PCM,
|
||||||
|
BitsPerSample = 16,
|
||||||
|
Channels = (ushort) channels,
|
||||||
|
SampleRate = samplerate
|
||||||
|
};
|
||||||
|
|
||||||
|
return new AudioBuffer(device, format, (nint) buffer, bufferLengthInBytes, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe UInt64 ReverseEndianness(UInt64 value)
|
||||||
|
{
|
||||||
|
byte* bytes = (byte*) &value;
|
||||||
|
|
||||||
|
return
|
||||||
|
((UInt64)(bytes[0]) << 56) | ((UInt64)(bytes[1]) << 48) |
|
||||||
|
((UInt64)(bytes[2]) << 40) | ((UInt64)(bytes[3]) << 32) |
|
||||||
|
((UInt64)(bytes[4]) << 24) | ((UInt64)(bytes[5]) << 16) |
|
||||||
|
((UInt64)(bytes[6]) << 8) | ((UInt64)(bytes[7]) << 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Use this in conjunction with a StreamingVoice to play back streaming audio data.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AudioDataStreamable : AudioResource
|
||||||
|
{
|
||||||
|
public Format Format { get; protected set; }
|
||||||
|
public abstract bool Loaded { get; }
|
||||||
|
public abstract uint DecodeBufferSize { get; }
|
||||||
|
|
||||||
|
protected AudioDataStreamable(AudioDevice device) : base(device)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the raw audio data into memory to prepare it for stream decoding.
|
||||||
|
/// </summary>
|
||||||
|
public abstract void Load();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unloads the raw audio data from memory.
|
||||||
|
/// </summary>
|
||||||
|
public abstract void Unload();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seeks to the given sample frame.
|
||||||
|
/// </summary>
|
||||||
|
public abstract void Seek(uint sampleFrame);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to decodes data of length bufferLengthInBytes into the provided buffer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer that decoded bytes will be placed into.</param>
|
||||||
|
/// <param name="bufferLengthInBytes">Requested length of decoded audio data.</param>
|
||||||
|
/// <param name="filledLengthInBytes">How much data was actually filled in by the decode.</param>
|
||||||
|
/// <param name="reachedEnd">Whether the end of the data was reached on this decode.</param>
|
||||||
|
public abstract unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd);
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
Unload();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
public static class AudioDataWav
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create an AudioBuffer containing all the WAV audio data in a file.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath)
|
||||||
|
{
|
||||||
|
// mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385
|
||||||
|
|
||||||
|
// WaveFormatEx data
|
||||||
|
ushort wFormatTag;
|
||||||
|
ushort nChannels;
|
||||||
|
uint nSamplesPerSec;
|
||||||
|
uint nAvgBytesPerSec;
|
||||||
|
ushort nBlockAlign;
|
||||||
|
ushort wBitsPerSample;
|
||||||
|
|
||||||
|
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||||
|
using var reader = new BinaryReader(stream);
|
||||||
|
|
||||||
|
// RIFF Signature
|
||||||
|
string signature = new string(reader.ReadChars(4));
|
||||||
|
if (signature != "RIFF")
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Specified stream is not a wave file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.ReadUInt32(); // Riff Chunk Size
|
||||||
|
|
||||||
|
string wformat = new string(reader.ReadChars(4));
|
||||||
|
if (wformat != "WAVE")
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Specified stream is not a wave file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// WAVE Header
|
||||||
|
string format_signature = new string(reader.ReadChars(4));
|
||||||
|
while (format_signature != "fmt ")
|
||||||
|
{
|
||||||
|
reader.ReadBytes(reader.ReadInt32());
|
||||||
|
format_signature = new string(reader.ReadChars(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
int format_chunk_size = reader.ReadInt32();
|
||||||
|
|
||||||
|
wFormatTag = reader.ReadUInt16();
|
||||||
|
nChannels = reader.ReadUInt16();
|
||||||
|
nSamplesPerSec = reader.ReadUInt32();
|
||||||
|
nAvgBytesPerSec = reader.ReadUInt32();
|
||||||
|
nBlockAlign = reader.ReadUInt16();
|
||||||
|
wBitsPerSample = reader.ReadUInt16();
|
||||||
|
|
||||||
|
// Reads residual bytes
|
||||||
|
if (format_chunk_size > 16)
|
||||||
|
{
|
||||||
|
reader.ReadBytes(format_chunk_size - 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// data Signature
|
||||||
|
string data_signature = new string(reader.ReadChars(4));
|
||||||
|
while (data_signature.ToLowerInvariant() != "data")
|
||||||
|
{
|
||||||
|
reader.ReadBytes(reader.ReadInt32());
|
||||||
|
data_signature = new string(reader.ReadChars(4));
|
||||||
|
}
|
||||||
|
if (data_signature != "data")
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Specified wave file is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int waveDataLength = reader.ReadInt32();
|
||||||
|
var waveDataBuffer = NativeMemory.Alloc((nuint) waveDataLength);
|
||||||
|
var waveDataSpan = new Span<byte>(waveDataBuffer, waveDataLength);
|
||||||
|
stream.ReadExactly(waveDataSpan);
|
||||||
|
|
||||||
|
var format = new Format
|
||||||
|
{
|
||||||
|
Tag = (FormatTag) wFormatTag,
|
||||||
|
BitsPerSample = wBitsPerSample,
|
||||||
|
Channels = nChannels,
|
||||||
|
SampleRate = nSamplesPerSec
|
||||||
|
};
|
||||||
|
|
||||||
|
return new AudioBuffer(
|
||||||
|
device,
|
||||||
|
format,
|
||||||
|
(nint) waveDataBuffer,
|
||||||
|
(uint) waveDataLength,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,41 +1,53 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// AudioDevice manages all audio-related concerns.
|
||||||
|
/// </summary>
|
||||||
public class AudioDevice : IDisposable
|
public class AudioDevice : IDisposable
|
||||||
{
|
{
|
||||||
public IntPtr Handle { get; }
|
public IntPtr Handle { get; }
|
||||||
public byte[] Handle3D { get; }
|
public byte[] Handle3D { get; }
|
||||||
public IntPtr MasteringVoice { get; }
|
|
||||||
public FAudio.FAudioDeviceDetails DeviceDetails { get; }
|
public FAudio.FAudioDeviceDetails DeviceDetails { get; }
|
||||||
public IntPtr ReverbVoice { get; }
|
|
||||||
|
private IntPtr trueMasteringVoice;
|
||||||
|
|
||||||
|
// this is a fun little trick where we use a submix voice as a "faux" mastering voice
|
||||||
|
// this lets us maintain API consistency for effects like panning and reverb
|
||||||
|
private SubmixVoice fauxMasteringVoice;
|
||||||
|
public SubmixVoice MasteringVoice => fauxMasteringVoice;
|
||||||
|
|
||||||
public float CurveDistanceScalar = 1f;
|
public float CurveDistanceScalar = 1f;
|
||||||
public float DopplerScale = 1f;
|
public float DopplerScale = 1f;
|
||||||
public float SpeedOfSound = 343.5f;
|
public float SpeedOfSound = 343.5f;
|
||||||
|
|
||||||
private float masteringVolume = 1f;
|
private readonly HashSet<GCHandle> resourceHandles = new HashSet<GCHandle>();
|
||||||
public float MasteringVolume
|
private readonly HashSet<UpdatingSourceVoice> updatingSourceVoices = new HashSet<UpdatingSourceVoice>();
|
||||||
|
|
||||||
|
private AudioTweenManager AudioTweenManager;
|
||||||
|
|
||||||
|
private SourceVoicePool VoicePool;
|
||||||
|
private List<SourceVoice> VoicesToReturn = new List<SourceVoice>();
|
||||||
|
|
||||||
|
private const int Step = 200;
|
||||||
|
private TimeSpan UpdateInterval;
|
||||||
|
private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch();
|
||||||
|
private long previousTickTime;
|
||||||
|
private Thread Thread;
|
||||||
|
private AutoResetEvent WakeSignal;
|
||||||
|
internal readonly object StateLock = new object();
|
||||||
|
|
||||||
|
private bool Running;
|
||||||
|
public bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
|
internal unsafe AudioDevice()
|
||||||
{
|
{
|
||||||
get => masteringVolume;
|
UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step);
|
||||||
set
|
|
||||||
{
|
|
||||||
masteringVolume = value;
|
|
||||||
FAudio.FAudioVoice_SetVolume(MasteringVoice, masteringVolume, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal FAudio.FAudioVoiceSends ReverbSends;
|
|
||||||
|
|
||||||
private readonly List<WeakReference<AudioResource>> resources = new List<WeakReference<AudioResource>>();
|
|
||||||
private readonly List<WeakReference<StreamingSound>> streamingSounds = new List<WeakReference<StreamingSound>>();
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
|
|
||||||
public unsafe AudioDevice()
|
|
||||||
{
|
|
||||||
FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR);
|
FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR);
|
||||||
Handle = handle;
|
Handle = handle;
|
||||||
|
|
||||||
|
@ -80,25 +92,24 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Init Mastering Voice */
|
/* Init Mastering Voice */
|
||||||
IntPtr masteringVoice;
|
var result = FAudio.FAudio_CreateMasteringVoice(
|
||||||
|
|
||||||
if (FAudio.FAudio_CreateMasteringVoice(
|
|
||||||
Handle,
|
Handle,
|
||||||
out masteringVoice,
|
out trueMasteringVoice,
|
||||||
FAudio.FAUDIO_DEFAULT_CHANNELS,
|
FAudio.FAUDIO_DEFAULT_CHANNELS,
|
||||||
FAudio.FAUDIO_DEFAULT_SAMPLERATE,
|
FAudio.FAUDIO_DEFAULT_SAMPLERATE,
|
||||||
0,
|
0,
|
||||||
i,
|
i,
|
||||||
IntPtr.Zero
|
IntPtr.Zero
|
||||||
) != 0)
|
);
|
||||||
|
|
||||||
|
if (result != 0)
|
||||||
{
|
{
|
||||||
Logger.LogError("No mastering voice found!");
|
Logger.LogError("Failed to create a mastering voice!");
|
||||||
Handle = IntPtr.Zero;
|
Logger.LogError("Audio device creation failed!");
|
||||||
FAudio.FAudio_Release(Handle);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MasteringVoice = masteringVoice;
|
fauxMasteringVoice = SubmixVoice.CreateFauxMasteringVoice(this);
|
||||||
|
|
||||||
/* Init 3D Audio */
|
/* Init 3D Audio */
|
||||||
|
|
||||||
|
@ -109,139 +120,169 @@ namespace MoonWorks.Audio
|
||||||
Handle3D
|
Handle3D
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Init reverb */
|
AudioTweenManager = new AudioTweenManager();
|
||||||
|
VoicePool = new SourceVoicePool(this);
|
||||||
|
|
||||||
IntPtr reverbVoice;
|
WakeSignal = new AutoResetEvent(true);
|
||||||
|
|
||||||
IntPtr reverb;
|
Thread = new Thread(ThreadMain);
|
||||||
FAudio.FAudioCreateReverb(out reverb, 0);
|
Thread.IsBackground = true;
|
||||||
|
Thread.Start();
|
||||||
|
|
||||||
IntPtr chainPtr;
|
Running = true;
|
||||||
chainPtr = Marshal.AllocHGlobal(
|
|
||||||
Marshal.SizeOf<FAudio.FAudioEffectChain>()
|
|
||||||
);
|
|
||||||
|
|
||||||
FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr;
|
TickStopwatch.Start();
|
||||||
reverbChain->EffectCount = 1;
|
previousTickTime = 0;
|
||||||
reverbChain->pEffectDescriptors = Marshal.AllocHGlobal(
|
|
||||||
Marshal.SizeOf<FAudio.FAudioEffectDescriptor>()
|
|
||||||
);
|
|
||||||
|
|
||||||
FAudio.FAudioEffectDescriptor* reverbDescriptor =
|
|
||||||
(FAudio.FAudioEffectDescriptor*) reverbChain->pEffectDescriptors;
|
|
||||||
|
|
||||||
reverbDescriptor->InitialState = 1;
|
|
||||||
reverbDescriptor->OutputChannels = (uint) (
|
|
||||||
(DeviceDetails.OutputFormat.Format.nChannels == 6) ? 6 : 1
|
|
||||||
);
|
|
||||||
reverbDescriptor->pEffect = reverb;
|
|
||||||
|
|
||||||
FAudio.FAudio_CreateSubmixVoice(
|
|
||||||
Handle,
|
|
||||||
out reverbVoice,
|
|
||||||
1, /* omnidirectional reverb */
|
|
||||||
DeviceDetails.OutputFormat.Format.nSamplesPerSec,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
IntPtr.Zero,
|
|
||||||
chainPtr
|
|
||||||
);
|
|
||||||
FAudio.FAPOBase_Release(reverb);
|
|
||||||
|
|
||||||
Marshal.FreeHGlobal(reverbChain->pEffectDescriptors);
|
|
||||||
Marshal.FreeHGlobal(chainPtr);
|
|
||||||
|
|
||||||
ReverbVoice = reverbVoice;
|
|
||||||
|
|
||||||
/* Init reverb params */
|
|
||||||
// Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC
|
|
||||||
|
|
||||||
IntPtr reverbParamsPtr = Marshal.AllocHGlobal(
|
|
||||||
Marshal.SizeOf<FAudio.FAudioFXReverbParameters>()
|
|
||||||
);
|
|
||||||
|
|
||||||
FAudio.FAudioFXReverbParameters* reverbParams = (FAudio.FAudioFXReverbParameters*) reverbParamsPtr;
|
|
||||||
reverbParams->WetDryMix = 100.0f;
|
|
||||||
reverbParams->ReflectionsDelay = 7;
|
|
||||||
reverbParams->ReverbDelay = 11;
|
|
||||||
reverbParams->RearDelay = FAudio.FAUDIOFX_REVERB_DEFAULT_REAR_DELAY;
|
|
||||||
reverbParams->PositionLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION;
|
|
||||||
reverbParams->PositionRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION;
|
|
||||||
reverbParams->PositionMatrixLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
|
|
||||||
reverbParams->PositionMatrixRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
|
|
||||||
reverbParams->EarlyDiffusion = 15;
|
|
||||||
reverbParams->LateDiffusion = 15;
|
|
||||||
reverbParams->LowEQGain = 8;
|
|
||||||
reverbParams->LowEQCutoff = 4;
|
|
||||||
reverbParams->HighEQGain = 8;
|
|
||||||
reverbParams->HighEQCutoff = 6;
|
|
||||||
reverbParams->RoomFilterFreq = 5000f;
|
|
||||||
reverbParams->RoomFilterMain = -10f;
|
|
||||||
reverbParams->RoomFilterHF = -1f;
|
|
||||||
reverbParams->ReflectionsGain = -26.0200005f;
|
|
||||||
reverbParams->ReverbGain = 10.0f;
|
|
||||||
reverbParams->DecayTime = 1.49000001f;
|
|
||||||
reverbParams->Density = 100.0f;
|
|
||||||
reverbParams->RoomSize = FAudio.FAUDIOFX_REVERB_DEFAULT_ROOM_SIZE;
|
|
||||||
FAudio.FAudioVoice_SetEffectParameters(
|
|
||||||
ReverbVoice,
|
|
||||||
0,
|
|
||||||
reverbParamsPtr,
|
|
||||||
(uint) Marshal.SizeOf<FAudio.FAudioFXReverbParameters>(),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
Marshal.FreeHGlobal(reverbParamsPtr);
|
|
||||||
|
|
||||||
/* Init reverb sends */
|
|
||||||
|
|
||||||
ReverbSends = new FAudio.FAudioVoiceSends
|
|
||||||
{
|
|
||||||
SendCount = 2,
|
|
||||||
pSends = Marshal.AllocHGlobal(
|
|
||||||
2 * Marshal.SizeOf<FAudio.FAudioSendDescriptor>()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
FAudio.FAudioSendDescriptor* sendDesc = (FAudio.FAudioSendDescriptor*) ReverbSends.pSends;
|
|
||||||
sendDesc[0].Flags = 0;
|
|
||||||
sendDesc[0].pOutputVoice = MasteringVoice;
|
|
||||||
sendDesc[1].Flags = 0;
|
|
||||||
sendDesc[1].pOutputVoice = ReverbVoice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Update()
|
private void ThreadMain()
|
||||||
{
|
{
|
||||||
for (var i = streamingSounds.Count - 1; i >= 0; i--)
|
while (Running)
|
||||||
{
|
{
|
||||||
var weakReference = streamingSounds[i];
|
lock (StateLock)
|
||||||
if (weakReference.TryGetTarget(out var streamingSound))
|
|
||||||
{
|
{
|
||||||
streamingSound.Update();
|
try
|
||||||
|
{
|
||||||
|
ThreadMainTick();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
WakeSignal.WaitOne(UpdateInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThreadMainTick()
|
||||||
|
{
|
||||||
|
long tickDelta = TickStopwatch.Elapsed.Ticks - previousTickTime;
|
||||||
|
previousTickTime = TickStopwatch.Elapsed.Ticks;
|
||||||
|
float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond;
|
||||||
|
|
||||||
|
AudioTweenManager.Update(elapsedSeconds);
|
||||||
|
|
||||||
|
foreach (var voice in updatingSourceVoices)
|
||||||
|
{
|
||||||
|
voice.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var voice in VoicesToReturn)
|
||||||
|
{
|
||||||
|
if (voice is UpdatingSourceVoice updatingSourceVoice)
|
||||||
{
|
{
|
||||||
streamingSounds.RemoveAt(i);
|
updatingSourceVoices.Remove(updatingSourceVoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
voice.Reset();
|
||||||
|
VoicePool.Return(voice);
|
||||||
|
}
|
||||||
|
|
||||||
|
VoicesToReturn.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers all pending operations with the given syncGroup value.
|
||||||
|
/// </summary>
|
||||||
|
public void TriggerSyncGroup(uint syncGroup)
|
||||||
|
{
|
||||||
|
FAudio.FAudio_CommitChanges(Handle, syncGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtains an appropriate source voice from the voice pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="format">The format that the voice must match.</param>
|
||||||
|
/// <returns>A source voice with the given format.</returns>
|
||||||
|
public T Obtain<T>(Format format) where T : SourceVoice, IPoolable<T>
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
var voice = VoicePool.Obtain<T>(format);
|
||||||
|
|
||||||
|
if (voice is UpdatingSourceVoice updatingSourceVoice)
|
||||||
|
{
|
||||||
|
updatingSourceVoices.Add(updatingSourceVoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return voice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the source voice to the voice pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="voice"></param>
|
||||||
|
internal void Return(SourceVoice voice)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
VoicesToReturn.Add(voice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void CreateTween(
|
||||||
|
Voice voice,
|
||||||
|
AudioTweenProperty property,
|
||||||
|
System.Func<float, float> easingFunction,
|
||||||
|
float start,
|
||||||
|
float end,
|
||||||
|
float duration,
|
||||||
|
float delayTime
|
||||||
|
) {
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
AudioTweenManager.CreateTween(
|
||||||
|
voice,
|
||||||
|
property,
|
||||||
|
easingFunction,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
duration,
|
||||||
|
delayTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ClearTweens(
|
||||||
|
Voice voice,
|
||||||
|
AudioTweenProperty property
|
||||||
|
) {
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
AudioTweenManager.ClearTweens(voice, property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void WakeThread()
|
||||||
|
{
|
||||||
|
WakeSignal.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AddResourceReference(GCHandle resourceReference)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
resourceHandles.Add(resourceReference);
|
||||||
|
|
||||||
|
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
|
||||||
|
{
|
||||||
|
updatingSourceVoices.Add(updatableVoice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AddDynamicSoundInstance(StreamingSound instance)
|
internal void RemoveResourceReference(GCHandle resourceReference)
|
||||||
{
|
{
|
||||||
streamingSounds.Add(new WeakReference<StreamingSound>(instance));
|
lock (StateLock)
|
||||||
}
|
|
||||||
|
|
||||||
internal void AddResourceReference(WeakReference<AudioResource> resourceReference)
|
|
||||||
{
|
|
||||||
lock (resources)
|
|
||||||
{
|
{
|
||||||
resources.Add(resourceReference);
|
resourceHandles.Remove(resourceReference);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void RemoveResourceReference(WeakReference<AudioResource> resourceReference)
|
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
|
||||||
{
|
{
|
||||||
lock (resources)
|
updatingSourceVoices.Remove(updatableVoice);
|
||||||
{
|
}
|
||||||
resources.Remove(resourceReference);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,29 +290,54 @@ namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
if (!IsDisposed)
|
if (!IsDisposed)
|
||||||
{
|
{
|
||||||
|
Running = false;
|
||||||
|
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
for (var i = resources.Count - 1; i >= 0; i--)
|
Thread.Join();
|
||||||
{
|
|
||||||
var weakReference = resources[i];
|
|
||||||
|
|
||||||
if (weakReference.TryGetTarget(out var resource))
|
// dispose all source voices first
|
||||||
|
foreach (var handle in resourceHandles)
|
||||||
|
{
|
||||||
|
if (handle.Target is SourceVoice voice)
|
||||||
|
{
|
||||||
|
voice.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispose all submix voices except the faux mastering voice
|
||||||
|
foreach (var handle in resourceHandles)
|
||||||
|
{
|
||||||
|
if (handle.Target is SubmixVoice voice && voice != fauxMasteringVoice)
|
||||||
|
{
|
||||||
|
voice.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispose the faux mastering voice
|
||||||
|
fauxMasteringVoice.Dispose();
|
||||||
|
|
||||||
|
// dispose the true mastering voice
|
||||||
|
FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice);
|
||||||
|
|
||||||
|
// destroy all other audio resources
|
||||||
|
foreach (var handle in resourceHandles)
|
||||||
|
{
|
||||||
|
if (handle.Target is AudioResource resource)
|
||||||
{
|
{
|
||||||
resource.Dispose();
|
resource.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resources.Clear();
|
|
||||||
|
resourceHandles.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
FAudio.FAudioVoice_DestroyVoice(ReverbVoice);
|
|
||||||
FAudio.FAudioVoice_DestroyVoice(MasteringVoice);
|
|
||||||
FAudio.FAudio_Release(Handle);
|
FAudio.FAudio_Release(Handle);
|
||||||
|
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
|
||||||
~AudioDevice()
|
~AudioDevice()
|
||||||
{
|
{
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||||
|
|
|
@ -4,6 +4,9 @@ using MoonWorks.Math.Float;
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An emitter for 3D spatial audio.
|
||||||
|
/// </summary>
|
||||||
public class AudioEmitter : AudioResource
|
public class AudioEmitter : AudioResource
|
||||||
{
|
{
|
||||||
internal FAudio.F3DAUDIO_EMITTER emitterData;
|
internal FAudio.F3DAUDIO_EMITTER emitterData;
|
||||||
|
@ -129,7 +132,5 @@ namespace MoonWorks.Audio
|
||||||
emitterData.pReverbCurve = IntPtr.Zero;
|
emitterData.pReverbCurve = IntPtr.Zero;
|
||||||
emitterData.CurveDistanceScaler = 1.0f;
|
emitterData.CurveDistanceScaler = 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Destroy() { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@ using MoonWorks.Math.Float;
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A listener for 3D spatial audio. Usually attached to a camera.
|
||||||
|
/// </summary>
|
||||||
public class AudioListener : AudioResource
|
public class AudioListener : AudioResource
|
||||||
{
|
{
|
||||||
internal FAudio.F3DAUDIO_LISTENER listenerData;
|
internal FAudio.F3DAUDIO_LISTENER listenerData;
|
||||||
|
@ -91,7 +94,5 @@ namespace MoonWorks.Audio
|
||||||
/* Unexposed variables, defaults based on XNA behavior */
|
/* Unexposed variables, defaults based on XNA behavior */
|
||||||
listenerData.pCone = IntPtr.Zero;
|
listenerData.pCone = IntPtr.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Destroy() { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
|
@ -8,28 +9,24 @@ namespace MoonWorks.Audio
|
||||||
|
|
||||||
public bool IsDisposed { get; private set; }
|
public bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
private WeakReference<AudioResource> selfReference;
|
private GCHandle SelfReference;
|
||||||
|
|
||||||
public AudioResource(AudioDevice device)
|
protected AudioResource(AudioDevice device)
|
||||||
{
|
{
|
||||||
Device = device;
|
Device = device;
|
||||||
|
|
||||||
selfReference = new WeakReference<AudioResource>(this);
|
SelfReference = GCHandle.Alloc(this, GCHandleType.Weak);
|
||||||
Device.AddResourceReference(selfReference);
|
Device.AddResourceReference(SelfReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void Destroy();
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!IsDisposed)
|
if (!IsDisposed)
|
||||||
{
|
{
|
||||||
Destroy();
|
if (disposing)
|
||||||
|
|
||||||
if (selfReference != null)
|
|
||||||
{
|
{
|
||||||
Device.RemoveResourceReference(selfReference);
|
Device.RemoveResourceReference(SelfReference);
|
||||||
selfReference = null;
|
SelfReference.Free();
|
||||||
}
|
}
|
||||||
|
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
|
@ -38,8 +35,12 @@ namespace MoonWorks.Audio
|
||||||
|
|
||||||
~AudioResource()
|
~AudioResource()
|
||||||
{
|
{
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
#if DEBUG
|
||||||
Dispose(disposing: false);
|
// If you see this log message, you leaked an audio resource without disposing it!
|
||||||
|
// We can't clean it up for you because this can cause catastrophic issues.
|
||||||
|
// You should really fix this when it happens.
|
||||||
|
Logger.LogWarn($"A resource of type {GetType().Name} was not Disposed.");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using EasingFunction = System.Func<float, float>;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
internal enum AudioTweenProperty
|
||||||
|
{
|
||||||
|
Pan,
|
||||||
|
Pitch,
|
||||||
|
Volume,
|
||||||
|
FilterFrequency,
|
||||||
|
Reverb
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AudioTween
|
||||||
|
{
|
||||||
|
public Voice Voice;
|
||||||
|
public AudioTweenProperty Property;
|
||||||
|
public EasingFunction EasingFunction;
|
||||||
|
public float Time;
|
||||||
|
public float StartValue;
|
||||||
|
public float EndValue;
|
||||||
|
public float DelayTime;
|
||||||
|
public float Duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AudioTweenPool
|
||||||
|
{
|
||||||
|
private Queue<AudioTween> Tweens = new Queue<AudioTween>(16);
|
||||||
|
|
||||||
|
public AudioTweenPool()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 16; i += 1)
|
||||||
|
{
|
||||||
|
Tweens.Enqueue(new AudioTween());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioTween Obtain()
|
||||||
|
{
|
||||||
|
if (Tweens.Count > 0)
|
||||||
|
{
|
||||||
|
var tween = Tweens.Dequeue();
|
||||||
|
return tween;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new AudioTween();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Free(AudioTween tween)
|
||||||
|
{
|
||||||
|
tween.Voice = null;
|
||||||
|
Tweens.Enqueue(tween);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
internal class AudioTweenManager
|
||||||
|
{
|
||||||
|
private AudioTweenPool AudioTweenPool = new AudioTweenPool();
|
||||||
|
private readonly Dictionary<(Voice, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(Voice, AudioTweenProperty), AudioTween>();
|
||||||
|
private readonly List<AudioTween> DelayedAudioTweens = new List<AudioTween>();
|
||||||
|
|
||||||
|
public void Update(float elapsedSeconds)
|
||||||
|
{
|
||||||
|
for (var i = DelayedAudioTweens.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var audioTween = DelayedAudioTweens[i];
|
||||||
|
var voice = audioTween.Voice;
|
||||||
|
|
||||||
|
audioTween.Time += elapsedSeconds;
|
||||||
|
|
||||||
|
if (audioTween.Time >= audioTween.DelayTime)
|
||||||
|
{
|
||||||
|
// set the tween start value to the current value of the property
|
||||||
|
switch (audioTween.Property)
|
||||||
|
{
|
||||||
|
case AudioTweenProperty.Pan:
|
||||||
|
audioTween.StartValue = voice.Pan;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AudioTweenProperty.Pitch:
|
||||||
|
audioTween.StartValue = voice.Pitch;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AudioTweenProperty.Volume:
|
||||||
|
audioTween.StartValue = voice.Volume;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AudioTweenProperty.FilterFrequency:
|
||||||
|
audioTween.StartValue = voice.FilterFrequency;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AudioTweenProperty.Reverb:
|
||||||
|
audioTween.StartValue = voice.Reverb;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
audioTween.Time = 0;
|
||||||
|
DelayedAudioTweens.RemoveAt(i);
|
||||||
|
|
||||||
|
AddTween(audioTween);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (key, audioTween) in AudioTweens)
|
||||||
|
{
|
||||||
|
bool finished = UpdateAudioTween(audioTween, elapsedSeconds);
|
||||||
|
|
||||||
|
if (finished)
|
||||||
|
{
|
||||||
|
AudioTweenPool.Free(audioTween);
|
||||||
|
AudioTweens.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateTween(
|
||||||
|
Voice voice,
|
||||||
|
AudioTweenProperty property,
|
||||||
|
System.Func<float, float> easingFunction,
|
||||||
|
float start,
|
||||||
|
float end,
|
||||||
|
float duration,
|
||||||
|
float delayTime
|
||||||
|
) {
|
||||||
|
var tween = AudioTweenPool.Obtain();
|
||||||
|
tween.Voice = voice;
|
||||||
|
tween.Property = property;
|
||||||
|
tween.EasingFunction = easingFunction;
|
||||||
|
tween.StartValue = start;
|
||||||
|
tween.EndValue = end;
|
||||||
|
tween.Duration = duration;
|
||||||
|
tween.Time = 0;
|
||||||
|
tween.DelayTime = delayTime;
|
||||||
|
|
||||||
|
if (delayTime == 0)
|
||||||
|
{
|
||||||
|
AddTween(tween);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DelayedAudioTweens.Add(tween);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearTweens(Voice voice, AudioTweenProperty property)
|
||||||
|
{
|
||||||
|
AudioTweens.Remove((voice, property));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTween(
|
||||||
|
AudioTween audioTween
|
||||||
|
) {
|
||||||
|
// if a tween with the same sound and property already exists, get rid of it
|
||||||
|
if (AudioTweens.TryGetValue((audioTween.Voice, audioTween.Property), out var currentTween))
|
||||||
|
{
|
||||||
|
AudioTweenPool.Free(currentTween);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioTweens[(audioTween.Voice, audioTween.Property)] = audioTween;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool UpdateAudioTween(AudioTween audioTween, float delta)
|
||||||
|
{
|
||||||
|
float value;
|
||||||
|
audioTween.Time += delta;
|
||||||
|
|
||||||
|
var finished = audioTween.Time >= audioTween.Duration;
|
||||||
|
if (finished)
|
||||||
|
{
|
||||||
|
value = audioTween.EndValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = MoonWorks.Math.Easing.Interp(
|
||||||
|
audioTween.StartValue,
|
||||||
|
audioTween.EndValue,
|
||||||
|
audioTween.Time,
|
||||||
|
audioTween.Duration,
|
||||||
|
audioTween.EasingFunction
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (audioTween.Property)
|
||||||
|
{
|
||||||
|
case AudioTweenProperty.Pan:
|
||||||
|
audioTween.Voice.Pan = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AudioTweenProperty.Pitch:
|
||||||
|
audioTween.Voice.Pitch = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AudioTweenProperty.Volume:
|
||||||
|
audioTween.Voice.Volume = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AudioTweenProperty.FilterFrequency:
|
||||||
|
audioTween.Voice.FilterFrequency = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AudioTweenProperty.Reverb:
|
||||||
|
audioTween.Voice.Reverb = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return finished;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
public enum FormatTag : ushort
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
PCM = 1,
|
||||||
|
MSADPCM = 2,
|
||||||
|
IEEE_FLOAT = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the format of audio data. Usually specified in an audio file's header information.
|
||||||
|
/// </summary>
|
||||||
|
public record struct Format
|
||||||
|
{
|
||||||
|
public FormatTag Tag;
|
||||||
|
public ushort Channels;
|
||||||
|
public uint SampleRate;
|
||||||
|
public ushort BitsPerSample;
|
||||||
|
|
||||||
|
internal FAudio.FAudioWaveFormatEx ToFAudioFormat()
|
||||||
|
{
|
||||||
|
var blockAlign = (ushort) ((BitsPerSample / 8) * Channels);
|
||||||
|
|
||||||
|
return new FAudio.FAudioWaveFormatEx
|
||||||
|
{
|
||||||
|
wFormatTag = (ushort) Tag,
|
||||||
|
nChannels = Channels,
|
||||||
|
nSamplesPerSec = SampleRate,
|
||||||
|
wBitsPerSample = BitsPerSample,
|
||||||
|
nBlockAlign = blockAlign,
|
||||||
|
nAvgBytesPerSec = blockAlign * SampleRate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
public interface IPoolable<T>
|
||||||
|
{
|
||||||
|
static abstract T Create(AudioDevice device, Format format);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PersistentVoice should be used when you need to maintain a long-term reference to a source voice.
|
||||||
|
/// </summary>
|
||||||
|
public class PersistentVoice : SourceVoice, IPoolable<PersistentVoice>
|
||||||
|
{
|
||||||
|
public PersistentVoice(AudioDevice device, Format format) : base(device, format)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PersistentVoice Create(AudioDevice device, Format format)
|
||||||
|
{
|
||||||
|
return new PersistentVoice(device, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an AudioBuffer to the voice queue.
|
||||||
|
/// The voice processes and plays back the buffers in its queue in the order that they were submitted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer to submit to the voice.</param>
|
||||||
|
/// <param name="loop">Whether the voice should loop this buffer.</param>
|
||||||
|
public void Submit(AudioBuffer buffer, bool loop = false)
|
||||||
|
{
|
||||||
|
Submit(buffer.ToFAudioBuffer(loop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Use this in conjunction with SourceVoice.SetReverbEffectChain to add reverb to a voice.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe class ReverbEffect : SubmixVoice
|
||||||
|
{
|
||||||
|
// Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC
|
||||||
|
public static FAudio.FAudioFXReverbParameters DefaultParams = new FAudio.FAudioFXReverbParameters
|
||||||
|
{
|
||||||
|
WetDryMix = 100.0f,
|
||||||
|
ReflectionsDelay = 7,
|
||||||
|
ReverbDelay = 11,
|
||||||
|
RearDelay = FAudio.FAUDIOFX_REVERB_DEFAULT_REAR_DELAY,
|
||||||
|
PositionLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION,
|
||||||
|
PositionRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION,
|
||||||
|
PositionMatrixLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX,
|
||||||
|
PositionMatrixRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX,
|
||||||
|
EarlyDiffusion = 15,
|
||||||
|
LateDiffusion = 15,
|
||||||
|
LowEQGain = 8,
|
||||||
|
LowEQCutoff = 4,
|
||||||
|
HighEQGain = 8,
|
||||||
|
HighEQCutoff = 6,
|
||||||
|
RoomFilterFreq = 5000f,
|
||||||
|
RoomFilterMain = -10f,
|
||||||
|
RoomFilterHF = -1f,
|
||||||
|
ReflectionsGain = -26.0200005f,
|
||||||
|
ReverbGain = 10.0f,
|
||||||
|
DecayTime = 1.49000001f,
|
||||||
|
Density = 100.0f,
|
||||||
|
RoomSize = FAudio.FAUDIOFX_REVERB_DEFAULT_ROOM_SIZE
|
||||||
|
};
|
||||||
|
|
||||||
|
public FAudio.FAudioFXReverbParameters Params { get; private set; }
|
||||||
|
|
||||||
|
public ReverbEffect(AudioDevice audioDevice, uint processingStage) : base(audioDevice, 1, audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec, processingStage)
|
||||||
|
{
|
||||||
|
/* Init reverb */
|
||||||
|
IntPtr reverb;
|
||||||
|
FAudio.FAudioCreateReverb(out reverb, 0);
|
||||||
|
|
||||||
|
var chain = new FAudio.FAudioEffectChain();
|
||||||
|
var descriptor = new FAudio.FAudioEffectDescriptor
|
||||||
|
{
|
||||||
|
InitialState = 1,
|
||||||
|
OutputChannels = 1,
|
||||||
|
pEffect = reverb
|
||||||
|
};
|
||||||
|
|
||||||
|
chain.EffectCount = 1;
|
||||||
|
chain.pEffectDescriptors = (nint) (&descriptor);
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetEffectChain(
|
||||||
|
Handle,
|
||||||
|
ref chain
|
||||||
|
);
|
||||||
|
|
||||||
|
FAudio.FAPOBase_Release(reverb);
|
||||||
|
|
||||||
|
SetParams(DefaultParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetParams(in FAudio.FAudioFXReverbParameters reverbParams)
|
||||||
|
{
|
||||||
|
Params = reverbParams;
|
||||||
|
|
||||||
|
fixed (FAudio.FAudioFXReverbParameters* reverbParamsPtr = &reverbParams)
|
||||||
|
{
|
||||||
|
FAudio.FAudioVoice_SetEffectParameters(
|
||||||
|
Handle,
|
||||||
|
0,
|
||||||
|
(nint) reverbParamsPtr,
|
||||||
|
(uint) Marshal.SizeOf<FAudio.FAudioFXReverbParameters>(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,366 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public abstract class SoundInstance : AudioResource
|
|
||||||
{
|
|
||||||
internal IntPtr Handle;
|
|
||||||
internal FAudio.FAudioWaveFormatEx Format;
|
|
||||||
|
|
||||||
protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
|
|
||||||
|
|
||||||
public bool Is3D { get; protected set; }
|
|
||||||
|
|
||||||
public virtual SoundState State { get; protected set; }
|
|
||||||
|
|
||||||
private float pan = 0;
|
|
||||||
public float Pan
|
|
||||||
{
|
|
||||||
get => pan;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
pan = value;
|
|
||||||
|
|
||||||
if (pan < -1f)
|
|
||||||
{
|
|
||||||
pan = -1f;
|
|
||||||
}
|
|
||||||
if (pan > 1f)
|
|
||||||
{
|
|
||||||
pan = 1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Is3D) { return; }
|
|
||||||
|
|
||||||
SetPanMatrixCoefficients();
|
|
||||||
FAudio.FAudioVoice_SetOutputMatrix(
|
|
||||||
Handle,
|
|
||||||
Device.MasteringVoice,
|
|
||||||
dspSettings.SrcChannelCount,
|
|
||||||
dspSettings.DstChannelCount,
|
|
||||||
dspSettings.pMatrixCoefficients,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float pitch = 0;
|
|
||||||
public float Pitch
|
|
||||||
{
|
|
||||||
get => pitch;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
pitch = Math.MathHelper.Clamp(value, -1f, 1f);
|
|
||||||
UpdatePitch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float volume = 1;
|
|
||||||
public float Volume
|
|
||||||
{
|
|
||||||
get => volume;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
volume = value;
|
|
||||||
FAudio.FAudioVoice_SetVolume(Handle, volume, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float reverb;
|
|
||||||
public unsafe float Reverb
|
|
||||||
{
|
|
||||||
get => reverb;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
reverb = value;
|
|
||||||
|
|
||||||
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
|
|
||||||
outputMatrix[0] = reverb;
|
|
||||||
if (dspSettings.SrcChannelCount == 2)
|
|
||||||
{
|
|
||||||
outputMatrix[1] = reverb;
|
|
||||||
}
|
|
||||||
|
|
||||||
FAudio.FAudioVoice_SetOutputMatrix(
|
|
||||||
Handle,
|
|
||||||
Device.ReverbVoice,
|
|
||||||
dspSettings.SrcChannelCount,
|
|
||||||
1,
|
|
||||||
dspSettings.pMatrixCoefficients,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const float MAX_FILTER_FREQUENCY = 1f;
|
|
||||||
private const float MAX_FILTER_ONEOVERQ = 1.5f;
|
|
||||||
|
|
||||||
private FAudio.FAudioFilterParameters filterParameters = new FAudio.FAudioFilterParameters
|
|
||||||
{
|
|
||||||
Type = FAudio.FAudioFilterType.FAudioLowPassFilter,
|
|
||||||
Frequency = 1f,
|
|
||||||
OneOverQ = 1f
|
|
||||||
};
|
|
||||||
|
|
||||||
private float FilterFrequency
|
|
||||||
{
|
|
||||||
get => filterParameters.Frequency;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY);
|
|
||||||
filterParameters.Frequency = value;
|
|
||||||
|
|
||||||
FAudio.FAudioVoice_SetFilterParameters(
|
|
||||||
Handle,
|
|
||||||
ref filterParameters,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float FilterOneOverQ
|
|
||||||
{
|
|
||||||
get => filterParameters.OneOverQ;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ);
|
|
||||||
filterParameters.OneOverQ = value;
|
|
||||||
|
|
||||||
FAudio.FAudioVoice_SetFilterParameters(
|
|
||||||
Handle,
|
|
||||||
ref filterParameters,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private FilterType filterType;
|
|
||||||
public FilterType FilterType
|
|
||||||
{
|
|
||||||
get => filterType;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
filterType = value;
|
|
||||||
|
|
||||||
switch (filterType)
|
|
||||||
{
|
|
||||||
case FilterType.None:
|
|
||||||
filterParameters = new FAudio.FAudioFilterParameters
|
|
||||||
{
|
|
||||||
Type = FAudio.FAudioFilterType.FAudioLowPassFilter,
|
|
||||||
Frequency = 1f,
|
|
||||||
OneOverQ = 1f
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FilterType.LowPass:
|
|
||||||
filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FilterType.BandPass:
|
|
||||||
filterParameters.Type = FAudio.FAudioFilterType.FAudioBandPassFilter;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FilterType.HighPass:
|
|
||||||
filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
FAudio.FAudioVoice_SetFilterParameters(
|
|
||||||
Handle,
|
|
||||||
ref filterParameters,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SoundInstance(
|
|
||||||
AudioDevice device,
|
|
||||||
ushort formatTag,
|
|
||||||
ushort bitsPerSample,
|
|
||||||
ushort blockAlign,
|
|
||||||
ushort channels,
|
|
||||||
uint samplesPerSecond
|
|
||||||
) : base(device)
|
|
||||||
{
|
|
||||||
var format = new FAudio.FAudioWaveFormatEx
|
|
||||||
{
|
|
||||||
wFormatTag = formatTag,
|
|
||||||
wBitsPerSample = bitsPerSample,
|
|
||||||
nChannels = channels,
|
|
||||||
nBlockAlign = blockAlign,
|
|
||||||
nSamplesPerSec = samplesPerSecond,
|
|
||||||
nAvgBytesPerSec = blockAlign * samplesPerSecond
|
|
||||||
};
|
|
||||||
|
|
||||||
Format = format;
|
|
||||||
|
|
||||||
FAudio.FAudio_CreateSourceVoice(
|
|
||||||
Device.Handle,
|
|
||||||
out Handle,
|
|
||||||
ref Format,
|
|
||||||
FAudio.FAUDIO_VOICE_USEFILTER,
|
|
||||||
FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
|
|
||||||
IntPtr.Zero,
|
|
||||||
IntPtr.Zero,
|
|
||||||
IntPtr.Zero
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Handle == IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Logger.LogError("SoundInstance failed to initialize!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
InitDSPSettings(Format.nChannels);
|
|
||||||
|
|
||||||
// FIXME: not everything should be running through reverb...
|
|
||||||
/*
|
|
||||||
FAudio.FAudioVoice_SetOutputVoices(
|
|
||||||
Handle,
|
|
||||||
ref Device.ReverbSends
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Apply3D(AudioListener listener, AudioEmitter emitter)
|
|
||||||
{
|
|
||||||
Is3D = true;
|
|
||||||
|
|
||||||
emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar;
|
|
||||||
emitter.emitterData.ChannelCount = dspSettings.SrcChannelCount;
|
|
||||||
|
|
||||||
FAudio.F3DAudioCalculate(
|
|
||||||
Device.Handle3D,
|
|
||||||
ref listener.listenerData,
|
|
||||||
ref emitter.emitterData,
|
|
||||||
FAudio.F3DAUDIO_CALCULATE_MATRIX | FAudio.F3DAUDIO_CALCULATE_DOPPLER,
|
|
||||||
ref dspSettings
|
|
||||||
);
|
|
||||||
|
|
||||||
UpdatePitch();
|
|
||||||
FAudio.FAudioVoice_SetOutputMatrix(
|
|
||||||
Handle,
|
|
||||||
Device.MasteringVoice,
|
|
||||||
dspSettings.SrcChannelCount,
|
|
||||||
dspSettings.DstChannelCount,
|
|
||||||
dspSettings.pMatrixCoefficients,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void Play();
|
|
||||||
public abstract void Pause();
|
|
||||||
public abstract void Stop();
|
|
||||||
public abstract void StopImmediate();
|
|
||||||
|
|
||||||
private void InitDSPSettings(uint srcChannels)
|
|
||||||
{
|
|
||||||
dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS();
|
|
||||||
dspSettings.DopplerFactor = 1f;
|
|
||||||
dspSettings.SrcChannelCount = srcChannels;
|
|
||||||
dspSettings.DstChannelCount = Device.DeviceDetails.OutputFormat.Format.nChannels;
|
|
||||||
|
|
||||||
int memsize = (
|
|
||||||
4 *
|
|
||||||
(int) dspSettings.SrcChannelCount *
|
|
||||||
(int) dspSettings.DstChannelCount
|
|
||||||
);
|
|
||||||
|
|
||||||
dspSettings.pMatrixCoefficients = Marshal.AllocHGlobal(memsize);
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
byte* memPtr = (byte*) dspSettings.pMatrixCoefficients;
|
|
||||||
for (int i = 0; i < memsize; i += 1)
|
|
||||||
{
|
|
||||||
memPtr[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SetPanMatrixCoefficients();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePitch()
|
|
||||||
{
|
|
||||||
float doppler;
|
|
||||||
float dopplerScale = Device.DopplerScale;
|
|
||||||
if (!Is3D || dopplerScale == 0.0f)
|
|
||||||
{
|
|
||||||
doppler = 1.0f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
doppler = dspSettings.DopplerFactor * dopplerScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_SetFrequencyRatio(
|
|
||||||
Handle,
|
|
||||||
(float) System.Math.Pow(2.0, pitch) * doppler,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken from https://github.com/FNA-XNA/FNA/blob/master/src/Audio/SoundEffectInstance.cs
|
|
||||||
private unsafe void SetPanMatrixCoefficients()
|
|
||||||
{
|
|
||||||
/* Two major things to notice:
|
|
||||||
* 1. The spec assumes any speaker count >= 2 has Front Left/Right.
|
|
||||||
* 2. Stereo panning is WAY more complicated than you think.
|
|
||||||
* The main thing is that hard panning does NOT eliminate an
|
|
||||||
* entire channel; the two channels are blended on each side.
|
|
||||||
* -flibit
|
|
||||||
*/
|
|
||||||
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
|
|
||||||
if (dspSettings.SrcChannelCount == 1)
|
|
||||||
{
|
|
||||||
if (dspSettings.DstChannelCount == 1)
|
|
||||||
{
|
|
||||||
outputMatrix[0] = 1.0f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
outputMatrix[0] = (pan > 0.0f) ? (1.0f - pan) : 1.0f;
|
|
||||||
outputMatrix[1] = (pan < 0.0f) ? (1.0f + pan) : 1.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (dspSettings.DstChannelCount == 1)
|
|
||||||
{
|
|
||||||
outputMatrix[0] = 1.0f;
|
|
||||||
outputMatrix[1] = 1.0f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (pan <= 0.0f)
|
|
||||||
{
|
|
||||||
// Left speaker blends left/right channels
|
|
||||||
outputMatrix[0] = 0.5f * pan + 1.0f;
|
|
||||||
outputMatrix[1] = 0.5f * -pan;
|
|
||||||
// Right speaker gets less of the right channel
|
|
||||||
outputMatrix[2] = 0.0f;
|
|
||||||
outputMatrix[3] = pan + 1.0f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Left speaker gets less of the left channel
|
|
||||||
outputMatrix[0] = -pan + 1.0f;
|
|
||||||
outputMatrix[1] = 0.0f;
|
|
||||||
// Right speaker blends right/left channels
|
|
||||||
outputMatrix[2] = 0.5f * pan;
|
|
||||||
outputMatrix[3] = 0.5f * -pan + 1.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Destroy()
|
|
||||||
{
|
|
||||||
StopImmediate();
|
|
||||||
FAudio.FAudioVoice_DestroyVoice(Handle);
|
|
||||||
Marshal.FreeHGlobal(dspSettings.pMatrixCoefficients);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Plays back a series of AudioBuffers in sequence. Set the OnSoundNeeded callback to add AudioBuffers dynamically.
|
||||||
|
/// </summary>
|
||||||
|
public class SoundSequence : UpdatingSourceVoice, IPoolable<SoundSequence>
|
||||||
|
{
|
||||||
|
public int NeedSoundThreshold = 0;
|
||||||
|
public delegate void OnSoundNeededFunc();
|
||||||
|
public OnSoundNeededFunc OnSoundNeeded;
|
||||||
|
|
||||||
|
public SoundSequence(AudioDevice device, Format format) : base(device, format)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoundSequence(AudioDevice device, AudioBuffer templateSound) : base(device, templateSound.Format)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SoundSequence Create(AudioDevice device, Format format)
|
||||||
|
{
|
||||||
|
return new SoundSequence(device, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
if (State != SoundState.Playing) { return; }
|
||||||
|
|
||||||
|
if (NeedSoundThreshold > 0)
|
||||||
|
{
|
||||||
|
var buffersNeeded = NeedSoundThreshold - (int) BuffersQueued;
|
||||||
|
|
||||||
|
for (int i = 0; i < buffersNeeded; i += 1)
|
||||||
|
{
|
||||||
|
if (OnSoundNeeded != null)
|
||||||
|
{
|
||||||
|
OnSoundNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnqueueSound(AudioBuffer buffer)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (!(buffer.Format == Format))
|
||||||
|
{
|
||||||
|
Logger.LogWarn("Sound sequence audio format mismatch!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
Submit(buffer.ToFAudioBuffer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Emits audio from submitted audio buffers.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SourceVoice : Voice
|
||||||
|
{
|
||||||
|
private Format format;
|
||||||
|
public Format Format => format;
|
||||||
|
|
||||||
|
protected bool PlaybackInitiated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of buffers queued in the voice.
|
||||||
|
/// This includes the currently playing voice!
|
||||||
|
/// </summary>
|
||||||
|
public uint BuffersQueued
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_GetState(
|
||||||
|
Handle,
|
||||||
|
out var state,
|
||||||
|
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
|
||||||
|
);
|
||||||
|
|
||||||
|
return state.BuffersQueued;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SoundState state = SoundState.Stopped;
|
||||||
|
public SoundState State
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (BuffersQueued == 0)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected object StateLock = new object();
|
||||||
|
|
||||||
|
public SourceVoice(
|
||||||
|
AudioDevice device,
|
||||||
|
Format format
|
||||||
|
) : base(device, format.Channels, device.DeviceDetails.OutputFormat.Format.nChannels)
|
||||||
|
{
|
||||||
|
this.format = format;
|
||||||
|
var fAudioFormat = format.ToFAudioFormat();
|
||||||
|
|
||||||
|
FAudio.FAudio_CreateSourceVoice(
|
||||||
|
device.Handle,
|
||||||
|
out handle,
|
||||||
|
ref fAudioFormat,
|
||||||
|
FAudio.FAUDIO_VOICE_USEFILTER,
|
||||||
|
FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
|
||||||
|
IntPtr.Zero,
|
||||||
|
IntPtr.Zero, // default sends to mastering voice!
|
||||||
|
IntPtr.Zero
|
||||||
|
);
|
||||||
|
|
||||||
|
SetOutputVoice(device.MasteringVoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts consumption and processing of audio by the voice.
|
||||||
|
/// Delivers the result to any connected submix or mastering voice.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
|
||||||
|
public void Play(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_Start(Handle, 0, syncGroup);
|
||||||
|
|
||||||
|
State = SoundState.Playing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pauses playback.
|
||||||
|
/// All source buffers that are queued on the voice and the current cursor position are preserved.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
|
||||||
|
public void Pause(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup);
|
||||||
|
|
||||||
|
State = SoundState.Paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops looping the voice when it reaches the end of the current loop region.
|
||||||
|
/// If the cursor for the voice is not in a loop region, ExitLoop does nothing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
|
||||||
|
public void ExitLoop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_ExitLoop(Handle, syncGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops playback and removes all pending audio buffers from the voice queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
|
||||||
|
public void Stop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup);
|
||||||
|
FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
|
||||||
|
|
||||||
|
State = SoundState.Stopped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an AudioBuffer to the voice queue.
|
||||||
|
/// The voice processes and plays back the buffers in its queue in the order that they were submitted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer to submit to the voice.</param>
|
||||||
|
public void Submit(AudioBuffer buffer)
|
||||||
|
{
|
||||||
|
Submit(buffer.ToFAudioBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates positional sound. This must be called continuously to update positional sound.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="listener"></param>
|
||||||
|
/// <param name="emitter"></param>
|
||||||
|
public unsafe void Apply3D(AudioListener listener, AudioEmitter emitter)
|
||||||
|
{
|
||||||
|
Is3D = true;
|
||||||
|
|
||||||
|
emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar;
|
||||||
|
emitter.emitterData.ChannelCount = SourceChannelCount;
|
||||||
|
|
||||||
|
var dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS
|
||||||
|
{
|
||||||
|
DopplerFactor = DopplerFactor,
|
||||||
|
SrcChannelCount = SourceChannelCount,
|
||||||
|
DstChannelCount = DestinationChannelCount,
|
||||||
|
pMatrixCoefficients = (nint) pMatrixCoefficients
|
||||||
|
};
|
||||||
|
|
||||||
|
FAudio.F3DAudioCalculate(
|
||||||
|
Device.Handle3D,
|
||||||
|
ref listener.listenerData,
|
||||||
|
ref emitter.emitterData,
|
||||||
|
FAudio.F3DAUDIO_CALCULATE_MATRIX | FAudio.F3DAUDIO_CALCULATE_DOPPLER,
|
||||||
|
ref dspSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
UpdatePitch();
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetOutputMatrix(
|
||||||
|
Handle,
|
||||||
|
OutputVoice.Handle,
|
||||||
|
SourceChannelCount,
|
||||||
|
DestinationChannelCount,
|
||||||
|
(nint) pMatrixCoefficients,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies that this source voice can be returned to the voice pool.
|
||||||
|
/// Holding on to the reference after calling this will cause problems!
|
||||||
|
/// </summary>
|
||||||
|
public void Return()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
Device.Return(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an FAudio buffer to the voice queue.
|
||||||
|
/// The voice processes and plays back the buffers in its queue in the order that they were submitted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer to submit to the voice.</param>
|
||||||
|
protected void Submit(FAudio.FAudioBuffer buffer)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
|
||||||
|
Handle,
|
||||||
|
ref buffer,
|
||||||
|
IntPtr.Zero
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Reset()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
PlaybackInitiated = false;
|
||||||
|
base.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
internal class SourceVoicePool
|
||||||
|
{
|
||||||
|
private AudioDevice Device;
|
||||||
|
|
||||||
|
Dictionary<(System.Type, Format), Queue<SourceVoice>> VoiceLists = new Dictionary<(System.Type, Format), Queue<SourceVoice>>();
|
||||||
|
|
||||||
|
public SourceVoicePool(AudioDevice device)
|
||||||
|
{
|
||||||
|
Device = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Obtain<T>(Format format) where T : SourceVoice, IPoolable<T>
|
||||||
|
{
|
||||||
|
if (!VoiceLists.ContainsKey((typeof(T), format)))
|
||||||
|
{
|
||||||
|
VoiceLists.Add((typeof(T), format), new Queue<SourceVoice>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = VoiceLists[(typeof(T), format)];
|
||||||
|
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
list.Enqueue(T.Create(Device, format));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (T) list.Dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Return(SourceVoice voice)
|
||||||
|
{
|
||||||
|
var list = VoiceLists[(voice.GetType(), voice.Format)];
|
||||||
|
list.Enqueue(voice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,295 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public class StaticSound : AudioResource
|
|
||||||
{
|
|
||||||
internal FAudio.FAudioBuffer Handle;
|
|
||||||
public ushort FormatTag { get; }
|
|
||||||
public ushort BitsPerSample { get; }
|
|
||||||
public ushort Channels { get; }
|
|
||||||
public uint SamplesPerSecond { get; }
|
|
||||||
public ushort BlockAlign { get; }
|
|
||||||
|
|
||||||
public uint LoopStart { get; set; } = 0;
|
|
||||||
public uint LoopLength { get; set; } = 0;
|
|
||||||
|
|
||||||
private Stack<StaticSoundInstance> Instances = new Stack<StaticSoundInstance>();
|
|
||||||
|
|
||||||
public static StaticSound LoadOgg(AudioDevice device, string filePath)
|
|
||||||
{
|
|
||||||
var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
|
|
||||||
|
|
||||||
if (error != 0)
|
|
||||||
{
|
|
||||||
throw new AudioLoadException("Error loading file!");
|
|
||||||
}
|
|
||||||
var info = FAudio.stb_vorbis_get_info(filePointer);
|
|
||||||
var bufferSize = FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels;
|
|
||||||
var buffer = new float[bufferSize];
|
|
||||||
|
|
||||||
FAudio.stb_vorbis_get_samples_float_interleaved(
|
|
||||||
filePointer,
|
|
||||||
info.channels,
|
|
||||||
buffer,
|
|
||||||
(int) bufferSize
|
|
||||||
);
|
|
||||||
|
|
||||||
FAudio.stb_vorbis_close(filePointer);
|
|
||||||
|
|
||||||
return new StaticSound(
|
|
||||||
device,
|
|
||||||
(ushort) info.channels,
|
|
||||||
info.sample_rate,
|
|
||||||
buffer,
|
|
||||||
0,
|
|
||||||
(uint) buffer.Length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385
|
|
||||||
public static StaticSound LoadWav(AudioDevice device, string filePath)
|
|
||||||
{
|
|
||||||
// Sample data
|
|
||||||
byte[] data;
|
|
||||||
|
|
||||||
// WaveFormatEx data
|
|
||||||
ushort wFormatTag;
|
|
||||||
ushort nChannels;
|
|
||||||
uint nSamplesPerSec;
|
|
||||||
uint nAvgBytesPerSec;
|
|
||||||
ushort nBlockAlign;
|
|
||||||
ushort wBitsPerSample;
|
|
||||||
int samplerLoopStart = 0;
|
|
||||||
int samplerLoopEnd = 0;
|
|
||||||
|
|
||||||
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
|
|
||||||
{
|
|
||||||
// RIFF Signature
|
|
||||||
string signature = new string(reader.ReadChars(4));
|
|
||||||
if (signature != "RIFF")
|
|
||||||
{
|
|
||||||
throw new NotSupportedException("Specified stream is not a wave file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.ReadUInt32(); // Riff Chunk Size
|
|
||||||
|
|
||||||
string wformat = new string(reader.ReadChars(4));
|
|
||||||
if (wformat != "WAVE")
|
|
||||||
{
|
|
||||||
throw new NotSupportedException("Specified stream is not a wave file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// WAVE Header
|
|
||||||
string format_signature = new string(reader.ReadChars(4));
|
|
||||||
while (format_signature != "fmt ")
|
|
||||||
{
|
|
||||||
reader.ReadBytes(reader.ReadInt32());
|
|
||||||
format_signature = new string(reader.ReadChars(4));
|
|
||||||
}
|
|
||||||
|
|
||||||
int format_chunk_size = reader.ReadInt32();
|
|
||||||
|
|
||||||
wFormatTag = reader.ReadUInt16();
|
|
||||||
nChannels = reader.ReadUInt16();
|
|
||||||
nSamplesPerSec = reader.ReadUInt32();
|
|
||||||
nAvgBytesPerSec = reader.ReadUInt32();
|
|
||||||
nBlockAlign = reader.ReadUInt16();
|
|
||||||
wBitsPerSample = reader.ReadUInt16();
|
|
||||||
|
|
||||||
// Reads residual bytes
|
|
||||||
if (format_chunk_size > 16)
|
|
||||||
{
|
|
||||||
reader.ReadBytes(format_chunk_size - 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
// data Signature
|
|
||||||
string data_signature = new string(reader.ReadChars(4));
|
|
||||||
while (data_signature.ToLowerInvariant() != "data")
|
|
||||||
{
|
|
||||||
reader.ReadBytes(reader.ReadInt32());
|
|
||||||
data_signature = new string(reader.ReadChars(4));
|
|
||||||
}
|
|
||||||
if (data_signature != "data")
|
|
||||||
{
|
|
||||||
throw new NotSupportedException("Specified wave file is not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
int waveDataLength = reader.ReadInt32();
|
|
||||||
data = reader.ReadBytes(waveDataLength);
|
|
||||||
|
|
||||||
// Scan for other chunks
|
|
||||||
while (reader.PeekChar() != -1)
|
|
||||||
{
|
|
||||||
char[] chunkIDChars = reader.ReadChars(4);
|
|
||||||
if (chunkIDChars.Length < 4)
|
|
||||||
{
|
|
||||||
break; // EOL!
|
|
||||||
}
|
|
||||||
byte[] chunkSizeBytes = reader.ReadBytes(4);
|
|
||||||
if (chunkSizeBytes.Length < 4)
|
|
||||||
{
|
|
||||||
break; // EOL!
|
|
||||||
}
|
|
||||||
string chunk_signature = new string(chunkIDChars);
|
|
||||||
int chunkDataSize = BitConverter.ToInt32(chunkSizeBytes, 0);
|
|
||||||
if (chunk_signature == "smpl") // "smpl", Sampler Chunk Found
|
|
||||||
{
|
|
||||||
reader.ReadUInt32(); // Manufacturer
|
|
||||||
reader.ReadUInt32(); // Product
|
|
||||||
reader.ReadUInt32(); // Sample Period
|
|
||||||
reader.ReadUInt32(); // MIDI Unity Note
|
|
||||||
reader.ReadUInt32(); // MIDI Pitch Fraction
|
|
||||||
reader.ReadUInt32(); // SMPTE Format
|
|
||||||
reader.ReadUInt32(); // SMPTE Offset
|
|
||||||
uint numSampleLoops = reader.ReadUInt32();
|
|
||||||
int samplerData = reader.ReadInt32();
|
|
||||||
|
|
||||||
for (int i = 0; i < numSampleLoops; i += 1)
|
|
||||||
{
|
|
||||||
reader.ReadUInt32(); // Cue Point ID
|
|
||||||
reader.ReadUInt32(); // Type
|
|
||||||
int start = reader.ReadInt32();
|
|
||||||
int end = reader.ReadInt32();
|
|
||||||
reader.ReadUInt32(); // Fraction
|
|
||||||
reader.ReadUInt32(); // Play Count
|
|
||||||
|
|
||||||
if (i == 0) // Grab loopStart and loopEnd from first sample loop
|
|
||||||
{
|
|
||||||
samplerLoopStart = start;
|
|
||||||
samplerLoopEnd = end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (samplerData != 0) // Read Sampler Data if it exists
|
|
||||||
{
|
|
||||||
reader.ReadBytes(samplerData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else // Read unwanted chunk data and try again
|
|
||||||
{
|
|
||||||
reader.ReadBytes(chunkDataSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// End scan
|
|
||||||
}
|
|
||||||
|
|
||||||
return new StaticSound(
|
|
||||||
device,
|
|
||||||
wFormatTag,
|
|
||||||
wBitsPerSample,
|
|
||||||
nBlockAlign,
|
|
||||||
nChannels,
|
|
||||||
nSamplesPerSec,
|
|
||||||
data,
|
|
||||||
0,
|
|
||||||
(uint) data.Length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public StaticSound(
|
|
||||||
AudioDevice device,
|
|
||||||
ushort formatTag,
|
|
||||||
ushort bitsPerSample,
|
|
||||||
ushort blockAlign,
|
|
||||||
ushort channels,
|
|
||||||
uint samplesPerSecond,
|
|
||||||
byte[] buffer,
|
|
||||||
uint bufferOffset, /* number of bytes */
|
|
||||||
uint bufferLength /* number of bytes */
|
|
||||||
) : base(device)
|
|
||||||
{
|
|
||||||
FormatTag = formatTag;
|
|
||||||
BitsPerSample = bitsPerSample;
|
|
||||||
BlockAlign = blockAlign;
|
|
||||||
Channels = channels;
|
|
||||||
SamplesPerSecond = samplesPerSecond;
|
|
||||||
|
|
||||||
Handle = new FAudio.FAudioBuffer();
|
|
||||||
Handle.Flags = FAudio.FAUDIO_END_OF_STREAM;
|
|
||||||
Handle.pContext = IntPtr.Zero;
|
|
||||||
Handle.AudioBytes = bufferLength;
|
|
||||||
Handle.pAudioData = Marshal.AllocHGlobal((int) bufferLength);
|
|
||||||
Marshal.Copy(buffer, (int) bufferOffset, Handle.pAudioData, (int) bufferLength);
|
|
||||||
Handle.PlayBegin = 0;
|
|
||||||
Handle.PlayLength = 0;
|
|
||||||
|
|
||||||
if (formatTag == 1)
|
|
||||||
{
|
|
||||||
Handle.PlayLength = (uint) (
|
|
||||||
bufferLength /
|
|
||||||
channels /
|
|
||||||
(bitsPerSample / 8)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (formatTag == 2)
|
|
||||||
{
|
|
||||||
Handle.PlayLength = (uint) (
|
|
||||||
bufferLength /
|
|
||||||
blockAlign *
|
|
||||||
(((blockAlign / channels) - 6) * 2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LoopStart = 0;
|
|
||||||
LoopLength = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StaticSound(
|
|
||||||
AudioDevice device,
|
|
||||||
ushort channels,
|
|
||||||
uint samplesPerSecond,
|
|
||||||
float[] buffer,
|
|
||||||
uint bufferOffset, /* in floats */
|
|
||||||
uint bufferLength /* in floats */
|
|
||||||
) : base(device)
|
|
||||||
{
|
|
||||||
FormatTag = 3;
|
|
||||||
BitsPerSample = 32;
|
|
||||||
BlockAlign = (ushort) (4 * channels);
|
|
||||||
Channels = channels;
|
|
||||||
SamplesPerSecond = samplesPerSecond;
|
|
||||||
|
|
||||||
var bufferLengthInBytes = (int) (bufferLength * sizeof(float));
|
|
||||||
Handle = new FAudio.FAudioBuffer();
|
|
||||||
Handle.Flags = FAudio.FAUDIO_END_OF_STREAM;
|
|
||||||
Handle.pContext = IntPtr.Zero;
|
|
||||||
Handle.AudioBytes = (uint) bufferLengthInBytes;
|
|
||||||
Handle.pAudioData = Marshal.AllocHGlobal(bufferLengthInBytes);
|
|
||||||
Marshal.Copy(buffer, (int) bufferOffset, Handle.pAudioData, (int) bufferLength);
|
|
||||||
Handle.PlayBegin = 0;
|
|
||||||
Handle.PlayLength = 0;
|
|
||||||
|
|
||||||
LoopStart = 0;
|
|
||||||
LoopLength = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a sound instance from the pool.
|
|
||||||
/// NOTE: If you lose track of instances, you will create garbage collection pressure!
|
|
||||||
/// </summary>
|
|
||||||
public StaticSoundInstance GetInstance()
|
|
||||||
{
|
|
||||||
if (Instances.Count == 0)
|
|
||||||
{
|
|
||||||
Instances.Push(new StaticSoundInstance(Device, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Instances.Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void FreeInstance(StaticSoundInstance instance)
|
|
||||||
{
|
|
||||||
instance.Reset();
|
|
||||||
Instances.Push(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Destroy()
|
|
||||||
{
|
|
||||||
Marshal.FreeHGlobal(Handle.pAudioData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public class StaticSoundInstance : SoundInstance
|
|
||||||
{
|
|
||||||
public StaticSound Parent { get; }
|
|
||||||
|
|
||||||
public bool Loop { get; set; }
|
|
||||||
|
|
||||||
private SoundState _state = SoundState.Stopped;
|
|
||||||
public override SoundState State
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_GetState(
|
|
||||||
Handle,
|
|
||||||
out var state,
|
|
||||||
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
|
|
||||||
);
|
|
||||||
if (state.BuffersQueued == 0)
|
|
||||||
{
|
|
||||||
StopImmediate();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _state;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected set
|
|
||||||
{
|
|
||||||
_state = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal StaticSoundInstance(
|
|
||||||
AudioDevice device,
|
|
||||||
StaticSound parent
|
|
||||||
) : base(device, parent.FormatTag, parent.BitsPerSample, parent.BlockAlign, parent.Channels, parent.SamplesPerSecond)
|
|
||||||
{
|
|
||||||
Parent = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Play()
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Loop)
|
|
||||||
{
|
|
||||||
Parent.Handle.LoopCount = 255;
|
|
||||||
Parent.Handle.LoopBegin = Parent.LoopStart;
|
|
||||||
Parent.Handle.LoopLength = Parent.LoopLength;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Parent.Handle.LoopCount = 0;
|
|
||||||
Parent.Handle.LoopBegin = 0;
|
|
||||||
Parent.Handle.LoopLength = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
|
|
||||||
Handle,
|
|
||||||
ref Parent.Handle,
|
|
||||||
IntPtr.Zero
|
|
||||||
);
|
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_Start(Handle, 0, 0);
|
|
||||||
State = SoundState.Playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Pause()
|
|
||||||
{
|
|
||||||
if (State == SoundState.Paused)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
|
|
||||||
State = SoundState.Paused;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Stop()
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_ExitLoop(Handle, 0);
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void StopImmediate()
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
|
|
||||||
FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Seek(uint sampleFrame)
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
|
|
||||||
FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
Parent.Handle.PlayBegin = sampleFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Free()
|
|
||||||
{
|
|
||||||
Parent.FreeInstance(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Reset()
|
|
||||||
{
|
|
||||||
Pan = 0;
|
|
||||||
Pitch = 0;
|
|
||||||
Volume = 1;
|
|
||||||
Reverb = 0;
|
|
||||||
Loop = false;
|
|
||||||
Is3D = false;
|
|
||||||
FilterType = FilterType.None;
|
|
||||||
Reverb = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// For streaming long playback.
|
|
||||||
/// Must be extended with a decoder routine called by FillBuffer.
|
|
||||||
/// See StreamingSoundOgg for an example.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class StreamingSound : SoundInstance
|
|
||||||
{
|
|
||||||
private const int BUFFER_COUNT = 3;
|
|
||||||
private readonly IntPtr[] buffers;
|
|
||||||
private int nextBufferIndex = 0;
|
|
||||||
private uint queuedBufferCount = 0;
|
|
||||||
protected abstract int BUFFER_SIZE { get; }
|
|
||||||
|
|
||||||
public unsafe StreamingSound(
|
|
||||||
AudioDevice device,
|
|
||||||
ushort formatTag,
|
|
||||||
ushort bitsPerSample,
|
|
||||||
ushort blockAlign,
|
|
||||||
ushort channels,
|
|
||||||
uint samplesPerSecond
|
|
||||||
) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
|
|
||||||
{
|
|
||||||
device.AddDynamicSoundInstance(this);
|
|
||||||
|
|
||||||
buffers = new IntPtr[BUFFER_COUNT];
|
|
||||||
for (int i = 0; i < BUFFER_COUNT; i += 1)
|
|
||||||
{
|
|
||||||
buffers[i] = (IntPtr) NativeMemory.Alloc((nuint) BUFFER_SIZE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Play()
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
State = SoundState.Playing;
|
|
||||||
|
|
||||||
Update();
|
|
||||||
FAudio.FAudioSourceVoice_Start(Handle, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Pause()
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
|
|
||||||
State = SoundState.Paused;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Stop()
|
|
||||||
{
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void StopImmediate()
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
|
|
||||||
FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
|
|
||||||
ClearBuffers();
|
|
||||||
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal unsafe void Update()
|
|
||||||
{
|
|
||||||
if (State != SoundState.Playing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_GetState(
|
|
||||||
Handle,
|
|
||||||
out var state,
|
|
||||||
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
|
|
||||||
);
|
|
||||||
|
|
||||||
queuedBufferCount = state.BuffersQueued;
|
|
||||||
|
|
||||||
QueueBuffers();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void QueueBuffers()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1)
|
|
||||||
{
|
|
||||||
AddBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected unsafe void ClearBuffers()
|
|
||||||
{
|
|
||||||
nextBufferIndex = 0;
|
|
||||||
queuedBufferCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected unsafe void AddBuffer()
|
|
||||||
{
|
|
||||||
var buffer = buffers[nextBufferIndex];
|
|
||||||
nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT;
|
|
||||||
|
|
||||||
FillBuffer(
|
|
||||||
(void*) buffer,
|
|
||||||
BUFFER_SIZE,
|
|
||||||
out int filledLengthInBytes,
|
|
||||||
out bool reachedEnd
|
|
||||||
);
|
|
||||||
|
|
||||||
FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
|
|
||||||
{
|
|
||||||
AudioBytes = (uint) filledLengthInBytes,
|
|
||||||
pAudioData = (IntPtr) buffer,
|
|
||||||
PlayLength = (
|
|
||||||
(uint) (filledLengthInBytes /
|
|
||||||
Format.nChannels /
|
|
||||||
(uint) (Format.wBitsPerSample / 8))
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
|
|
||||||
Handle,
|
|
||||||
ref buf,
|
|
||||||
IntPtr.Zero
|
|
||||||
);
|
|
||||||
|
|
||||||
queuedBufferCount += 1;
|
|
||||||
|
|
||||||
/* We have reached the end of the file, what do we do? */
|
|
||||||
if (reachedEnd)
|
|
||||||
{
|
|
||||||
OnReachedEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void OnReachedEnd()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected unsafe abstract void FillBuffer(
|
|
||||||
void* buffer,
|
|
||||||
int bufferLengthInBytes, /* in bytes */
|
|
||||||
out int filledLengthInBytes, /* in bytes */
|
|
||||||
out bool reachedEnd
|
|
||||||
);
|
|
||||||
|
|
||||||
protected unsafe override void Destroy()
|
|
||||||
{
|
|
||||||
StopImmediate();
|
|
||||||
|
|
||||||
for (int i = 0; i < BUFFER_COUNT; i += 1)
|
|
||||||
{
|
|
||||||
NativeMemory.Free((void*) buffers[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public class StreamingSoundOgg : StreamingSoundSeekable
|
|
||||||
{
|
|
||||||
private IntPtr VorbisHandle;
|
|
||||||
private IntPtr FileDataPtr;
|
|
||||||
private FAudio.stb_vorbis_info Info;
|
|
||||||
|
|
||||||
protected override int BUFFER_SIZE => 32768;
|
|
||||||
|
|
||||||
public unsafe static StreamingSoundOgg Load(AudioDevice device, string filePath)
|
|
||||||
{
|
|
||||||
var fileData = File.ReadAllBytes(filePath);
|
|
||||||
var fileDataPtr = NativeMemory.Alloc((nuint) fileData.Length);
|
|
||||||
Marshal.Copy(fileData, 0, (IntPtr) fileDataPtr, fileData.Length);
|
|
||||||
var vorbisHandle = FAudio.stb_vorbis_open_memory((IntPtr) fileDataPtr, fileData.Length, out int error, IntPtr.Zero);
|
|
||||||
if (error != 0)
|
|
||||||
{
|
|
||||||
NativeMemory.Free(fileDataPtr);
|
|
||||||
Logger.LogError("Error opening OGG file!");
|
|
||||||
Logger.LogError("Error: " + error);
|
|
||||||
throw new AudioLoadException("Error opening OGG file!");
|
|
||||||
}
|
|
||||||
var info = FAudio.stb_vorbis_get_info(vorbisHandle);
|
|
||||||
|
|
||||||
return new StreamingSoundOgg(
|
|
||||||
device,
|
|
||||||
(IntPtr) fileDataPtr,
|
|
||||||
vorbisHandle,
|
|
||||||
info
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal StreamingSoundOgg(
|
|
||||||
AudioDevice device,
|
|
||||||
IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!!
|
|
||||||
IntPtr vorbisHandle,
|
|
||||||
FAudio.stb_vorbis_info info
|
|
||||||
) : base(
|
|
||||||
device,
|
|
||||||
3, /* float type */
|
|
||||||
32, /* size of float */
|
|
||||||
(ushort) (4 * info.channels),
|
|
||||||
(ushort) info.channels,
|
|
||||||
info.sample_rate
|
|
||||||
)
|
|
||||||
{
|
|
||||||
FileDataPtr = fileDataPtr;
|
|
||||||
VorbisHandle = vorbisHandle;
|
|
||||||
Info = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Seek(uint sampleFrame)
|
|
||||||
{
|
|
||||||
FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected unsafe override void FillBuffer(
|
|
||||||
void* buffer,
|
|
||||||
int bufferLengthInBytes,
|
|
||||||
out int filledLengthInBytes,
|
|
||||||
out bool reachedEnd
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var lengthInFloats = bufferLengthInBytes / sizeof(float);
|
|
||||||
|
|
||||||
/* NOTE: this function returns samples per channel, not total samples */
|
|
||||||
var samples = FAudio.stb_vorbis_get_samples_float_interleaved(
|
|
||||||
VorbisHandle,
|
|
||||||
Info.channels,
|
|
||||||
(IntPtr) buffer,
|
|
||||||
lengthInFloats
|
|
||||||
);
|
|
||||||
|
|
||||||
var sampleCount = samples * Info.channels;
|
|
||||||
reachedEnd = sampleCount < lengthInFloats;
|
|
||||||
filledLengthInBytes = sampleCount * sizeof(float);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected unsafe override void Destroy()
|
|
||||||
{
|
|
||||||
FAudio.stb_vorbis_close(VorbisHandle);
|
|
||||||
NativeMemory.Free((void*) FileDataPtr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public abstract class StreamingSoundSeekable : StreamingSound
|
|
||||||
{
|
|
||||||
public bool Loop { get; set; }
|
|
||||||
|
|
||||||
protected StreamingSoundSeekable(AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void Seek(uint sampleFrame);
|
|
||||||
|
|
||||||
protected override void OnReachedEnd()
|
|
||||||
{
|
|
||||||
if (Loop)
|
|
||||||
{
|
|
||||||
Seek(0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Use in conjunction with an AudioDataStreamable object to play back streaming audio data.
|
||||||
|
/// </summary>
|
||||||
|
public class StreamingVoice : UpdatingSourceVoice, IPoolable<StreamingVoice>
|
||||||
|
{
|
||||||
|
private const int BUFFER_COUNT = 3;
|
||||||
|
private readonly IntPtr[] buffers;
|
||||||
|
private int nextBufferIndex = 0;
|
||||||
|
private uint BufferSize;
|
||||||
|
|
||||||
|
public bool Loop { get; set; }
|
||||||
|
|
||||||
|
public AudioDataStreamable AudioData { get; protected set; }
|
||||||
|
|
||||||
|
public unsafe StreamingVoice(AudioDevice device, Format format) : base(device, format)
|
||||||
|
{
|
||||||
|
buffers = new IntPtr[BUFFER_COUNT];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StreamingVoice Create(AudioDevice device, Format format)
|
||||||
|
{
|
||||||
|
return new StreamingVoice(device, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads and prepares an AudioDataStreamable for streaming playback.
|
||||||
|
/// This automatically calls Load on the given AudioDataStreamable.
|
||||||
|
/// </summary>
|
||||||
|
public void Load(AudioDataStreamable data)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
if (AudioData != null)
|
||||||
|
{
|
||||||
|
AudioData.Unload();
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Load();
|
||||||
|
AudioData = data;
|
||||||
|
|
||||||
|
InitializeBuffers();
|
||||||
|
QueueBuffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unloads AudioDataStreamable from this voice.
|
||||||
|
/// This automatically calls Unload on the given AudioDataStreamable.
|
||||||
|
/// </summary>
|
||||||
|
public void Unload()
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
if (AudioData != null)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
AudioData.Unload();
|
||||||
|
AudioData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Reset()
|
||||||
|
{
|
||||||
|
Unload();
|
||||||
|
base.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
if (AudioData == null || State != SoundState.Playing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueBuffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void QueueBuffers()
|
||||||
|
{
|
||||||
|
int buffersNeeded = BUFFER_COUNT - (int) BuffersQueued; // don't get got by uint underflow!
|
||||||
|
for (int i = 0; i < buffersNeeded; i += 1)
|
||||||
|
{
|
||||||
|
AddBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void AddBuffer()
|
||||||
|
{
|
||||||
|
var buffer = buffers[nextBufferIndex];
|
||||||
|
nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT;
|
||||||
|
|
||||||
|
AudioData.Decode(
|
||||||
|
(void*) buffer,
|
||||||
|
(int) BufferSize,
|
||||||
|
out int filledLengthInBytes,
|
||||||
|
out bool reachedEnd
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filledLengthInBytes > 0)
|
||||||
|
{
|
||||||
|
var buf = new FAudio.FAudioBuffer
|
||||||
|
{
|
||||||
|
AudioBytes = (uint) filledLengthInBytes,
|
||||||
|
pAudioData = buffer,
|
||||||
|
PlayLength = (
|
||||||
|
(uint) (filledLengthInBytes /
|
||||||
|
Format.Channels /
|
||||||
|
(uint) (Format.BitsPerSample / 8))
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Submit(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reachedEnd)
|
||||||
|
{
|
||||||
|
/* We have reached the end of the data, what do we do? */
|
||||||
|
if (Loop)
|
||||||
|
{
|
||||||
|
AudioData.Seek(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void InitializeBuffers()
|
||||||
|
{
|
||||||
|
BufferSize = AudioData.DecodeBufferSize;
|
||||||
|
|
||||||
|
for (int i = 0; i < BUFFER_COUNT; i += 1)
|
||||||
|
{
|
||||||
|
if (buffers[i] != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
NativeMemory.Free((void*) buffers[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers[i] = (IntPtr) NativeMemory.Alloc(BufferSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
for (int i = 0; i < BUFFER_COUNT; i += 1)
|
||||||
|
{
|
||||||
|
if (buffers[i] != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
NativeMemory.Free((void*) buffers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SourceVoices can send audio to a SubmixVoice for convenient effects processing.
|
||||||
|
/// Submixes process in order of processingStage, from lowest to highest.
|
||||||
|
/// Therefore submixes early in a chain should have a low processingStage, and later in the chain they should have a higher one.
|
||||||
|
/// </summary>
|
||||||
|
public class SubmixVoice : Voice
|
||||||
|
{
|
||||||
|
public SubmixVoice(
|
||||||
|
AudioDevice device,
|
||||||
|
uint sourceChannelCount,
|
||||||
|
uint sampleRate,
|
||||||
|
uint processingStage
|
||||||
|
) : base(device, sourceChannelCount, device.DeviceDetails.OutputFormat.Format.nChannels)
|
||||||
|
{
|
||||||
|
FAudio.FAudio_CreateSubmixVoice(
|
||||||
|
device.Handle,
|
||||||
|
out handle,
|
||||||
|
sourceChannelCount,
|
||||||
|
sampleRate,
|
||||||
|
FAudio.FAUDIO_VOICE_USEFILTER,
|
||||||
|
processingStage,
|
||||||
|
IntPtr.Zero,
|
||||||
|
IntPtr.Zero
|
||||||
|
);
|
||||||
|
|
||||||
|
SetOutputVoice(device.MasteringVoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubmixVoice(
|
||||||
|
AudioDevice device
|
||||||
|
) : base(device, device.DeviceDetails.OutputFormat.Format.nChannels, device.DeviceDetails.OutputFormat.Format.nChannels)
|
||||||
|
{
|
||||||
|
FAudio.FAudio_CreateSubmixVoice(
|
||||||
|
device.Handle,
|
||||||
|
out handle,
|
||||||
|
device.DeviceDetails.OutputFormat.Format.nChannels,
|
||||||
|
device.DeviceDetails.OutputFormat.Format.nSamplesPerSec,
|
||||||
|
FAudio.FAUDIO_VOICE_USEFILTER,
|
||||||
|
int.MaxValue,
|
||||||
|
IntPtr.Zero, // default sends to mastering voice
|
||||||
|
IntPtr.Zero
|
||||||
|
);
|
||||||
|
|
||||||
|
OutputVoice = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static SubmixVoice CreateFauxMasteringVoice(AudioDevice device)
|
||||||
|
{
|
||||||
|
return new SubmixVoice(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// TransientVoice is intended for playing one-off sound effects that don't have a long term reference. <br/>
|
||||||
|
/// It will be automatically returned to the AudioDevice SourceVoice pool once it is done playing back.
|
||||||
|
/// </summary>
|
||||||
|
public class TransientVoice : UpdatingSourceVoice, IPoolable<TransientVoice>
|
||||||
|
{
|
||||||
|
static TransientVoice IPoolable<TransientVoice>.Create(AudioDevice device, Format format)
|
||||||
|
{
|
||||||
|
return new TransientVoice(device, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransientVoice(AudioDevice device, Format format) : base(device, format)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
if (PlaybackInitiated && BuffersQueued == 0)
|
||||||
|
{
|
||||||
|
Return();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
public abstract class UpdatingSourceVoice : SourceVoice
|
||||||
|
{
|
||||||
|
protected UpdatingSourceVoice(AudioDevice device, Format format) : base(device, format)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void Update();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,578 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using EasingFunction = System.Func<float, float>;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles audio playback from audio buffer data. Can be configured with a variety of parameters.
|
||||||
|
/// </summary>
|
||||||
|
public abstract unsafe class Voice : AudioResource
|
||||||
|
{
|
||||||
|
protected IntPtr handle;
|
||||||
|
public IntPtr Handle => handle;
|
||||||
|
|
||||||
|
public uint SourceChannelCount { get; }
|
||||||
|
public uint DestinationChannelCount { get; }
|
||||||
|
|
||||||
|
protected SubmixVoice OutputVoice;
|
||||||
|
private ReverbEffect ReverbEffect;
|
||||||
|
|
||||||
|
protected byte* pMatrixCoefficients;
|
||||||
|
|
||||||
|
public bool Is3D { get; protected set; }
|
||||||
|
|
||||||
|
private float dopplerFactor;
|
||||||
|
/// <summary>
|
||||||
|
/// The strength of the doppler effect on this voice.
|
||||||
|
/// </summary>
|
||||||
|
public float DopplerFactor
|
||||||
|
{
|
||||||
|
get => dopplerFactor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (dopplerFactor != value)
|
||||||
|
{
|
||||||
|
dopplerFactor = value;
|
||||||
|
UpdatePitch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float volume = 1;
|
||||||
|
/// <summary>
|
||||||
|
/// The overall volume level for the voice.
|
||||||
|
/// </summary>
|
||||||
|
public float Volume
|
||||||
|
{
|
||||||
|
get => volume;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
value = Math.MathHelper.Max(0, value);
|
||||||
|
if (volume != value)
|
||||||
|
{
|
||||||
|
volume = value;
|
||||||
|
FAudio.FAudioVoice_SetVolume(Handle, volume, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float pitch = 0;
|
||||||
|
/// <summary>
|
||||||
|
/// The pitch of the voice.
|
||||||
|
/// </summary>
|
||||||
|
public float Pitch
|
||||||
|
{
|
||||||
|
get => pitch;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
value = Math.MathHelper.Clamp(value, -1f, 1f);
|
||||||
|
if (pitch != value)
|
||||||
|
{
|
||||||
|
pitch = value;
|
||||||
|
UpdatePitch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const float MAX_FILTER_FREQUENCY = 1f;
|
||||||
|
private const float MAX_FILTER_ONEOVERQ = 1.5f;
|
||||||
|
|
||||||
|
private FAudio.FAudioFilterParameters filterParameters = new FAudio.FAudioFilterParameters
|
||||||
|
{
|
||||||
|
Type = FAudio.FAudioFilterType.FAudioLowPassFilter,
|
||||||
|
Frequency = 1f,
|
||||||
|
OneOverQ = 1f
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The frequency cutoff on the voice filter.
|
||||||
|
/// </summary>
|
||||||
|
public float FilterFrequency
|
||||||
|
{
|
||||||
|
get => filterParameters.Frequency;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY);
|
||||||
|
if (filterParameters.Frequency != value)
|
||||||
|
{
|
||||||
|
filterParameters.Frequency = value;
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetFilterParameters(
|
||||||
|
Handle,
|
||||||
|
ref filterParameters,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reciprocal of Q factor.
|
||||||
|
/// Controls how quickly frequencies beyond the filter frequency are dampened.
|
||||||
|
/// </summary>
|
||||||
|
public float FilterOneOverQ
|
||||||
|
{
|
||||||
|
get => filterParameters.OneOverQ;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ);
|
||||||
|
if (filterParameters.OneOverQ != value)
|
||||||
|
{
|
||||||
|
filterParameters.OneOverQ = value;
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetFilterParameters(
|
||||||
|
Handle,
|
||||||
|
ref filterParameters,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private FilterType filterType;
|
||||||
|
/// <summary>
|
||||||
|
/// The frequency filter that is applied to the voice.
|
||||||
|
/// </summary>
|
||||||
|
public FilterType FilterType
|
||||||
|
{
|
||||||
|
get => filterType;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (filterType != value)
|
||||||
|
{
|
||||||
|
filterType = value;
|
||||||
|
|
||||||
|
switch (filterType)
|
||||||
|
{
|
||||||
|
case FilterType.None:
|
||||||
|
filterParameters = new FAudio.FAudioFilterParameters
|
||||||
|
{
|
||||||
|
Type = FAudio.FAudioFilterType.FAudioLowPassFilter,
|
||||||
|
Frequency = 1f,
|
||||||
|
OneOverQ = 1f
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FilterType.LowPass:
|
||||||
|
filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter;
|
||||||
|
filterParameters.Frequency = 1f;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FilterType.BandPass:
|
||||||
|
filterParameters.Type = FAudio.FAudioFilterType.FAudioBandPassFilter;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FilterType.HighPass:
|
||||||
|
filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter;
|
||||||
|
filterParameters.Frequency = 0f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetFilterParameters(
|
||||||
|
Handle,
|
||||||
|
ref filterParameters,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected float pan = 0;
|
||||||
|
/// <summary>
|
||||||
|
/// Left-right panning. -1 is hard left pan, 1 is hard right pan.
|
||||||
|
/// </summary>
|
||||||
|
public float Pan
|
||||||
|
{
|
||||||
|
get => pan;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
value = Math.MathHelper.Clamp(value, -1f, 1f);
|
||||||
|
if (pan != value)
|
||||||
|
{
|
||||||
|
pan = value;
|
||||||
|
|
||||||
|
if (pan < -1f)
|
||||||
|
{
|
||||||
|
pan = -1f;
|
||||||
|
}
|
||||||
|
if (pan > 1f)
|
||||||
|
{
|
||||||
|
pan = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Is3D) { return; }
|
||||||
|
|
||||||
|
SetPanMatrixCoefficients();
|
||||||
|
FAudio.FAudioVoice_SetOutputMatrix(
|
||||||
|
Handle,
|
||||||
|
OutputVoice.Handle,
|
||||||
|
SourceChannelCount,
|
||||||
|
DestinationChannelCount,
|
||||||
|
(nint) pMatrixCoefficients,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float reverb;
|
||||||
|
/// <summary>
|
||||||
|
/// The wet-dry mix of the reverb effect.
|
||||||
|
/// Has no effect if SetReverbEffectChain has not been called.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe float Reverb
|
||||||
|
{
|
||||||
|
get => reverb;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
if (ReverbEffect != null)
|
||||||
|
{
|
||||||
|
value = MathF.Max(0, value);
|
||||||
|
if (reverb != value)
|
||||||
|
{
|
||||||
|
reverb = value;
|
||||||
|
|
||||||
|
float* outputMatrix = (float*) pMatrixCoefficients;
|
||||||
|
outputMatrix[0] = reverb;
|
||||||
|
if (SourceChannelCount == 2)
|
||||||
|
{
|
||||||
|
outputMatrix[1] = reverb;
|
||||||
|
}
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetOutputMatrix(
|
||||||
|
Handle,
|
||||||
|
ReverbEffect.Handle,
|
||||||
|
SourceChannelCount,
|
||||||
|
1,
|
||||||
|
(nint) pMatrixCoefficients,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if (ReverbEffect == null)
|
||||||
|
{
|
||||||
|
Logger.LogWarn("Tried to set reverb value before applying a reverb effect");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Voice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device)
|
||||||
|
{
|
||||||
|
SourceChannelCount = sourceChannelCount;
|
||||||
|
DestinationChannelCount = destinationChannelCount;
|
||||||
|
nuint memsize = 4 * sourceChannelCount * destinationChannelCount;
|
||||||
|
pMatrixCoefficients = (byte*) NativeMemory.AllocZeroed(memsize);
|
||||||
|
SetPanMatrixCoefficients();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the pitch of the voice. Valid input range is -1f to 1f.
|
||||||
|
/// </summary>
|
||||||
|
public void SetPitch(float targetValue)
|
||||||
|
{
|
||||||
|
Pitch = targetValue;
|
||||||
|
Device.ClearTweens(this, AudioTweenProperty.Pitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the pitch of the voice over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public void SetPitch(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the pitch of the voice over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public void SetPitch(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, delayTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the volume of the voice. Minimum value is 0f.
|
||||||
|
/// </summary>
|
||||||
|
public void SetVolume(float targetValue)
|
||||||
|
{
|
||||||
|
Volume = targetValue;
|
||||||
|
Device.ClearTweens(this, AudioTweenProperty.Volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the volume of the voice over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public void SetVolume(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the volume of the voice over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public void SetVolume(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, delayTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the frequency cutoff on the voice filter. Valid range is 0.01f to 1f.
|
||||||
|
/// </summary>
|
||||||
|
public void SetFilterFrequency(float targetValue)
|
||||||
|
{
|
||||||
|
FilterFrequency = targetValue;
|
||||||
|
Device.ClearTweens(this, AudioTweenProperty.FilterFrequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the frequency cutoff on the voice filter over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public void SetFilterFrequency(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the frequency cutoff on the voice filter over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public void SetFilterFrequency(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, delayTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets reciprocal of Q factor on the frequency filter.
|
||||||
|
/// Controls how quickly frequencies beyond the filter frequency are dampened.
|
||||||
|
/// </summary>
|
||||||
|
public void SetFilterOneOverQ(float targetValue)
|
||||||
|
{
|
||||||
|
FilterOneOverQ = targetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a left-right panning value. -1f is hard left pan, 1f is hard right pan.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void SetPan(float targetValue)
|
||||||
|
{
|
||||||
|
Pan = targetValue;
|
||||||
|
Device.ClearTweens(this, AudioTweenProperty.Pan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a left-right panning value over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public virtual void SetPan(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a left-right panning value over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public virtual void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the wet-dry mix value of the reverb effect. Minimum value is 0f.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void SetReverb(float targetValue)
|
||||||
|
{
|
||||||
|
Reverb = targetValue;
|
||||||
|
Device.ClearTweens(this, AudioTweenProperty.Reverb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the wet-dry mix value of the reverb effect over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public virtual void SetReverb(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the wet-dry mix value of the reverb effect over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public virtual void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the output voice for this voice.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send">Where the output should be sent.</param>
|
||||||
|
public unsafe void SetOutputVoice(SubmixVoice send)
|
||||||
|
{
|
||||||
|
OutputVoice = send;
|
||||||
|
|
||||||
|
if (ReverbEffect != null)
|
||||||
|
{
|
||||||
|
SetReverbEffectChain(ReverbEffect);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FAudio.FAudioSendDescriptor* sendDesc = stackalloc FAudio.FAudioSendDescriptor[1];
|
||||||
|
sendDesc[0].Flags = 0;
|
||||||
|
sendDesc[0].pOutputVoice = send.Handle;
|
||||||
|
|
||||||
|
var sends = new FAudio.FAudioVoiceSends();
|
||||||
|
sends.SendCount = 1;
|
||||||
|
sends.pSends = (nint) sendDesc;
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetOutputVoices(
|
||||||
|
Handle,
|
||||||
|
ref sends
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a reverb effect chain to this voice.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe void SetReverbEffectChain(ReverbEffect reverbEffect)
|
||||||
|
{
|
||||||
|
var sendDesc = stackalloc FAudio.FAudioSendDescriptor[2];
|
||||||
|
sendDesc[0].Flags = 0;
|
||||||
|
sendDesc[0].pOutputVoice = OutputVoice.Handle;
|
||||||
|
sendDesc[1].Flags = 0;
|
||||||
|
sendDesc[1].pOutputVoice = reverbEffect.Handle;
|
||||||
|
|
||||||
|
var sends = new FAudio.FAudioVoiceSends();
|
||||||
|
sends.SendCount = 2;
|
||||||
|
sends.pSends = (nint) sendDesc;
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetOutputVoices(
|
||||||
|
Handle,
|
||||||
|
ref sends
|
||||||
|
);
|
||||||
|
|
||||||
|
ReverbEffect = reverbEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the reverb effect chain from this voice.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveReverbEffectChain()
|
||||||
|
{
|
||||||
|
if (ReverbEffect != null)
|
||||||
|
{
|
||||||
|
ReverbEffect = null;
|
||||||
|
reverb = 0;
|
||||||
|
SetOutputVoice(OutputVoice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets all voice parameters to defaults.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Reset()
|
||||||
|
{
|
||||||
|
RemoveReverbEffectChain();
|
||||||
|
Volume = 1;
|
||||||
|
Pan = 0;
|
||||||
|
Pitch = 0;
|
||||||
|
FilterType = FilterType.None;
|
||||||
|
SetOutputVoice(Device.MasteringVoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken from https://github.com/FNA-XNA/FNA/blob/master/src/Audio/SoundEffectInstance.cs
|
||||||
|
private unsafe void SetPanMatrixCoefficients()
|
||||||
|
{
|
||||||
|
/* Two major things to notice:
|
||||||
|
* 1. The spec assumes any speaker count >= 2 has Front Left/Right.
|
||||||
|
* 2. Stereo panning is WAY more complicated than you think.
|
||||||
|
* The main thing is that hard panning does NOT eliminate an
|
||||||
|
* entire channel; the two channels are blended on each side.
|
||||||
|
* -flibit
|
||||||
|
*/
|
||||||
|
float* outputMatrix = (float*) pMatrixCoefficients;
|
||||||
|
if (SourceChannelCount == 1)
|
||||||
|
{
|
||||||
|
if (DestinationChannelCount == 1)
|
||||||
|
{
|
||||||
|
outputMatrix[0] = 1.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputMatrix[0] = (pan > 0.0f) ? (1.0f - pan) : 1.0f;
|
||||||
|
outputMatrix[1] = (pan < 0.0f) ? (1.0f + pan) : 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (DestinationChannelCount == 1)
|
||||||
|
{
|
||||||
|
outputMatrix[0] = 1.0f;
|
||||||
|
outputMatrix[1] = 1.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (pan <= 0.0f)
|
||||||
|
{
|
||||||
|
// Left speaker blends left/right channels
|
||||||
|
outputMatrix[0] = 0.5f * pan + 1.0f;
|
||||||
|
outputMatrix[1] = 0.5f * -pan;
|
||||||
|
// Right speaker gets less of the right channel
|
||||||
|
outputMatrix[2] = 0.0f;
|
||||||
|
outputMatrix[3] = pan + 1.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Left speaker gets less of the left channel
|
||||||
|
outputMatrix[0] = -pan + 1.0f;
|
||||||
|
outputMatrix[1] = 0.0f;
|
||||||
|
// Right speaker blends right/left channels
|
||||||
|
outputMatrix[2] = 0.5f * pan;
|
||||||
|
outputMatrix[3] = 0.5f * -pan + 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdatePitch()
|
||||||
|
{
|
||||||
|
float doppler;
|
||||||
|
float dopplerScale = Device.DopplerScale;
|
||||||
|
if (!Is3D || dopplerScale == 0.0f)
|
||||||
|
{
|
||||||
|
doppler = 1.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
doppler = DopplerFactor * dopplerScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
FAudio.FAudioSourceVoice_SetFrequencyRatio(
|
||||||
|
Handle,
|
||||||
|
(float) System.Math.Pow(2.0, pitch) * doppler,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
NativeMemory.Free(pMatrixCoefficients);
|
||||||
|
FAudio.FAudioVoice_DestroyVoice(Handle);
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,181 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Axis-aligned bounding box.
|
|
||||||
/// </summary>
|
|
||||||
public struct AABB2D : System.IEquatable<AABB2D>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The top-left position of the AABB.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
public Vector2 Min { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The bottom-right position of the AABB.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
public Vector2 Max { get; private set; }
|
|
||||||
|
|
||||||
public Fix64 Width { get { return Max.X - Min.X; } }
|
|
||||||
public Fix64 Height { get { return Max.Y - Min.Y; } }
|
|
||||||
|
|
||||||
public Fix64 Right { get { return Max.X; } }
|
|
||||||
public Fix64 Left { get { return Min.X; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The top of the AABB. Assumes a downward-aligned Y axis, so this value will be smaller than Bottom.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
public Fix64 Top { get { return Min.Y; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The bottom of the AABB. Assumes a downward-aligned Y axis, so this value will be larger than Top.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
public Fix64 Bottom { get { return Max.Y; } }
|
|
||||||
|
|
||||||
public AABB2D(Fix64 minX, Fix64 minY, Fix64 maxX, Fix64 maxY)
|
|
||||||
{
|
|
||||||
Min = new Vector2(minX, minY);
|
|
||||||
Max = new Vector2(maxX, maxY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D(int minX, int minY, int maxX, int maxY)
|
|
||||||
{
|
|
||||||
Min = new Vector2(minX, minY);
|
|
||||||
Max = new Vector2(maxX, maxY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D(Vector2 min, Vector2 max)
|
|
||||||
{
|
|
||||||
Min = min;
|
|
||||||
Max = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Matrix3x2 AbsoluteMatrix(Matrix3x2 matrix)
|
|
||||||
{
|
|
||||||
return new Matrix3x2
|
|
||||||
(
|
|
||||||
Fix64.Abs(matrix.M11), Fix64.Abs(matrix.M12),
|
|
||||||
Fix64.Abs(matrix.M21), Fix64.Abs(matrix.M22),
|
|
||||||
Fix64.Abs(matrix.M31), Fix64.Abs(matrix.M32)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Efficiently transforms the AABB by a Transform2D.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="aabb"></param>
|
|
||||||
/// <param name="transform"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static AABB2D Transformed(AABB2D aabb, Transform2D transform)
|
|
||||||
{
|
|
||||||
var two = new Fix64(2);
|
|
||||||
var center = (aabb.Min + aabb.Max) / two;
|
|
||||||
var extent = (aabb.Max - aabb.Min) / two;
|
|
||||||
|
|
||||||
var newCenter = Vector2.Transform(center, transform.TransformMatrix);
|
|
||||||
var newExtent = Vector2.TransformNormal(extent, AbsoluteMatrix(transform.TransformMatrix));
|
|
||||||
|
|
||||||
return new AABB2D(newCenter - newExtent, newCenter + newExtent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D Compose(AABB2D aabb)
|
|
||||||
{
|
|
||||||
Fix64 left = Left;
|
|
||||||
Fix64 top = Top;
|
|
||||||
Fix64 right = Right;
|
|
||||||
Fix64 bottom = Bottom;
|
|
||||||
|
|
||||||
if (aabb.Left < left)
|
|
||||||
{
|
|
||||||
left = aabb.Left;
|
|
||||||
}
|
|
||||||
if (aabb.Right > right)
|
|
||||||
{
|
|
||||||
right = aabb.Right;
|
|
||||||
}
|
|
||||||
if (aabb.Top < top)
|
|
||||||
{
|
|
||||||
top = aabb.Top;
|
|
||||||
}
|
|
||||||
if (aabb.Bottom > bottom)
|
|
||||||
{
|
|
||||||
bottom = aabb.Bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AABB2D(left, top, right, bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an AABB for an arbitrary collection of positions.
|
|
||||||
/// This is less efficient than defining a custom AABB method for most shapes, so avoid using this if possible.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="vertices"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static AABB2D FromVertices(IEnumerable<Vector2> vertices)
|
|
||||||
{
|
|
||||||
var minX = Fix64.MaxValue;
|
|
||||||
var minY = Fix64.MaxValue;
|
|
||||||
var maxX = Fix64.MinValue;
|
|
||||||
var maxY = Fix64.MinValue;
|
|
||||||
|
|
||||||
foreach (var vertex in vertices)
|
|
||||||
{
|
|
||||||
if (vertex.X < minX)
|
|
||||||
{
|
|
||||||
minX = vertex.X;
|
|
||||||
}
|
|
||||||
if (vertex.Y < minY)
|
|
||||||
{
|
|
||||||
minY = vertex.Y;
|
|
||||||
}
|
|
||||||
if (vertex.X > maxX)
|
|
||||||
{
|
|
||||||
maxX = vertex.X;
|
|
||||||
}
|
|
||||||
if (vertex.Y > maxY)
|
|
||||||
{
|
|
||||||
maxY = vertex.Y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AABB2D(minX, minY, maxX, maxY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestOverlap(AABB2D a, AABB2D b)
|
|
||||||
{
|
|
||||||
return a.Left < b.Right && a.Right > b.Left && a.Top < b.Bottom && a.Bottom > b.Top;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is AABB2D aabb && Equals(aabb);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(AABB2D other)
|
|
||||||
{
|
|
||||||
return Min == other.Min &&
|
|
||||||
Max == other.Max;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Min, Max);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(AABB2D left, AABB2D right)
|
|
||||||
{
|
|
||||||
return left.Equals(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(AABB2D left, AABB2D right)
|
|
||||||
{
|
|
||||||
return !(left == right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
public interface ICollidable
|
|
||||||
{
|
|
||||||
IEnumerable<IShape2D> Shapes { get; }
|
|
||||||
AABB2D AABB { get; }
|
|
||||||
AABB2D TransformedAABB(Transform2D transform);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
public interface IShape2D : ICollidable, System.IEquatable<IShape2D>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A Minkowski support function. Gives the farthest point on the edge of a shape along the given direction.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="direction">A normalized Vector2.</param>
|
|
||||||
/// <param name="transform">A Transform for transforming the shape vertices.</param>
|
|
||||||
/// <returns>The farthest point on the edge of the shape along the given direction.</returns>
|
|
||||||
Vector2 Support(Vector2 direction, Transform2D transform);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A Minkowski difference between two shapes.
|
|
||||||
/// </summary>
|
|
||||||
public struct MinkowskiDifference : System.IEquatable<MinkowskiDifference>
|
|
||||||
{
|
|
||||||
private IShape2D ShapeA { get; }
|
|
||||||
private Transform2D TransformA { get; }
|
|
||||||
private IShape2D ShapeB { get; }
|
|
||||||
private Transform2D TransformB { get; }
|
|
||||||
|
|
||||||
public MinkowskiDifference(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
ShapeA = shapeA;
|
|
||||||
TransformA = transformA;
|
|
||||||
ShapeB = shapeB;
|
|
||||||
TransformB = transformB;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction)
|
|
||||||
{
|
|
||||||
return ShapeA.Support(direction, TransformA) - ShapeB.Support(-direction, TransformB);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
return other is MinkowskiDifference minkowskiDifference && Equals(minkowskiDifference);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(MinkowskiDifference other)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
ShapeA == other.ShapeA &&
|
|
||||||
TransformA == other.TransformA &&
|
|
||||||
ShapeB == other.ShapeB &&
|
|
||||||
TransformB == other.TransformB;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(ShapeA, TransformA, ShapeB, TransformB);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(MinkowskiDifference a, MinkowskiDifference b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(MinkowskiDifference a, MinkowskiDifference b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,333 +0,0 @@
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
public static class NarrowPhase
|
|
||||||
{
|
|
||||||
private struct Edge
|
|
||||||
{
|
|
||||||
public Fix64 Distance;
|
|
||||||
public Vector2 Normal;
|
|
||||||
public int Index;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestCollision(ICollidable collidableA, Transform2D transformA, ICollidable collidableB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
foreach (var shapeA in collidableA.Shapes)
|
|
||||||
{
|
|
||||||
foreach (var shapeB in collidableB.Shapes)
|
|
||||||
{
|
|
||||||
if (TestCollision(shapeA, transformA, shapeB, transformB))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestCollision(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
// If we can use a fast path check, let's do that!
|
|
||||||
if (shapeA is Rectangle rectangleA && shapeB is Rectangle rectangleB && transformA.IsAxisAligned && transformB.IsAxisAligned)
|
|
||||||
{
|
|
||||||
return TestRectangleOverlap(rectangleA, transformA, rectangleB, transformB);
|
|
||||||
}
|
|
||||||
else if (shapeA is Point && shapeB is Rectangle && transformB.IsAxisAligned)
|
|
||||||
{
|
|
||||||
return TestPointRectangleOverlap((Point) shapeA, transformA, (Rectangle) shapeB, transformB);
|
|
||||||
}
|
|
||||||
else if (shapeA is Rectangle && shapeB is Point && transformA.IsAxisAligned)
|
|
||||||
{
|
|
||||||
return TestPointRectangleOverlap((Point) shapeB, transformB, (Rectangle) shapeA, transformA);
|
|
||||||
}
|
|
||||||
else if (shapeA is Rectangle && shapeB is Circle && transformA.IsAxisAligned && transformB.IsUniformScale)
|
|
||||||
{
|
|
||||||
return TestCircleRectangleOverlap((Circle) shapeB, transformB, (Rectangle) shapeA, transformA);
|
|
||||||
}
|
|
||||||
else if (shapeA is Circle && shapeB is Rectangle && transformA.IsUniformScale && transformB.IsAxisAligned)
|
|
||||||
{
|
|
||||||
return TestCircleRectangleOverlap((Circle) shapeA, transformA, (Rectangle) shapeB, transformB);
|
|
||||||
}
|
|
||||||
else if (shapeA is Circle && shapeB is Point && transformA.IsUniformScale)
|
|
||||||
{
|
|
||||||
return TestCirclePointOverlap((Circle) shapeA, transformA, (Point) shapeB, transformB);
|
|
||||||
}
|
|
||||||
else if (shapeA is Point && shapeB is Circle && transformB.IsUniformScale)
|
|
||||||
{
|
|
||||||
return TestCirclePointOverlap((Circle) shapeB, transformB, (Point) shapeA, transformA);
|
|
||||||
}
|
|
||||||
else if (shapeA is Circle circleA && shapeB is Circle circleB && transformA.IsUniformScale && transformB.IsUniformScale)
|
|
||||||
{
|
|
||||||
return TestCircleOverlap(circleA, transformA, circleB, transformB);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sad, we can't do a fast path optimization. Time for a simplex reduction.
|
|
||||||
return FindCollisionSimplex(shapeA, transformA, shapeB, transformB).Item1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestRectangleOverlap(Rectangle rectangleA, Transform2D transformA, Rectangle rectangleB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
var firstAABB = rectangleA.TransformedAABB(transformA);
|
|
||||||
var secondAABB = rectangleB.TransformedAABB(transformB);
|
|
||||||
|
|
||||||
return firstAABB.Left < secondAABB.Right && firstAABB.Right > secondAABB.Left && firstAABB.Top < secondAABB.Bottom && firstAABB.Bottom > secondAABB.Top;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestPointRectangleOverlap(Point point, Transform2D pointTransform, Rectangle rectangle, Transform2D rectangleTransform)
|
|
||||||
{
|
|
||||||
var transformedPoint = pointTransform.Position;
|
|
||||||
var AABB = rectangle.TransformedAABB(rectangleTransform);
|
|
||||||
|
|
||||||
return transformedPoint.X > AABB.Left && transformedPoint.X < AABB.Right && transformedPoint.Y < AABB.Bottom && transformedPoint.Y > AABB.Top;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestCirclePointOverlap(Circle circle, Transform2D circleTransform, Point point, Transform2D pointTransform)
|
|
||||||
{
|
|
||||||
var circleCenter = circleTransform.Position;
|
|
||||||
var circleRadius = circle.Radius * circleTransform.Scale.X;
|
|
||||||
|
|
||||||
var distanceX = circleCenter.X - pointTransform.Position.X;
|
|
||||||
var distanceY = circleCenter.Y - pointTransform.Position.Y;
|
|
||||||
|
|
||||||
return (distanceX * distanceX) + (distanceY * distanceY) < (circleRadius * circleRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// NOTE: The rectangle must be axis aligned, and the scaling of the circle must be uniform.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TestCircleRectangleOverlap(Circle circle, Transform2D circleTransform, Rectangle rectangle, Transform2D rectangleTransform)
|
|
||||||
{
|
|
||||||
var circleCenter = circleTransform.Position;
|
|
||||||
var circleRadius = circle.Radius * circleTransform.Scale.X;
|
|
||||||
var AABB = rectangle.TransformedAABB(rectangleTransform);
|
|
||||||
|
|
||||||
var closestX = Fix64.Clamp(circleCenter.X, AABB.Left, AABB.Right);
|
|
||||||
var closestY = Fix64.Clamp(circleCenter.Y, AABB.Top, AABB.Bottom);
|
|
||||||
|
|
||||||
var distanceX = circleCenter.X - closestX;
|
|
||||||
var distanceY = circleCenter.Y - closestY;
|
|
||||||
|
|
||||||
var distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
|
|
||||||
return distanceSquared < (circleRadius * circleRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestCircleOverlap(Circle circleA, Transform2D transformA, Circle circleB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
var radiusA = circleA.Radius * transformA.Scale.X;
|
|
||||||
var radiusB = circleB.Radius * transformB.Scale.Y;
|
|
||||||
|
|
||||||
var centerA = transformA.Position;
|
|
||||||
var centerB = transformB.Position;
|
|
||||||
|
|
||||||
var distanceSquared = (centerA - centerB).LengthSquared();
|
|
||||||
var radiusSumSquared = (radiusA + radiusB) * (radiusA + radiusB);
|
|
||||||
|
|
||||||
return distanceSquared < radiusSumSquared;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static (bool, Simplex2D) FindCollisionSimplex(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
var minkowskiDifference = new MinkowskiDifference(shapeA, transformA, shapeB, transformB);
|
|
||||||
var c = minkowskiDifference.Support(Vector2.UnitX);
|
|
||||||
var b = minkowskiDifference.Support(-Vector2.UnitX);
|
|
||||||
return Check(minkowskiDifference, c, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe static Vector2 Intersect(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Simplex2D simplex)
|
|
||||||
{
|
|
||||||
if (shapeA == null) { throw new System.ArgumentNullException(nameof(shapeA)); }
|
|
||||||
if (shapeB == null) { throw new System.ArgumentNullException(nameof(shapeB)); }
|
|
||||||
if (!simplex.TwoSimplex) { throw new System.ArgumentException("Simplex must be a 2-Simplex.", nameof(simplex)); }
|
|
||||||
|
|
||||||
var epsilon = Fix64.FromFraction(1, 10000);
|
|
||||||
|
|
||||||
var a = simplex.A;
|
|
||||||
var b = simplex.B.Value;
|
|
||||||
var c = simplex.C.Value;
|
|
||||||
|
|
||||||
Vector2 intersection = default;
|
|
||||||
|
|
||||||
for (var i = 0; i < 32; i++)
|
|
||||||
{
|
|
||||||
var edge = FindClosestEdge(simplex);
|
|
||||||
var support = CalculateSupport(shapeA, Transform2DA, shapeB, Transform2DB, edge.Normal);
|
|
||||||
var distance = Vector2.Dot(support, edge.Normal);
|
|
||||||
|
|
||||||
intersection = edge.Normal;
|
|
||||||
intersection *= distance;
|
|
||||||
|
|
||||||
if (Fix64.Abs(distance - edge.Distance) <= epsilon)
|
|
||||||
{
|
|
||||||
return intersection;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
simplex.Insert(support, edge.Index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return intersection; // close enough
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe Edge FindClosestEdge(Simplex2D simplex)
|
|
||||||
{
|
|
||||||
var closestDistance = Fix64.MaxValue;
|
|
||||||
var closestNormal = Vector2.Zero;
|
|
||||||
var closestIndex = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < 4; i += 1)
|
|
||||||
{
|
|
||||||
var j = (i + 1 == 3) ? 0 : i + 1;
|
|
||||||
|
|
||||||
var a = simplex[i];
|
|
||||||
var b = simplex[j];
|
|
||||||
|
|
||||||
var e = b - a;
|
|
||||||
|
|
||||||
var oa = a;
|
|
||||||
|
|
||||||
var n = Vector2.Normalize(TripleProduct(e, oa, e));
|
|
||||||
|
|
||||||
var d = Vector2.Dot(n, a);
|
|
||||||
|
|
||||||
if (d < closestDistance)
|
|
||||||
{
|
|
||||||
closestDistance = d;
|
|
||||||
closestNormal = n;
|
|
||||||
closestIndex = j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Edge
|
|
||||||
{
|
|
||||||
Distance = closestDistance,
|
|
||||||
Normal = closestNormal,
|
|
||||||
Index = closestIndex
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector2 CalculateSupport(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Vector2 direction)
|
|
||||||
{
|
|
||||||
return shapeA.Support(direction, Transform2DA) - shapeB.Support(-direction, Transform2DB);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D) Check(MinkowskiDifference minkowskiDifference, Vector2 c, Vector2 b)
|
|
||||||
{
|
|
||||||
var cb = c - b;
|
|
||||||
var c0 = -c;
|
|
||||||
var d = Direction(cb, c0);
|
|
||||||
return DoSimplex(minkowskiDifference, new Simplex2D(b, c), d);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D) DoSimplex(MinkowskiDifference minkowskiDifference, Simplex2D simplex, Vector2 direction)
|
|
||||||
{
|
|
||||||
var a = minkowskiDifference.Support(direction);
|
|
||||||
var notPastOrigin = Vector2.Dot(a, direction) < Fix64.Zero;
|
|
||||||
var (intersects, newSimplex, newDirection) = EnclosesOrigin(a, simplex);
|
|
||||||
|
|
||||||
if (notPastOrigin)
|
|
||||||
{
|
|
||||||
return (false, default(Simplex2D));
|
|
||||||
}
|
|
||||||
else if (intersects)
|
|
||||||
{
|
|
||||||
return (true, new Simplex2D(simplex.A, simplex.B.Value, a));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return DoSimplex(minkowskiDifference, newSimplex, newDirection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D, Vector2) EnclosesOrigin(Vector2 a, Simplex2D simplex)
|
|
||||||
{
|
|
||||||
if (simplex.ZeroSimplex)
|
|
||||||
{
|
|
||||||
return HandleZeroSimplex(a, simplex.A);
|
|
||||||
}
|
|
||||||
else if (simplex.OneSimplex)
|
|
||||||
{
|
|
||||||
return HandleOneSimplex(a, simplex.A, simplex.B.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (false, simplex, Vector2.Zero);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D, Vector2) HandleZeroSimplex(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
var ab = b - a;
|
|
||||||
var a0 = -a;
|
|
||||||
var (newSimplex, newDirection) = SameDirection(ab, a0) ? (new Simplex2D(a, b), Perpendicular(ab, a0)) : (new Simplex2D(a), a0);
|
|
||||||
return (false, newSimplex, newDirection);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D, Vector2) HandleOneSimplex(Vector2 a, Vector2 b, Vector2 c)
|
|
||||||
{
|
|
||||||
var a0 = -a;
|
|
||||||
var ab = b - a;
|
|
||||||
var ac = c - a;
|
|
||||||
var abp = Perpendicular(ab, -ac);
|
|
||||||
var acp = Perpendicular(ac, -ab);
|
|
||||||
|
|
||||||
if (SameDirection(abp, a0))
|
|
||||||
{
|
|
||||||
if (SameDirection(ab, a0))
|
|
||||||
{
|
|
||||||
return (false, new Simplex2D(a, b), abp);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (false, new Simplex2D(a), a0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (SameDirection(acp, a0))
|
|
||||||
{
|
|
||||||
if (SameDirection(ac, a0))
|
|
||||||
{
|
|
||||||
return (false, new Simplex2D(a, c), acp);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (false, new Simplex2D(a), a0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (true, new Simplex2D(b, c), a0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector2 TripleProduct(Vector2 a, Vector2 b, Vector2 c)
|
|
||||||
{
|
|
||||||
var A = new Vector3(a.X, a.Y, Fix64.Zero);
|
|
||||||
var B = new Vector3(b.X, b.Y, Fix64.Zero);
|
|
||||||
var C = new Vector3(c.X, c.Y, Fix64.Zero);
|
|
||||||
|
|
||||||
var first = Vector3.Cross(A, B);
|
|
||||||
var second = Vector3.Cross(first, C);
|
|
||||||
|
|
||||||
return new Vector2(second.X, second.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector2 Direction(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
var d = TripleProduct(a, b, a);
|
|
||||||
var collinear = d == Vector2.Zero;
|
|
||||||
return collinear ? new Vector2(a.Y, -a.X) : d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool SameDirection(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
return Vector2.Dot(a, b) > Fix64.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector2 Perpendicular(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
return TripleProduct(a, b, a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A Circle is a shape defined by a radius.
|
|
||||||
/// </summary>
|
|
||||||
public struct Circle : IShape2D, System.IEquatable<Circle>
|
|
||||||
{
|
|
||||||
public Fix64 Radius { get; }
|
|
||||||
public AABB2D AABB { get; }
|
|
||||||
public IEnumerable<IShape2D> Shapes
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Circle(Fix64 radius)
|
|
||||||
{
|
|
||||||
Radius = radius;
|
|
||||||
AABB = new AABB2D(-Radius, -Radius, Radius, Radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Circle(int radius)
|
|
||||||
{
|
|
||||||
Radius = (Fix64) radius;
|
|
||||||
AABB = new AABB2D(-Radius, -Radius, Radius, Radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
return Vector2.Transform(Vector2.Normalize(direction) * Radius, transform.TransformMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D TransformedAABB(Transform2D transform2D)
|
|
||||||
{
|
|
||||||
return AABB2D.Transformed(AABB, transform2D);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is IShape2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(IShape2D other)
|
|
||||||
{
|
|
||||||
return other is Circle circle && Equals(circle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Circle other)
|
|
||||||
{
|
|
||||||
return Radius == other.Radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Circle a, Circle b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Circle a, Circle b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A line is a shape defined by exactly two points in space.
|
|
||||||
/// </summary>
|
|
||||||
public struct Line : IShape2D, System.IEquatable<Line>
|
|
||||||
{
|
|
||||||
public Vector2 Start { get; }
|
|
||||||
public Vector2 End { get; }
|
|
||||||
|
|
||||||
public AABB2D AABB { get; }
|
|
||||||
|
|
||||||
public IEnumerable<IShape2D> Shapes
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Line(Vector2 start, Vector2 end)
|
|
||||||
{
|
|
||||||
Start = start;
|
|
||||||
End = end;
|
|
||||||
|
|
||||||
AABB = new AABB2D(
|
|
||||||
Fix64.Min(Start.X, End.X),
|
|
||||||
Fix64.Min(Start.Y, End.Y),
|
|
||||||
Fix64.Max(Start.X, End.X),
|
|
||||||
Fix64.Max(Start.Y, End.Y)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
var transformedStart = Vector2.Transform(Start, transform.TransformMatrix);
|
|
||||||
var transformedEnd = Vector2.Transform(End, transform.TransformMatrix);
|
|
||||||
return Vector2.Dot(transformedStart, direction) > Vector2.Dot(transformedEnd, direction) ?
|
|
||||||
transformedStart :
|
|
||||||
transformedEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D TransformedAABB(Transform2D transform)
|
|
||||||
{
|
|
||||||
return AABB2D.Transformed(AABB, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is IShape2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(IShape2D other)
|
|
||||||
{
|
|
||||||
return other is Line otherLine && Equals(otherLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Line other)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
(Start == other.Start && End == other.End) ||
|
|
||||||
(End == other.Start && Start == other.End);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Start, End);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Line a, Line b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Line a, Line b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A Point is "that which has no part".
|
|
||||||
/// All points by themselves are identical.
|
|
||||||
/// </summary>
|
|
||||||
public struct Point : IShape2D, System.IEquatable<Point>
|
|
||||||
{
|
|
||||||
public AABB2D AABB { get; }
|
|
||||||
public IEnumerable<IShape2D> Shapes
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D TransformedAABB(Transform2D transform)
|
|
||||||
{
|
|
||||||
return AABB2D.Transformed(AABB, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
return Vector2.Transform(Vector2.Zero, transform.TransformMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is IShape2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(IShape2D other)
|
|
||||||
{
|
|
||||||
return other is Point otherPoint && Equals(otherPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Point other)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Point a, Point b)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Point a, Point b)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A rectangle is a shape defined by a width and height. The origin is the center of the rectangle.
|
|
||||||
/// </summary>
|
|
||||||
public struct Rectangle : IShape2D, System.IEquatable<Rectangle>
|
|
||||||
{
|
|
||||||
public AABB2D AABB { get; }
|
|
||||||
public Fix64 Width { get; }
|
|
||||||
public Fix64 Height { get; }
|
|
||||||
|
|
||||||
public Fix64 Right { get; }
|
|
||||||
public Fix64 Left { get; }
|
|
||||||
public Fix64 Top { get; }
|
|
||||||
public Fix64 Bottom { get; }
|
|
||||||
public Vector2 TopLeft { get; }
|
|
||||||
public Vector2 BottomRight { get; }
|
|
||||||
|
|
||||||
public Vector2 Min { get; }
|
|
||||||
public Vector2 Max { get; }
|
|
||||||
|
|
||||||
public IEnumerable<IShape2D> Shapes
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Rectangle(Fix64 left, Fix64 top, Fix64 width, Fix64 height)
|
|
||||||
{
|
|
||||||
Width = width;
|
|
||||||
Height = height;
|
|
||||||
Left = left;
|
|
||||||
Right = left + width;
|
|
||||||
Top = top;
|
|
||||||
Bottom = top + height;
|
|
||||||
AABB = new AABB2D(left, top, Right, Bottom);
|
|
||||||
TopLeft = new Vector2(Left, Top);
|
|
||||||
BottomRight = new Vector2(Right, Bottom);
|
|
||||||
Min = AABB.Min;
|
|
||||||
Max = AABB.Max;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Rectangle(int left, int top, int width, int height)
|
|
||||||
{
|
|
||||||
Width = (Fix64) width;
|
|
||||||
Height = (Fix64) height;
|
|
||||||
Left = (Fix64) left;
|
|
||||||
Right = (Fix64) (left + width);
|
|
||||||
Top = (Fix64) top;
|
|
||||||
Bottom = (Fix64) (top + height);
|
|
||||||
AABB = new AABB2D(Left, Top, Right, Bottom);
|
|
||||||
TopLeft = new Vector2(Left, Top);
|
|
||||||
BottomRight = new Vector2(Right, Bottom);
|
|
||||||
Min = AABB.Min;
|
|
||||||
Max = AABB.Max;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector2 Support(Vector2 direction)
|
|
||||||
{
|
|
||||||
if (direction.X >= Fix64.Zero && direction.Y >= Fix64.Zero)
|
|
||||||
{
|
|
||||||
return Max;
|
|
||||||
}
|
|
||||||
else if (direction.X >= Fix64.Zero && direction.Y < Fix64.Zero)
|
|
||||||
{
|
|
||||||
return new Vector2(Max.X, Min.Y);
|
|
||||||
}
|
|
||||||
else if (direction.X < Fix64.Zero && direction.Y >= Fix64.Zero)
|
|
||||||
{
|
|
||||||
return new Vector2(Min.X, Max.Y);
|
|
||||||
}
|
|
||||||
else if (direction.X < Fix64.Zero && direction.Y < Fix64.Zero)
|
|
||||||
{
|
|
||||||
return new Vector2(Min.X, Min.Y);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new System.ArgumentException("Support vector direction cannot be zero.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
Matrix3x2 inverseTransform;
|
|
||||||
Matrix3x2.Invert(transform.TransformMatrix, out inverseTransform);
|
|
||||||
var inverseDirection = Vector2.TransformNormal(direction, inverseTransform);
|
|
||||||
return Vector2.Transform(Support(inverseDirection), transform.TransformMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D TransformedAABB(Transform2D transform)
|
|
||||||
{
|
|
||||||
return AABB2D.Transformed(AABB, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is IShape2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(IShape2D other)
|
|
||||||
{
|
|
||||||
return (other is Rectangle rectangle && Equals(rectangle));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Rectangle other)
|
|
||||||
{
|
|
||||||
return Min == other.Min && Max == other.Max;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Min, Max);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Rectangle a, Rectangle b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Rectangle a, Rectangle b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A simplex is a shape with up to n - 2 vertices in the nth dimension.
|
|
||||||
/// </summary>
|
|
||||||
public struct Simplex2D : System.IEquatable<Simplex2D>
|
|
||||||
{
|
|
||||||
private Vector2 a;
|
|
||||||
private Vector2? b;
|
|
||||||
private Vector2? c;
|
|
||||||
|
|
||||||
public Vector2 A => a;
|
|
||||||
public Vector2? B => b;
|
|
||||||
public Vector2? C => c;
|
|
||||||
|
|
||||||
public bool ZeroSimplex { get { return !b.HasValue && !c.HasValue; } }
|
|
||||||
public bool OneSimplex { get { return b.HasValue && !c.HasValue; } }
|
|
||||||
public bool TwoSimplex { get { return b.HasValue && c.HasValue; } }
|
|
||||||
|
|
||||||
public int Count => TwoSimplex ? 3 : (OneSimplex ? 2 : 1);
|
|
||||||
|
|
||||||
public Simplex2D(Vector2 a)
|
|
||||||
{
|
|
||||||
this.a = a;
|
|
||||||
b = null;
|
|
||||||
c = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Simplex2D(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
this.a = a;
|
|
||||||
this.b = b;
|
|
||||||
c = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Simplex2D(Vector2 a, Vector2 b, Vector2 c)
|
|
||||||
{
|
|
||||||
this.a = a;
|
|
||||||
this.b = b;
|
|
||||||
this.c = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (index == 0) { return a; }
|
|
||||||
if (index == 1) { return b.Value; }
|
|
||||||
if (index == 2) { return c.Value; }
|
|
||||||
throw new System.IndexOutOfRangeException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Vector2> Vertices
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return (Vector2) a;
|
|
||||||
if (b.HasValue) { yield return (Vector2) b; }
|
|
||||||
if (c.HasValue) { yield return (Vector2) c; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
var maxDotProduct = Fix64.MinValue;
|
|
||||||
var maxVertex = a;
|
|
||||||
foreach (var vertex in Vertices)
|
|
||||||
{
|
|
||||||
var transformed = Vector2.Transform(vertex, transform.TransformMatrix);
|
|
||||||
var dot = Vector2.Dot(transformed, direction);
|
|
||||||
if (dot > maxDotProduct)
|
|
||||||
{
|
|
||||||
maxVertex = transformed;
|
|
||||||
maxDotProduct = dot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return maxVertex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Insert(Vector2 point, int index)
|
|
||||||
{
|
|
||||||
if (index == 0)
|
|
||||||
{
|
|
||||||
c = b;
|
|
||||||
b = a;
|
|
||||||
a = point;
|
|
||||||
}
|
|
||||||
else if (index == 1)
|
|
||||||
{
|
|
||||||
c = b;
|
|
||||||
b = point;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
c = point;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is Simplex2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Simplex2D other)
|
|
||||||
{
|
|
||||||
if (Count != other.Count) { return false; }
|
|
||||||
|
|
||||||
return
|
|
||||||
(A == other.A && B == other.B && C == other.C) ||
|
|
||||||
(A == other.A && B == other.C && C == other.B) ||
|
|
||||||
(A == other.B && B == other.A && C == other.C) ||
|
|
||||||
(A == other.B && B == other.C && C == other.A) ||
|
|
||||||
(A == other.C && B == other.A && C == other.B) ||
|
|
||||||
(A == other.C && B == other.B && C == other.A);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Vertices);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Simplex2D a, Simplex2D b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Simplex2D a, Simplex2D b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,253 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Fixed;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Fixed
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Used to quickly check if two shapes are potentially overlapping.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type that will be used to uniquely identify shape-transform pairs.</typeparam>
|
|
||||||
public class SpatialHash2D<T> where T : System.IEquatable<T>
|
|
||||||
{
|
|
||||||
private readonly Fix64 cellSize;
|
|
||||||
|
|
||||||
private readonly Dictionary<long, HashSet<T>> hashDictionary = new Dictionary<long, HashSet<T>>();
|
|
||||||
// FIXME: this ICollidable causes boxing which triggers garbage collection
|
|
||||||
private readonly Dictionary<T, (ICollidable, Transform2D, uint)> IDLookup = new Dictionary<T, (ICollidable, Transform2D, uint)>();
|
|
||||||
|
|
||||||
public int MinX { get; private set; } = 0;
|
|
||||||
public int MaxX { get; private set; } = 0;
|
|
||||||
public int MinY { get; private set; } = 0;
|
|
||||||
public int MaxY { get; private set; } = 0;
|
|
||||||
|
|
||||||
private Queue<HashSet<T>> hashSetPool = new Queue<HashSet<T>>();
|
|
||||||
|
|
||||||
public SpatialHash2D(int cellSize)
|
|
||||||
{
|
|
||||||
this.cellSize = new Fix64(cellSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (int, int) Hash(Vector2 position)
|
|
||||||
{
|
|
||||||
return ((int) (position.X / cellSize), (int) (position.Y / cellSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Inserts an element into the SpatialHash.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">A unique ID for the shape-transform pair.</param>
|
|
||||||
/// <param name="shape"></param>
|
|
||||||
/// <param name="transform2D"></param>
|
|
||||||
/// <param name="collisionGroups">A bitmask value specifying the groups this object belongs to.</param>
|
|
||||||
public void Insert(T id, ICollidable shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue)
|
|
||||||
{
|
|
||||||
var box = shape.TransformedAABB(transform2D);
|
|
||||||
var minHash = Hash(box.Min);
|
|
||||||
var maxHash = Hash(box.Max);
|
|
||||||
|
|
||||||
foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2))
|
|
||||||
{
|
|
||||||
if (!hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
hashDictionary.Add(key, new HashSet<T>());
|
|
||||||
}
|
|
||||||
|
|
||||||
hashDictionary[key].Add(id);
|
|
||||||
IDLookup[id] = (shape, transform2D, collisionGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
MinX = System.Math.Min(MinX, minHash.Item1);
|
|
||||||
MinY = System.Math.Min(MinY, minHash.Item2);
|
|
||||||
MaxX = System.Math.Max(MaxX, maxHash.Item1);
|
|
||||||
MaxY = System.Math.Max(MaxY, maxHash.Item2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves all the potential collisions of a shape-transform pair. Excludes any shape-transforms with the given ID.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(T id, ICollidable shape, Transform2D transform2D, uint collisionMask = uint.MaxValue)
|
|
||||||
{
|
|
||||||
var returned = AcquireHashSet();
|
|
||||||
|
|
||||||
var box = shape.TransformedAABB(transform2D);
|
|
||||||
var (minX, minY) = Hash(box.Min);
|
|
||||||
var (maxX, maxY) = Hash(box.Max);
|
|
||||||
|
|
||||||
if (minX < MinX) { minX = MinX; }
|
|
||||||
if (maxX > MaxX) { maxX = MaxX; }
|
|
||||||
if (minY < MinY) { minY = MinY; }
|
|
||||||
if (maxY > MaxY) { maxY = MaxY; }
|
|
||||||
|
|
||||||
foreach (var key in Keys(minX, minY, maxX, maxY))
|
|
||||||
{
|
|
||||||
if (hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
foreach (var t in hashDictionary[key])
|
|
||||||
{
|
|
||||||
if (!returned.Contains(t))
|
|
||||||
{
|
|
||||||
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
|
|
||||||
if (!id.Equals(t) && ((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(box, otherShape.TransformedAABB(otherTransform)))
|
|
||||||
{
|
|
||||||
returned.Add(t);
|
|
||||||
yield return (t, otherShape, otherTransform, collisionGroups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FreeHashSet(returned);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves all the potential collisions of a shape-transform pair.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(ICollidable shape, Transform2D transform2D, uint collisionMask = uint.MaxValue)
|
|
||||||
{
|
|
||||||
var returned = AcquireHashSet();
|
|
||||||
|
|
||||||
var box = shape.TransformedAABB(transform2D);
|
|
||||||
var (minX, minY) = Hash(box.Min);
|
|
||||||
var (maxX, maxY) = Hash(box.Max);
|
|
||||||
|
|
||||||
if (minX < MinX) { minX = MinX; }
|
|
||||||
if (maxX > MaxX) { maxX = MaxX; }
|
|
||||||
if (minY < MinY) { minY = MinY; }
|
|
||||||
if (maxY > MaxY) { maxY = MaxY; }
|
|
||||||
|
|
||||||
foreach (var key in Keys(minX, minY, maxX, maxY))
|
|
||||||
{
|
|
||||||
if (hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
foreach (var t in hashDictionary[key])
|
|
||||||
{
|
|
||||||
if (!returned.Contains(t))
|
|
||||||
{
|
|
||||||
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
|
|
||||||
if (((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(box, otherShape.TransformedAABB(otherTransform)))
|
|
||||||
{
|
|
||||||
returned.Add(t);
|
|
||||||
yield return (t, otherShape, otherTransform, collisionGroups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FreeHashSet(returned);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves objects based on a pre-transformed AABB.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="aabb">A transformed AABB.</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(AABB2D aabb, uint collisionMask = uint.MaxValue)
|
|
||||||
{
|
|
||||||
var returned = AcquireHashSet();
|
|
||||||
|
|
||||||
var (minX, minY) = Hash(aabb.Min);
|
|
||||||
var (maxX, maxY) = Hash(aabb.Max);
|
|
||||||
|
|
||||||
if (minX < MinX) { minX = MinX; }
|
|
||||||
if (maxX > MaxX) { maxX = MaxX; }
|
|
||||||
if (minY < MinY) { minY = MinY; }
|
|
||||||
if (maxY > MaxY) { maxY = MaxY; }
|
|
||||||
|
|
||||||
foreach (var key in Keys(minX, minY, maxX, maxY))
|
|
||||||
{
|
|
||||||
if (hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
foreach (var t in hashDictionary[key])
|
|
||||||
{
|
|
||||||
if (!returned.Contains(t))
|
|
||||||
{
|
|
||||||
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
|
|
||||||
if (((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(aabb, otherShape.TransformedAABB(otherTransform)))
|
|
||||||
{
|
|
||||||
yield return (t, otherShape, otherTransform, collisionGroups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FreeHashSet(returned);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Update(T id, ICollidable shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue)
|
|
||||||
{
|
|
||||||
Remove(id);
|
|
||||||
Insert(id, shape, transform2D, collisionGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a specific ID from the SpatialHash.
|
|
||||||
/// </summary>
|
|
||||||
public void Remove(T id)
|
|
||||||
{
|
|
||||||
var (shape, transform, collisionGroups) = IDLookup[id];
|
|
||||||
|
|
||||||
var box = shape.TransformedAABB(transform);
|
|
||||||
var minHash = Hash(box.Min);
|
|
||||||
var maxHash = Hash(box.Max);
|
|
||||||
|
|
||||||
foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2))
|
|
||||||
{
|
|
||||||
if (hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
hashDictionary[key].Remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IDLookup.Remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes everything that has been inserted into the SpatialHash.
|
|
||||||
/// </summary>
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
foreach (var hash in hashDictionary.Values)
|
|
||||||
{
|
|
||||||
hash.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
IDLookup.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long MakeLong(int left, int right)
|
|
||||||
{
|
|
||||||
return ((long) left << 32) | ((uint) right);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<long> Keys(int minX, int minY, int maxX, int maxY)
|
|
||||||
{
|
|
||||||
for (var i = minX; i <= maxX; i++)
|
|
||||||
{
|
|
||||||
for (var j = minY; j <= maxY; j++)
|
|
||||||
{
|
|
||||||
yield return MakeLong(i, j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private HashSet<T> AcquireHashSet()
|
|
||||||
{
|
|
||||||
if (hashSetPool.Count == 0)
|
|
||||||
{
|
|
||||||
hashSetPool.Enqueue(new HashSet<T>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var hashSet = hashSetPool.Dequeue();
|
|
||||||
hashSet.Clear();
|
|
||||||
return hashSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FreeHashSet(HashSet<T> hashSet)
|
|
||||||
{
|
|
||||||
hashSetPool.Enqueue(hashSet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,174 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Axis-aligned bounding box.
|
|
||||||
/// </summary>
|
|
||||||
public struct AABB2D : System.IEquatable<AABB2D>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The top-left position of the AABB.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
public Vector2 Min { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The bottom-right position of the AABB.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
public Vector2 Max { get; private set; }
|
|
||||||
|
|
||||||
public float Width { get { return Max.X - Min.X; } }
|
|
||||||
public float Height { get { return Max.Y - Min.Y; } }
|
|
||||||
|
|
||||||
public float Right { get { return Max.X; } }
|
|
||||||
public float Left { get { return Min.X; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The top of the AABB. Assumes a downward-aligned Y axis, so this value will be smaller than Bottom.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
public float Top { get { return Min.Y; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The bottom of the AABB. Assumes a downward-aligned Y axis, so this value will be larger than Top.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
public float Bottom { get { return Max.Y; } }
|
|
||||||
|
|
||||||
public AABB2D(float minX, float minY, float maxX, float maxY)
|
|
||||||
{
|
|
||||||
Min = new Vector2(minX, minY);
|
|
||||||
Max = new Vector2(maxX, maxY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D(Vector2 min, Vector2 max)
|
|
||||||
{
|
|
||||||
Min = min;
|
|
||||||
Max = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Matrix3x2 AbsoluteMatrix(Matrix3x2 matrix)
|
|
||||||
{
|
|
||||||
return new Matrix3x2
|
|
||||||
(
|
|
||||||
System.Math.Abs(matrix.M11), System.Math.Abs(matrix.M12),
|
|
||||||
System.Math.Abs(matrix.M21), System.Math.Abs(matrix.M22),
|
|
||||||
System.Math.Abs(matrix.M31), System.Math.Abs(matrix.M32)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Efficiently transforms the AABB by a Transform2D.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="aabb"></param>
|
|
||||||
/// <param name="transform"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static AABB2D Transformed(AABB2D aabb, Transform2D transform)
|
|
||||||
{
|
|
||||||
var center = (aabb.Min + aabb.Max) / 2f;
|
|
||||||
var extent = (aabb.Max - aabb.Min) / 2f;
|
|
||||||
|
|
||||||
var newCenter = Vector2.Transform(center, transform.TransformMatrix);
|
|
||||||
var newExtent = Vector2.TransformNormal(extent, AbsoluteMatrix(transform.TransformMatrix));
|
|
||||||
|
|
||||||
return new AABB2D(newCenter - newExtent, newCenter + newExtent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D Compose(AABB2D aabb)
|
|
||||||
{
|
|
||||||
float left = Left;
|
|
||||||
float top = Top;
|
|
||||||
float right = Right;
|
|
||||||
float bottom = Bottom;
|
|
||||||
|
|
||||||
if (aabb.Left < left)
|
|
||||||
{
|
|
||||||
left = aabb.Left;
|
|
||||||
}
|
|
||||||
if (aabb.Right > right)
|
|
||||||
{
|
|
||||||
right = aabb.Right;
|
|
||||||
}
|
|
||||||
if (aabb.Top < top)
|
|
||||||
{
|
|
||||||
top = aabb.Top;
|
|
||||||
}
|
|
||||||
if (aabb.Bottom > bottom)
|
|
||||||
{
|
|
||||||
bottom = aabb.Bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AABB2D(left, top, right, bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an AABB for an arbitrary collection of positions.
|
|
||||||
/// This is less efficient than defining a custom AABB method for most shapes, so avoid using this if possible.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="vertices"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static AABB2D FromVertices(IEnumerable<Vector2> vertices)
|
|
||||||
{
|
|
||||||
var minX = float.MaxValue;
|
|
||||||
var minY = float.MaxValue;
|
|
||||||
var maxX = float.MinValue;
|
|
||||||
var maxY = float.MinValue;
|
|
||||||
|
|
||||||
foreach (var vertex in vertices)
|
|
||||||
{
|
|
||||||
if (vertex.X < minX)
|
|
||||||
{
|
|
||||||
minX = vertex.X;
|
|
||||||
}
|
|
||||||
if (vertex.Y < minY)
|
|
||||||
{
|
|
||||||
minY = vertex.Y;
|
|
||||||
}
|
|
||||||
if (vertex.X > maxX)
|
|
||||||
{
|
|
||||||
maxX = vertex.X;
|
|
||||||
}
|
|
||||||
if (vertex.Y > maxY)
|
|
||||||
{
|
|
||||||
maxY = vertex.Y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AABB2D(minX, minY, maxX, maxY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestOverlap(AABB2D a, AABB2D b)
|
|
||||||
{
|
|
||||||
return a.Left < b.Right && a.Right > b.Left && a.Top < b.Bottom && a.Bottom > b.Top;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is AABB2D aabb && Equals(aabb);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(AABB2D other)
|
|
||||||
{
|
|
||||||
return Min == other.Min &&
|
|
||||||
Max == other.Max;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Min, Max);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(AABB2D left, AABB2D right)
|
|
||||||
{
|
|
||||||
return left.Equals(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(AABB2D left, AABB2D right)
|
|
||||||
{
|
|
||||||
return !(left == right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
public interface ICollidable
|
|
||||||
{
|
|
||||||
IEnumerable<IShape2D> Shapes { get; }
|
|
||||||
AABB2D AABB { get; }
|
|
||||||
AABB2D TransformedAABB(Transform2D transform);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
public interface IShape2D : ICollidable, System.IEquatable<IShape2D>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A Minkowski support function. Gives the farthest point on the edge of a shape along the given direction.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="direction">A normalized Vector2.</param>
|
|
||||||
/// <param name="transform">A Transform for transforming the shape vertices.</param>
|
|
||||||
/// <returns>The farthest point on the edge of the shape along the given direction.</returns>
|
|
||||||
Vector2 Support(Vector2 direction, Transform2D transform);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A Minkowski difference between two shapes.
|
|
||||||
/// </summary>
|
|
||||||
public struct MinkowskiDifference : System.IEquatable<MinkowskiDifference>
|
|
||||||
{
|
|
||||||
private IShape2D ShapeA { get; }
|
|
||||||
private Transform2D TransformA { get; }
|
|
||||||
private IShape2D ShapeB { get; }
|
|
||||||
private Transform2D TransformB { get; }
|
|
||||||
|
|
||||||
public MinkowskiDifference(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
ShapeA = shapeA;
|
|
||||||
TransformA = transformA;
|
|
||||||
ShapeB = shapeB;
|
|
||||||
TransformB = transformB;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction)
|
|
||||||
{
|
|
||||||
return ShapeA.Support(direction, TransformA) - ShapeB.Support(-direction, TransformB);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
return other is MinkowskiDifference minkowskiDifference && Equals(minkowskiDifference);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(MinkowskiDifference other)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
ShapeA == other.ShapeA &&
|
|
||||||
TransformA == other.TransformA &&
|
|
||||||
ShapeB == other.ShapeB &&
|
|
||||||
TransformB == other.TransformB;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(ShapeA, TransformA, ShapeB, TransformB);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(MinkowskiDifference a, MinkowskiDifference b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(MinkowskiDifference a, MinkowskiDifference b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,331 +0,0 @@
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
public static class NarrowPhase
|
|
||||||
{
|
|
||||||
private struct Edge
|
|
||||||
{
|
|
||||||
public float Distance;
|
|
||||||
public Vector2 Normal;
|
|
||||||
public int Index;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestCollision(ICollidable collidableA, Transform2D transformA, ICollidable collidableB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
foreach (var shapeA in collidableA.Shapes)
|
|
||||||
{
|
|
||||||
foreach (var shapeB in collidableB.Shapes)
|
|
||||||
{
|
|
||||||
if (TestCollision(shapeA, transformA, shapeB, transformB))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestCollision(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
// If we can use a fast path check, let's do that!
|
|
||||||
if (shapeA is Rectangle rectangleA && shapeB is Rectangle rectangleB && transformA.IsAxisAligned && transformB.IsAxisAligned)
|
|
||||||
{
|
|
||||||
return TestRectangleOverlap(rectangleA, transformA, rectangleB, transformB);
|
|
||||||
}
|
|
||||||
else if (shapeA is Point && shapeB is Rectangle && transformB.IsAxisAligned)
|
|
||||||
{
|
|
||||||
return TestPointRectangleOverlap((Point) shapeA, transformA, (Rectangle) shapeB, transformB);
|
|
||||||
}
|
|
||||||
else if (shapeA is Rectangle && shapeB is Point && transformA.IsAxisAligned)
|
|
||||||
{
|
|
||||||
return TestPointRectangleOverlap((Point) shapeB, transformB, (Rectangle) shapeA, transformA);
|
|
||||||
}
|
|
||||||
else if (shapeA is Rectangle && shapeB is Circle && transformA.IsAxisAligned && transformB.IsUniformScale)
|
|
||||||
{
|
|
||||||
return TestCircleRectangleOverlap((Circle) shapeB, transformB, (Rectangle) shapeA, transformA);
|
|
||||||
}
|
|
||||||
else if (shapeA is Circle && shapeB is Rectangle && transformA.IsUniformScale && transformB.IsAxisAligned)
|
|
||||||
{
|
|
||||||
return TestCircleRectangleOverlap((Circle) shapeA, transformA, (Rectangle) shapeB, transformB);
|
|
||||||
}
|
|
||||||
else if (shapeA is Circle && shapeB is Point && transformA.IsUniformScale)
|
|
||||||
{
|
|
||||||
return TestCirclePointOverlap((Circle) shapeA, transformA, (Point) shapeB, transformB);
|
|
||||||
}
|
|
||||||
else if (shapeA is Point && shapeB is Circle && transformB.IsUniformScale)
|
|
||||||
{
|
|
||||||
return TestCirclePointOverlap((Circle) shapeB, transformB, (Point) shapeA, transformA);
|
|
||||||
}
|
|
||||||
else if (shapeA is Circle circleA && shapeB is Circle circleB && transformA.IsUniformScale && transformB.IsUniformScale)
|
|
||||||
{
|
|
||||||
return TestCircleOverlap(circleA, transformA, circleB, transformB);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sad, we can't do a fast path optimization. Time for a simplex reduction.
|
|
||||||
return FindCollisionSimplex(shapeA, transformA, shapeB, transformB).Item1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestRectangleOverlap(Rectangle rectangleA, Transform2D transformA, Rectangle rectangleB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
var firstAABB = rectangleA.TransformedAABB(transformA);
|
|
||||||
var secondAABB = rectangleB.TransformedAABB(transformB);
|
|
||||||
|
|
||||||
return firstAABB.Left < secondAABB.Right && firstAABB.Right > secondAABB.Left && firstAABB.Top < secondAABB.Bottom && firstAABB.Bottom > secondAABB.Top;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestPointRectangleOverlap(Point point, Transform2D pointTransform, Rectangle rectangle, Transform2D rectangleTransform)
|
|
||||||
{
|
|
||||||
var transformedPoint = pointTransform.Position;
|
|
||||||
var AABB = rectangle.TransformedAABB(rectangleTransform);
|
|
||||||
|
|
||||||
return transformedPoint.X > AABB.Left && transformedPoint.X < AABB.Right && transformedPoint.Y < AABB.Bottom && transformedPoint.Y > AABB.Top;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestCirclePointOverlap(Circle circle, Transform2D circleTransform, Point point, Transform2D pointTransform)
|
|
||||||
{
|
|
||||||
var circleCenter = circleTransform.Position;
|
|
||||||
var circleRadius = circle.Radius * circleTransform.Scale.X;
|
|
||||||
|
|
||||||
var distanceX = circleCenter.X - pointTransform.Position.X;
|
|
||||||
var distanceY = circleCenter.Y - pointTransform.Position.Y;
|
|
||||||
|
|
||||||
return (distanceX * distanceX) + (distanceY * distanceY) < (circleRadius * circleRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// NOTE: The rectangle must be axis aligned, and the scaling of the circle must be uniform.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TestCircleRectangleOverlap(Circle circle, Transform2D circleTransform, Rectangle rectangle, Transform2D rectangleTransform)
|
|
||||||
{
|
|
||||||
var circleCenter = circleTransform.Position;
|
|
||||||
var circleRadius = circle.Radius * circleTransform.Scale.X;
|
|
||||||
var AABB = rectangle.TransformedAABB(rectangleTransform);
|
|
||||||
|
|
||||||
var closestX = Math.MathHelper.Clamp(circleCenter.X, AABB.Left, AABB.Right);
|
|
||||||
var closestY = Math.MathHelper.Clamp(circleCenter.Y, AABB.Top, AABB.Bottom);
|
|
||||||
|
|
||||||
var distanceX = circleCenter.X - closestX;
|
|
||||||
var distanceY = circleCenter.Y - closestY;
|
|
||||||
|
|
||||||
var distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
|
|
||||||
return distanceSquared < (circleRadius * circleRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TestCircleOverlap(Circle circleA, Transform2D transformA, Circle circleB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
var radiusA = circleA.Radius * transformA.Scale.X;
|
|
||||||
var radiusB = circleB.Radius * transformB.Scale.Y;
|
|
||||||
|
|
||||||
var centerA = transformA.Position;
|
|
||||||
var centerB = transformB.Position;
|
|
||||||
|
|
||||||
var distanceSquared = (centerA - centerB).LengthSquared();
|
|
||||||
var radiusSumSquared = (radiusA + radiusB) * (radiusA + radiusB);
|
|
||||||
|
|
||||||
return distanceSquared < radiusSumSquared;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static (bool, Simplex2D) FindCollisionSimplex(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
|
|
||||||
{
|
|
||||||
var minkowskiDifference = new MinkowskiDifference(shapeA, transformA, shapeB, transformB);
|
|
||||||
var c = minkowskiDifference.Support(Vector2.UnitX);
|
|
||||||
var b = minkowskiDifference.Support(-Vector2.UnitX);
|
|
||||||
return Check(minkowskiDifference, c, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe static Vector2 Intersect(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Simplex2D simplex)
|
|
||||||
{
|
|
||||||
if (shapeA == null) { throw new System.ArgumentNullException(nameof(shapeA)); }
|
|
||||||
if (shapeB == null) { throw new System.ArgumentNullException(nameof(shapeB)); }
|
|
||||||
if (!simplex.TwoSimplex) { throw new System.ArgumentException("Simplex must be a 2-Simplex.", nameof(simplex)); }
|
|
||||||
|
|
||||||
var a = simplex.A;
|
|
||||||
var b = simplex.B.Value;
|
|
||||||
var c = simplex.C.Value;
|
|
||||||
|
|
||||||
Vector2 intersection = default;
|
|
||||||
|
|
||||||
for (var i = 0; i < 32; i++)
|
|
||||||
{
|
|
||||||
var edge = FindClosestEdge(simplex);
|
|
||||||
var support = CalculateSupport(shapeA, Transform2DA, shapeB, Transform2DB, edge.Normal);
|
|
||||||
var distance = Vector2.Dot(support, edge.Normal);
|
|
||||||
|
|
||||||
intersection = edge.Normal;
|
|
||||||
intersection *= distance;
|
|
||||||
|
|
||||||
if (System.Math.Abs(distance - edge.Distance) <= 0.00001f)
|
|
||||||
{
|
|
||||||
return intersection;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
simplex.Insert(support, edge.Index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return intersection; // close enough
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe Edge FindClosestEdge(Simplex2D simplex)
|
|
||||||
{
|
|
||||||
var closestDistance = float.PositiveInfinity;
|
|
||||||
var closestNormal = Vector2.Zero;
|
|
||||||
var closestIndex = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < 4; i += 1)
|
|
||||||
{
|
|
||||||
var j = (i + 1 == 3) ? 0 : i + 1;
|
|
||||||
|
|
||||||
var a = simplex[i];
|
|
||||||
var b = simplex[j];
|
|
||||||
|
|
||||||
var e = b - a;
|
|
||||||
|
|
||||||
var oa = a;
|
|
||||||
|
|
||||||
var n = Vector2.Normalize(TripleProduct(e, oa, e));
|
|
||||||
|
|
||||||
var d = Vector2.Dot(n, a);
|
|
||||||
|
|
||||||
if (d < closestDistance)
|
|
||||||
{
|
|
||||||
closestDistance = d;
|
|
||||||
closestNormal = n;
|
|
||||||
closestIndex = j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Edge
|
|
||||||
{
|
|
||||||
Distance = closestDistance,
|
|
||||||
Normal = closestNormal,
|
|
||||||
Index = closestIndex
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector2 CalculateSupport(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Vector2 direction)
|
|
||||||
{
|
|
||||||
return shapeA.Support(direction, Transform2DA) - shapeB.Support(-direction, Transform2DB);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D) Check(MinkowskiDifference minkowskiDifference, Vector2 c, Vector2 b)
|
|
||||||
{
|
|
||||||
var cb = c - b;
|
|
||||||
var c0 = -c;
|
|
||||||
var d = Direction(cb, c0);
|
|
||||||
return DoSimplex(minkowskiDifference, new Simplex2D(b, c), d);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D) DoSimplex(MinkowskiDifference minkowskiDifference, Simplex2D simplex, Vector2 direction)
|
|
||||||
{
|
|
||||||
var a = minkowskiDifference.Support(direction);
|
|
||||||
var notPastOrigin = Vector2.Dot(a, direction) < 0;
|
|
||||||
var (intersects, newSimplex, newDirection) = EnclosesOrigin(a, simplex);
|
|
||||||
|
|
||||||
if (notPastOrigin)
|
|
||||||
{
|
|
||||||
return (false, default(Simplex2D));
|
|
||||||
}
|
|
||||||
else if (intersects)
|
|
||||||
{
|
|
||||||
return (true, new Simplex2D(simplex.A, simplex.B.Value, a));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return DoSimplex(minkowskiDifference, newSimplex, newDirection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D, Vector2) EnclosesOrigin(Vector2 a, Simplex2D simplex)
|
|
||||||
{
|
|
||||||
if (simplex.ZeroSimplex)
|
|
||||||
{
|
|
||||||
return HandleZeroSimplex(a, simplex.A);
|
|
||||||
}
|
|
||||||
else if (simplex.OneSimplex)
|
|
||||||
{
|
|
||||||
return HandleOneSimplex(a, simplex.A, simplex.B.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (false, simplex, Vector2.Zero);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D, Vector2) HandleZeroSimplex(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
var ab = b - a;
|
|
||||||
var a0 = -a;
|
|
||||||
var (newSimplex, newDirection) = SameDirection(ab, a0) ? (new Simplex2D(a, b), Perpendicular(ab, a0)) : (new Simplex2D(a), a0);
|
|
||||||
return (false, newSimplex, newDirection);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool, Simplex2D, Vector2) HandleOneSimplex(Vector2 a, Vector2 b, Vector2 c)
|
|
||||||
{
|
|
||||||
var a0 = -a;
|
|
||||||
var ab = b - a;
|
|
||||||
var ac = c - a;
|
|
||||||
var abp = Perpendicular(ab, -ac);
|
|
||||||
var acp = Perpendicular(ac, -ab);
|
|
||||||
|
|
||||||
if (SameDirection(abp, a0))
|
|
||||||
{
|
|
||||||
if (SameDirection(ab, a0))
|
|
||||||
{
|
|
||||||
return (false, new Simplex2D(a, b), abp);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (false, new Simplex2D(a), a0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (SameDirection(acp, a0))
|
|
||||||
{
|
|
||||||
if (SameDirection(ac, a0))
|
|
||||||
{
|
|
||||||
return (false, new Simplex2D(a, c), acp);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (false, new Simplex2D(a), a0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (true, new Simplex2D(b, c), a0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector2 TripleProduct(Vector2 a, Vector2 b, Vector2 c)
|
|
||||||
{
|
|
||||||
var A = new Vector3(a.X, a.Y, 0);
|
|
||||||
var B = new Vector3(b.X, b.Y, 0);
|
|
||||||
var C = new Vector3(c.X, c.Y, 0);
|
|
||||||
|
|
||||||
var first = Vector3.Cross(A, B);
|
|
||||||
var second = Vector3.Cross(first, C);
|
|
||||||
|
|
||||||
return new Vector2(second.X, second.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector2 Direction(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
var d = TripleProduct(a, b, a);
|
|
||||||
var collinear = d == Vector2.Zero;
|
|
||||||
return collinear ? new Vector2(a.Y, -a.X) : d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool SameDirection(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
return Vector2.Dot(a, b) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector2 Perpendicular(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
return TripleProduct(a, b, a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A Circle is a shape defined by a radius.
|
|
||||||
/// </summary>
|
|
||||||
public struct Circle : IShape2D, System.IEquatable<Circle>
|
|
||||||
{
|
|
||||||
public float Radius { get; }
|
|
||||||
public AABB2D AABB { get; }
|
|
||||||
public IEnumerable<IShape2D> Shapes
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Circle(float radius)
|
|
||||||
{
|
|
||||||
Radius = radius;
|
|
||||||
AABB = new AABB2D(-Radius, -Radius, Radius, Radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
return Vector2.Transform(Vector2.Normalize(direction) * Radius, transform.TransformMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D TransformedAABB(Transform2D transform2D)
|
|
||||||
{
|
|
||||||
return AABB2D.Transformed(AABB, transform2D);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is IShape2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(IShape2D other)
|
|
||||||
{
|
|
||||||
return other is Circle circle && Equals(circle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Circle other)
|
|
||||||
{
|
|
||||||
return Radius == other.Radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Circle a, Circle b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Circle a, Circle b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A line is a shape defined by exactly two points in space.
|
|
||||||
/// </summary>
|
|
||||||
public struct Line : IShape2D, System.IEquatable<Line>
|
|
||||||
{
|
|
||||||
public Vector2 Start { get; }
|
|
||||||
public Vector2 End { get; }
|
|
||||||
|
|
||||||
public AABB2D AABB { get; }
|
|
||||||
|
|
||||||
public IEnumerable<IShape2D> Shapes
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Line(Vector2 start, Vector2 end)
|
|
||||||
{
|
|
||||||
Start = start;
|
|
||||||
End = end;
|
|
||||||
|
|
||||||
AABB = new AABB2D(
|
|
||||||
System.Math.Min(Start.X, End.X),
|
|
||||||
System.Math.Min(Start.Y, End.Y),
|
|
||||||
System.Math.Max(Start.X, End.X),
|
|
||||||
System.Math.Max(Start.Y, End.Y)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
var transformedStart = Vector2.Transform(Start, transform.TransformMatrix);
|
|
||||||
var transformedEnd = Vector2.Transform(End, transform.TransformMatrix);
|
|
||||||
return Vector2.Dot(transformedStart, direction) > Vector2.Dot(transformedEnd, direction) ?
|
|
||||||
transformedStart :
|
|
||||||
transformedEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D TransformedAABB(Transform2D transform)
|
|
||||||
{
|
|
||||||
return AABB2D.Transformed(AABB, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is IShape2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(IShape2D other)
|
|
||||||
{
|
|
||||||
return other is Line otherLine && Equals(otherLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Line other)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
(Start == other.Start && End == other.End) ||
|
|
||||||
(End == other.Start && Start == other.End);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Start, End);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Line a, Line b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Line a, Line b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A Point is "that which has no part".
|
|
||||||
/// All points by themselves are identical.
|
|
||||||
/// </summary>
|
|
||||||
public struct Point : IShape2D, System.IEquatable<Point>
|
|
||||||
{
|
|
||||||
public AABB2D AABB { get; }
|
|
||||||
public IEnumerable<IShape2D> Shapes
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D TransformedAABB(Transform2D transform)
|
|
||||||
{
|
|
||||||
return AABB2D.Transformed(AABB, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
return Vector2.Transform(Vector2.Zero, transform.TransformMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is IShape2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(IShape2D other)
|
|
||||||
{
|
|
||||||
return other is Point otherPoint && Equals(otherPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Point other)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Point a, Point b)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Point a, Point b)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A rectangle is a shape defined by a width and height. The origin is the center of the rectangle.
|
|
||||||
/// </summary>
|
|
||||||
public struct Rectangle : IShape2D, System.IEquatable<Rectangle>
|
|
||||||
{
|
|
||||||
public AABB2D AABB { get; }
|
|
||||||
public float Width { get; }
|
|
||||||
public float Height { get; }
|
|
||||||
|
|
||||||
public float Right { get; }
|
|
||||||
public float Left { get; }
|
|
||||||
public float Top { get; }
|
|
||||||
public float Bottom { get; }
|
|
||||||
public Vector2 TopLeft { get; }
|
|
||||||
public Vector2 BottomRight { get; }
|
|
||||||
|
|
||||||
public Vector2 Min { get; }
|
|
||||||
public Vector2 Max { get; }
|
|
||||||
|
|
||||||
public IEnumerable<IShape2D> Shapes
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Rectangle(float left, float top, float width, float height)
|
|
||||||
{
|
|
||||||
Width = width;
|
|
||||||
Height = height;
|
|
||||||
Left = left;
|
|
||||||
Right = left + width;
|
|
||||||
Top = top;
|
|
||||||
Bottom = top + height;
|
|
||||||
AABB = new AABB2D(left, top, Right, Bottom);
|
|
||||||
TopLeft = new Vector2(Left, Top);
|
|
||||||
BottomRight = new Vector2(Right, Bottom);
|
|
||||||
Min = AABB.Min;
|
|
||||||
Max = AABB.Max;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector2 Support(Vector2 direction)
|
|
||||||
{
|
|
||||||
if (direction.X >= 0 && direction.Y >= 0)
|
|
||||||
{
|
|
||||||
return Max;
|
|
||||||
}
|
|
||||||
else if (direction.X >= 0 && direction.Y < 0)
|
|
||||||
{
|
|
||||||
return new Vector2(Max.X, Min.Y);
|
|
||||||
}
|
|
||||||
else if (direction.X < 0 && direction.Y >= 0)
|
|
||||||
{
|
|
||||||
return new Vector2(Min.X, Max.Y);
|
|
||||||
}
|
|
||||||
else if (direction.X < 0 && direction.Y < 0)
|
|
||||||
{
|
|
||||||
return new Vector2(Min.X, Min.Y);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new System.ArgumentException("Support vector direction cannot be zero.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
Matrix3x2 inverseTransform;
|
|
||||||
Matrix3x2.Invert(transform.TransformMatrix, out inverseTransform);
|
|
||||||
var inverseDirection = Vector2.TransformNormal(direction, inverseTransform);
|
|
||||||
return Vector2.Transform(Support(inverseDirection), transform.TransformMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AABB2D TransformedAABB(Transform2D transform)
|
|
||||||
{
|
|
||||||
return AABB2D.Transformed(AABB, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is IShape2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(IShape2D other)
|
|
||||||
{
|
|
||||||
return (other is Rectangle rectangle && Equals(rectangle));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Rectangle other)
|
|
||||||
{
|
|
||||||
return Min == other.Min && Max == other.Max;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Min, Max);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Rectangle a, Rectangle b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Rectangle a, Rectangle b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A simplex is a shape with up to n - 2 vertices in the nth dimension.
|
|
||||||
/// </summary>
|
|
||||||
public struct Simplex2D : System.IEquatable<Simplex2D>
|
|
||||||
{
|
|
||||||
private Vector2 a;
|
|
||||||
private Vector2? b;
|
|
||||||
private Vector2? c;
|
|
||||||
|
|
||||||
public Vector2 A => a;
|
|
||||||
public Vector2? B => b;
|
|
||||||
public Vector2? C => c;
|
|
||||||
|
|
||||||
public bool ZeroSimplex { get { return !b.HasValue && !c.HasValue; } }
|
|
||||||
public bool OneSimplex { get { return b.HasValue && !c.HasValue; } }
|
|
||||||
public bool TwoSimplex { get { return b.HasValue && c.HasValue; } }
|
|
||||||
|
|
||||||
public int Count => TwoSimplex ? 3 : (OneSimplex ? 2 : 1);
|
|
||||||
|
|
||||||
public Simplex2D(Vector2 a)
|
|
||||||
{
|
|
||||||
this.a = a;
|
|
||||||
b = null;
|
|
||||||
c = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Simplex2D(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
this.a = a;
|
|
||||||
this.b = b;
|
|
||||||
c = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Simplex2D(Vector2 a, Vector2 b, Vector2 c)
|
|
||||||
{
|
|
||||||
this.a = a;
|
|
||||||
this.b = b;
|
|
||||||
this.c = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (index == 0) { return a; }
|
|
||||||
if (index == 1) { return b.Value; }
|
|
||||||
if (index == 2) { return c.Value; }
|
|
||||||
throw new System.IndexOutOfRangeException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Vector2> Vertices
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return (Vector2) a;
|
|
||||||
if (b.HasValue) { yield return (Vector2) b; }
|
|
||||||
if (c.HasValue) { yield return (Vector2) c; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2 Support(Vector2 direction, Transform2D transform)
|
|
||||||
{
|
|
||||||
var maxDotProduct = float.NegativeInfinity;
|
|
||||||
var maxVertex = a;
|
|
||||||
foreach (var vertex in Vertices)
|
|
||||||
{
|
|
||||||
var transformed = Vector2.Transform(vertex, transform.TransformMatrix);
|
|
||||||
var dot = Vector2.Dot(transformed, direction);
|
|
||||||
if (dot > maxDotProduct)
|
|
||||||
{
|
|
||||||
maxVertex = transformed;
|
|
||||||
maxDotProduct = dot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return maxVertex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Insert(Vector2 point, int index)
|
|
||||||
{
|
|
||||||
if (index == 0)
|
|
||||||
{
|
|
||||||
c = b;
|
|
||||||
b = a;
|
|
||||||
a = point;
|
|
||||||
}
|
|
||||||
else if (index == 1)
|
|
||||||
{
|
|
||||||
c = b;
|
|
||||||
b = point;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
c = point;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is Simplex2D other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Simplex2D other)
|
|
||||||
{
|
|
||||||
if (Count != other.Count) { return false; }
|
|
||||||
|
|
||||||
return
|
|
||||||
(A == other.A && B == other.B && C == other.C) ||
|
|
||||||
(A == other.A && B == other.C && C == other.B) ||
|
|
||||||
(A == other.B && B == other.A && C == other.C) ||
|
|
||||||
(A == other.B && B == other.C && C == other.A) ||
|
|
||||||
(A == other.C && B == other.A && C == other.B) ||
|
|
||||||
(A == other.C && B == other.B && C == other.A);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Vertices);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Simplex2D a, Simplex2D b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Simplex2D a, Simplex2D b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,253 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MoonWorks.Math.Float;
|
|
||||||
|
|
||||||
namespace MoonWorks.Collision.Float
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Used to quickly check if two shapes are potentially overlapping.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type that will be used to uniquely identify shape-transform pairs.</typeparam>
|
|
||||||
public class SpatialHash2D<T> where T : System.IEquatable<T>
|
|
||||||
{
|
|
||||||
private readonly int cellSize;
|
|
||||||
|
|
||||||
private readonly Dictionary<long, HashSet<T>> hashDictionary = new Dictionary<long, HashSet<T>>();
|
|
||||||
// FIXME: this ICollidable causes boxing which triggers garbage collection
|
|
||||||
private readonly Dictionary<T, (ICollidable, Transform2D, uint)> IDLookup = new Dictionary<T, (ICollidable, Transform2D, uint)>();
|
|
||||||
|
|
||||||
public int MinX { get; private set; } = 0;
|
|
||||||
public int MaxX { get; private set; } = 0;
|
|
||||||
public int MinY { get; private set; } = 0;
|
|
||||||
public int MaxY { get; private set; } = 0;
|
|
||||||
|
|
||||||
private Queue<HashSet<T>> hashSetPool = new Queue<HashSet<T>>();
|
|
||||||
|
|
||||||
public SpatialHash2D(int cellSize)
|
|
||||||
{
|
|
||||||
this.cellSize = cellSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
private (int, int) Hash(Vector2 position)
|
|
||||||
{
|
|
||||||
return ((int) System.Math.Floor(position.X / cellSize), (int) System.Math.Floor(position.Y / cellSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Inserts an element into the SpatialHash.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">A unique ID for the shape-transform pair.</param>
|
|
||||||
/// <param name="shape"></param>
|
|
||||||
/// <param name="transform2D"></param>
|
|
||||||
/// <param name="collisionGroups">A bitmask value specifying the groups this object belongs to.</param>
|
|
||||||
public void Insert(T id, ICollidable shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue)
|
|
||||||
{
|
|
||||||
var box = shape.TransformedAABB(transform2D);
|
|
||||||
var minHash = Hash(box.Min);
|
|
||||||
var maxHash = Hash(box.Max);
|
|
||||||
|
|
||||||
foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2))
|
|
||||||
{
|
|
||||||
if (!hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
hashDictionary.Add(key, new HashSet<T>());
|
|
||||||
}
|
|
||||||
|
|
||||||
hashDictionary[key].Add(id);
|
|
||||||
IDLookup[id] = (shape, transform2D, collisionGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
MinX = System.Math.Min(MinX, minHash.Item1);
|
|
||||||
MinY = System.Math.Min(MinY, minHash.Item2);
|
|
||||||
MaxX = System.Math.Max(MaxX, maxHash.Item1);
|
|
||||||
MaxY = System.Math.Max(MaxY, maxHash.Item2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves all the potential collisions of a shape-transform pair. Excludes any shape-transforms with the given ID.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(T id, ICollidable shape, Transform2D transform2D, uint collisionMask = uint.MaxValue)
|
|
||||||
{
|
|
||||||
var returned = AcquireHashSet();
|
|
||||||
|
|
||||||
var box = shape.TransformedAABB(transform2D);
|
|
||||||
var (minX, minY) = Hash(box.Min);
|
|
||||||
var (maxX, maxY) = Hash(box.Max);
|
|
||||||
|
|
||||||
if (minX < MinX) { minX = MinX; }
|
|
||||||
if (maxX > MaxX) { maxX = MaxX; }
|
|
||||||
if (minY < MinY) { minY = MinY; }
|
|
||||||
if (maxY > MaxY) { maxY = MaxY; }
|
|
||||||
|
|
||||||
foreach (var key in Keys(minX, minY, maxX, maxY))
|
|
||||||
{
|
|
||||||
if (hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
foreach (var t in hashDictionary[key])
|
|
||||||
{
|
|
||||||
if (!returned.Contains(t))
|
|
||||||
{
|
|
||||||
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
|
|
||||||
if (!id.Equals(t) && ((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(box, otherShape.TransformedAABB(otherTransform)))
|
|
||||||
{
|
|
||||||
returned.Add(t);
|
|
||||||
yield return (t, otherShape, otherTransform, collisionGroups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FreeHashSet(returned);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves all the potential collisions of a shape-transform pair.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(ICollidable shape, Transform2D transform2D, uint collisionMask = uint.MaxValue)
|
|
||||||
{
|
|
||||||
var returned = AcquireHashSet();
|
|
||||||
|
|
||||||
var box = shape.TransformedAABB(transform2D);
|
|
||||||
var (minX, minY) = Hash(box.Min);
|
|
||||||
var (maxX, maxY) = Hash(box.Max);
|
|
||||||
|
|
||||||
if (minX < MinX) { minX = MinX; }
|
|
||||||
if (maxX > MaxX) { maxX = MaxX; }
|
|
||||||
if (minY < MinY) { minY = MinY; }
|
|
||||||
if (maxY > MaxY) { maxY = MaxY; }
|
|
||||||
|
|
||||||
foreach (var key in Keys(minX, minY, maxX, maxY))
|
|
||||||
{
|
|
||||||
if (hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
foreach (var t in hashDictionary[key])
|
|
||||||
{
|
|
||||||
if (!returned.Contains(t))
|
|
||||||
{
|
|
||||||
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
|
|
||||||
if (((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(box, otherShape.TransformedAABB(otherTransform)))
|
|
||||||
{
|
|
||||||
returned.Add(t);
|
|
||||||
yield return (t, otherShape, otherTransform, collisionGroups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FreeHashSet(returned);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves objects based on a pre-transformed AABB.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="aabb">A transformed AABB.</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(AABB2D aabb, uint collisionMask = uint.MaxValue)
|
|
||||||
{
|
|
||||||
var returned = AcquireHashSet();
|
|
||||||
|
|
||||||
var (minX, minY) = Hash(aabb.Min);
|
|
||||||
var (maxX, maxY) = Hash(aabb.Max);
|
|
||||||
|
|
||||||
if (minX < MinX) { minX = MinX; }
|
|
||||||
if (maxX > MaxX) { maxX = MaxX; }
|
|
||||||
if (minY < MinY) { minY = MinY; }
|
|
||||||
if (maxY > MaxY) { maxY = MaxY; }
|
|
||||||
|
|
||||||
foreach (var key in Keys(minX, minY, maxX, maxY))
|
|
||||||
{
|
|
||||||
if (hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
foreach (var t in hashDictionary[key])
|
|
||||||
{
|
|
||||||
if (!returned.Contains(t))
|
|
||||||
{
|
|
||||||
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
|
|
||||||
if (((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(aabb, otherShape.TransformedAABB(otherTransform)))
|
|
||||||
{
|
|
||||||
yield return (t, otherShape, otherTransform, collisionGroups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FreeHashSet(returned);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Update(T id, ICollidable shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue)
|
|
||||||
{
|
|
||||||
Remove(id);
|
|
||||||
Insert(id, shape, transform2D, collisionGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a specific ID from the SpatialHash.
|
|
||||||
/// </summary>
|
|
||||||
public void Remove(T id)
|
|
||||||
{
|
|
||||||
var (shape, transform, collisionGroups) = IDLookup[id];
|
|
||||||
|
|
||||||
var box = shape.TransformedAABB(transform);
|
|
||||||
var minHash = Hash(box.Min);
|
|
||||||
var maxHash = Hash(box.Max);
|
|
||||||
|
|
||||||
foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2))
|
|
||||||
{
|
|
||||||
if (hashDictionary.ContainsKey(key))
|
|
||||||
{
|
|
||||||
hashDictionary[key].Remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IDLookup.Remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes everything that has been inserted into the SpatialHash.
|
|
||||||
/// </summary>
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
foreach (var hash in hashDictionary.Values)
|
|
||||||
{
|
|
||||||
hash.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
IDLookup.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long MakeLong(int left, int right)
|
|
||||||
{
|
|
||||||
return ((long) left << 32) | ((uint) right);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<long> Keys(int minX, int minY, int maxX, int maxY)
|
|
||||||
{
|
|
||||||
for (var i = minX; i <= maxX; i++)
|
|
||||||
{
|
|
||||||
for (var j = minY; j <= maxY; j++)
|
|
||||||
{
|
|
||||||
yield return MakeLong(i, j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private HashSet<T> AcquireHashSet()
|
|
||||||
{
|
|
||||||
if (hashSetPool.Count == 0)
|
|
||||||
{
|
|
||||||
hashSetPool.Enqueue(new HashSet<T>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var hashSet = hashSetPool.Dequeue();
|
|
||||||
hashSet.Clear();
|
|
||||||
return hashSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FreeHashSet(HashSet<T> hashSet)
|
|
||||||
{
|
|
||||||
hashSetPool.Enqueue(hashSet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,13 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using MoonWorks.Graphics;
|
using MoonWorks.Graphics;
|
||||||
|
using MoonWorks.Graphics.PackedVector;
|
||||||
|
|
||||||
namespace MoonWorks
|
namespace MoonWorks
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Conversion utilities for interop.
|
||||||
|
/// </summary>
|
||||||
public static class Conversions
|
public static class Conversions
|
||||||
{
|
{
|
||||||
private readonly static Dictionary<VertexElementFormat, uint> Sizes = new Dictionary<VertexElementFormat, uint>
|
private readonly static Dictionary<VertexElementFormat, uint> Sizes = new Dictionary<VertexElementFormat, uint>
|
|
@ -1,12 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MoonWorks
|
|
||||||
{
|
|
||||||
public class AudioLoadException : Exception
|
|
||||||
{
|
|
||||||
public AudioLoadException(string message) : base(message)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,13 +2,27 @@ namespace MoonWorks
|
||||||
{
|
{
|
||||||
public enum FrameLimiterMode
|
public enum FrameLimiterMode
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The game will render at the maximum possible framerate that the computing resources allow. <br/>
|
||||||
|
/// Note that this may lead to overheating, resource starvation, etc.
|
||||||
|
/// </summary>
|
||||||
Uncapped,
|
Uncapped,
|
||||||
|
/// <summary>
|
||||||
|
/// The game will render no more than the specified frames per second.
|
||||||
|
/// </summary>
|
||||||
Capped
|
Capped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Game's frame limiter setting. Specifies uncapped framerate or a maximum rendering frames per second value. <br/>
|
||||||
|
/// Note that this is separate from the Game's Update timestep and can be a different value.
|
||||||
|
/// </summary>
|
||||||
public struct FrameLimiterSettings
|
public struct FrameLimiterSettings
|
||||||
{
|
{
|
||||||
public FrameLimiterMode Mode;
|
public FrameLimiterMode Mode;
|
||||||
|
/// <summary>
|
||||||
|
/// If Mode is set to Capped, this is the maximum frames per second that will be rendered.
|
||||||
|
/// </summary>
|
||||||
public int Cap;
|
public int Cap;
|
||||||
|
|
||||||
public FrameLimiterSettings(
|
public FrameLimiterSettings(
|
||||||
|
|
99
src/Game.cs
99
src/Game.cs
|
@ -1,5 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using SDL2;
|
||||||
using SDL2;
|
|
||||||
using MoonWorks.Audio;
|
using MoonWorks.Audio;
|
||||||
using MoonWorks.Graphics;
|
using MoonWorks.Graphics;
|
||||||
using MoonWorks.Input;
|
using MoonWorks.Input;
|
||||||
|
@ -9,6 +8,12 @@ using System.Diagnostics;
|
||||||
|
|
||||||
namespace MoonWorks
|
namespace MoonWorks
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This class is your entry point into controlling your game. <br/>
|
||||||
|
/// It manages the main game loop and subsystems. <br/>
|
||||||
|
/// You should inherit this class and implement Update and Draw methods. <br/>
|
||||||
|
/// Then instantiate your Game subclass from your Program.Main method and call the Run method.
|
||||||
|
/// </summary>
|
||||||
public abstract class Game
|
public abstract class Game
|
||||||
{
|
{
|
||||||
public TimeSpan MAX_DELTA_TIME = TimeSpan.FromMilliseconds(100);
|
public TimeSpan MAX_DELTA_TIME = TimeSpan.FromMilliseconds(100);
|
||||||
|
@ -33,8 +38,18 @@ namespace MoonWorks
|
||||||
public AudioDevice AudioDevice { get; }
|
public AudioDevice AudioDevice { get; }
|
||||||
public Inputs Inputs { get; }
|
public Inputs Inputs { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This Window is automatically created when your Game is instantiated.
|
||||||
|
/// </summary>
|
||||||
public Window MainWindow { get; }
|
public Window MainWindow { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates your Game.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="windowCreateInfo">The parameters that will be used to create the MainWindow.</param>
|
||||||
|
/// <param name="frameLimiterSettings">The frame limiter settings.</param>
|
||||||
|
/// <param name="targetTimestep">How often Game.Update will run in terms of ticks per second.</param>
|
||||||
|
/// <param name="debugMode">If true, enables extra debug checks. Should be turned off for release builds.</param>
|
||||||
public Game(
|
public Game(
|
||||||
WindowCreateInfo windowCreateInfo,
|
WindowCreateInfo windowCreateInfo,
|
||||||
FrameLimiterSettings frameLimiterSettings,
|
FrameLimiterSettings frameLimiterSettings,
|
||||||
|
@ -42,6 +57,7 @@ namespace MoonWorks
|
||||||
bool debugMode = false
|
bool debugMode = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
Logger.LogInfo("Initializing frame limiter...");
|
||||||
Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep);
|
Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep);
|
||||||
gameTimer = Stopwatch.StartNew();
|
gameTimer = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
@ -52,21 +68,25 @@ namespace MoonWorks
|
||||||
previousSleepTimes[i] = TimeSpan.FromMilliseconds(1);
|
previousSleepTimes[i] = TimeSpan.FromMilliseconds(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo("Initializing SDL...");
|
||||||
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0)
|
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine("Failed to initialize SDL!");
|
Logger.LogError("Failed to initialize SDL!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Initialize();
|
Logger.Initialize();
|
||||||
|
|
||||||
|
Logger.LogInfo("Initializing input...");
|
||||||
Inputs = new Inputs();
|
Inputs = new Inputs();
|
||||||
|
|
||||||
|
Logger.LogInfo("Initializing graphics device...");
|
||||||
GraphicsDevice = new GraphicsDevice(
|
GraphicsDevice = new GraphicsDevice(
|
||||||
Backend.Vulkan,
|
Backend.Vulkan,
|
||||||
debugMode
|
debugMode
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Logger.LogInfo("Initializing main window...");
|
||||||
MainWindow = new Window(windowCreateInfo, GraphicsDevice.WindowFlags | SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN);
|
MainWindow = new Window(windowCreateInfo, GraphicsDevice.WindowFlags | SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN);
|
||||||
|
|
||||||
if (!GraphicsDevice.ClaimWindow(MainWindow, windowCreateInfo.PresentMode))
|
if (!GraphicsDevice.ClaimWindow(MainWindow, windowCreateInfo.PresentMode))
|
||||||
|
@ -74,9 +94,13 @@ namespace MoonWorks
|
||||||
throw new System.SystemException("Could not claim window!");
|
throw new System.SystemException("Could not claim window!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo("Initializing audio thread...");
|
||||||
AudioDevice = new AudioDevice();
|
AudioDevice = new AudioDevice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initiates the main game loop. Call this once from your Program.Main method.
|
||||||
|
/// </summary>
|
||||||
public void Run()
|
public void Run()
|
||||||
{
|
{
|
||||||
MainWindow.Show();
|
MainWindow.Show();
|
||||||
|
@ -86,15 +110,29 @@ namespace MoonWorks
|
||||||
Tick();
|
Tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo("Starting shutdown sequence...");
|
||||||
|
|
||||||
|
Logger.LogInfo("Cleaning up game...");
|
||||||
Destroy();
|
Destroy();
|
||||||
|
|
||||||
AudioDevice.Dispose();
|
Logger.LogInfo("Unclaiming window...");
|
||||||
|
GraphicsDevice.UnclaimWindow(MainWindow);
|
||||||
|
|
||||||
|
Logger.LogInfo("Disposing window...");
|
||||||
MainWindow.Dispose();
|
MainWindow.Dispose();
|
||||||
|
|
||||||
|
Logger.LogInfo("Disposing graphics device...");
|
||||||
GraphicsDevice.Dispose();
|
GraphicsDevice.Dispose();
|
||||||
|
|
||||||
|
Logger.LogInfo("Closing audio thread...");
|
||||||
|
AudioDevice.Dispose();
|
||||||
|
|
||||||
SDL.SDL_Quit();
|
SDL.SDL_Quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the frame limiter settings.
|
||||||
|
/// </summary>
|
||||||
public void SetFrameLimiter(FrameLimiterSettings settings)
|
public void SetFrameLimiter(FrameLimiterSettings settings)
|
||||||
{
|
{
|
||||||
FramerateCapped = settings.Mode == FrameLimiterMode.Capped;
|
FramerateCapped = settings.Mode == FrameLimiterMode.Capped;
|
||||||
|
@ -109,18 +147,42 @@ namespace MoonWorks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the game shutdown process.
|
||||||
|
/// </summary>
|
||||||
|
public void Quit()
|
||||||
|
{
|
||||||
|
quit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Will execute at the specified targetTimestep you provided when instantiating your Game class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delta"></param>
|
||||||
protected abstract void Update(TimeSpan delta);
|
protected abstract void Update(TimeSpan delta);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If the frame limiter mode is Capped, this will run at most Cap times per second. <br />
|
||||||
|
/// Otherwise it will run as many times as possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="alpha">A value from 0-1 describing how "in-between" update ticks it is called. Useful for interpolation.</param>
|
||||||
protected abstract void Draw(double alpha);
|
protected abstract void Draw(double alpha);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// You can optionally override this to perform cleanup tasks before the game quits.
|
||||||
|
/// </summary>
|
||||||
protected virtual void Destroy() {}
|
protected virtual void Destroy() {}
|
||||||
|
|
||||||
// Called when a file is dropped on the game window.
|
/// <summary>
|
||||||
|
/// Called when a file is dropped on the game window.
|
||||||
|
/// </summary>
|
||||||
protected virtual void DropFile(string filePath) {}
|
protected virtual void DropFile(string filePath) {}
|
||||||
|
|
||||||
/* Required to distinguish between multiple files dropped at once
|
/// <summary>
|
||||||
* vs multiple files dropped one at a time.
|
/// Required to distinguish between multiple files dropped at once
|
||||||
*
|
/// vs multiple files dropped one at a time.
|
||||||
* Called once for every multi-file drop.
|
/// Called once for every multi-file drop.
|
||||||
*/
|
/// </summary>
|
||||||
protected virtual void DropBegin() {}
|
protected virtual void DropBegin() {}
|
||||||
protected virtual void DropComplete() {}
|
protected virtual void DropComplete() {}
|
||||||
|
|
||||||
|
@ -168,9 +230,8 @@ namespace MoonWorks
|
||||||
while (accumulatedUpdateTime >= Timestep)
|
while (accumulatedUpdateTime >= Timestep)
|
||||||
{
|
{
|
||||||
Inputs.Update();
|
Inputs.Update();
|
||||||
AudioDevice.Update();
|
|
||||||
|
|
||||||
Update(Timestep);
|
Update(Timestep);
|
||||||
|
AudioDevice.WakeThread();
|
||||||
|
|
||||||
accumulatedUpdateTime -= Timestep;
|
accumulatedUpdateTime -= Timestep;
|
||||||
}
|
}
|
||||||
|
@ -282,17 +343,27 @@ namespace MoonWorks
|
||||||
var index = evt.cdevice.which;
|
var index = evt.cdevice.which;
|
||||||
if (SDL.SDL_IsGameController(index) == SDL.SDL_bool.SDL_TRUE)
|
if (SDL.SDL_IsGameController(index) == SDL.SDL_bool.SDL_TRUE)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine($"New controller detected!");
|
Logger.LogInfo("New controller detected!");
|
||||||
Inputs.AddGamepad(index);
|
Inputs.AddGamepad(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleControllerRemoved(SDL.SDL_Event evt)
|
private void HandleControllerRemoved(SDL.SDL_Event evt)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine($"Controller removal detected!");
|
Logger.LogInfo("Controller removal detected!");
|
||||||
Inputs.RemoveGamepad(evt.cdevice.which);
|
Inputs.RemoveGamepad(evt.cdevice.which);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ShowRuntimeError(string title, string message)
|
||||||
|
{
|
||||||
|
SDL.SDL_ShowSimpleMessageBox(
|
||||||
|
SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
|
||||||
|
title ?? "",
|
||||||
|
message ?? "",
|
||||||
|
IntPtr.Zero
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private TimeSpan AdvanceElapsedTime()
|
private TimeSpan AdvanceElapsedTime()
|
||||||
{
|
{
|
||||||
long currentTicks = gameTimer.Elapsed.Ticks;
|
long currentTicks = gameTimer.Elapsed.Ticks;
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A buffer-offset pair to be used when binding vertex buffers.
|
||||||
|
/// </summary>
|
||||||
public struct BufferBinding
|
public struct BufferBinding
|
||||||
{
|
{
|
||||||
public Buffer Buffer;
|
public Buffer Buffer;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
using System;
|
namespace MoonWorks.Graphics
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A texture-sampler pair to be used when binding samplers.
|
||||||
|
/// </summary>
|
||||||
public struct TextureSamplerBinding
|
public struct TextureSamplerBinding
|
||||||
{
|
{
|
||||||
public Texture Texture;
|
public Texture Texture;
|
||||||
|
|
|
@ -20,6 +20,7 @@ using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using MoonWorks.Math;
|
using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
|
using MoonWorks.Graphics.PackedVector;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
|
@ -1758,6 +1759,67 @@ namespace MoonWorks.Graphics
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modified from one of the responses here:
|
||||||
|
// https://stackoverflow.com/questions/3018313/algorithm-to-convert-rgb-to-hsv-and-hsv-to-rgb-in-range-0-255-for-both/6930407#6930407
|
||||||
|
public static Color FromHSV(float r, float g, float b)
|
||||||
|
{
|
||||||
|
r = (100 + r) % 1f;
|
||||||
|
|
||||||
|
float hueSlice = 6 * r; // [0, 6)
|
||||||
|
float hueSliceInteger = MathF.Floor(hueSlice);
|
||||||
|
|
||||||
|
// In [0,1) for each hue slice
|
||||||
|
float hueSliceInterpolant = hueSlice - hueSliceInteger;
|
||||||
|
|
||||||
|
Vector3 tempRGB = new Vector3(
|
||||||
|
b * (1f - g),
|
||||||
|
b * (1f - g * hueSliceInterpolant),
|
||||||
|
b * (1f - g * (1f - hueSliceInterpolant))
|
||||||
|
);
|
||||||
|
|
||||||
|
// The idea here to avoid conditions is to notice that the conversion code can be rewritten:
|
||||||
|
// if ( var_i == 0 ) { R = V ; G = TempRGB.z ; B = TempRGB.x }
|
||||||
|
// else if ( var_i == 2 ) { R = TempRGB.x ; G = V ; B = TempRGB.z }
|
||||||
|
// else if ( var_i == 4 ) { R = TempRGB.z ; G = TempRGB.x ; B = V }
|
||||||
|
//
|
||||||
|
// else if ( var_i == 1 ) { R = TempRGB.y ; G = V ; B = TempRGB.x }
|
||||||
|
// else if ( var_i == 3 ) { R = TempRGB.x ; G = TempRGB.y ; B = V }
|
||||||
|
// else if ( var_i == 5 ) { R = V ; G = TempRGB.x ; B = TempRGB.y }
|
||||||
|
//
|
||||||
|
// This shows several things:
|
||||||
|
// . A separation between even and odd slices
|
||||||
|
// . If slices (0,2,4) and (1,3,5) can be rewritten as basically being slices (0,1,2) then
|
||||||
|
// the operation simply amounts to performing a "rotate right" on the RGB components
|
||||||
|
// . The base value to rotate is either (V, B, R) for even slices or (G, V, R) for odd slices
|
||||||
|
//
|
||||||
|
float isOddSlice = hueSliceInteger % 2f; // 0 if even (slices 0, 2, 4), 1 if odd (slices 1, 3, 5)
|
||||||
|
float threeSliceSelector = 0.5f * (hueSliceInteger - isOddSlice); // (0, 1, 2) corresponding to slices (0, 2, 4) and (1, 3, 5)
|
||||||
|
|
||||||
|
Vector3 scrollingRGBForEvenSlices = new Vector3(b, tempRGB.Z, tempRGB.X); // (V, Temp Blue, Temp Red) for even slices (0, 2, 4)
|
||||||
|
Vector3 scrollingRGBForOddSlices = new Vector3(tempRGB.Y, b, tempRGB.X); // (Temp Green, V, Temp Red) for odd slices (1, 3, 5)
|
||||||
|
Vector3 scrollingRGB = Vector3.Lerp(scrollingRGBForEvenSlices, scrollingRGBForOddSlices, isOddSlice);
|
||||||
|
|
||||||
|
float IsNotFirstSlice = MathHelper.Clamp(threeSliceSelector, 0f, 1f); // 1 if NOT the first slice (true for slices 1 and 2)
|
||||||
|
float IsNotSecondSlice = MathHelper.Clamp(threeSliceSelector - 1f, 0f, 1f); // 1 if NOT the first or second slice (true only for slice 2)
|
||||||
|
|
||||||
|
Vector3 color = Vector3.Lerp(
|
||||||
|
scrollingRGB,
|
||||||
|
Vector3.Lerp(
|
||||||
|
new Vector3(scrollingRGB.Z, scrollingRGB.X, scrollingRGB.Y),
|
||||||
|
new Vector3(scrollingRGB.Y, scrollingRGB.Z, scrollingRGB.X),
|
||||||
|
IsNotSecondSlice
|
||||||
|
),
|
||||||
|
IsNotFirstSlice
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Color(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color FromHSV(int r, int g, int b)
|
||||||
|
{
|
||||||
|
return Color.FromHSV(r / 255f, g / 255f, b / 255f);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Public Static Operators and Override Methods
|
#region Public Static Operators and Override Methods
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,34 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MoonWorks.Graphics
|
||||||
|
{
|
||||||
|
internal class CommandBufferPool
|
||||||
|
{
|
||||||
|
private GraphicsDevice GraphicsDevice;
|
||||||
|
private ConcurrentQueue<CommandBuffer> CommandBuffers = new ConcurrentQueue<CommandBuffer>();
|
||||||
|
|
||||||
|
public CommandBufferPool(GraphicsDevice graphicsDevice)
|
||||||
|
{
|
||||||
|
GraphicsDevice = graphicsDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandBuffer Obtain()
|
||||||
|
{
|
||||||
|
if (CommandBuffers.TryDequeue(out var commandBuffer))
|
||||||
|
{
|
||||||
|
return commandBuffer;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new CommandBuffer(GraphicsDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Return(CommandBuffer commandBuffer)
|
||||||
|
{
|
||||||
|
commandBuffer.Handle = IntPtr.Zero;
|
||||||
|
CommandBuffers.Enqueue(commandBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MoonWorks.Graphics
|
||||||
|
{
|
||||||
|
internal class FencePool
|
||||||
|
{
|
||||||
|
private GraphicsDevice GraphicsDevice;
|
||||||
|
private ConcurrentQueue<Fence> Fences = new ConcurrentQueue<Fence>();
|
||||||
|
|
||||||
|
public FencePool(GraphicsDevice graphicsDevice)
|
||||||
|
{
|
||||||
|
GraphicsDevice = graphicsDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Fence Obtain()
|
||||||
|
{
|
||||||
|
if (Fences.TryDequeue(out var fence))
|
||||||
|
{
|
||||||
|
return fence;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new Fence(GraphicsDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Return(Fence fence)
|
||||||
|
{
|
||||||
|
Fences.Enqueue(fence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,44 +1,121 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using WellspringCS;
|
using WellspringCS;
|
||||||
|
|
||||||
namespace MoonWorks.Graphics.Font
|
namespace MoonWorks.Graphics.Font
|
||||||
{
|
{
|
||||||
public class Font : IDisposable
|
public unsafe class Font : GraphicsResource
|
||||||
{
|
{
|
||||||
public IntPtr Handle { get; }
|
public Texture Texture { get; }
|
||||||
|
public float PixelsPerEm { get; }
|
||||||
|
public float DistanceRange { get; }
|
||||||
|
|
||||||
private bool IsDisposed;
|
internal IntPtr Handle { get; }
|
||||||
|
|
||||||
public unsafe Font(string path)
|
private byte* StringBytes;
|
||||||
{
|
private int StringBytesLength;
|
||||||
var bytes = File.ReadAllBytes(path);
|
|
||||||
fixed (byte* pByte = &bytes[0])
|
/// <summary>
|
||||||
|
/// Loads a TTF or OTF font from a path for use in MSDF rendering.
|
||||||
|
/// Note that there must be an msdf-atlas-gen JSON and image file alongside.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public unsafe static Font Load(
|
||||||
|
GraphicsDevice graphicsDevice,
|
||||||
|
CommandBuffer commandBuffer,
|
||||||
|
string fontPath
|
||||||
|
) {
|
||||||
|
var fontFileStream = new FileStream(fontPath, FileMode.Open, FileAccess.Read);
|
||||||
|
var fontFileByteBuffer = NativeMemory.Alloc((nuint) fontFileStream.Length);
|
||||||
|
var fontFileByteSpan = new Span<byte>(fontFileByteBuffer, (int) fontFileStream.Length);
|
||||||
|
fontFileStream.ReadExactly(fontFileByteSpan);
|
||||||
|
fontFileStream.Close();
|
||||||
|
|
||||||
|
var atlasFileStream = new FileStream(Path.ChangeExtension(fontPath, ".json"), FileMode.Open, FileAccess.Read);
|
||||||
|
var atlasFileByteBuffer = NativeMemory.Alloc((nuint) atlasFileStream.Length);
|
||||||
|
var atlasFileByteSpan = new Span<byte>(atlasFileByteBuffer, (int) atlasFileStream.Length);
|
||||||
|
atlasFileStream.ReadExactly(atlasFileByteSpan);
|
||||||
|
atlasFileStream.Close();
|
||||||
|
|
||||||
|
var handle = Wellspring.Wellspring_CreateFont(
|
||||||
|
(IntPtr) fontFileByteBuffer,
|
||||||
|
(uint) fontFileByteSpan.Length,
|
||||||
|
(IntPtr) atlasFileByteBuffer,
|
||||||
|
(uint) atlasFileByteSpan.Length,
|
||||||
|
out float pixelsPerEm,
|
||||||
|
out float distanceRange
|
||||||
|
);
|
||||||
|
|
||||||
|
var texture = Texture.FromImageFile(graphicsDevice, commandBuffer, Path.ChangeExtension(fontPath, ".png"));
|
||||||
|
|
||||||
|
NativeMemory.Free(fontFileByteBuffer);
|
||||||
|
NativeMemory.Free(atlasFileByteBuffer);
|
||||||
|
|
||||||
|
return new Font(graphicsDevice, handle, texture, pixelsPerEm, distanceRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Font(GraphicsDevice device, IntPtr handle, Texture texture, float pixelsPerEm, float distanceRange) : base(device)
|
||||||
|
{
|
||||||
|
Handle = handle;
|
||||||
|
Texture = texture;
|
||||||
|
PixelsPerEm = pixelsPerEm;
|
||||||
|
DistanceRange = distanceRange;
|
||||||
|
|
||||||
|
StringBytesLength = 32;
|
||||||
|
StringBytes = (byte*) NativeMemory.Alloc((nuint) StringBytesLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool TextBounds(
|
||||||
|
string text,
|
||||||
|
int pixelSize,
|
||||||
|
HorizontalAlignment horizontalAlignment,
|
||||||
|
VerticalAlignment verticalAlignment,
|
||||||
|
out Wellspring.Rectangle rectangle
|
||||||
|
) {
|
||||||
|
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
|
||||||
|
|
||||||
|
if (StringBytesLength < byteCount)
|
||||||
{
|
{
|
||||||
Handle = Wellspring.Wellspring_CreateFont((IntPtr) pByte, (uint) bytes.Length);
|
StringBytes = (byte*) NativeMemory.Realloc(StringBytes, (nuint) byteCount);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
fixed (char* chars = text)
|
||||||
|
{
|
||||||
|
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount);
|
||||||
|
|
||||||
|
var result = Wellspring.Wellspring_TextBounds(
|
||||||
|
Handle,
|
||||||
|
pixelSize,
|
||||||
|
(Wellspring.HorizontalAlignment) horizontalAlignment,
|
||||||
|
(Wellspring.VerticalAlignment) verticalAlignment,
|
||||||
|
(IntPtr) StringBytes,
|
||||||
|
(uint) byteCount,
|
||||||
|
out rectangle
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
Logger.LogWarn("Could not decode string: " + text);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!IsDisposed)
|
if (!IsDisposed)
|
||||||
{
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
Texture.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Wellspring.Wellspring_DestroyFont(Handle);
|
Wellspring.Wellspring_DestroyFont(Handle);
|
||||||
IsDisposed = true;
|
|
||||||
}
|
}
|
||||||
}
|
base.Dispose(disposing);
|
||||||
|
|
||||||
~Font()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using WellspringCS;
|
|
||||||
|
|
||||||
namespace MoonWorks.Graphics.Font
|
|
||||||
{
|
|
||||||
public class Packer : IDisposable
|
|
||||||
{
|
|
||||||
public IntPtr Handle { get; }
|
|
||||||
public Texture Texture { get; }
|
|
||||||
|
|
||||||
public Font Font { get; }
|
|
||||||
|
|
||||||
private byte[] StringBytes;
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
|
|
||||||
public unsafe Packer(GraphicsDevice graphicsDevice, Font font, float fontSize, uint textureWidth, uint textureHeight, uint padding = 1)
|
|
||||||
{
|
|
||||||
Font = font;
|
|
||||||
Handle = Wellspring.Wellspring_CreatePacker(Font.Handle, fontSize, textureWidth, textureHeight, 0, padding);
|
|
||||||
Texture = Texture.CreateTexture2D(graphicsDevice, textureWidth, textureHeight, TextureFormat.R8, TextureUsageFlags.Sampler);
|
|
||||||
StringBytes = new byte[128];
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe bool PackFontRanges(params FontRange[] fontRanges)
|
|
||||||
{
|
|
||||||
fixed (FontRange *pFontRanges = &fontRanges[0])
|
|
||||||
{
|
|
||||||
var nativeSize = fontRanges.Length * Marshal.SizeOf<Wellspring.FontRange>();
|
|
||||||
void* fontRangeMemory = NativeMemory.Alloc((nuint) fontRanges.Length, (nuint) Marshal.SizeOf<Wellspring.FontRange>());
|
|
||||||
System.Buffer.MemoryCopy(pFontRanges, fontRangeMemory, nativeSize, nativeSize);
|
|
||||||
|
|
||||||
var result = Wellspring.Wellspring_PackFontRanges(Handle, (IntPtr) fontRangeMemory, (uint) fontRanges.Length);
|
|
||||||
|
|
||||||
NativeMemory.Free(fontRangeMemory);
|
|
||||||
|
|
||||||
return result > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void SetTextureData(CommandBuffer commandBuffer)
|
|
||||||
{
|
|
||||||
var pixelDataPointer = Wellspring.Wellspring_GetPixelDataPointer(Handle);
|
|
||||||
commandBuffer.SetTextureData(Texture, pixelDataPointer, Texture.Width * Texture.Height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void TextBounds(
|
|
||||||
string text,
|
|
||||||
float x,
|
|
||||||
float y,
|
|
||||||
HorizontalAlignment horizontalAlignment,
|
|
||||||
VerticalAlignment verticalAlignment,
|
|
||||||
out Wellspring.Rectangle rectangle
|
|
||||||
) {
|
|
||||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
|
|
||||||
|
|
||||||
if (StringBytes.Length < byteCount)
|
|
||||||
{
|
|
||||||
System.Array.Resize(ref StringBytes, byteCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
fixed (char* chars = text)
|
|
||||||
fixed (byte* bytes = StringBytes)
|
|
||||||
{
|
|
||||||
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, bytes, byteCount);
|
|
||||||
Wellspring.Wellspring_TextBounds(
|
|
||||||
Handle,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
(Wellspring.HorizontalAlignment) horizontalAlignment,
|
|
||||||
(Wellspring.VerticalAlignment) verticalAlignment,
|
|
||||||
(IntPtr) bytes,
|
|
||||||
(uint) byteCount,
|
|
||||||
out rectangle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!IsDisposed)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
Texture.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Wellspring.Wellspring_DestroyPacker(Handle);
|
|
||||||
|
|
||||||
IsDisposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~Packer()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,19 +4,17 @@ using MoonWorks.Math.Float;
|
||||||
namespace MoonWorks.Graphics.Font
|
namespace MoonWorks.Graphics.Font
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
public struct FontRange
|
public struct Vertex : IVertexType
|
||||||
{
|
|
||||||
public uint FirstCodepoint;
|
|
||||||
public uint NumChars;
|
|
||||||
public byte OversampleH;
|
|
||||||
public byte OversampleV;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
public struct Vertex
|
|
||||||
{
|
{
|
||||||
public Vector3 Position;
|
public Vector3 Position;
|
||||||
public Vector2 TexCoord;
|
public Vector2 TexCoord;
|
||||||
public Color Color;
|
public Color Color;
|
||||||
|
|
||||||
|
public static VertexElementFormat[] Formats { get; } = new VertexElementFormat[]
|
||||||
|
{
|
||||||
|
VertexElementFormat.Vector3,
|
||||||
|
VertexElementFormat.Vector2,
|
||||||
|
VertexElementFormat.Color
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +1,87 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using WellspringCS;
|
using WellspringCS;
|
||||||
|
|
||||||
namespace MoonWorks.Graphics.Font
|
namespace MoonWorks.Graphics.Font
|
||||||
{
|
{
|
||||||
public class TextBatch
|
public unsafe class TextBatch : GraphicsResource
|
||||||
{
|
{
|
||||||
|
public const int INITIAL_CHAR_COUNT = 64;
|
||||||
|
public const int INITIAL_VERTEX_COUNT = INITIAL_CHAR_COUNT * 4;
|
||||||
|
public const int INITIAL_INDEX_COUNT = INITIAL_CHAR_COUNT * 6;
|
||||||
|
|
||||||
private GraphicsDevice GraphicsDevice { get; }
|
private GraphicsDevice GraphicsDevice { get; }
|
||||||
public IntPtr Handle { get; }
|
public IntPtr Handle { get; }
|
||||||
|
|
||||||
public Buffer VertexBuffer { get; protected set; } = null;
|
public Buffer VertexBuffer { get; protected set; } = null;
|
||||||
public Buffer IndexBuffer { get; protected set; } = null;
|
public Buffer IndexBuffer { get; protected set; } = null;
|
||||||
public Texture Texture { get; protected set; }
|
|
||||||
public uint PrimitiveCount { get; protected set; }
|
public uint PrimitiveCount { get; protected set; }
|
||||||
|
|
||||||
private byte[] StringBytes;
|
public Font CurrentFont { get; private set; }
|
||||||
|
|
||||||
public TextBatch(GraphicsDevice graphicsDevice)
|
private byte* StringBytes;
|
||||||
|
private int StringBytesLength;
|
||||||
|
|
||||||
|
public TextBatch(GraphicsDevice device) : base(device)
|
||||||
{
|
{
|
||||||
GraphicsDevice = graphicsDevice;
|
GraphicsDevice = device;
|
||||||
Handle = Wellspring.Wellspring_CreateTextBatch();
|
Handle = Wellspring.Wellspring_CreateTextBatch();
|
||||||
StringBytes = new byte[128];
|
|
||||||
|
StringBytesLength = 128;
|
||||||
|
StringBytes = (byte*) NativeMemory.Alloc((nuint) StringBytesLength);
|
||||||
|
|
||||||
|
VertexBuffer = Buffer.Create<Vertex>(GraphicsDevice, BufferUsageFlags.Vertex, INITIAL_VERTEX_COUNT);
|
||||||
|
IndexBuffer = Buffer.Create<uint>(GraphicsDevice, BufferUsageFlags.Index, INITIAL_INDEX_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start(Packer packer)
|
// Call this to initialize or reset the batch.
|
||||||
|
public void Start(Font font)
|
||||||
{
|
{
|
||||||
Wellspring.Wellspring_StartTextBatch(Handle, packer.Handle);
|
Wellspring.Wellspring_StartTextBatch(Handle, font.Handle);
|
||||||
Texture = packer.Texture;
|
CurrentFont = font;
|
||||||
PrimitiveCount = 0;
|
PrimitiveCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe void Draw(
|
// Add text with size and color to the batch
|
||||||
|
public unsafe bool Add(
|
||||||
string text,
|
string text,
|
||||||
float x,
|
int pixelSize,
|
||||||
float y,
|
|
||||||
float depth,
|
|
||||||
Color color,
|
Color color,
|
||||||
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
|
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
|
||||||
VerticalAlignment verticalAlignment = VerticalAlignment.Baseline
|
VerticalAlignment verticalAlignment = VerticalAlignment.Baseline
|
||||||
) {
|
) {
|
||||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
|
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
|
||||||
|
|
||||||
if (StringBytes.Length < byteCount)
|
if (StringBytesLength < byteCount)
|
||||||
{
|
{
|
||||||
System.Array.Resize(ref StringBytes, byteCount);
|
StringBytes = (byte*) NativeMemory.Realloc(StringBytes, (nuint) byteCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
fixed (char* chars = text)
|
fixed (char* chars = text)
|
||||||
fixed (byte* bytes = StringBytes)
|
|
||||||
{
|
{
|
||||||
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, bytes, byteCount);
|
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount);
|
||||||
|
|
||||||
var result = Wellspring.Wellspring_Draw(
|
var result = Wellspring.Wellspring_AddToTextBatch(
|
||||||
Handle,
|
Handle,
|
||||||
x,
|
pixelSize,
|
||||||
y,
|
|
||||||
depth,
|
|
||||||
new Wellspring.Color { R = color.R, G = color.G, B = color.B, A = color.A },
|
new Wellspring.Color { R = color.R, G = color.G, B = color.B, A = color.A },
|
||||||
(Wellspring.HorizontalAlignment) horizontalAlignment,
|
(Wellspring.HorizontalAlignment) horizontalAlignment,
|
||||||
(Wellspring.VerticalAlignment) verticalAlignment,
|
(Wellspring.VerticalAlignment) verticalAlignment,
|
||||||
(IntPtr) bytes,
|
(IntPtr) StringBytes,
|
||||||
(uint) byteCount
|
(uint) byteCount
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result == 0)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
throw new System.ArgumentException("Could not decode string!");
|
Logger.LogWarn("Could not decode string: " + text);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call this after you have made all the Draw calls you want.
|
// Call this after you have made all the Add calls you want, but before beginning a render pass.
|
||||||
public unsafe void UploadBufferData(CommandBuffer commandBuffer)
|
public unsafe void UploadBufferData(CommandBuffer commandBuffer)
|
||||||
{
|
{
|
||||||
Wellspring.Wellspring_GetBufferData(
|
Wellspring.Wellspring_GetBufferData(
|
||||||
|
@ -81,24 +93,16 @@ namespace MoonWorks.Graphics.Font
|
||||||
out uint indexDataLengthInBytes
|
out uint indexDataLengthInBytes
|
||||||
);
|
);
|
||||||
|
|
||||||
if (VertexBuffer == null)
|
if (VertexBuffer.Size < vertexDataLengthInBytes)
|
||||||
{
|
|
||||||
VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
|
|
||||||
}
|
|
||||||
else if (VertexBuffer.Size < vertexDataLengthInBytes)
|
|
||||||
{
|
{
|
||||||
VertexBuffer.Dispose();
|
VertexBuffer.Dispose();
|
||||||
VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
|
VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IndexBuffer == null)
|
if (IndexBuffer.Size < indexDataLengthInBytes)
|
||||||
{
|
|
||||||
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes);
|
|
||||||
}
|
|
||||||
else if (IndexBuffer.Size < indexDataLengthInBytes)
|
|
||||||
{
|
{
|
||||||
IndexBuffer.Dispose();
|
IndexBuffer.Dispose();
|
||||||
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes);
|
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, vertexDataLengthInBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0)
|
if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0)
|
||||||
|
@ -107,7 +111,41 @@ namespace MoonWorks.Graphics.Font
|
||||||
commandBuffer.SetBufferData(IndexBuffer, indexDataPointer, 0, indexDataLengthInBytes);
|
commandBuffer.SetBufferData(IndexBuffer, indexDataPointer, 0, indexDataLengthInBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
PrimitiveCount = vertexCount / 2; // FIXME: is this jank?
|
PrimitiveCount = vertexCount / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this AFTER binding your text pipeline!
|
||||||
|
public void Render(CommandBuffer commandBuffer, Math.Float.Matrix4x4 transformMatrix)
|
||||||
|
{
|
||||||
|
commandBuffer.BindFragmentSamplers(new TextureSamplerBinding(
|
||||||
|
CurrentFont.Texture,
|
||||||
|
GraphicsDevice.LinearSampler
|
||||||
|
));
|
||||||
|
commandBuffer.BindVertexBuffers(VertexBuffer);
|
||||||
|
commandBuffer.BindIndexBuffer(IndexBuffer, IndexElementSize.ThirtyTwo);
|
||||||
|
commandBuffer.DrawIndexedPrimitives(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
PrimitiveCount,
|
||||||
|
commandBuffer.PushVertexShaderUniforms(transformMatrix),
|
||||||
|
commandBuffer.PushFragmentShaderUniforms(CurrentFont.DistanceRange)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
VertexBuffer.Dispose();
|
||||||
|
IndexBuffer.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeMemory.Free(StringBytes);
|
||||||
|
Wellspring.Wellspring_DestroyTextBatch(Handle);
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using MoonWorks.Video;
|
||||||
using RefreshCS;
|
using RefreshCS;
|
||||||
|
using WellspringCS;
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// GraphicsDevice manages all graphics-related concerns.
|
||||||
|
/// </summary>
|
||||||
public class GraphicsDevice : IDisposable
|
public class GraphicsDevice : IDisposable
|
||||||
{
|
{
|
||||||
public IntPtr Handle { get; }
|
public IntPtr Handle { get; }
|
||||||
|
@ -16,11 +22,22 @@ namespace MoonWorks.Graphics
|
||||||
// Built-in video pipeline
|
// Built-in video pipeline
|
||||||
internal GraphicsPipeline VideoPipeline { get; }
|
internal GraphicsPipeline VideoPipeline { get; }
|
||||||
|
|
||||||
|
// Built-in text shader info
|
||||||
|
public GraphicsShaderInfo TextVertexShaderInfo { get; }
|
||||||
|
public GraphicsShaderInfo TextFragmentShaderInfo { get; }
|
||||||
|
public VertexInputState TextVertexInputState { get; }
|
||||||
|
|
||||||
|
// Built-in samplers
|
||||||
|
public Sampler PointSampler { get; }
|
||||||
|
public Sampler LinearSampler { get; }
|
||||||
|
|
||||||
public bool IsDisposed { get; private set; }
|
public bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
private readonly List<WeakReference<GraphicsResource>> resources = new List<WeakReference<GraphicsResource>>();
|
private readonly HashSet<GCHandle> resources = new HashSet<GCHandle>();
|
||||||
|
private FencePool FencePool;
|
||||||
|
private CommandBufferPool CommandBufferPool;
|
||||||
|
|
||||||
public GraphicsDevice(
|
internal GraphicsDevice(
|
||||||
Backend preferredBackend,
|
Backend preferredBackend,
|
||||||
bool debugMode
|
bool debugMode
|
||||||
) {
|
) {
|
||||||
|
@ -35,47 +52,109 @@ namespace MoonWorks.Graphics
|
||||||
Conversions.BoolToByte(debugMode)
|
Conversions.BoolToByte(debugMode)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for optional video shaders
|
// TODO: check for CreateDevice fail
|
||||||
string basePath = SDL2.SDL.SDL_GetBasePath();
|
|
||||||
string videoVertPath = Path.Combine(basePath, "video_fullscreen.refresh");
|
// Check for replacement stock shaders
|
||||||
string videoFragPath = Path.Combine(basePath, "video_yuv2rgba.refresh");
|
string basePath = System.AppContext.BaseDirectory;
|
||||||
|
|
||||||
|
string videoVertPath = Path.Combine(basePath, "video_fullscreen.vert.refresh");
|
||||||
|
string videoFragPath = Path.Combine(basePath, "video_yuv2rgba.frag.refresh");
|
||||||
|
|
||||||
|
string textVertPath = Path.Combine(basePath, "text_transform.vert.refresh");
|
||||||
|
string textFragPath = Path.Combine(basePath, "text_msdf.frag.refresh");
|
||||||
|
|
||||||
|
ShaderModule videoVertShader;
|
||||||
|
ShaderModule videoFragShader;
|
||||||
|
|
||||||
|
ShaderModule textVertShader;
|
||||||
|
ShaderModule textFragShader;
|
||||||
|
|
||||||
if (File.Exists(videoVertPath) && File.Exists(videoFragPath))
|
if (File.Exists(videoVertPath) && File.Exists(videoFragPath))
|
||||||
{
|
{
|
||||||
ShaderModule videoVertShader = new ShaderModule(this, videoVertPath);
|
videoVertShader = new ShaderModule(this, videoVertPath);
|
||||||
ShaderModule videoFragShader = new ShaderModule(this, videoFragPath);
|
videoFragShader = new ShaderModule(this, videoFragPath);
|
||||||
|
|
||||||
VideoPipeline = new GraphicsPipeline(
|
|
||||||
this,
|
|
||||||
new GraphicsPipelineCreateInfo
|
|
||||||
{
|
|
||||||
AttachmentInfo = new GraphicsPipelineAttachmentInfo(
|
|
||||||
new ColorAttachmentDescription(
|
|
||||||
TextureFormat.R8G8B8A8,
|
|
||||||
ColorAttachmentBlendState.None
|
|
||||||
)
|
|
||||||
),
|
|
||||||
DepthStencilState = DepthStencilState.Disable,
|
|
||||||
VertexShaderInfo = GraphicsShaderInfo.Create(
|
|
||||||
videoVertShader,
|
|
||||||
"main",
|
|
||||||
0
|
|
||||||
),
|
|
||||||
FragmentShaderInfo = GraphicsShaderInfo.Create(
|
|
||||||
videoFragShader,
|
|
||||||
"main",
|
|
||||||
3
|
|
||||||
),
|
|
||||||
VertexInputState = VertexInputState.Empty,
|
|
||||||
RasterizerState = RasterizerState.CCW_CullNone,
|
|
||||||
PrimitiveType = PrimitiveType.TriangleList,
|
|
||||||
MultisampleState = MultisampleState.None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// use defaults
|
||||||
|
var assembly = typeof(GraphicsDevice).Assembly;
|
||||||
|
|
||||||
|
using var vertStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.VideoFullscreen.vert.refresh");
|
||||||
|
using var fragStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.VideoYUV2RGBA.frag.refresh");
|
||||||
|
|
||||||
|
videoVertShader = new ShaderModule(this, vertStream);
|
||||||
|
videoFragShader = new ShaderModule(this, fragStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(textVertPath) && File.Exists(textFragPath))
|
||||||
|
{
|
||||||
|
textVertShader = new ShaderModule(this, textVertPath);
|
||||||
|
textFragShader = new ShaderModule(this, textFragPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// use defaults
|
||||||
|
var assembly = typeof(GraphicsDevice).Assembly;
|
||||||
|
|
||||||
|
using var vertStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.TextTransform.vert.refresh");
|
||||||
|
using var fragStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.TextMSDF.frag.refresh");
|
||||||
|
|
||||||
|
textVertShader = new ShaderModule(this, vertStream);
|
||||||
|
textFragShader = new ShaderModule(this, fragStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoPipeline = new GraphicsPipeline(
|
||||||
|
this,
|
||||||
|
new GraphicsPipelineCreateInfo
|
||||||
|
{
|
||||||
|
AttachmentInfo = new GraphicsPipelineAttachmentInfo(
|
||||||
|
new ColorAttachmentDescription(
|
||||||
|
TextureFormat.R8G8B8A8,
|
||||||
|
ColorAttachmentBlendState.None
|
||||||
|
)
|
||||||
|
),
|
||||||
|
DepthStencilState = DepthStencilState.Disable,
|
||||||
|
VertexShaderInfo = GraphicsShaderInfo.Create(
|
||||||
|
videoVertShader,
|
||||||
|
"main",
|
||||||
|
0
|
||||||
|
),
|
||||||
|
FragmentShaderInfo = GraphicsShaderInfo.Create(
|
||||||
|
videoFragShader,
|
||||||
|
"main",
|
||||||
|
3
|
||||||
|
),
|
||||||
|
VertexInputState = VertexInputState.Empty,
|
||||||
|
RasterizerState = RasterizerState.CCW_CullNone,
|
||||||
|
PrimitiveType = PrimitiveType.TriangleList,
|
||||||
|
MultisampleState = MultisampleState.None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TextVertexShaderInfo = GraphicsShaderInfo.Create<Math.Float.Matrix4x4>(textVertShader, "main", 0);
|
||||||
|
TextFragmentShaderInfo = GraphicsShaderInfo.Create<float>(textFragShader, "main", 1);
|
||||||
|
TextVertexInputState = VertexInputState.CreateSingleBinding<Font.Vertex>();
|
||||||
|
|
||||||
|
PointSampler = new Sampler(this, SamplerCreateInfo.PointClamp);
|
||||||
|
LinearSampler = new Sampler(this, SamplerCreateInfo.LinearClamp);
|
||||||
|
|
||||||
|
FencePool = new FencePool(this);
|
||||||
|
CommandBufferPool = new CommandBufferPool(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares a window so that frames can be presented to it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="presentMode">The desired presentation mode for the window. Roughly equivalent to V-Sync.</param>
|
||||||
|
/// <returns>True if successfully claimed.</returns>
|
||||||
public bool ClaimWindow(Window window, PresentMode presentMode)
|
public bool ClaimWindow(Window window, PresentMode presentMode)
|
||||||
{
|
{
|
||||||
|
if (window.Claimed)
|
||||||
|
{
|
||||||
|
Logger.LogError("Window already claimed!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var success = Conversions.ByteToBool(
|
var success = Conversions.ByteToBool(
|
||||||
Refresh.Refresh_ClaimWindow(
|
Refresh.Refresh_ClaimWindow(
|
||||||
Handle,
|
Handle,
|
||||||
|
@ -90,24 +169,46 @@ namespace MoonWorks.Graphics
|
||||||
window.SwapchainFormat = GetSwapchainFormat(window);
|
window.SwapchainFormat = GetSwapchainFormat(window);
|
||||||
if (window.SwapchainTexture == null)
|
if (window.SwapchainTexture == null)
|
||||||
{
|
{
|
||||||
window.SwapchainTexture = new Texture(this, IntPtr.Zero, window.SwapchainFormat, 0, 0);
|
window.SwapchainTexture = new Texture(this, window.SwapchainFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unclaims a window, making it unavailable for presenting and freeing associated resources.
|
||||||
|
/// </summary>
|
||||||
public void UnclaimWindow(Window window)
|
public void UnclaimWindow(Window window)
|
||||||
{
|
{
|
||||||
Refresh.Refresh_UnclaimWindow(
|
if (window.Claimed)
|
||||||
Handle,
|
{
|
||||||
window.Handle
|
Refresh.Refresh_UnclaimWindow(
|
||||||
);
|
Handle,
|
||||||
window.Claimed = false;
|
window.Handle
|
||||||
|
);
|
||||||
|
window.Claimed = false;
|
||||||
|
|
||||||
|
// The swapchain texture doesn't actually have a permanent texture reference, so we zero the handle before disposing.
|
||||||
|
window.SwapchainTexture.Handle = IntPtr.Zero;
|
||||||
|
window.SwapchainTexture.Dispose();
|
||||||
|
window.SwapchainTexture = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the present mode of a claimed window. Does nothing if the window is not claimed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="window"></param>
|
||||||
|
/// <param name="presentMode"></param>
|
||||||
public void SetPresentMode(Window window, PresentMode presentMode)
|
public void SetPresentMode(Window window, PresentMode presentMode)
|
||||||
{
|
{
|
||||||
|
if (!window.Claimed)
|
||||||
|
{
|
||||||
|
Logger.LogError("Cannot set present mode on unclaimed window!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Refresh.Refresh_SetSwapchainPresentMode(
|
Refresh.Refresh_SetSwapchainPresentMode(
|
||||||
Handle,
|
Handle,
|
||||||
window.Handle,
|
window.Handle,
|
||||||
|
@ -115,105 +216,208 @@ namespace MoonWorks.Graphics
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acquires a command buffer.
|
||||||
|
/// This is the start of your rendering process.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public CommandBuffer AcquireCommandBuffer()
|
public CommandBuffer AcquireCommandBuffer()
|
||||||
{
|
{
|
||||||
return new CommandBuffer(this, Refresh.Refresh_AcquireCommandBuffer(Handle));
|
var commandBuffer = CommandBufferPool.Obtain();
|
||||||
|
commandBuffer.SetHandle(Refresh.Refresh_AcquireCommandBuffer(Handle));
|
||||||
|
#if DEBUG
|
||||||
|
commandBuffer.ResetStateTracking();
|
||||||
|
#endif
|
||||||
|
return commandBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe void Submit(CommandBuffer commandBuffer)
|
/// <summary>
|
||||||
|
/// Submits a command buffer to the GPU for processing.
|
||||||
|
/// </summary>
|
||||||
|
public void Submit(CommandBuffer commandBuffer)
|
||||||
{
|
{
|
||||||
var commandBufferPtrs = stackalloc IntPtr[1];
|
#if DEBUG
|
||||||
|
if (commandBuffer.Submitted)
|
||||||
commandBufferPtrs[0] = commandBuffer.Handle;
|
|
||||||
|
|
||||||
Refresh.Refresh_Submit(
|
|
||||||
Handle,
|
|
||||||
1,
|
|
||||||
(IntPtr) commandBufferPtrs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void Submit(
|
|
||||||
CommandBuffer commandBufferOne,
|
|
||||||
CommandBuffer commandBufferTwo
|
|
||||||
) {
|
|
||||||
var commandBufferPtrs = stackalloc IntPtr[2];
|
|
||||||
|
|
||||||
commandBufferPtrs[0] = commandBufferOne.Handle;
|
|
||||||
commandBufferPtrs[1] = commandBufferTwo.Handle;
|
|
||||||
|
|
||||||
Refresh.Refresh_Submit(
|
|
||||||
Handle,
|
|
||||||
2,
|
|
||||||
(IntPtr) commandBufferPtrs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void Submit(
|
|
||||||
CommandBuffer commandBufferOne,
|
|
||||||
CommandBuffer commandBufferTwo,
|
|
||||||
CommandBuffer commandBufferThree
|
|
||||||
) {
|
|
||||||
var commandBufferPtrs = stackalloc IntPtr[3];
|
|
||||||
|
|
||||||
commandBufferPtrs[0] = commandBufferOne.Handle;
|
|
||||||
commandBufferPtrs[1] = commandBufferTwo.Handle;
|
|
||||||
commandBufferPtrs[2] = commandBufferThree.Handle;
|
|
||||||
|
|
||||||
Refresh.Refresh_Submit(
|
|
||||||
Handle,
|
|
||||||
3,
|
|
||||||
(IntPtr) commandBufferPtrs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void Submit(
|
|
||||||
CommandBuffer commandBufferOne,
|
|
||||||
CommandBuffer commandBufferTwo,
|
|
||||||
CommandBuffer commandBufferThree,
|
|
||||||
CommandBuffer commandBufferFour
|
|
||||||
) {
|
|
||||||
var commandBufferPtrs = stackalloc IntPtr[4];
|
|
||||||
|
|
||||||
commandBufferPtrs[0] = commandBufferOne.Handle;
|
|
||||||
commandBufferPtrs[1] = commandBufferTwo.Handle;
|
|
||||||
commandBufferPtrs[2] = commandBufferThree.Handle;
|
|
||||||
commandBufferPtrs[3] = commandBufferFour.Handle;
|
|
||||||
|
|
||||||
Refresh.Refresh_Submit(
|
|
||||||
Handle,
|
|
||||||
4,
|
|
||||||
(IntPtr) commandBufferPtrs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void Submit(params CommandBuffer[] commandBuffers)
|
|
||||||
{
|
|
||||||
var commandBufferPtrs = stackalloc IntPtr[commandBuffers.Length];
|
|
||||||
|
|
||||||
for (var i = 0; i < commandBuffers.Length; i += 1)
|
|
||||||
{
|
{
|
||||||
commandBufferPtrs[i] = commandBuffers[i].Handle;
|
throw new System.InvalidOperationException("Command buffer already submitted!");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
Refresh.Refresh_Submit(
|
Refresh.Refresh_Submit(
|
||||||
Handle,
|
Handle,
|
||||||
(uint) commandBuffers.Length,
|
commandBuffer.Handle
|
||||||
(IntPtr) commandBufferPtrs
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CommandBufferPool.Return(commandBuffer);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
commandBuffer.Submitted = true;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Submits a command buffer to the GPU for processing and acquires a fence associated with the submission.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Fence SubmitAndAcquireFence(CommandBuffer commandBuffer)
|
||||||
|
{
|
||||||
|
var fenceHandle = Refresh.Refresh_SubmitAndAcquireFence(
|
||||||
|
Handle,
|
||||||
|
commandBuffer.Handle
|
||||||
|
);
|
||||||
|
|
||||||
|
var fence = FencePool.Obtain();
|
||||||
|
fence.SetHandle(fenceHandle);
|
||||||
|
|
||||||
|
return fence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait for the graphics device to become idle.
|
||||||
|
/// </summary>
|
||||||
public void Wait()
|
public void Wait()
|
||||||
{
|
{
|
||||||
Refresh.Refresh_Wait(Handle);
|
Refresh.Refresh_Wait(Handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for the given fence to become signaled.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe void WaitForFences(Fence fence)
|
||||||
|
{
|
||||||
|
var handlePtr = stackalloc nint[1];
|
||||||
|
handlePtr[0] = fence.Handle;
|
||||||
|
|
||||||
|
Refresh.Refresh_WaitForFences(
|
||||||
|
Handle,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
(nint) handlePtr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait for one or more fences to become signaled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="waitAll">If true, will wait for all given fences to be signaled.</param>
|
||||||
|
public unsafe void WaitForFences(
|
||||||
|
Fence fenceOne,
|
||||||
|
Fence fenceTwo,
|
||||||
|
bool waitAll
|
||||||
|
) {
|
||||||
|
var handlePtr = stackalloc nint[2];
|
||||||
|
handlePtr[0] = fenceOne.Handle;
|
||||||
|
handlePtr[1] = fenceTwo.Handle;
|
||||||
|
|
||||||
|
Refresh.Refresh_WaitForFences(
|
||||||
|
Handle,
|
||||||
|
Conversions.BoolToByte(waitAll),
|
||||||
|
2,
|
||||||
|
(nint) handlePtr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait for one or more fences to become signaled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="waitAll">If true, will wait for all given fences to be signaled.</param>
|
||||||
|
public unsafe void WaitForFences(
|
||||||
|
Fence fenceOne,
|
||||||
|
Fence fenceTwo,
|
||||||
|
Fence fenceThree,
|
||||||
|
bool waitAll
|
||||||
|
) {
|
||||||
|
var handlePtr = stackalloc nint[3];
|
||||||
|
handlePtr[0] = fenceOne.Handle;
|
||||||
|
handlePtr[1] = fenceTwo.Handle;
|
||||||
|
handlePtr[2] = fenceThree.Handle;
|
||||||
|
|
||||||
|
Refresh.Refresh_WaitForFences(
|
||||||
|
Handle,
|
||||||
|
Conversions.BoolToByte(waitAll),
|
||||||
|
3,
|
||||||
|
(nint) handlePtr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait for one or more fences to become signaled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="waitAll">If true, will wait for all given fences to be signaled.</param>
|
||||||
|
public unsafe void WaitForFences(
|
||||||
|
Fence fenceOne,
|
||||||
|
Fence fenceTwo,
|
||||||
|
Fence fenceThree,
|
||||||
|
Fence fenceFour,
|
||||||
|
bool waitAll
|
||||||
|
) {
|
||||||
|
var handlePtr = stackalloc nint[4];
|
||||||
|
handlePtr[0] = fenceOne.Handle;
|
||||||
|
handlePtr[1] = fenceTwo.Handle;
|
||||||
|
handlePtr[2] = fenceThree.Handle;
|
||||||
|
handlePtr[3] = fenceFour.Handle;
|
||||||
|
|
||||||
|
Refresh.Refresh_WaitForFences(
|
||||||
|
Handle,
|
||||||
|
Conversions.BoolToByte(waitAll),
|
||||||
|
4,
|
||||||
|
(nint) handlePtr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait for one or more fences to become signaled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="waitAll">If true, will wait for all given fences to be signaled.</param>
|
||||||
|
public unsafe void WaitForFences(Fence[] fences, bool waitAll)
|
||||||
|
{
|
||||||
|
var handlePtr = stackalloc nint[fences.Length];
|
||||||
|
|
||||||
|
for (var i = 0; i < fences.Length; i += 1)
|
||||||
|
{
|
||||||
|
handlePtr[i] = fences[i].Handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Refresh.Refresh_WaitForFences(
|
||||||
|
Handle,
|
||||||
|
Conversions.BoolToByte(waitAll),
|
||||||
|
4,
|
||||||
|
(nint) handlePtr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the fence is signaled, indicating that the associated command buffer has finished processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">Throws if the fence query indicates that the graphics device has been lost.</exception>
|
||||||
|
public bool QueryFence(Fence fence)
|
||||||
|
{
|
||||||
|
var result = Refresh.Refresh_QueryFence(Handle, fence.Handle);
|
||||||
|
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The graphics device has been lost.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Release reference to an acquired fence, enabling it to be reused.
|
||||||
|
/// </summary>
|
||||||
|
public void ReleaseFence(Fence fence)
|
||||||
|
{
|
||||||
|
Refresh.Refresh_ReleaseFence(Handle, fence.Handle);
|
||||||
|
fence.Handle = IntPtr.Zero;
|
||||||
|
FencePool.Return(fence);
|
||||||
|
}
|
||||||
|
|
||||||
private TextureFormat GetSwapchainFormat(Window window)
|
private TextureFormat GetSwapchainFormat(Window window)
|
||||||
{
|
{
|
||||||
return (TextureFormat) Refresh.Refresh_GetSwapchainFormat(Handle, window.Handle);
|
return (TextureFormat) Refresh.Refresh_GetSwapchainFormat(Handle, window.Handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AddResourceReference(WeakReference<GraphicsResource> resourceReference)
|
internal void AddResourceReference(GCHandle resourceReference)
|
||||||
{
|
{
|
||||||
lock (resources)
|
lock (resources)
|
||||||
{
|
{
|
||||||
|
@ -221,7 +425,7 @@ namespace MoonWorks.Graphics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void RemoveResourceReference(WeakReference<GraphicsResource> resourceReference)
|
internal void RemoveResourceReference(GCHandle resourceReference)
|
||||||
{
|
{
|
||||||
lock (resources)
|
lock (resources)
|
||||||
{
|
{
|
||||||
|
@ -237,20 +441,29 @@ namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
lock (resources)
|
lock (resources)
|
||||||
{
|
{
|
||||||
for (var i = resources.Count - 1; i >= 0; i--)
|
// Dispose video players first to avoid race condition on threaded decoding
|
||||||
|
foreach (var resource in resources)
|
||||||
{
|
{
|
||||||
var resource = resources[i];
|
if (resource.Target is VideoPlayer player)
|
||||||
if (resource.TryGetTarget(out var target))
|
|
||||||
{
|
{
|
||||||
target.Dispose();
|
player.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose everything else
|
||||||
|
foreach (var resource in resources)
|
||||||
|
{
|
||||||
|
if (resource.Target is IDisposable disposable)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resources.Clear();
|
resources.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
Refresh.Refresh_DestroyDevice(Handle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Refresh.Refresh_DestroyDevice(Handle);
|
||||||
|
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,33 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
|
// TODO: give this a Name property for debugging use
|
||||||
public abstract class GraphicsResource : IDisposable
|
public abstract class GraphicsResource : IDisposable
|
||||||
{
|
{
|
||||||
public GraphicsDevice Device { get; }
|
public GraphicsDevice Device { get; }
|
||||||
public IntPtr Handle { get; internal set; }
|
|
||||||
|
private GCHandle SelfReference;
|
||||||
|
|
||||||
public bool IsDisposed { get; private set; }
|
public bool IsDisposed { get; private set; }
|
||||||
protected abstract Action<IntPtr, IntPtr> QueueDestroyFunction { get; }
|
|
||||||
|
|
||||||
private WeakReference<GraphicsResource> selfReference;
|
protected GraphicsResource(GraphicsDevice device)
|
||||||
|
|
||||||
public GraphicsResource(GraphicsDevice device, bool trackResource = true)
|
|
||||||
{
|
{
|
||||||
Device = device;
|
Device = device;
|
||||||
|
|
||||||
if (trackResource)
|
SelfReference = GCHandle.Alloc(this, GCHandleType.Weak);
|
||||||
{
|
Device.AddResourceReference(SelfReference);
|
||||||
selfReference = new WeakReference<GraphicsResource>(this);
|
|
||||||
Device.AddResourceReference(selfReference);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!IsDisposed)
|
if (!IsDisposed)
|
||||||
{
|
{
|
||||||
if (selfReference != null)
|
if (disposing)
|
||||||
{
|
{
|
||||||
QueueDestroyFunction(Device.Handle, Handle);
|
Device.RemoveResourceReference(SelfReference);
|
||||||
Device.RemoveResourceReference(selfReference);
|
SelfReference.Free();
|
||||||
selfReference = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
|
@ -40,8 +36,13 @@ namespace MoonWorks.Graphics
|
||||||
|
|
||||||
~GraphicsResource()
|
~GraphicsResource()
|
||||||
{
|
{
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
#if DEBUG
|
||||||
Dispose(disposing: false);
|
// If you see this log message, you leaked a graphics resource without disposing it!
|
||||||
|
// We'll try to clean it up for you but you really should fix this.
|
||||||
|
Logger.LogWarn($"A resource of type {GetType().Name} was not Disposed.");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Dispose(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Can be defined on your struct type to enable simplified vertex input state definition.
|
||||||
|
/// </summary>
|
||||||
public interface IVertexType
|
public interface IVertexType
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An ordered list of the types in your vertex struct.
|
||||||
|
/// </summary>
|
||||||
static abstract VertexElementFormat[] Formats { get; }
|
static abstract VertexElementFormat[] Formats { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing unsigned normalized values, ranging from 0 to 1, using
|
/// Packed vector type containing unsigned normalized values, ranging from 0 to 1, using
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing four 8-bit unsigned integer values, ranging from 0 to 255.
|
/// Packed vector type containing four 8-bit unsigned integer values, ranging from 0 to 255.
|
||||||
|
|
|
@ -19,7 +19,7 @@ using System;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
public struct HalfSingle : IPackedVector<ushort>, IEquatable<HalfSingle>, IPackedVector
|
public struct HalfSingle : IPackedVector<ushort>, IEquatable<HalfSingle>, IPackedVector
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,7 +19,7 @@ using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
internal static class HalfTypeHelper
|
internal static class HalfTypeHelper
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,7 +19,7 @@ using System;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
public struct HalfVector2 : IPackedVector<uint>, IPackedVector, IEquatable<HalfVector2>
|
public struct HalfVector2 : IPackedVector<uint>, IPackedVector, IEquatable<HalfVector2>
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,7 +19,7 @@ using System;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing four 16-bit floating-point values.
|
/// Packed vector type containing four 16-bit floating-point values.
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
// http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.packedvector.ipackedvector.aspx
|
// http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.packedvector.ipackedvector.aspx
|
||||||
public interface IPackedVector
|
public interface IPackedVector
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
public struct NormalizedByte2 : IPackedVector<ushort>, IEquatable<NormalizedByte2>
|
public struct NormalizedByte2 : IPackedVector<ushort>, IEquatable<NormalizedByte2>
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
public struct NormalizedByte4 : IPackedVector<uint>, IEquatable<NormalizedByte4>
|
public struct NormalizedByte4 : IPackedVector<uint>, IEquatable<NormalizedByte4>
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
public struct NormalizedShort2 : IPackedVector<uint>, IEquatable<NormalizedShort2>
|
public struct NormalizedShort2 : IPackedVector<uint>, IEquatable<NormalizedShort2>
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
public struct NormalizedShort4 : IPackedVector<ulong>, IEquatable<NormalizedShort4>
|
public struct NormalizedShort4 : IPackedVector<ulong>, IEquatable<NormalizedShort4>
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
public struct Short2 : IPackedVector<uint>, IEquatable<Short2>
|
public struct Short2 : IPackedVector<uint>, IEquatable<Short2>
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
||||||
using MoonWorks.Math.Float;
|
using MoonWorks.Math.Float;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics.PackedVector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packed vector type containing four 16-bit signed integer values.
|
/// Packed vector type containing four 16-bit signed integer values.
|
||||||
|
|
|
@ -2,11 +2,31 @@
|
||||||
|
|
||||||
namespace MoonWorks
|
namespace MoonWorks
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Presentation mode for a window.
|
||||||
|
/// </summary>
|
||||||
public enum PresentMode
|
public enum PresentMode
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Does not wait for v-blank to update the window. Can cause visible tearing.
|
||||||
|
/// </summary>
|
||||||
Immediate,
|
Immediate,
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for v-blank and uses a queue to hold present requests.
|
||||||
|
/// Allows for low latency while preventing tearing.
|
||||||
|
/// May not be supported on non-Vulkan non-Linux systems or older hardware.
|
||||||
|
/// </summary>
|
||||||
Mailbox,
|
Mailbox,
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for v-blank and adds present requests to a queue.
|
||||||
|
/// Will probably cause latency.
|
||||||
|
/// Required to be supported by all compliant hardware.
|
||||||
|
/// </summary>
|
||||||
FIFO,
|
FIFO,
|
||||||
|
/// <summary>
|
||||||
|
/// Usually waits for v-blank, but if v-blank has passed since last update will update immediately.
|
||||||
|
/// May cause visible tearing.
|
||||||
|
/// </summary>
|
||||||
FIFORelaxed
|
FIFORelaxed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace MoonWorks.Graphics;
|
||||||
|
|
||||||
|
public abstract class RefreshResource : GraphicsResource
|
||||||
|
{
|
||||||
|
public IntPtr Handle { get => handle; internal set => handle = value; }
|
||||||
|
private IntPtr handle;
|
||||||
|
|
||||||
|
protected abstract Action<IntPtr, IntPtr> QueueDestroyFunction { get; }
|
||||||
|
|
||||||
|
protected RefreshResource(GraphicsDevice device) : base(device)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
// Atomically call destroy function in case this is called from the finalizer thread
|
||||||
|
var toDispose = Interlocked.Exchange(ref handle, IntPtr.Zero);
|
||||||
|
if (toDispose != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
QueueDestroyFunction(Device.Handle, toDispose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
|
@ -127,12 +127,12 @@ namespace MoonWorks.Graphics
|
||||||
public uint Stride;
|
public uint Stride;
|
||||||
public VertexInputRate InputRate;
|
public VertexInputRate InputRate;
|
||||||
|
|
||||||
public static VertexBinding Create<T>(uint binding = 0) where T : unmanaged
|
public static VertexBinding Create<T>(uint binding = 0, VertexInputRate inputRate = VertexInputRate.Vertex) where T : unmanaged
|
||||||
{
|
{
|
||||||
return new VertexBinding
|
return new VertexBinding
|
||||||
{
|
{
|
||||||
Binding = binding,
|
Binding = binding,
|
||||||
InputRate = VertexInputRate.Vertex,
|
InputRate = inputRate,
|
||||||
Stride = (uint) Marshal.SizeOf<T>()
|
Stride = (uint) Marshal.SizeOf<T>()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ namespace MoonWorks.Graphics
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Buffers are generic data containers that can be used by the GPU.
|
/// Buffers are generic data containers that can be used by the GPU.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Buffer : GraphicsResource
|
public class Buffer : RefreshResource
|
||||||
{
|
{
|
||||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyBuffer;
|
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyBuffer;
|
||||||
|
|
||||||
|
@ -58,17 +58,29 @@ namespace MoonWorks.Graphics
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads data out of a buffer and into an array.
|
/// Reads data out of a buffer and into a span.
|
||||||
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait is called first.
|
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait or GraphicsDevice.WaitForFences is called first.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The array that data will be copied to.</param>
|
/// <param name="data">The span that data will be copied to.</param>
|
||||||
/// <param name="dataLengthInBytes">The length of the data to read.</param>
|
/// <param name="dataLengthInBytes">The length of the data to read.</param>
|
||||||
public unsafe void GetData<T>(
|
public unsafe void GetData<T>(
|
||||||
T[] data,
|
Span<T> data,
|
||||||
uint dataLengthInBytes
|
uint dataLengthInBytes
|
||||||
) where T : unmanaged
|
) where T : unmanaged
|
||||||
{
|
{
|
||||||
fixed (T* ptr = &data[0])
|
#if DEBUG
|
||||||
|
if (dataLengthInBytes > Size)
|
||||||
|
{
|
||||||
|
Logger.LogWarn("Requested too many bytes from buffer!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLengthInBytes > data.Length * Marshal.SizeOf<T>())
|
||||||
|
{
|
||||||
|
Logger.LogWarn("Data length is larger than the provided Span!");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
fixed (T* ptr = data)
|
||||||
{
|
{
|
||||||
Refresh.Refresh_GetBufferData(
|
Refresh.Refresh_GetBufferData(
|
||||||
Device.Handle,
|
Device.Handle,
|
||||||
|
@ -79,6 +91,48 @@ namespace MoonWorks.Graphics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads data out of a buffer and into an array.
|
||||||
|
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait or GraphicsDevice.WaitForFences is called first.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The span that data will be copied to.</param>
|
||||||
|
/// <param name="dataLengthInBytes">The length of the data to read.</param>
|
||||||
|
public unsafe void GetData<T>(
|
||||||
|
T[] data,
|
||||||
|
uint dataLengthInBytes
|
||||||
|
) where T : unmanaged
|
||||||
|
{
|
||||||
|
GetData(new Span<T>(data), dataLengthInBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads data out of a buffer and into a span.
|
||||||
|
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait or GraphicsDevice.WaitForFences is called first.
|
||||||
|
/// Fills the span with as much data from the buffer as it can.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The span that data will be copied to.</param>
|
||||||
|
public unsafe void GetData<T>(
|
||||||
|
Span<T> data
|
||||||
|
) where T : unmanaged
|
||||||
|
{
|
||||||
|
var lengthInBytes = System.Math.Min(data.Length * Marshal.SizeOf<T>(), Size);
|
||||||
|
GetData(data, (uint) lengthInBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads data out of a buffer and into an array.
|
||||||
|
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait or GraphicsDevice.WaitForFences is called first.
|
||||||
|
/// Fills the array with as much data from the buffer as it can.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The span that data will be copied to.</param>
|
||||||
|
public unsafe void GetData<T>(
|
||||||
|
T[] data
|
||||||
|
) where T : unmanaged
|
||||||
|
{
|
||||||
|
var lengthInBytes = System.Math.Min(data.Length * Marshal.SizeOf<T>(), Size);
|
||||||
|
GetData(new Span<T>(data), (uint) lengthInBytes);
|
||||||
|
}
|
||||||
|
|
||||||
public static implicit operator BufferBinding(Buffer b)
|
public static implicit operator BufferBinding(Buffer b)
|
||||||
{
|
{
|
||||||
return new BufferBinding(b, 0);
|
return new BufferBinding(b, 0);
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
using RefreshCS;
|
using RefreshCS;
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
public class ComputePipeline : GraphicsResource
|
/// <summary>
|
||||||
|
/// Compute pipelines perform arbitrary parallel processing on input data.
|
||||||
|
/// </summary>
|
||||||
|
public class ComputePipeline : RefreshResource
|
||||||
{
|
{
|
||||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyComputePipeline;
|
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyComputePipeline;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
using System;
|
||||||
|
using RefreshCS;
|
||||||
|
|
||||||
|
namespace MoonWorks.Graphics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fences allow you to track the status of a submitted command buffer. <br/>
|
||||||
|
/// You should only acquire a Fence if you will need to track the command buffer. <br/>
|
||||||
|
/// You should make sure to call GraphicsDevice.ReleaseFence when done with a Fence to avoid memory growth. <br/>
|
||||||
|
/// The Fence object itself is basically just a wrapper for the Refresh_Fence. <br/>
|
||||||
|
/// The internal handle is replaced so that we can pool Fence objects to manage garbage.
|
||||||
|
/// </summary>
|
||||||
|
public class Fence : RefreshResource
|
||||||
|
{
|
||||||
|
protected override Action<nint, nint> QueueDestroyFunction => Refresh.Refresh_ReleaseFence;
|
||||||
|
|
||||||
|
internal Fence(GraphicsDevice device) : base(device)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetHandle(nint handle)
|
||||||
|
{
|
||||||
|
Handle = handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,10 +5,10 @@ using RefreshCS;
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Graphics pipelines encapsulate all of the render state in a single object.
|
/// Graphics pipelines encapsulate all of the render state in a single object. <br/>
|
||||||
/// These pipelines are bound before draw calls are issued.
|
/// These pipelines are bound before draw calls are issued.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GraphicsPipeline : GraphicsResource
|
public class GraphicsPipeline : RefreshResource
|
||||||
{
|
{
|
||||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyGraphicsPipeline;
|
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyGraphicsPipeline;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace MoonWorks.Graphics
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A sampler specifies how a texture will be sampled in a shader.
|
/// A sampler specifies how a texture will be sampled in a shader.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Sampler : GraphicsResource
|
public class Sampler : RefreshResource
|
||||||
{
|
{
|
||||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroySampler;
|
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroySampler;
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
using RefreshCS;
|
using RefreshCS;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shader modules expect input in Refresh bytecode format.
|
/// Shader modules expect input in Refresh bytecode format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ShaderModule : GraphicsResource
|
public class ShaderModule : RefreshResource
|
||||||
{
|
{
|
||||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyShaderModule;
|
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyShaderModule;
|
||||||
|
|
||||||
public unsafe ShaderModule(GraphicsDevice device, string filePath) : base(device)
|
public unsafe ShaderModule(GraphicsDevice device, string filePath) : base(device)
|
||||||
{
|
{
|
||||||
using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||||
{
|
Handle = CreateFromStream(device, stream);
|
||||||
Handle = CreateFromStream(device, stream);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe ShaderModule(GraphicsDevice device, Stream stream) : base(device)
|
public unsafe ShaderModule(GraphicsDevice device, Stream stream) : base(device)
|
||||||
|
@ -24,19 +23,20 @@ namespace MoonWorks.Graphics
|
||||||
Handle = CreateFromStream(device, stream);
|
Handle = CreateFromStream(device, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe static IntPtr CreateFromStream(GraphicsDevice device, Stream stream)
|
private static unsafe IntPtr CreateFromStream(GraphicsDevice device, Stream stream)
|
||||||
{
|
{
|
||||||
var bytecode = new byte[stream.Length];
|
var bytecodeBuffer = NativeMemory.Alloc((nuint) stream.Length);
|
||||||
stream.Read(bytecode, 0, (int) stream.Length);
|
var bytecodeSpan = new Span<byte>(bytecodeBuffer, (int) stream.Length);
|
||||||
|
stream.ReadExactly(bytecodeSpan);
|
||||||
|
|
||||||
fixed (byte* ptr = bytecode)
|
Refresh.ShaderModuleCreateInfo shaderModuleCreateInfo;
|
||||||
{
|
shaderModuleCreateInfo.codeSize = (nuint) stream.Length;
|
||||||
Refresh.ShaderModuleCreateInfo shaderModuleCreateInfo;
|
shaderModuleCreateInfo.byteCode = (nint) bytecodeBuffer;
|
||||||
shaderModuleCreateInfo.codeSize = (UIntPtr) bytecode.Length;
|
|
||||||
shaderModuleCreateInfo.byteCode = (IntPtr) ptr;
|
|
||||||
|
|
||||||
return Refresh.Refresh_CreateShaderModule(device.Handle, shaderModuleCreateInfo);
|
var shaderModule = Refresh.Refresh_CreateShaderModule(device.Handle, shaderModuleCreateInfo);
|
||||||
}
|
|
||||||
|
NativeMemory.Free(bytecodeBuffer);
|
||||||
|
return shaderModule;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using RefreshCS;
|
using RefreshCS;
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
|
@ -7,7 +8,7 @@ namespace MoonWorks.Graphics
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A container for pixel data.
|
/// A container for pixel data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Texture : GraphicsResource
|
public class Texture : RefreshResource
|
||||||
{
|
{
|
||||||
public uint Width { get; internal set; }
|
public uint Width { get; internal set; }
|
||||||
public uint Height { get; internal set; }
|
public uint Height { get; internal set; }
|
||||||
|
@ -17,104 +18,165 @@ namespace MoonWorks.Graphics
|
||||||
public uint LevelCount { get; }
|
public uint LevelCount { get; }
|
||||||
public SampleCount SampleCount { get; }
|
public SampleCount SampleCount { get; }
|
||||||
public TextureUsageFlags UsageFlags { get; }
|
public TextureUsageFlags UsageFlags { get; }
|
||||||
|
public uint Size { get; }
|
||||||
|
|
||||||
// FIXME: this allocates a delegate instance
|
// FIXME: this allocates a delegate instance
|
||||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyTexture;
|
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyTexture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a PNG from a file path.
|
/// Creates a 2D Texture using PNG or QOI data from raw byte data.
|
||||||
/// NOTE: You can queue as many of these as you want on to a command buffer but it MUST be submitted!
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="device"></param>
|
public static unsafe Texture FromImageBytes(
|
||||||
/// <param name="commandBuffer"></param>
|
GraphicsDevice device,
|
||||||
/// <param name="filePath"></param>
|
CommandBuffer commandBuffer,
|
||||||
/// <returns></returns>
|
Span<byte> data
|
||||||
public static Texture LoadPNG(GraphicsDevice device, CommandBuffer commandBuffer, string filePath)
|
) {
|
||||||
{
|
Texture texture;
|
||||||
var pixels = Refresh.Refresh_Image_Load(
|
|
||||||
filePath,
|
|
||||||
out var width,
|
|
||||||
out var height,
|
|
||||||
out var channels
|
|
||||||
);
|
|
||||||
|
|
||||||
var byteCount = (uint) (width * height * channels);
|
fixed (byte *dataPtr = data)
|
||||||
|
{
|
||||||
|
var pixels = Refresh.Refresh_Image_Load((nint) dataPtr, data.Length, out var width, out var height, out var len);
|
||||||
|
|
||||||
TextureCreateInfo textureCreateInfo = new TextureCreateInfo();
|
TextureCreateInfo textureCreateInfo = new TextureCreateInfo();
|
||||||
textureCreateInfo.Width = (uint) width;
|
textureCreateInfo.Width = (uint) width;
|
||||||
textureCreateInfo.Height = (uint) height;
|
textureCreateInfo.Height = (uint) height;
|
||||||
textureCreateInfo.Depth = 1;
|
textureCreateInfo.Depth = 1;
|
||||||
textureCreateInfo.Format = TextureFormat.R8G8B8A8;
|
textureCreateInfo.Format = TextureFormat.R8G8B8A8;
|
||||||
textureCreateInfo.IsCube = false;
|
textureCreateInfo.IsCube = false;
|
||||||
textureCreateInfo.LevelCount = 1;
|
textureCreateInfo.LevelCount = 1;
|
||||||
textureCreateInfo.SampleCount = SampleCount.One;
|
textureCreateInfo.SampleCount = SampleCount.One;
|
||||||
textureCreateInfo.UsageFlags = TextureUsageFlags.Sampler;
|
textureCreateInfo.UsageFlags = TextureUsageFlags.Sampler;
|
||||||
|
|
||||||
var texture = new Texture(device, textureCreateInfo);
|
texture = new Texture(device, textureCreateInfo);
|
||||||
commandBuffer.SetTextureData(texture, pixels, byteCount);
|
commandBuffer.SetTextureData(texture, pixels, (uint) len);
|
||||||
|
|
||||||
Refresh.Refresh_Image_Free(pixels);
|
Refresh.Refresh_Image_Free(pixels);
|
||||||
|
}
|
||||||
|
|
||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves RGBA or BGRA pixel data to a file in PNG format.
|
/// Creates a 2D Texture using PNG or QOI data from a stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe static void SavePNG(string path, int width, int height, TextureFormat format, byte[] pixels)
|
public static unsafe Texture FromImageStream(
|
||||||
{
|
GraphicsDevice device,
|
||||||
if (format != TextureFormat.R8G8B8A8 && format != TextureFormat.B8G8R8A8)
|
CommandBuffer commandBuffer,
|
||||||
{
|
Stream stream
|
||||||
throw new ArgumentException("Texture format must be RGBA8 or BGRA8!", "format");
|
) {
|
||||||
}
|
var length = stream.Length;
|
||||||
|
var buffer = NativeMemory.Alloc((nuint) length);
|
||||||
|
var span = new Span<byte>(buffer, (int) length);
|
||||||
|
stream.ReadExactly(span);
|
||||||
|
|
||||||
fixed (byte* ptr = &pixels[0])
|
var texture = FromImageBytes(device, commandBuffer, span);
|
||||||
|
|
||||||
|
NativeMemory.Free((void*) buffer);
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a 2D Texture using PNG or QOI data from a file.
|
||||||
|
/// </summary>
|
||||||
|
public static Texture FromImageFile(
|
||||||
|
GraphicsDevice device,
|
||||||
|
CommandBuffer commandBuffer,
|
||||||
|
string path
|
||||||
|
) {
|
||||||
|
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
|
return FromImageStream(device, commandBuffer, fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe void SetDataFromImageBytes(
|
||||||
|
CommandBuffer commandBuffer,
|
||||||
|
TextureSlice textureSlice,
|
||||||
|
Span<byte> data
|
||||||
|
) {
|
||||||
|
fixed (byte* ptr = data)
|
||||||
{
|
{
|
||||||
Refresh.Refresh_Image_SavePNG(path, width, height, Conversions.BoolToByte(format == TextureFormat.B8G8R8A8), (IntPtr) ptr);
|
var pixels = Refresh.Refresh_Image_Load(
|
||||||
|
(nint) ptr,
|
||||||
|
(int) data.Length,
|
||||||
|
out var w,
|
||||||
|
out var h,
|
||||||
|
out var len
|
||||||
|
);
|
||||||
|
|
||||||
|
commandBuffer.SetTextureData(textureSlice, pixels, (uint) len);
|
||||||
|
|
||||||
|
Refresh.Refresh_Image_Free(pixels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Texture LoadDDS(GraphicsDevice graphicsDevice, CommandBuffer commandBuffer, System.IO.Stream stream)
|
/// <summary>
|
||||||
|
/// Sets data for a texture slice using PNG or QOI data from a stream.
|
||||||
|
/// </summary>
|
||||||
|
public static unsafe void SetDataFromImageStream(
|
||||||
|
CommandBuffer commandBuffer,
|
||||||
|
TextureSlice textureSlice,
|
||||||
|
Stream stream
|
||||||
|
) {
|
||||||
|
var length = stream.Length;
|
||||||
|
var buffer = NativeMemory.Alloc((nuint) length);
|
||||||
|
var span = new Span<byte>(buffer, (int) length);
|
||||||
|
stream.ReadExactly(span);
|
||||||
|
|
||||||
|
SetDataFromImageBytes(commandBuffer, textureSlice, span);
|
||||||
|
|
||||||
|
NativeMemory.Free((void*) buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets data for a texture slice using PNG or QOI data from a file.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetDataFromImageFile(
|
||||||
|
CommandBuffer commandBuffer,
|
||||||
|
TextureSlice textureSlice,
|
||||||
|
string path
|
||||||
|
) {
|
||||||
|
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
|
SetDataFromImageStream(commandBuffer, textureSlice, fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe static Texture LoadDDS(GraphicsDevice graphicsDevice, CommandBuffer commandBuffer, System.IO.Stream stream)
|
||||||
{
|
{
|
||||||
using (var reader = new BinaryReader(stream))
|
using var reader = new BinaryReader(stream);
|
||||||
|
Texture texture;
|
||||||
|
int faces;
|
||||||
|
ParseDDS(reader, out var format, out var width, out var height, out var levels, out var isCube);
|
||||||
|
|
||||||
|
if (isCube)
|
||||||
{
|
{
|
||||||
Texture texture;
|
texture = CreateTextureCube(graphicsDevice, (uint) width, format, TextureUsageFlags.Sampler, (uint) levels);
|
||||||
int faces;
|
faces = 6;
|
||||||
ParseDDS(reader, out var format, out var width, out var height, out var levels, out var isCube);
|
|
||||||
|
|
||||||
if (isCube)
|
|
||||||
{
|
|
||||||
texture = CreateTextureCube(graphicsDevice, (uint) width, format, TextureUsageFlags.Sampler, (uint) levels);
|
|
||||||
faces = 6;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
texture = CreateTexture2D(graphicsDevice, (uint) width, (uint) height, format, TextureUsageFlags.Sampler, (uint) levels);
|
|
||||||
faces = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < faces; i += 1)
|
|
||||||
{
|
|
||||||
for (int j = 0; j < levels; j += 1)
|
|
||||||
{
|
|
||||||
var levelWidth = width >> j;
|
|
||||||
var levelHeight = height >> j;
|
|
||||||
|
|
||||||
var pixels = reader.ReadBytes(
|
|
||||||
Texture.CalculateDDSLevelSize(
|
|
||||||
levelWidth,
|
|
||||||
levelHeight,
|
|
||||||
format
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
var textureSlice = new TextureSlice(texture, new Rect(0, 0, levelWidth, levelHeight), 0, (uint) i, (uint) j);
|
|
||||||
commandBuffer.SetTextureData(textureSlice, pixels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return texture;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
texture = CreateTexture2D(graphicsDevice, (uint) width, (uint) height, format, TextureUsageFlags.Sampler, (uint) levels);
|
||||||
|
faces = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < faces; i += 1)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < levels; j += 1)
|
||||||
|
{
|
||||||
|
var levelWidth = width >> j;
|
||||||
|
var levelHeight = height >> j;
|
||||||
|
|
||||||
|
var levelSize = CalculateDDSLevelSize(levelWidth, levelHeight, format);
|
||||||
|
var byteBuffer = NativeMemory.Alloc((nuint) levelSize);
|
||||||
|
var byteSpan = new Span<byte>(byteBuffer, levelSize);
|
||||||
|
stream.ReadExactly(byteSpan);
|
||||||
|
|
||||||
|
var textureSlice = new TextureSlice(texture, new Rect(0, 0, levelWidth, levelHeight), 0, (uint) i, (uint) j);
|
||||||
|
commandBuffer.SetTextureData(textureSlice, (nint) byteBuffer, (uint) levelSize);
|
||||||
|
|
||||||
|
NativeMemory.Free(byteBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -235,6 +297,7 @@ namespace MoonWorks.Graphics
|
||||||
LevelCount = textureCreateInfo.LevelCount;
|
LevelCount = textureCreateInfo.LevelCount;
|
||||||
SampleCount = textureCreateInfo.SampleCount;
|
SampleCount = textureCreateInfo.SampleCount;
|
||||||
UsageFlags = textureCreateInfo.UsageFlags;
|
UsageFlags = textureCreateInfo.UsageFlags;
|
||||||
|
Size = Width * Height * BytesPerPixel(Format) / BlockSizeSquared(Format);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static implicit operator TextureSlice(Texture t) => new TextureSlice(t);
|
public static implicit operator TextureSlice(Texture t) => new TextureSlice(t);
|
||||||
|
@ -243,22 +306,20 @@ namespace MoonWorks.Graphics
|
||||||
// Should not be tracked, because swapchain textures are managed by Vulkan.
|
// Should not be tracked, because swapchain textures are managed by Vulkan.
|
||||||
internal Texture(
|
internal Texture(
|
||||||
GraphicsDevice device,
|
GraphicsDevice device,
|
||||||
IntPtr handle,
|
TextureFormat format
|
||||||
TextureFormat format,
|
) : base(device)
|
||||||
uint width,
|
|
||||||
uint height
|
|
||||||
) : base(device, false)
|
|
||||||
{
|
{
|
||||||
Handle = handle;
|
Handle = IntPtr.Zero;
|
||||||
|
|
||||||
Format = format;
|
Format = format;
|
||||||
Width = width;
|
Width = 0;
|
||||||
Height = height;
|
Height = 0;
|
||||||
Depth = 1;
|
Depth = 1;
|
||||||
IsCube = false;
|
IsCube = false;
|
||||||
LevelCount = 1;
|
LevelCount = 1;
|
||||||
SampleCount = SampleCount.One;
|
SampleCount = SampleCount.One;
|
||||||
UsageFlags = TextureUsageFlags.ColorTarget;
|
UsageFlags = TextureUsageFlags.ColorTarget;
|
||||||
|
Size = Width * Height * BytesPerPixel(Format) / BlockSizeSquared(Format);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DDS loading extension, based on MojoDDS
|
// DDS loading extension, based on MojoDDS
|
||||||
|
@ -528,5 +589,155 @@ namespace MoonWorks.Graphics
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously saves RGBA or BGRA pixel data to a file in PNG format. <br/>
|
||||||
|
/// Warning: this is expensive and will block to wait for data download from GPU! <br/>
|
||||||
|
/// You can avoid blocking by calling this method from a thread.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe void SavePNG(string path)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (Format != TextureFormat.R8G8B8A8 && Format != TextureFormat.B8G8R8A8)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Texture format must be RGBA or BGRA!", "format");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var buffer = new Buffer(Device, 0, Width * Height * 4); // this creates garbage... oh well
|
||||||
|
|
||||||
|
// immediately request the data copy
|
||||||
|
var commandBuffer = Device.AcquireCommandBuffer();
|
||||||
|
commandBuffer.CopyTextureToBuffer(this, buffer);
|
||||||
|
var fence = Device.SubmitAndAcquireFence(commandBuffer);
|
||||||
|
|
||||||
|
var byteCount = buffer.Size;
|
||||||
|
|
||||||
|
var pixelsPtr = NativeMemory.Alloc((nuint) byteCount);
|
||||||
|
var pixelsSpan = new Span<byte>(pixelsPtr, (int) byteCount);
|
||||||
|
|
||||||
|
Device.WaitForFences(fence); // make sure the data transfer is done...
|
||||||
|
Device.ReleaseFence(fence); // and then release the fence
|
||||||
|
|
||||||
|
buffer.GetData(pixelsSpan);
|
||||||
|
|
||||||
|
if (Format == TextureFormat.B8G8R8A8)
|
||||||
|
{
|
||||||
|
var rgbaPtr = NativeMemory.Alloc((nuint) byteCount);
|
||||||
|
var rgbaSpan = new Span<byte>(rgbaPtr, (int) byteCount);
|
||||||
|
|
||||||
|
for (var i = 0; i < byteCount; i += 4)
|
||||||
|
{
|
||||||
|
rgbaSpan[i] = pixelsSpan[i + 2];
|
||||||
|
rgbaSpan[i + 1] = pixelsSpan[i + 1];
|
||||||
|
rgbaSpan[i + 2] = pixelsSpan[i];
|
||||||
|
rgbaSpan[i + 3] = pixelsSpan[i + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
Refresh.Refresh_Image_SavePNG(path, (nint) rgbaPtr, (int) Width, (int) Height);
|
||||||
|
|
||||||
|
NativeMemory.Free((void*) rgbaPtr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fixed (byte* ptr = pixelsSpan)
|
||||||
|
{
|
||||||
|
Refresh.Refresh_Image_SavePNG(path, (nint) ptr, (int) Width, (int) Height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeMemory.Free(pixelsPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint BytesPerPixel(TextureFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case TextureFormat.R8:
|
||||||
|
case TextureFormat.R8_UINT:
|
||||||
|
return 1;
|
||||||
|
case TextureFormat.R5G6B5:
|
||||||
|
case TextureFormat.B4G4R4A4:
|
||||||
|
case TextureFormat.A1R5G5B5:
|
||||||
|
case TextureFormat.R16_SFLOAT:
|
||||||
|
case TextureFormat.R8G8_SNORM:
|
||||||
|
case TextureFormat.R8G8_UINT:
|
||||||
|
case TextureFormat.R16_UINT:
|
||||||
|
case TextureFormat.D16:
|
||||||
|
return 2;
|
||||||
|
case TextureFormat.D16S8:
|
||||||
|
return 3;
|
||||||
|
case TextureFormat.R8G8B8A8:
|
||||||
|
case TextureFormat.B8G8R8A8:
|
||||||
|
case TextureFormat.R32_SFLOAT:
|
||||||
|
case TextureFormat.R16G16:
|
||||||
|
case TextureFormat.R16G16_SFLOAT:
|
||||||
|
case TextureFormat.R8G8B8A8_SNORM:
|
||||||
|
case TextureFormat.A2R10G10B10:
|
||||||
|
case TextureFormat.R8G8B8A8_UINT:
|
||||||
|
case TextureFormat.R16G16_UINT:
|
||||||
|
case TextureFormat.D32:
|
||||||
|
return 4;
|
||||||
|
case TextureFormat.D32S8:
|
||||||
|
return 5;
|
||||||
|
case TextureFormat.R16G16B16A16_SFLOAT:
|
||||||
|
case TextureFormat.R16G16B16A16:
|
||||||
|
case TextureFormat.R32G32_SFLOAT:
|
||||||
|
case TextureFormat.R16G16B16A16_UINT:
|
||||||
|
case TextureFormat.BC1:
|
||||||
|
return 8;
|
||||||
|
case TextureFormat.R32G32B32A32_SFLOAT:
|
||||||
|
case TextureFormat.BC2:
|
||||||
|
case TextureFormat.BC3:
|
||||||
|
case TextureFormat.BC7:
|
||||||
|
return 16;
|
||||||
|
default:
|
||||||
|
Logger.LogError("Texture format not recognized!");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint BlockSizeSquared(TextureFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case TextureFormat.BC1:
|
||||||
|
case TextureFormat.BC2:
|
||||||
|
case TextureFormat.BC3:
|
||||||
|
case TextureFormat.BC7:
|
||||||
|
return 16;
|
||||||
|
case TextureFormat.R8G8B8A8:
|
||||||
|
case TextureFormat.B8G8R8A8:
|
||||||
|
case TextureFormat.R5G6B5:
|
||||||
|
case TextureFormat.A1R5G5B5:
|
||||||
|
case TextureFormat.B4G4R4A4:
|
||||||
|
case TextureFormat.A2R10G10B10:
|
||||||
|
case TextureFormat.R16G16:
|
||||||
|
case TextureFormat.R16G16B16A16:
|
||||||
|
case TextureFormat.R8:
|
||||||
|
case TextureFormat.R8G8_SNORM:
|
||||||
|
case TextureFormat.R8G8B8A8_SNORM:
|
||||||
|
case TextureFormat.R16_SFLOAT:
|
||||||
|
case TextureFormat.R16G16_SFLOAT:
|
||||||
|
case TextureFormat.R16G16B16A16_SFLOAT:
|
||||||
|
case TextureFormat.R32_SFLOAT:
|
||||||
|
case TextureFormat.R32G32_SFLOAT:
|
||||||
|
case TextureFormat.R32G32B32A32_SFLOAT:
|
||||||
|
case TextureFormat.R8_UINT:
|
||||||
|
case TextureFormat.R8G8_UINT:
|
||||||
|
case TextureFormat.R8G8B8A8_UINT:
|
||||||
|
case TextureFormat.R16_UINT:
|
||||||
|
case TextureFormat.R16G16_UINT:
|
||||||
|
case TextureFormat.R16G16B16A16_UINT:
|
||||||
|
case TextureFormat.D16:
|
||||||
|
case TextureFormat.D32:
|
||||||
|
case TextureFormat.D16S8:
|
||||||
|
case TextureFormat.D32S8:
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
Logger.LogError("Texture format not recognized!");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines how color blending will be performed in a GraphicsPipeline.
|
||||||
|
/// </summary>
|
||||||
public struct ColorAttachmentBlendState
|
public struct ColorAttachmentBlendState
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Information that the pipeline needs about a shader.
|
/// Information that the compute pipeline needs about a compute shader.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct ComputeShaderInfo
|
public struct ComputeShaderInfo
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All of the information that is used to create a GraphicsPipeline.
|
||||||
|
/// </summary>
|
||||||
public struct GraphicsPipelineCreateInfo
|
public struct GraphicsPipelineCreateInfo
|
||||||
{
|
{
|
||||||
public DepthStencilState DepthStencilState;
|
public DepthStencilState DepthStencilState;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Information that the pipeline needs about a shader.
|
/// Information that the pipeline needs about a graphics shader.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct GraphicsShaderInfo
|
public struct GraphicsShaderInfo
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,21 +2,60 @@
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All of the information that is used to create a sampler.
|
||||||
|
/// </summary>
|
||||||
public struct SamplerCreateInfo
|
public struct SamplerCreateInfo
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minification filter mode. Used when the image is downscaled.
|
||||||
|
/// </summary>
|
||||||
public Filter MinFilter;
|
public Filter MinFilter;
|
||||||
|
/// <summary>
|
||||||
|
/// Magnification filter mode. Used when the image is upscaled.
|
||||||
|
/// </summary>
|
||||||
public Filter MagFilter;
|
public Filter MagFilter;
|
||||||
|
/// <summary>
|
||||||
|
/// Filter mode applied to mipmap lookups.
|
||||||
|
/// </summary>
|
||||||
public SamplerMipmapMode MipmapMode;
|
public SamplerMipmapMode MipmapMode;
|
||||||
|
/// <summary>
|
||||||
|
/// Horizontal addressing mode.
|
||||||
|
/// </summary>
|
||||||
public SamplerAddressMode AddressModeU;
|
public SamplerAddressMode AddressModeU;
|
||||||
|
/// <summary>
|
||||||
|
/// Vertical addressing mode.
|
||||||
|
/// </summary>
|
||||||
public SamplerAddressMode AddressModeV;
|
public SamplerAddressMode AddressModeV;
|
||||||
|
/// <summary>
|
||||||
|
/// Depth addressing mode.
|
||||||
|
/// </summary>
|
||||||
public SamplerAddressMode AddressModeW;
|
public SamplerAddressMode AddressModeW;
|
||||||
|
/// <summary>
|
||||||
|
/// Bias value added to mipmap level of detail calculation.
|
||||||
|
/// </summary>
|
||||||
public float MipLodBias;
|
public float MipLodBias;
|
||||||
|
/// <summary>
|
||||||
|
/// Enables anisotropic filtering.
|
||||||
|
/// </summary>
|
||||||
public bool AnisotropyEnable;
|
public bool AnisotropyEnable;
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum anisotropy value.
|
||||||
|
/// </summary>
|
||||||
public float MaxAnisotropy;
|
public float MaxAnisotropy;
|
||||||
public bool CompareEnable;
|
public bool CompareEnable;
|
||||||
public CompareOp CompareOp;
|
public CompareOp CompareOp;
|
||||||
|
/// <summary>
|
||||||
|
/// Clamps the LOD value to a specified minimum.
|
||||||
|
/// </summary>
|
||||||
public float MinLod;
|
public float MinLod;
|
||||||
|
/// <summary>
|
||||||
|
/// Clamps the LOD value to a specified maximum.
|
||||||
|
/// </summary>
|
||||||
public float MaxLod;
|
public float MaxLod;
|
||||||
|
/// <summary>
|
||||||
|
/// If an address mode is set to ClampToBorder, will replace color with this color when samples are outside the [0, 1) range.
|
||||||
|
/// </summary>
|
||||||
public BorderColor BorderColor;
|
public BorderColor BorderColor;
|
||||||
|
|
||||||
public static readonly SamplerCreateInfo AnisotropicClamp = new SamplerCreateInfo
|
public static readonly SamplerCreateInfo AnisotropicClamp = new SamplerCreateInfo
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All of the information that is used to create a texture.
|
||||||
|
/// </summary>
|
||||||
public struct TextureCreateInfo
|
public struct TextureCreateInfo
|
||||||
{
|
{
|
||||||
public uint Width;
|
public uint Width;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specifies how to interpet vertex data in a buffer to be passed to the vertex shader.
|
/// Specifies how the vertex shader will interpet vertex data in a buffer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct VertexInputState
|
public struct VertexInputState
|
||||||
{
|
{
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,34 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 1, binding = 0) uniform sampler2D msdf;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 inTexCoord;
|
||||||
|
layout(location = 1) in vec4 inColor;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
layout(binding = 0, set = 3) uniform UBO
|
||||||
|
{
|
||||||
|
float pxRange;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
float median(float r, float g, float b)
|
||||||
|
{
|
||||||
|
return max(min(r, g), min(max(r, g), b));
|
||||||
|
}
|
||||||
|
|
||||||
|
float screenPxRange()
|
||||||
|
{
|
||||||
|
vec2 unitRange = vec2(ubo.pxRange)/vec2(textureSize(msdf, 0));
|
||||||
|
vec2 screenTexSize = vec2(1.0)/fwidth(inTexCoord);
|
||||||
|
return max(0.5*dot(unitRange, screenTexSize), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec3 msd = texture(msdf, inTexCoord).rgb;
|
||||||
|
float sd = median(msd.r, msd.g, msd.b);
|
||||||
|
float screenPxDistance = screenPxRange() * (sd - 0.5);
|
||||||
|
float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
|
||||||
|
outColor = mix(vec4(0.0, 0.0, 0.0, 0.0), inColor, opacity);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 inPos;
|
||||||
|
layout(location = 1) in vec2 inTexCoord;
|
||||||
|
layout(location = 2) in vec4 inColor;
|
||||||
|
|
||||||
|
layout(location = 0) out vec2 outTexCoord;
|
||||||
|
layout(location = 1) out vec4 outColor;
|
||||||
|
|
||||||
|
layout(binding = 0, set = 2) uniform UBO
|
||||||
|
{
|
||||||
|
mat4 ViewProjection;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = ubo.ViewProjection * vec4(inPos, 1.0);
|
||||||
|
outTexCoord = inTexCoord;
|
||||||
|
outColor = inColor;
|
||||||
|
}
|
|
@ -14,6 +14,8 @@ namespace MoonWorks.Graphics
|
||||||
public uint Layer { get; }
|
public uint Layer { get; }
|
||||||
public uint Level { get; }
|
public uint Level { get; }
|
||||||
|
|
||||||
|
public uint Size => (uint) (Rectangle.W * Rectangle.H * Texture.BytesPerPixel(Texture.Format) / Texture.BlockSizeSquared(Texture.Format));
|
||||||
|
|
||||||
public TextureSlice(Texture texture)
|
public TextureSlice(Texture texture)
|
||||||
{
|
{
|
||||||
Texture = texture;
|
Texture = texture;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
namespace MoonWorks.Graphics
|
namespace MoonWorks.Graphics
|
||||||
{
|
{
|
||||||
// This is a convenience structure for pairing a vertex binding with its associated attributes.
|
/// <summary>
|
||||||
|
/// A convenience structure for pairing a vertex binding with its associated attributes.
|
||||||
|
/// </summary>
|
||||||
public struct VertexBindingAndAttributes
|
public struct VertexBindingAndAttributes
|
||||||
{
|
{
|
||||||
public VertexBinding VertexBinding { get; }
|
public VertexBinding VertexBinding { get; }
|
||||||
|
@ -12,9 +14,9 @@ namespace MoonWorks.Graphics
|
||||||
VertexAttributes = attributes;
|
VertexAttributes = attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static VertexBindingAndAttributes Create<T>(uint bindingIndex) where T : unmanaged, IVertexType
|
public static VertexBindingAndAttributes Create<T>(uint bindingIndex, uint locationOffset = 0, VertexInputRate inputRate = VertexInputRate.Vertex) where T : unmanaged, IVertexType
|
||||||
{
|
{
|
||||||
VertexBinding binding = VertexBinding.Create<T>(bindingIndex);
|
VertexBinding binding = VertexBinding.Create<T>(bindingIndex, inputRate);
|
||||||
VertexAttribute[] attributes = new VertexAttribute[T.Formats.Length];
|
VertexAttribute[] attributes = new VertexAttribute[T.Formats.Length];
|
||||||
uint offset = 0;
|
uint offset = 0;
|
||||||
|
|
||||||
|
@ -25,7 +27,7 @@ namespace MoonWorks.Graphics
|
||||||
attributes[i] = new VertexAttribute
|
attributes[i] = new VertexAttribute
|
||||||
{
|
{
|
||||||
Binding = bindingIndex,
|
Binding = bindingIndex,
|
||||||
Location = i,
|
Location = locationOffset + i,
|
||||||
Format = format,
|
Format = format,
|
||||||
Offset = offset
|
Offset = offset
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,9 @@ using SDL2;
|
||||||
|
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a specific joystick direction on a gamepad.
|
||||||
|
/// </summary>
|
||||||
public class Axis
|
public class Axis
|
||||||
{
|
{
|
||||||
public Gamepad Parent { get; }
|
public Gamepad Parent { get; }
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Can be used to access a gamepad axis virtual button without a direct reference to the button object.
|
||||||
|
/// </summary>
|
||||||
public enum AxisButtonCode
|
public enum AxisButtonCode
|
||||||
{
|
{
|
||||||
LeftX_Left,
|
LeftX_Left,
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
// Enum values are equivalent to SDL GameControllerAxis
|
/// <summary>
|
||||||
|
/// Can be used to access a gamepad axis without a direct reference to the axis object.
|
||||||
|
/// Enum values are equivalent to SDL_GameControllerAxis.
|
||||||
|
/// </summary>
|
||||||
public enum AxisCode
|
public enum AxisCode
|
||||||
{
|
{
|
||||||
LeftX,
|
LeftX,
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
// Enum values are equivalent to the SDL GameControllerButton value.
|
/// <summary>
|
||||||
|
/// Can be used to access a gamepad button without a direct reference to the button object.
|
||||||
|
/// Enum values are equivalent to the SDL GameControllerButton value.
|
||||||
|
/// </summary>
|
||||||
public enum GamepadButtonCode
|
public enum GamepadButtonCode
|
||||||
{
|
{
|
||||||
A,
|
A,
|
||||||
|
|
|
@ -1,14 +1,40 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Container for the current state of a binary input.
|
||||||
|
/// </summary>
|
||||||
public struct ButtonState
|
public struct ButtonState
|
||||||
{
|
{
|
||||||
public ButtonStatus ButtonStatus { get; }
|
public ButtonStatus ButtonStatus { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was pressed this frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsPressed => ButtonStatus == ButtonStatus.Pressed;
|
public bool IsPressed => ButtonStatus == ButtonStatus.Pressed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was pressed this frame and the previous frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsHeld => ButtonStatus == ButtonStatus.Held;
|
public bool IsHeld => ButtonStatus == ButtonStatus.Held;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was either pressed or continued to be held this frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsDown => ButtonStatus == ButtonStatus.Pressed || ButtonStatus == ButtonStatus.Held;
|
public bool IsDown => ButtonStatus == ButtonStatus.Pressed || ButtonStatus == ButtonStatus.Held;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was let go this frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsReleased => ButtonStatus == ButtonStatus.Released;
|
public bool IsReleased => ButtonStatus == ButtonStatus.Released;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was not pressed this frame or the previous frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsIdle => ButtonStatus == ButtonStatus.Idle;
|
public bool IsIdle => ButtonStatus == ButtonStatus.Idle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was either idle or released this frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsUp => ButtonStatus == ButtonStatus.Idle || ButtonStatus == ButtonStatus.Released;
|
public bool IsUp => ButtonStatus == ButtonStatus.Idle || ButtonStatus == ButtonStatus.Released;
|
||||||
|
|
||||||
public ButtonState(ButtonStatus buttonStatus)
|
public ButtonState(ButtonStatus buttonStatus)
|
||||||
|
@ -43,7 +69,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Combines two button states. Useful for alt controls or input buffering.
|
/// Combines two button states. Useful for alt control sets or input buffering.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static ButtonState operator |(ButtonState a, ButtonState b)
|
public static ButtonState operator |(ButtonState a, ButtonState b)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the current status of a binary input.
|
||||||
|
/// </summary>
|
||||||
public enum ButtonStatus
|
public enum ButtonStatus
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
namespace MoonWorks.Input
|
|
||||||
{
|
|
||||||
public enum DeviceKind
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
Keyboard,
|
|
||||||
Mouse,
|
|
||||||
Gamepad,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,6 +5,12 @@ using SDL2;
|
||||||
|
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A Gamepad input abstraction that represents input coming from a console controller or other such devices.
|
||||||
|
/// The button names map to a standard Xbox 360 controller.
|
||||||
|
/// For different controllers the relative position of the face buttons will determine the button mapping.
|
||||||
|
/// For example on a DualShock controller the Cross button will map to the A button.
|
||||||
|
/// </summary>
|
||||||
public class Gamepad
|
public class Gamepad
|
||||||
{
|
{
|
||||||
internal IntPtr Handle;
|
internal IntPtr Handle;
|
||||||
|
@ -51,7 +57,14 @@ namespace MoonWorks.Input
|
||||||
|
|
||||||
public bool IsDummy => Handle == IntPtr.Zero;
|
public bool IsDummy => Handle == IntPtr.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if any input on the gamepad is active. Useful for input remapping.
|
||||||
|
/// </summary>
|
||||||
public bool AnyPressed { get; private set; }
|
public bool AnyPressed { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a reference to an arbitrary VirtualButton that was pressed on the gamepad this frame. Useful for input remapping.
|
||||||
|
/// </summary>
|
||||||
public VirtualButton AnyPressedButton { get; private set; }
|
public VirtualButton AnyPressedButton { get; private set; }
|
||||||
|
|
||||||
private Dictionary<SDL.SDL_GameControllerButton, GamepadButton> EnumToButton;
|
private Dictionary<SDL.SDL_GameControllerButton, GamepadButton> EnumToButton;
|
||||||
|
@ -100,13 +113,13 @@ namespace MoonWorks.Input
|
||||||
|
|
||||||
LeftXLeft = new AxisButton(LeftX, false);
|
LeftXLeft = new AxisButton(LeftX, false);
|
||||||
LeftXRight = new AxisButton(LeftX, true);
|
LeftXRight = new AxisButton(LeftX, true);
|
||||||
LeftYUp = new AxisButton(LeftY, false);
|
LeftYUp = new AxisButton(LeftY, true);
|
||||||
LeftYDown = new AxisButton(LeftY, true);
|
LeftYDown = new AxisButton(LeftY, false);
|
||||||
|
|
||||||
RightXLeft = new AxisButton(RightX, false);
|
RightXLeft = new AxisButton(RightX, false);
|
||||||
RightXRight = new AxisButton(RightX, true);
|
RightXRight = new AxisButton(RightX, true);
|
||||||
RightYUp = new AxisButton(RightY, false);
|
RightYUp = new AxisButton(RightY, true);
|
||||||
RightYDown = new AxisButton(RightY, true);
|
RightYDown = new AxisButton(RightY, false);
|
||||||
|
|
||||||
TriggerLeft = new Trigger(this, TriggerCode.Left, SDL.SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT);
|
TriggerLeft = new Trigger(this, TriggerCode.Left, SDL.SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT);
|
||||||
TriggerRight = new Trigger(this, TriggerCode.Right, SDL.SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
|
TriggerRight = new Trigger(this, TriggerCode.Right, SDL.SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
|
||||||
|
@ -195,6 +208,20 @@ namespace MoonWorks.Input
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void Register(IntPtr handle)
|
||||||
|
{
|
||||||
|
Handle = handle;
|
||||||
|
|
||||||
|
IntPtr joystickHandle = SDL.SDL_GameControllerGetJoystick(Handle);
|
||||||
|
JoystickInstanceID = SDL.SDL_JoystickInstanceID(joystickHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Unregister()
|
||||||
|
{
|
||||||
|
Handle = IntPtr.Zero;
|
||||||
|
JoystickInstanceID = -1;
|
||||||
|
}
|
||||||
|
|
||||||
internal void Update()
|
internal void Update()
|
||||||
{
|
{
|
||||||
AnyPressed = false;
|
AnyPressed = false;
|
||||||
|
@ -253,16 +280,25 @@ namespace MoonWorks.Input
|
||||||
) == 0;
|
) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtains a gamepad button object given a button code.
|
||||||
|
/// </summary>
|
||||||
public GamepadButton Button(GamepadButtonCode buttonCode)
|
public GamepadButton Button(GamepadButtonCode buttonCode)
|
||||||
{
|
{
|
||||||
return EnumToButton[(SDL.SDL_GameControllerButton) buttonCode];
|
return EnumToButton[(SDL.SDL_GameControllerButton) buttonCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtains an axis button object given a button code.
|
||||||
|
/// </summary>
|
||||||
public AxisButton Button(AxisButtonCode axisButtonCode)
|
public AxisButton Button(AxisButtonCode axisButtonCode)
|
||||||
{
|
{
|
||||||
return AxisButtonCodeToAxisButton[axisButtonCode];
|
return AxisButtonCodeToAxisButton[axisButtonCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtains a trigger button object given a button code.
|
||||||
|
/// </summary>
|
||||||
public TriggerButton Button(TriggerCode triggerCode)
|
public TriggerButton Button(TriggerCode triggerCode)
|
||||||
{
|
{
|
||||||
return TriggerCodeToTriggerButton[triggerCode];
|
return TriggerCodeToTriggerButton[triggerCode];
|
||||||
|
|
|
@ -3,31 +3,65 @@ using System;
|
||||||
|
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The main container class for all input tracking.
|
||||||
|
/// Your Game class will automatically have a reference to this class.
|
||||||
|
/// </summary>
|
||||||
public class Inputs
|
public class Inputs
|
||||||
{
|
{
|
||||||
public const int MAX_GAMEPADS = 4;
|
public const int MAX_GAMEPADS = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The reference to the Keyboard input abstraction.
|
||||||
|
/// </summary>
|
||||||
public Keyboard Keyboard { get; }
|
public Keyboard Keyboard { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The reference to the Mouse input abstraction.
|
||||||
|
/// </summary>
|
||||||
public Mouse Mouse { get; }
|
public Mouse Mouse { get; }
|
||||||
|
|
||||||
Gamepad[] gamepads;
|
Gamepad[] Gamepads;
|
||||||
|
|
||||||
public static event Action<char> TextInput;
|
public static event Action<char> TextInput;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if any input on any input device is active. Useful for input remapping.
|
||||||
|
/// </summary>
|
||||||
public bool AnyPressed { get; private set; }
|
public bool AnyPressed { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a reference to an arbitrary VirtualButton that was pressed this frame. Useful for input remapping.
|
||||||
|
/// </summary>
|
||||||
public VirtualButton AnyPressedButton { get; private set; }
|
public VirtualButton AnyPressedButton { get; private set; }
|
||||||
|
|
||||||
|
public delegate void OnGamepadConnectedFunc(int slot);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a gamepad has been connected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slot">The slot where the connection occurred.</param>
|
||||||
|
public OnGamepadConnectedFunc OnGamepadConnected = delegate { };
|
||||||
|
|
||||||
|
public delegate void OnGamepadDisconnectedFunc(int slot);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a gamepad has been disconnected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slot">The slot where the disconnection occurred.</param>
|
||||||
|
public OnGamepadDisconnectedFunc OnGamepadDisconnected = delegate { };
|
||||||
|
|
||||||
internal Inputs()
|
internal Inputs()
|
||||||
{
|
{
|
||||||
Keyboard = new Keyboard();
|
Keyboard = new Keyboard();
|
||||||
Mouse = new Mouse();
|
Mouse = new Mouse();
|
||||||
|
|
||||||
gamepads = new Gamepad[MAX_GAMEPADS];
|
Gamepads = new Gamepad[MAX_GAMEPADS];
|
||||||
|
|
||||||
// initialize dummy controllers
|
// initialize dummy controllers
|
||||||
for (var slot = 0; slot < MAX_GAMEPADS; slot += 1)
|
for (var slot = 0; slot < MAX_GAMEPADS; slot += 1)
|
||||||
{
|
{
|
||||||
gamepads[slot] = new Gamepad(IntPtr.Zero, slot);
|
Gamepads[slot] = new Gamepad(IntPtr.Zero, slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +87,7 @@ namespace MoonWorks.Input
|
||||||
AnyPressedButton = Mouse.AnyPressedButton;
|
AnyPressedButton = Mouse.AnyPressedButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var gamepad in gamepads)
|
foreach (var gamepad in Gamepads)
|
||||||
{
|
{
|
||||||
gamepad.Update();
|
gamepad.Update();
|
||||||
|
|
||||||
|
@ -65,6 +99,11 @@ namespace MoonWorks.Input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if a gamepad is currently connected in the given slot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slot">Range: 0-3</param>
|
||||||
|
/// <returns></returns>
|
||||||
public bool GamepadExists(int slot)
|
public bool GamepadExists(int slot)
|
||||||
{
|
{
|
||||||
if (slot < 0 || slot >= MAX_GAMEPADS)
|
if (slot < 0 || slot >= MAX_GAMEPADS)
|
||||||
|
@ -72,13 +111,19 @@ namespace MoonWorks.Input
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !gamepads[slot].IsDummy;
|
return !Gamepads[slot].IsDummy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From 0-4
|
/// <summary>
|
||||||
|
/// Gets a gamepad associated with the given slot.
|
||||||
|
/// The first n slots are guaranteed to occupied with gamepads if they are connected.
|
||||||
|
/// If a gamepad does not exist for the given slot, a dummy object with all inputs in default state will be returned.
|
||||||
|
/// You can check if a gamepad is connected in a slot with the GamepadExists function.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slot">Range: 0-3</param>
|
||||||
public Gamepad GetGamepad(int slot)
|
public Gamepad GetGamepad(int slot)
|
||||||
{
|
{
|
||||||
return gamepads[slot];
|
return Gamepads[slot];
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AddGamepad(int index)
|
internal void AddGamepad(int index)
|
||||||
|
@ -87,24 +132,40 @@ namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
if (!GamepadExists(slot))
|
if (!GamepadExists(slot))
|
||||||
{
|
{
|
||||||
gamepads[slot].Handle = SDL.SDL_GameControllerOpen(index);
|
var openResult = SDL.SDL_GameControllerOpen(index);
|
||||||
System.Console.WriteLine($"Gamepad added to slot {slot}!");
|
if (openResult == 0)
|
||||||
|
{
|
||||||
|
Logger.LogError("Error opening gamepad!");
|
||||||
|
Logger.LogError(SDL.SDL_GetError());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Gamepads[slot].Register(openResult);
|
||||||
|
Logger.LogInfo($"Gamepad added to slot {slot}!");
|
||||||
|
|
||||||
|
if (OnGamepadConnected != null)
|
||||||
|
{
|
||||||
|
OnGamepadConnected(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.Console.WriteLine("Too many gamepads already!");
|
Logger.LogInfo("Too many gamepads already!");
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void RemoveGamepad(int joystickInstanceID)
|
internal void RemoveGamepad(int joystickInstanceID)
|
||||||
{
|
{
|
||||||
for (int slot = 0; slot < MAX_GAMEPADS; slot += 1)
|
for (int slot = 0; slot < MAX_GAMEPADS; slot += 1)
|
||||||
{
|
{
|
||||||
if (joystickInstanceID == gamepads[slot].JoystickInstanceID)
|
if (joystickInstanceID == Gamepads[slot].JoystickInstanceID)
|
||||||
{
|
{
|
||||||
SDL.SDL_GameControllerClose(gamepads[slot].Handle);
|
SDL.SDL_GameControllerClose(Gamepads[slot].Handle);
|
||||||
gamepads[slot].Handle = IntPtr.Zero;
|
Gamepads[slot].Unregister();
|
||||||
System.Console.WriteLine($"Removing gamepad from slot {slot}!");
|
Logger.LogInfo($"Removing gamepad from slot {slot}!");
|
||||||
|
OnGamepadDisconnected(slot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
// Enum values are equivalent to the SDL Scancode value.
|
/// <summary>
|
||||||
|
/// Can be used to determine key state without a direct reference to the virtual button object.
|
||||||
|
/// Enum values are equivalent to the SDL Scancode value.
|
||||||
|
/// </summary>
|
||||||
public enum KeyCode : int
|
public enum KeyCode : int
|
||||||
{
|
{
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
|
|
|
@ -4,12 +4,22 @@ using SDL2;
|
||||||
|
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The keyboard input device abstraction.
|
||||||
|
/// </summary>
|
||||||
public class Keyboard
|
public class Keyboard
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True if any button on the keyboard is active. Useful for input remapping.
|
||||||
|
/// </summary>
|
||||||
public bool AnyPressed { get; private set; }
|
public bool AnyPressed { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a reference to an arbitrary KeyboardButton that was pressed this frame. Useful for input remapping.
|
||||||
|
/// </summary>
|
||||||
public KeyboardButton AnyPressedButton { get; private set; }
|
public KeyboardButton AnyPressedButton { get; private set; }
|
||||||
|
|
||||||
public IntPtr State { get; private set; }
|
internal IntPtr State { get; private set; }
|
||||||
|
|
||||||
private KeyCode[] KeyCodes;
|
private KeyCode[] KeyCodes;
|
||||||
private KeyboardButton[] Keys { get; }
|
private KeyboardButton[] Keys { get; }
|
||||||
|
@ -78,41 +88,65 @@ namespace MoonWorks.Input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was pressed this frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsPressed(KeyCode keycode)
|
public bool IsPressed(KeyCode keycode)
|
||||||
{
|
{
|
||||||
return Keys[(int) keycode].IsPressed;
|
return Keys[(int) keycode].IsPressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was pressed this frame and the previous frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsHeld(KeyCode keycode)
|
public bool IsHeld(KeyCode keycode)
|
||||||
{
|
{
|
||||||
return Keys[(int) keycode].IsHeld;
|
return Keys[(int) keycode].IsHeld;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was either pressed or continued to be held this frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsDown(KeyCode keycode)
|
public bool IsDown(KeyCode keycode)
|
||||||
{
|
{
|
||||||
return Keys[(int) keycode].IsDown;
|
return Keys[(int) keycode].IsDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was let go this frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsReleased(KeyCode keycode)
|
public bool IsReleased(KeyCode keycode)
|
||||||
{
|
{
|
||||||
return Keys[(int) keycode].IsReleased;
|
return Keys[(int) keycode].IsReleased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was not pressed this frame or the previous frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsIdle(KeyCode keycode)
|
public bool IsIdle(KeyCode keycode)
|
||||||
{
|
{
|
||||||
return Keys[(int) keycode].IsIdle;
|
return Keys[(int) keycode].IsIdle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the button was either idle or released this frame.
|
||||||
|
/// </summary>
|
||||||
public bool IsUp(KeyCode keycode)
|
public bool IsUp(KeyCode keycode)
|
||||||
{
|
{
|
||||||
return Keys[(int) keycode].IsUp;
|
return Keys[(int) keycode].IsUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a reference to a keyboard button object using a key code.
|
||||||
|
/// </summary>
|
||||||
public KeyboardButton Button(KeyCode keycode)
|
public KeyboardButton Button(KeyCode keycode)
|
||||||
{
|
{
|
||||||
return Keys[(int) keycode];
|
return Keys[(int) keycode];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the state of a keyboard button from a key code.
|
||||||
|
/// </summary>
|
||||||
public ButtonState ButtonState(KeyCode keycode)
|
public ButtonState ButtonState(KeyCode keycode)
|
||||||
{
|
{
|
||||||
return Keys[(int) keycode].State;
|
return Keys[(int) keycode].State;
|
||||||
|
|
|
@ -3,6 +3,9 @@ using SDL2;
|
||||||
|
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The mouse input device abstraction.
|
||||||
|
/// </summary>
|
||||||
public class Mouse
|
public class Mouse
|
||||||
{
|
{
|
||||||
public MouseButton LeftButton { get; }
|
public MouseButton LeftButton { get; }
|
||||||
|
@ -21,12 +24,23 @@ namespace MoonWorks.Input
|
||||||
internal int WheelRaw;
|
internal int WheelRaw;
|
||||||
private int previousWheelRaw = 0;
|
private int previousWheelRaw = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if any button on the keyboard is active. Useful for input remapping.
|
||||||
|
/// </summary>
|
||||||
public bool AnyPressed { get; private set; }
|
public bool AnyPressed { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a reference to an arbitrary MouseButton that was pressed this frame. Useful for input remapping.
|
||||||
|
/// </summary>
|
||||||
public MouseButton AnyPressedButton { get; private set; }
|
public MouseButton AnyPressedButton { get; private set; }
|
||||||
|
|
||||||
public uint ButtonMask { get; private set; }
|
internal uint ButtonMask { get; private set; }
|
||||||
|
|
||||||
private bool relativeMode;
|
private bool relativeMode;
|
||||||
|
/// <summary>
|
||||||
|
/// If set to true, the cursor is hidden, the mouse position is constrained to the window,
|
||||||
|
/// and relative mouse motion will be reported even if the mouse is at the edge of the window.
|
||||||
|
/// </summary>
|
||||||
public bool RelativeMode
|
public bool RelativeMode
|
||||||
{
|
{
|
||||||
get => relativeMode;
|
get => relativeMode;
|
||||||
|
@ -41,9 +55,23 @@ namespace MoonWorks.Input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool hidden;
|
||||||
|
/// <summary>
|
||||||
|
/// If set to true, the OS cursor will not be shown in your application window.
|
||||||
|
/// </summary>
|
||||||
|
public bool Hidden
|
||||||
|
{
|
||||||
|
get => hidden;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
hidden = value;
|
||||||
|
SDL.SDL_ShowCursor(hidden ? SDL.SDL_DISABLE : SDL.SDL_ENABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly Dictionary<MouseButtonCode, MouseButton> CodeToButton;
|
private readonly Dictionary<MouseButtonCode, MouseButton> CodeToButton;
|
||||||
|
|
||||||
public Mouse()
|
internal Mouse()
|
||||||
{
|
{
|
||||||
LeftButton = new MouseButton(this, MouseButtonCode.Left, SDL.SDL_BUTTON_LMASK);
|
LeftButton = new MouseButton(this, MouseButtonCode.Left, SDL.SDL_BUTTON_LMASK);
|
||||||
MiddleButton = new MouseButton(this, MouseButtonCode.Middle, SDL.SDL_BUTTON_MMASK);
|
MiddleButton = new MouseButton(this, MouseButtonCode.Middle, SDL.SDL_BUTTON_MMASK);
|
||||||
|
@ -88,6 +116,17 @@ namespace MoonWorks.Input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a button from the mouse given a MouseButtonCode.
|
||||||
|
/// </summary>
|
||||||
|
public MouseButton Button(MouseButtonCode buttonCode)
|
||||||
|
{
|
||||||
|
return CodeToButton[buttonCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a button state from a mouse button corresponding to the given MouseButtonCode.
|
||||||
|
/// </summary>
|
||||||
public ButtonState ButtonState(MouseButtonCode buttonCode)
|
public ButtonState ButtonState(MouseButtonCode buttonCode)
|
||||||
{
|
{
|
||||||
return CodeToButton[buttonCode].State;
|
return CodeToButton[buttonCode].State;
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Can be used to determine virtual mouse button state without a direct reference to the button object.
|
||||||
|
/// </summary>
|
||||||
public enum MouseButtonCode
|
public enum MouseButtonCode
|
||||||
{
|
{
|
||||||
Left,
|
Left,
|
||||||
|
|
|
@ -3,6 +3,9 @@ using SDL2;
|
||||||
|
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a trigger input on a gamepad.
|
||||||
|
/// </summary>
|
||||||
public class Trigger
|
public class Trigger
|
||||||
{
|
{
|
||||||
public Gamepad Parent { get; }
|
public Gamepad Parent { get; }
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
// Enum values correspond to SDL GameControllerAxis
|
/// <summary>
|
||||||
|
/// Can be used to determine trigger state or trigger virtual button state without direct reference to the trigger object or virtual button object.
|
||||||
|
/// Enum values correspond to SDL_GameControllerAxis.
|
||||||
|
/// </summary>
|
||||||
public enum TriggerCode
|
public enum TriggerCode
|
||||||
{
|
{
|
||||||
Left = 4,
|
Left = 4,
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// VirtualButtons map inputs to binary inputs, like a trigger threshold or joystick axis threshold.
|
||||||
|
/// </summary>
|
||||||
public abstract class VirtualButton
|
public abstract class VirtualButton
|
||||||
{
|
{
|
||||||
public ButtonState State { get; protected set; }
|
public ButtonState State { get; protected set; }
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A virtual button corresponding to a direction on a joystick.
|
||||||
|
/// If the axis value exceeds the threshold, it will be treated as a press.
|
||||||
|
/// </summary>
|
||||||
public class AxisButton : VirtualButton
|
public class AxisButton : VirtualButton
|
||||||
{
|
{
|
||||||
public Axis Parent { get; }
|
public Axis Parent { get; }
|
||||||
public AxisButtonCode Code { get; }
|
public AxisButtonCode Code { get; }
|
||||||
|
|
||||||
private float threshold = 0.9f;
|
private float threshold = 0.5f;
|
||||||
public float Threshold
|
public float Threshold
|
||||||
{
|
{
|
||||||
get => threshold;
|
get => threshold;
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A dummy button that can never be pressed. Used for the dummy gamepad.
|
||||||
|
/// </summary>
|
||||||
public class EmptyButton : VirtualButton
|
public class EmptyButton : VirtualButton
|
||||||
{
|
{
|
||||||
internal override bool CheckPressed()
|
internal override bool CheckPressed()
|
||||||
|
|
|
@ -2,6 +2,9 @@ using SDL2;
|
||||||
|
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A virtual button corresponding to a gamepad button.
|
||||||
|
/// </summary>
|
||||||
public class GamepadButton : VirtualButton
|
public class GamepadButton : VirtualButton
|
||||||
{
|
{
|
||||||
public Gamepad Parent { get; }
|
public Gamepad Parent { get; }
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A virtual button corresponding to a keyboard button.
|
||||||
|
/// </summary>
|
||||||
public class KeyboardButton : VirtualButton
|
public class KeyboardButton : VirtualButton
|
||||||
{
|
{
|
||||||
Keyboard Parent;
|
Keyboard Parent;
|
||||||
KeyCode KeyCode;
|
public KeyCode KeyCode { get; }
|
||||||
|
|
||||||
internal KeyboardButton(Keyboard parent, KeyCode keyCode)
|
internal KeyboardButton(Keyboard parent, KeyCode keyCode)
|
||||||
{
|
{
|
||||||
|
@ -13,9 +14,9 @@ namespace MoonWorks.Input
|
||||||
KeyCode = keyCode;
|
KeyCode = keyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override bool CheckPressed()
|
internal unsafe override bool CheckPressed()
|
||||||
{
|
{
|
||||||
return Conversions.ByteToBool(Marshal.ReadByte(Parent.State, (int) KeyCode));
|
return Conversions.ByteToBool(((byte*) Parent.State)[(int) KeyCode]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A virtual button corresponding to a mouse button.
|
||||||
|
/// </summary>
|
||||||
public class MouseButton : VirtualButton
|
public class MouseButton : VirtualButton
|
||||||
{
|
{
|
||||||
Mouse Parent;
|
Mouse Parent;
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
namespace MoonWorks.Input
|
namespace MoonWorks.Input
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A virtual button corresponding to a trigger on a gamepad.
|
||||||
|
/// If the trigger value exceeds the threshold, it will be treated as a press.
|
||||||
|
/// </summary>
|
||||||
public class TriggerButton : VirtualButton
|
public class TriggerButton : VirtualButton
|
||||||
{
|
{
|
||||||
public Trigger Parent { get; }
|
public Trigger Parent { get; }
|
||||||
|
|
|
@ -5,9 +5,9 @@ namespace MoonWorks
|
||||||
{
|
{
|
||||||
public static class Logger
|
public static class Logger
|
||||||
{
|
{
|
||||||
public static Action<string> LogInfo;
|
public static Action<string> LogInfo = LogInfoDefault;
|
||||||
public static Action<string> LogWarn;
|
public static Action<string> LogWarn = LogWarnDefault;
|
||||||
public static Action<string> LogError;
|
public static Action<string> LogError = LogErrorDefault;
|
||||||
|
|
||||||
private static RefreshCS.Refresh.Refresh_LogFunc LogInfoFunc = RefreshLogInfo;
|
private static RefreshCS.Refresh.Refresh_LogFunc LogInfoFunc = RefreshLogInfo;
|
||||||
private static RefreshCS.Refresh.Refresh_LogFunc LogWarnFunc = RefreshLogWarn;
|
private static RefreshCS.Refresh.Refresh_LogFunc LogWarnFunc = RefreshLogWarn;
|
||||||
|
@ -15,19 +15,6 @@ namespace MoonWorks
|
||||||
|
|
||||||
internal static void Initialize()
|
internal static void Initialize()
|
||||||
{
|
{
|
||||||
if (Logger.LogInfo == null)
|
|
||||||
{
|
|
||||||
Logger.LogInfo = Console.WriteLine;
|
|
||||||
}
|
|
||||||
if (Logger.LogWarn == null)
|
|
||||||
{
|
|
||||||
Logger.LogWarn = Console.WriteLine;
|
|
||||||
}
|
|
||||||
if (Logger.LogError == null)
|
|
||||||
{
|
|
||||||
Logger.LogError = Console.WriteLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
Refresh.Refresh_HookLogFunctions(
|
Refresh.Refresh_HookLogFunctions(
|
||||||
LogInfoFunc,
|
LogInfoFunc,
|
||||||
LogWarnFunc,
|
LogWarnFunc,
|
||||||
|
@ -35,6 +22,30 @@ namespace MoonWorks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void LogInfoDefault(string str)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Green;
|
||||||
|
Console.Write("INFO: ");
|
||||||
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
|
Console.WriteLine(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogWarnDefault(string str)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||||
|
Console.Write("WARN: ");
|
||||||
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
|
Console.WriteLine(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogErrorDefault(string str)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
Console.Write("ERROR: ");
|
||||||
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
|
Console.WriteLine(str);
|
||||||
|
}
|
||||||
|
|
||||||
private static void RefreshLogInfo(IntPtr msg)
|
private static void RefreshLogInfo(IntPtr msg)
|
||||||
{
|
{
|
||||||
LogInfo(UTF8_ToManaged(msg));
|
LogInfo(UTF8_ToManaged(msg));
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,7 +7,7 @@ namespace MoonWorks.Math.Fixed
|
||||||
{
|
{
|
||||||
public struct Fix64 : IEquatable<Fix64>, IComparable<Fix64>
|
public struct Fix64 : IEquatable<Fix64>, IComparable<Fix64>
|
||||||
{
|
{
|
||||||
private readonly long RawValue;
|
public long RawValue { get; }
|
||||||
|
|
||||||
const long MAX_VALUE = long.MaxValue;
|
const long MAX_VALUE = long.MaxValue;
|
||||||
const long MIN_VALUE = long.MinValue;
|
const long MIN_VALUE = long.MinValue;
|
||||||
|
@ -17,6 +17,9 @@ namespace MoonWorks.Math.Fixed
|
||||||
const long PI_TIMES_2 = 0x6487ED511;
|
const long PI_TIMES_2 = 0x6487ED511;
|
||||||
const long PI = 0x3243F6A88;
|
const long PI = 0x3243F6A88;
|
||||||
const long PI_OVER_2 = 0x1921FB544;
|
const long PI_OVER_2 = 0x1921FB544;
|
||||||
|
const long LN2 = 0xB17217F7;
|
||||||
|
const long LOG2MAX = 0x1F00000000;
|
||||||
|
const long LOG2MIN = -0x2000000000;
|
||||||
|
|
||||||
public static readonly Fix64 MaxValue = new Fix64(MAX_VALUE);
|
public static readonly Fix64 MaxValue = new Fix64(MAX_VALUE);
|
||||||
public static readonly Fix64 MinValue = new Fix64(MIN_VALUE);
|
public static readonly Fix64 MinValue = new Fix64(MIN_VALUE);
|
||||||
|
@ -28,6 +31,10 @@ namespace MoonWorks.Math.Fixed
|
||||||
public static readonly Fix64 PiOver4 = PiOver2 / new Fix64(2);
|
public static readonly Fix64 PiOver4 = PiOver2 / new Fix64(2);
|
||||||
public static readonly Fix64 PiTimes2 = new Fix64(PI_TIMES_2);
|
public static readonly Fix64 PiTimes2 = new Fix64(PI_TIMES_2);
|
||||||
|
|
||||||
|
static readonly Fix64 Ln2 = new Fix64(LN2);
|
||||||
|
static readonly Fix64 Log2Max = new Fix64(LOG2MAX);
|
||||||
|
static readonly Fix64 Log2Min = new Fix64(LOG2MIN);
|
||||||
|
|
||||||
const int LUT_SIZE = (int)(PI_OVER_2 >> 15);
|
const int LUT_SIZE = (int)(PI_OVER_2 >> 15);
|
||||||
static readonly Fix64 LutInterval = (Fix64)(LUT_SIZE - 1) / PiOver2;
|
static readonly Fix64 LutInterval = (Fix64)(LUT_SIZE - 1) / PiOver2;
|
||||||
|
|
||||||
|
@ -52,6 +59,11 @@ namespace MoonWorks.Math.Fixed
|
||||||
return new Fix64(numerator) / new Fix64(denominator);
|
return new Fix64(numerator) / new Fix64(denominator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Fix64 FromRawValue(long value)
|
||||||
|
{
|
||||||
|
return new Fix64(value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the fractional component of this Fix64 value.
|
/// Gets the fractional component of this Fix64 value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -60,30 +72,6 @@ namespace MoonWorks.Math.Fixed
|
||||||
return new Fix64(number.RawValue & 0x00000000FFFFFFFF);
|
return new Fix64(number.RawValue & 0x00000000FFFFFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Fix64 Random(System.Random random, int max)
|
|
||||||
{
|
|
||||||
return new Fix64(random.NextInt64(new Fix64(max).RawValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Fix64 Random(System.Random random, Fix64 max)
|
|
||||||
{
|
|
||||||
return new Fix64(random.NextInt64(max.RawValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Fix64 Random(System.Random random, Fix64 min, Fix64 max)
|
|
||||||
{
|
|
||||||
return new Fix64(random.NextInt64(min.RawValue, max.RawValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max should be between 0.0 and 1.0.
|
|
||||||
public static Fix64 RandomFraction(System.Random random, Fix64 max)
|
|
||||||
{
|
|
||||||
long fractionalPart = (max.RawValue & 0x00000000FFFFFFFF);
|
|
||||||
long fractional = random.NextInt64(fractionalPart);
|
|
||||||
|
|
||||||
return new Fix64(fractional);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns an int indicating the sign of a Fix64 number.
|
/// Returns an int indicating the sign of a Fix64 number.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -117,27 +105,27 @@ namespace MoonWorks.Math.Fixed
|
||||||
return new Fix64((value.RawValue + mask) ^ mask);
|
return new Fix64((value.RawValue + mask) ^ mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the largest integral value less than or equal to the specified number.
|
/// Returns the largest integral value less than or equal to the specified number.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Fix64 Floor(Fix64 value)
|
public static Fix64 Floor(Fix64 value)
|
||||||
{
|
{
|
||||||
// Zero out the fractional part.
|
// Zero out the fractional part.
|
||||||
return new Fix64((long)((ulong)value.RawValue & 0xFFFFFFFF00000000));
|
return new Fix64((long)((ulong)value.RawValue & 0xFFFFFFFF00000000));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the smallest integral value that is greater than or equal to the specified number.
|
/// Returns the smallest integral value that is greater than or equal to the specified number.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Fix64 Ceiling(Fix64 value)
|
public static Fix64 Ceiling(Fix64 value)
|
||||||
{
|
{
|
||||||
return value.IsFractional ? Floor(value) + One : value;
|
return value.IsFractional ? Floor(value) + One : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rounds to the nearest integral value.
|
/// Rounds to the nearest integral value.
|
||||||
/// If the value is halfway between an even and an uneven value, returns the even value.
|
/// If the value is halfway between an even and an uneven value, returns the even value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Fix64 Round(Fix64 value)
|
public static Fix64 Round(Fix64 value)
|
||||||
{
|
{
|
||||||
var fractionalPart = value.RawValue & 0x00000000FFFFFFFF;
|
var fractionalPart = value.RawValue & 0x00000000FFFFFFFF;
|
||||||
|
@ -212,7 +200,145 @@ namespace MoonWorks.Math.Fixed
|
||||||
return Fix64.Floor(value / step) * step;
|
return Fix64.Floor(value / step) * step;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigonometry functions
|
// Exponentiation functions
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns 2 raised to the specified power.
|
||||||
|
/// Provides at least 6 decimals of accuracy.
|
||||||
|
/// </summary>
|
||||||
|
internal static Fix64 Pow2(Fix64 x)
|
||||||
|
{
|
||||||
|
if (x.RawValue == 0)
|
||||||
|
{
|
||||||
|
return One;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid negative arguments by exploiting that exp(-x) = 1/exp(x).
|
||||||
|
bool neg = x.RawValue < 0;
|
||||||
|
if (neg)
|
||||||
|
{
|
||||||
|
x = -x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x == One)
|
||||||
|
{
|
||||||
|
return neg ? One / (Fix64)2 : (Fix64)2;
|
||||||
|
}
|
||||||
|
if (x >= Log2Max)
|
||||||
|
{
|
||||||
|
return neg ? One / MaxValue : MaxValue;
|
||||||
|
}
|
||||||
|
if (x <= Log2Min)
|
||||||
|
{
|
||||||
|
return neg ? MaxValue : Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The algorithm is based on the power series for exp(x):
|
||||||
|
* http://en.wikipedia.org/wiki/Exponential_function#Formal_definition
|
||||||
|
*
|
||||||
|
* From term n, we get term n+1 by multiplying with x/n.
|
||||||
|
* When the sum term drops to zero, we can stop summing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
int integerPart = (int)Floor(x);
|
||||||
|
// Take fractional part of exponent
|
||||||
|
x = new Fix64(x.RawValue & 0x00000000FFFFFFFF);
|
||||||
|
|
||||||
|
var result = One;
|
||||||
|
var term = One;
|
||||||
|
int i = 1;
|
||||||
|
while (term.RawValue != 0)
|
||||||
|
{
|
||||||
|
term = FastMul(FastMul(x, term), Ln2) / (Fix64)i;
|
||||||
|
result += term;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new Fix64(result.RawValue << integerPart);
|
||||||
|
if (neg)
|
||||||
|
{
|
||||||
|
result = One / result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the base-2 logarithm of a specified number.
|
||||||
|
/// Provides at least 9 decimals of accuracy.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">
|
||||||
|
/// The argument was non-positive
|
||||||
|
/// </exception>
|
||||||
|
internal static Fix64 Log2(Fix64 x)
|
||||||
|
{
|
||||||
|
if (x.RawValue <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException("Non-positive value passed to Ln", "x");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This implementation is based on Clay. S. Turner's fast binary logarithm
|
||||||
|
// algorithm (C. S. Turner, "A Fast Binary Logarithm Algorithm", IEEE Signal
|
||||||
|
// Processing Mag., pp. 124,140, Sep. 2010.)
|
||||||
|
|
||||||
|
long b = 1U << (FRACTIONAL_PLACES - 1);
|
||||||
|
long y = 0;
|
||||||
|
|
||||||
|
long rawX = x.RawValue;
|
||||||
|
while (rawX < ONE)
|
||||||
|
{
|
||||||
|
rawX <<= 1;
|
||||||
|
y -= ONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (rawX >= (ONE << 1))
|
||||||
|
{
|
||||||
|
rawX >>= 1;
|
||||||
|
y += ONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
var z = new Fix64(rawX);
|
||||||
|
|
||||||
|
for (int i = 0; i < FRACTIONAL_PLACES; i++)
|
||||||
|
{
|
||||||
|
z = FastMul(z, z);
|
||||||
|
if (z.RawValue >= (ONE << 1))
|
||||||
|
{
|
||||||
|
z = new Fix64(z.RawValue >> 1);
|
||||||
|
y += b;
|
||||||
|
}
|
||||||
|
b >>= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Fix64(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Fix64 Pow(Fix64 b, Fix64 exp)
|
||||||
|
{
|
||||||
|
if (b == One)
|
||||||
|
{
|
||||||
|
return One;
|
||||||
|
}
|
||||||
|
if (exp.RawValue == 0)
|
||||||
|
{
|
||||||
|
return One;
|
||||||
|
}
|
||||||
|
if (exp.RawValue == ONE)
|
||||||
|
{
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
if (b.RawValue == 0)
|
||||||
|
{
|
||||||
|
if (exp.RawValue < 0)
|
||||||
|
{
|
||||||
|
throw new DivideByZeroException();
|
||||||
|
}
|
||||||
|
return Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
Fix64 log2 = Log2(b);
|
||||||
|
return Pow2(exp * log2);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the square root of the given Fix64 value.
|
/// Returns the square root of the given Fix64 value.
|
||||||
|
|
|
@ -996,8 +996,8 @@ namespace MoonWorks.Math.Fixed
|
||||||
z = Vector3.Normalize(forward);
|
z = Vector3.Normalize(forward);
|
||||||
Vector3.Cross(ref forward, ref up, out x);
|
Vector3.Cross(ref forward, ref up, out x);
|
||||||
Vector3.Cross(ref x, ref forward, out y);
|
Vector3.Cross(ref x, ref forward, out y);
|
||||||
x.Normalize();
|
x = Vector3.Normalize(x);
|
||||||
y.Normalize();
|
y = Vector3.Normalize(y);
|
||||||
|
|
||||||
result = new Matrix4x4();
|
result = new Matrix4x4();
|
||||||
result.Right = x;
|
result.Right = x;
|
||||||
|
|
|
@ -214,23 +214,6 @@ namespace MoonWorks.Math.Fixed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the quaternion magnitude to unit length.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
Fix64 num = Fix64.One / (Fix64.Sqrt(
|
|
||||||
(X * X) +
|
|
||||||
(Y * Y) +
|
|
||||||
(Z * Z) +
|
|
||||||
(W * W)
|
|
||||||
));
|
|
||||||
this.X *= num;
|
|
||||||
this.Y *= num;
|
|
||||||
this.Z *= num;
|
|
||||||
this.W *= num;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a <see cref="String"/> representation of this <see cref="Quaternion"/> in the format:
|
/// Returns a <see cref="String"/> representation of this <see cref="Quaternion"/> in the format:
|
||||||
/// {X:[<see cref="X"/>] Y:[<see cref="Y"/>] Z:[<see cref="Z"/>] W:[<see cref="W"/>]}
|
/// {X:[<see cref="X"/>] Y:[<see cref="Y"/>] Z:[<see cref="Z"/>] W:[<see cref="W"/>]}
|
||||||
|
@ -759,12 +742,16 @@ namespace MoonWorks.Math.Fixed
|
||||||
/// <param name="result">The unit length quaternion an output parameter.</param>
|
/// <param name="result">The unit length quaternion an output parameter.</param>
|
||||||
public static void Normalize(ref Quaternion quaternion, out Quaternion result)
|
public static void Normalize(ref Quaternion quaternion, out Quaternion result)
|
||||||
{
|
{
|
||||||
Fix64 num = Fix64.One / (Fix64.Sqrt(
|
Fix64 lengthSquared = (quaternion.X * quaternion.X) + (quaternion.Y * quaternion.Y) +
|
||||||
(quaternion.X * quaternion.X) +
|
(quaternion.Z * quaternion.Z) + (quaternion.W * quaternion.W);
|
||||||
(quaternion.Y * quaternion.Y) +
|
|
||||||
(quaternion.Z * quaternion.Z) +
|
if (lengthSquared == Fix64.Zero)
|
||||||
(quaternion.W * quaternion.W)
|
{
|
||||||
));
|
result = Identity;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Fix64 num = Fix64.One / Fix64.Sqrt(lengthSquared);
|
||||||
result.X = quaternion.X * num;
|
result.X = quaternion.X * num;
|
||||||
result.Y = quaternion.Y * num;
|
result.Y = quaternion.Y * num;
|
||||||
result.Z = quaternion.Z * num;
|
result.Z = quaternion.Z * num;
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
namespace MoonWorks.Math.Fixed
|
|
||||||
{
|
|
||||||
public struct Transform2D : System.IEquatable<Transform2D>
|
|
||||||
{
|
|
||||||
public Vector2 Position { get; }
|
|
||||||
public Fix64 Rotation { get; }
|
|
||||||
public Vector2 Scale { get; }
|
|
||||||
|
|
||||||
private bool transformMatrixCalculated;
|
|
||||||
private Matrix3x2 transformMatrix;
|
|
||||||
|
|
||||||
public Matrix3x2 TransformMatrix
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!transformMatrixCalculated)
|
|
||||||
{
|
|
||||||
transformMatrix = CreateTransformMatrix(Position, Rotation, Scale);
|
|
||||||
transformMatrixCalculated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformMatrix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsAxisAligned => Rotation % Fix64.PiOver2 == Fix64.Zero;
|
|
||||||
public bool IsUniformScale => Scale.X == Scale.Y;
|
|
||||||
|
|
||||||
public static readonly Transform2D Identity = new Transform2D(Vector2.Zero, Fix64.Zero, Vector2.One);
|
|
||||||
|
|
||||||
public Transform2D(Vector2 position)
|
|
||||||
{
|
|
||||||
Position = position;
|
|
||||||
Rotation = Fix64.Zero;
|
|
||||||
Scale = Vector2.One;
|
|
||||||
transformMatrixCalculated = false;
|
|
||||||
transformMatrix = Matrix3x2.Identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Transform2D(Vector2 position, Fix64 rotation)
|
|
||||||
{
|
|
||||||
Position = position;
|
|
||||||
Rotation = rotation;
|
|
||||||
Scale = Vector2.One;
|
|
||||||
transformMatrixCalculated = false;
|
|
||||||
transformMatrix = Matrix3x2.Identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Transform2D(Vector2 position, Fix64 rotation, Vector2 scale)
|
|
||||||
{
|
|
||||||
Position = position;
|
|
||||||
Rotation = rotation;
|
|
||||||
Scale = scale;
|
|
||||||
transformMatrixCalculated = false;
|
|
||||||
transformMatrix = Matrix3x2.Identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Transform2D Compose(Transform2D other)
|
|
||||||
{
|
|
||||||
return new Transform2D(Position + other.Position, Rotation + other.Rotation, Scale * other.Scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Matrix3x2 CreateTransformMatrix(Vector2 position, Fix64 rotation, Vector2 scale)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
Matrix3x2.CreateScale(scale) *
|
|
||||||
Matrix3x2.CreateRotation(rotation) *
|
|
||||||
Matrix3x2.CreateTranslation(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Transform2D other)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
Position == other.Position &&
|
|
||||||
Rotation == other.Rotation &&
|
|
||||||
Scale == other.Scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override bool Equals(System.Object other)
|
|
||||||
{
|
|
||||||
if (other is Transform2D otherTransform)
|
|
||||||
{
|
|
||||||
return Equals(otherTransform);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Position, Rotation, Scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Transform2D a, Transform2D b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Transform2D a, Transform2D b)
|
|
||||||
{
|
|
||||||
return !a.Equals(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -200,16 +200,6 @@ namespace MoonWorks.Math.Fixed
|
||||||
return (X * X) + (Y * Y);
|
return (X * X) + (Y * Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Turns this <see cref="Vector2"/> to a unit vector with the same direction.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
Fix64 val = Fix64.One / Fix64.Sqrt((X * X) + (Y * Y));
|
|
||||||
X *= val;
|
|
||||||
Y *= val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Turns this <see cref="Vector2"/> to an angle in radians.
|
/// Turns this <see cref="Vector2"/> to an angle in radians.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -423,7 +413,14 @@ namespace MoonWorks.Math.Fixed
|
||||||
/// <returns>Unit vector.</returns>
|
/// <returns>Unit vector.</returns>
|
||||||
public static Vector2 Normalize(Vector2 value)
|
public static Vector2 Normalize(Vector2 value)
|
||||||
{
|
{
|
||||||
Fix64 val = Fix64.One / Fix64.Sqrt((value.X * value.X) + (value.Y * value.Y));
|
Fix64 lengthSquared = (value.X * value.X) + (value.Y * value.Y);
|
||||||
|
|
||||||
|
if (lengthSquared == Fix64.Zero)
|
||||||
|
{
|
||||||
|
return Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
Fix64 val = Fix64.One / Fix64.Sqrt(lengthSquared);
|
||||||
value.X *= val;
|
value.X *= val;
|
||||||
value.Y *= val;
|
value.Y *= val;
|
||||||
return value;
|
return value;
|
||||||
|
|
|
@ -309,21 +309,6 @@ namespace MoonWorks.Math.Fixed
|
||||||
return (X * X) + (Y * Y) + (Z * Z);
|
return (X * X) + (Y * Y) + (Z * Z);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Turns this <see cref="Vector3"/> to a unit vector with the same direction.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
Fix64 factor = Fix64.One / Fix64.Sqrt(
|
|
||||||
(X * X) +
|
|
||||||
(Y * Y) +
|
|
||||||
(Z * Z)
|
|
||||||
);
|
|
||||||
X *= factor;
|
|
||||||
Y *= factor;
|
|
||||||
Z *= factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a <see cref="String"/> representation of this <see cref="Vector3"/> in the format:
|
/// Returns a <see cref="String"/> representation of this <see cref="Vector3"/> in the format:
|
||||||
/// {X:[<see cref="X"/>] Y:[<see cref="Y"/>] Z:[<see cref="Z"/>]}
|
/// {X:[<see cref="X"/>] Y:[<see cref="Y"/>] Z:[<see cref="Z"/>]}
|
||||||
|
@ -733,11 +718,14 @@ namespace MoonWorks.Math.Fixed
|
||||||
/// <returns>Unit vector.</returns>
|
/// <returns>Unit vector.</returns>
|
||||||
public static Vector3 Normalize(Vector3 value)
|
public static Vector3 Normalize(Vector3 value)
|
||||||
{
|
{
|
||||||
Fix64 factor = Fix64.One / Fix64.Sqrt(
|
Fix64 lengthSquared = (value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z);
|
||||||
(value.X * value.X) +
|
|
||||||
(value.Y * value.Y) +
|
if (lengthSquared == Fix64.Zero)
|
||||||
(value.Z * value.Z)
|
{
|
||||||
);
|
return Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
Fix64 factor = Fix64.One / Fix64.Sqrt(lengthSquared);
|
||||||
return new Vector3(
|
return new Vector3(
|
||||||
value.X * factor,
|
value.X * factor,
|
||||||
value.Y * factor,
|
value.Y * factor,
|
||||||
|
|
|
@ -611,7 +611,7 @@ namespace MoonWorks.Math.Float
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Vector3.Cross(ref cameraUpVector, ref vector, out vector3);
|
Vector3.Cross(ref cameraUpVector, ref vector, out vector3);
|
||||||
vector3.Normalize();
|
vector3 = Vector3.Normalize(vector3);
|
||||||
Vector3.Cross(ref vector, ref vector3, out vector2);
|
Vector3.Cross(ref vector, ref vector3, out vector2);
|
||||||
result.M11 = vector3.X;
|
result.M11 = vector3.X;
|
||||||
result.M12 = vector3.Y;
|
result.M12 = vector3.Y;
|
||||||
|
@ -730,16 +730,16 @@ namespace MoonWorks.Math.Float
|
||||||
Vector3.Forward;
|
Vector3.Forward;
|
||||||
}
|
}
|
||||||
Vector3.Cross(ref rotateAxis, ref vector, out vector3);
|
Vector3.Cross(ref rotateAxis, ref vector, out vector3);
|
||||||
vector3.Normalize();
|
vector3 = Vector3.Normalize(vector3);
|
||||||
Vector3.Cross(ref vector3, ref rotateAxis, out vector);
|
Vector3.Cross(ref vector3, ref rotateAxis, out vector);
|
||||||
vector.Normalize();
|
vector = Vector3.Normalize(vector);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Vector3.Cross(ref rotateAxis, ref vector2, out vector3);
|
Vector3.Cross(ref rotateAxis, ref vector2, out vector3);
|
||||||
vector3.Normalize();
|
vector3 = Vector3.Normalize(vector3);
|
||||||
Vector3.Cross(ref vector3, ref vector4, out vector);
|
Vector3.Cross(ref vector3, ref vector4, out vector);
|
||||||
vector.Normalize();
|
vector = Vector3.Normalize(vector);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.M11 = vector3.X;
|
result.M11 = vector3.X;
|
||||||
|
@ -1701,8 +1701,8 @@ namespace MoonWorks.Math.Float
|
||||||
Vector3.Normalize(ref forward, out z);
|
Vector3.Normalize(ref forward, out z);
|
||||||
Vector3.Cross(ref forward, ref up, out x);
|
Vector3.Cross(ref forward, ref up, out x);
|
||||||
Vector3.Cross(ref x, ref forward, out y);
|
Vector3.Cross(ref x, ref forward, out y);
|
||||||
x.Normalize();
|
x = Vector3.Normalize(x);
|
||||||
y.Normalize();
|
y = Vector3.Normalize(y);
|
||||||
|
|
||||||
result = new Matrix4x4();
|
result = new Matrix4x4();
|
||||||
result.Right = x;
|
result.Right = x;
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
namespace MoonWorks.Math.Float
|
|
||||||
{
|
|
||||||
public struct Transform2D : System.IEquatable<Transform2D>
|
|
||||||
{
|
|
||||||
public Vector2 Position { get; }
|
|
||||||
public float Rotation { get; }
|
|
||||||
public Vector2 Scale { get; }
|
|
||||||
|
|
||||||
private bool transformMatrixCalculated;
|
|
||||||
private Matrix3x2 transformMatrix;
|
|
||||||
|
|
||||||
public Matrix3x2 TransformMatrix
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!transformMatrixCalculated)
|
|
||||||
{
|
|
||||||
transformMatrix = CreateTransformMatrix(Position, Rotation, Scale);
|
|
||||||
transformMatrixCalculated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformMatrix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsAxisAligned => Rotation % MathHelper.PiOver2 == 0;
|
|
||||||
public bool IsUniformScale => Scale.X == Scale.Y;
|
|
||||||
|
|
||||||
public static readonly Transform2D Identity = new Transform2D(Vector2.Zero, 0, Vector2.One);
|
|
||||||
|
|
||||||
public Transform2D(Vector2 position)
|
|
||||||
{
|
|
||||||
Position = position;
|
|
||||||
Rotation = 0;
|
|
||||||
Scale = Vector2.One;
|
|
||||||
transformMatrixCalculated = false;
|
|
||||||
transformMatrix = Matrix3x2.Identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Transform2D(Vector2 position, float rotation)
|
|
||||||
{
|
|
||||||
Position = position;
|
|
||||||
Rotation = rotation;
|
|
||||||
Scale = Vector2.One;
|
|
||||||
transformMatrixCalculated = false;
|
|
||||||
transformMatrix = Matrix3x2.Identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Transform2D(Vector2 position, float rotation, Vector2 scale)
|
|
||||||
{
|
|
||||||
Position = position;
|
|
||||||
Rotation = rotation;
|
|
||||||
Scale = scale;
|
|
||||||
transformMatrixCalculated = false;
|
|
||||||
transformMatrix = Matrix3x2.Identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Transform2D Compose(Transform2D other)
|
|
||||||
{
|
|
||||||
return new Transform2D(Position + other.Position, Rotation + other.Rotation, Scale * other.Scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Matrix3x2 CreateTransformMatrix(Vector2 position, float rotation, Vector2 scale)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
Matrix3x2.CreateScale(scale) *
|
|
||||||
Matrix3x2.CreateRotation(rotation) *
|
|
||||||
Matrix3x2.CreateTranslation(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Transform2D other)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
Position == other.Position &&
|
|
||||||
Rotation == other.Rotation &&
|
|
||||||
Scale == other.Scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override bool Equals(System.Object other)
|
|
||||||
{
|
|
||||||
if (other is Transform2D otherTransform)
|
|
||||||
{
|
|
||||||
return Equals(otherTransform);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return System.HashCode.Combine(Position, Rotation, Scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Transform2D a, Transform2D b)
|
|
||||||
{
|
|
||||||
return a.Equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Transform2D a, Transform2D b)
|
|
||||||
{
|
|
||||||
return !a.Equals(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -194,16 +194,6 @@ namespace MoonWorks.Math.Float
|
||||||
return (X * X) + (Y * Y);
|
return (X * X) + (Y * Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Turns this <see cref="Vector2"/> to a unit vector with the same direction.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
float val = 1.0f / (float) System.Math.Sqrt((X * X) + (Y * Y));
|
|
||||||
X *= val;
|
|
||||||
Y *= val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Turns this <see cref="Vector2"/> to an angle in radians.
|
/// Turns this <see cref="Vector2"/> to an angle in radians.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -717,7 +707,14 @@ namespace MoonWorks.Math.Float
|
||||||
/// <returns>Unit vector.</returns>
|
/// <returns>Unit vector.</returns>
|
||||||
public static Vector2 Normalize(Vector2 value)
|
public static Vector2 Normalize(Vector2 value)
|
||||||
{
|
{
|
||||||
float val = 1.0f / (float) System.Math.Sqrt((value.X * value.X) + (value.Y * value.Y));
|
float lengthSquared = (value.X * value.X) + (value.Y * value.Y);
|
||||||
|
|
||||||
|
if (lengthSquared == 0)
|
||||||
|
{
|
||||||
|
return Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
float val = 1.0f / System.MathF.Sqrt(lengthSquared);
|
||||||
value.X *= val;
|
value.X *= val;
|
||||||
value.Y *= val;
|
value.Y *= val;
|
||||||
return value;
|
return value;
|
||||||
|
|
|
@ -302,21 +302,6 @@ namespace MoonWorks.Math.Float
|
||||||
return (X * X) + (Y * Y) + (Z * Z);
|
return (X * X) + (Y * Y) + (Z * Z);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Turns this <see cref="Vector3"/> to a unit vector with the same direction.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
float factor = 1.0f / (float) System.Math.Sqrt(
|
|
||||||
(X * X) +
|
|
||||||
(Y * Y) +
|
|
||||||
(Z * Z)
|
|
||||||
);
|
|
||||||
X *= factor;
|
|
||||||
Y *= factor;
|
|
||||||
Z *= factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a <see cref="String"/> representation of this <see cref="Vector3"/> in the format:
|
/// Returns a <see cref="String"/> representation of this <see cref="Vector3"/> in the format:
|
||||||
/// {X:[<see cref="X"/>] Y:[<see cref="Y"/>] Z:[<see cref="Z"/>]}
|
/// {X:[<see cref="X"/>] Y:[<see cref="Y"/>] Z:[<see cref="Z"/>]}
|
||||||
|
@ -900,11 +885,14 @@ namespace MoonWorks.Math.Float
|
||||||
/// <returns>Unit vector.</returns>
|
/// <returns>Unit vector.</returns>
|
||||||
public static Vector3 Normalize(Vector3 value)
|
public static Vector3 Normalize(Vector3 value)
|
||||||
{
|
{
|
||||||
float factor = 1.0f / (float) System.Math.Sqrt(
|
float lengthSquared = (value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z);
|
||||||
(value.X * value.X) +
|
|
||||||
(value.Y * value.Y) +
|
if (lengthSquared == 0f)
|
||||||
(value.Z * value.Z)
|
{
|
||||||
);
|
return Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
float factor = 1.0f / System.MathF.Sqrt(lengthSquared);
|
||||||
return new Vector3(
|
return new Vector3(
|
||||||
value.X * factor,
|
value.X * factor,
|
||||||
value.Y * factor,
|
value.Y * factor,
|
||||||
|
|
|
@ -267,23 +267,6 @@ namespace MoonWorks.Math.Float
|
||||||
return (X * X) + (Y * Y) + (Z * Z) + (W * W);
|
return (X * X) + (Y * Y) + (Z * Z) + (W * W);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Turns this <see cref="Vector4"/> to a unit vector with the same direction.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
float factor = 1.0f / (float) System.Math.Sqrt(
|
|
||||||
(X * X) +
|
|
||||||
(Y * Y) +
|
|
||||||
(Z * Z) +
|
|
||||||
(W * W)
|
|
||||||
);
|
|
||||||
X *= factor;
|
|
||||||
Y *= factor;
|
|
||||||
Z *= factor;
|
|
||||||
W *= factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
|
@ -853,12 +836,15 @@ namespace MoonWorks.Math.Float
|
||||||
/// <returns>Unit vector.</returns>
|
/// <returns>Unit vector.</returns>
|
||||||
public static Vector4 Normalize(Vector4 vector)
|
public static Vector4 Normalize(Vector4 vector)
|
||||||
{
|
{
|
||||||
float factor = 1.0f / (float) System.Math.Sqrt(
|
var lengthSquared = (vector.X * vector.X) + (vector.Y * vector.Y) +
|
||||||
(vector.X * vector.X) +
|
(vector.Z * vector.Z) + (vector.W * vector.W);
|
||||||
(vector.Y * vector.Y) +
|
|
||||||
(vector.Z * vector.Z) +
|
if (lengthSquared == 0)
|
||||||
(vector.W * vector.W)
|
{
|
||||||
);
|
return Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
float factor = 1.0f / System.MathF.Sqrt(lengthSquared);
|
||||||
return new Vector4(
|
return new Vector4(
|
||||||
vector.X * factor,
|
vector.X * factor,
|
||||||
vector.Y * factor,
|
vector.Y * factor,
|
||||||
|
@ -870,16 +856,20 @@ namespace MoonWorks.Math.Float
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="Vector4"/> that contains a normalized values from another vector.
|
/// Creates a new <see cref="Vector4"/> that contains a normalized values from another vector.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">Source <see cref="Vector4"/>.</param>
|
/// <param name="vector">Source <see cref="Vector4"/>.</param>
|
||||||
/// <param name="result">Unit vector as an output parameter.</param>
|
/// <param name="result">Unit vector as an output parameter.</param>
|
||||||
public static void Normalize(ref Vector4 vector, out Vector4 result)
|
public static void Normalize(ref Vector4 vector, out Vector4 result)
|
||||||
{
|
{
|
||||||
float factor = 1.0f / (float) System.Math.Sqrt(
|
float lengthSquared = (vector.X * vector.X) + (vector.Y * vector.Y) +
|
||||||
(vector.X * vector.X) +
|
(vector.Z * vector.Z) + (vector.W * vector.W);
|
||||||
(vector.Y * vector.Y) +
|
|
||||||
(vector.Z * vector.Z) +
|
if (lengthSquared == 0)
|
||||||
(vector.W * vector.W)
|
{
|
||||||
);
|
result = Zero;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float factor = 1.0f / System.MathF.Sqrt(lengthSquared);
|
||||||
result.X = vector.X * factor;
|
result.X = vector.X * factor;
|
||||||
result.Y = vector.Y * factor;
|
result.Y = vector.Y * factor;
|
||||||
result.Z = vector.Z * factor;
|
result.Z = vector.Z * factor;
|
||||||
|
|
|
@ -160,6 +160,26 @@ namespace MoonWorks.Math
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restricts a value to be within a specified range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to clamp.</param>
|
||||||
|
/// <param name="min">
|
||||||
|
/// The minimum value. If <c>value</c> is less than <c>min</c>, <c>min</c>
|
||||||
|
/// will be returned.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="max">
|
||||||
|
/// The maximum value. If <c>value</c> is greater than <c>max</c>, <c>max</c>
|
||||||
|
/// will be returned.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>The clamped value.</returns>
|
||||||
|
public static int Clamp(int value, int min, int max)
|
||||||
|
{
|
||||||
|
value = (value > max) ? max : value;
|
||||||
|
value = (value < min) ? min : value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the absolute value of the difference of two values.
|
/// Calculates the absolute value of the difference of two values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -282,7 +302,12 @@ namespace MoonWorks.Math
|
||||||
|
|
||||||
public static float Quantize(float value, float step)
|
public static float Quantize(float value, float step)
|
||||||
{
|
{
|
||||||
return (float) System.Math.Floor(value / step) * step;
|
return System.MathF.Round(value / step) * step;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Fixed.Fix64 Quantize(Fixed.Fix64 value, Fixed.Fix64 step)
|
||||||
|
{
|
||||||
|
return Fixed.Fix64.Round(value / step) * step;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -395,27 +420,6 @@ namespace MoonWorks.Math
|
||||||
|
|
||||||
#region Internal Static Methods
|
#region Internal Static Methods
|
||||||
|
|
||||||
// FIXME: This could be an extension! ClampIntEXT? -flibit
|
|
||||||
/// <summary>
|
|
||||||
/// Restricts a value to be within a specified range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value to clamp.</param>
|
|
||||||
/// <param name="min">
|
|
||||||
/// The minimum value. If <c>value</c> is less than <c>min</c>, <c>min</c>
|
|
||||||
/// will be returned.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="max">
|
|
||||||
/// The maximum value. If <c>value</c> is greater than <c>max</c>, <c>max</c>
|
|
||||||
/// will be returned.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>The clamped value.</returns>
|
|
||||||
internal static int Clamp(int value, int min, int max)
|
|
||||||
{
|
|
||||||
value = (value > max) ? max : value;
|
|
||||||
value = (value < min) ? min : value;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool WithinEpsilon(float floatA, float floatB)
|
internal static bool WithinEpsilon(float floatA, float floatB)
|
||||||
{
|
{
|
||||||
return System.Math.Abs(floatA - floatB) < MachineEpsilonFloat;
|
return System.Math.Abs(floatA - floatB) < MachineEpsilonFloat;
|
||||||
|
|
|
@ -97,16 +97,12 @@ namespace MoonWorks
|
||||||
|
|
||||||
// Get the path to the assembly
|
// Get the path to the assembly
|
||||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
string assemblyPath = "";
|
string assemblyPath = System.AppContext.BaseDirectory;
|
||||||
if (assembly.Location != null)
|
|
||||||
{
|
|
||||||
assemblyPath = Path.GetDirectoryName(assembly.Location);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Locate the config file
|
// Locate the config file
|
||||||
string xmlPath = Path.Combine(
|
string xmlPath = Path.Combine(
|
||||||
assemblyPath,
|
assemblyPath,
|
||||||
assembly.GetName().Name + ".dll.config"
|
"MoonWorks.dll.config"
|
||||||
);
|
);
|
||||||
if (!File.Exists(xmlPath))
|
if (!File.Exists(xmlPath))
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
using System;
|
|
||||||
using MoonWorks.Audio;
|
|
||||||
|
|
||||||
namespace MoonWorks.Video
|
|
||||||
{
|
|
||||||
public unsafe class StreamingSoundTheora : StreamingSound
|
|
||||||
{
|
|
||||||
private IntPtr VideoHandle;
|
|
||||||
protected override int BUFFER_SIZE => 8192;
|
|
||||||
|
|
||||||
internal StreamingSoundTheora(
|
|
||||||
AudioDevice device,
|
|
||||||
IntPtr videoHandle,
|
|
||||||
int channels,
|
|
||||||
uint sampleRate
|
|
||||||
) : base(
|
|
||||||
device,
|
|
||||||
3, /* float type */
|
|
||||||
32, /* size of float */
|
|
||||||
(ushort) (4 * channels),
|
|
||||||
(ushort) channels,
|
|
||||||
sampleRate
|
|
||||||
) {
|
|
||||||
VideoHandle = videoHandle;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override unsafe void FillBuffer(
|
|
||||||
void* buffer,
|
|
||||||
int bufferLengthInBytes,
|
|
||||||
out int filledLengthInBytes,
|
|
||||||
out bool reachedEnd
|
|
||||||
) {
|
|
||||||
var lengthInFloats = bufferLengthInBytes / sizeof(float);
|
|
||||||
|
|
||||||
int samples = Theorafile.tf_readaudio(
|
|
||||||
VideoHandle,
|
|
||||||
(IntPtr) buffer,
|
|
||||||
lengthInFloats
|
|
||||||
);
|
|
||||||
|
|
||||||
filledLengthInBytes = samples * sizeof(float);
|
|
||||||
reachedEnd = Theorafile.tf_eos(VideoHandle) == 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
/* Heavily based on https://github.com/FNA-XNA/FNA/blob/master/src/Media/Xiph/VideoPlayer.cs */
|
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Video
|
|
||||||
{
|
|
||||||
public enum VideoState
|
|
||||||
{
|
|
||||||
Playing,
|
|
||||||
Paused,
|
|
||||||
Stopped
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe class Video : IDisposable
|
|
||||||
{
|
|
||||||
internal IntPtr Handle;
|
|
||||||
private IntPtr rwData;
|
|
||||||
private void* videoData;
|
|
||||||
|
|
||||||
public double FramesPerSecond => fps;
|
|
||||||
public int Width => yWidth;
|
|
||||||
public int Height => yHeight;
|
|
||||||
public int UVWidth { get; }
|
|
||||||
public int UVHeight { get; }
|
|
||||||
|
|
||||||
private double fps;
|
|
||||||
private int yWidth;
|
|
||||||
private int yHeight;
|
|
||||||
|
|
||||||
private bool disposed;
|
|
||||||
|
|
||||||
public Video(string filename)
|
|
||||||
{
|
|
||||||
if (!System.IO.File.Exists(filename))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Video file not found!");
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = System.IO.File.ReadAllBytes(filename);
|
|
||||||
videoData = NativeMemory.Alloc((nuint) bytes.Length);
|
|
||||||
Marshal.Copy(bytes, 0, (IntPtr) videoData, bytes.Length);
|
|
||||||
rwData = SDL2.SDL.SDL_RWFromMem((IntPtr) videoData, bytes.Length);
|
|
||||||
|
|
||||||
if (Theorafile.tf_open_callbacks(rwData, out Handle, callbacks) < 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Invalid video file!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Theorafile.th_pixel_fmt format;
|
|
||||||
Theorafile.tf_videoinfo(
|
|
||||||
Handle,
|
|
||||||
out yWidth,
|
|
||||||
out yHeight,
|
|
||||||
out fps,
|
|
||||||
out format
|
|
||||||
);
|
|
||||||
|
|
||||||
if (format == Theorafile.th_pixel_fmt.TH_PF_420)
|
|
||||||
{
|
|
||||||
UVWidth = Width / 2;
|
|
||||||
UVHeight = Height / 2;
|
|
||||||
}
|
|
||||||
else if (format == Theorafile.th_pixel_fmt.TH_PF_422)
|
|
||||||
{
|
|
||||||
UVWidth = Width / 2;
|
|
||||||
UVHeight = Height;
|
|
||||||
}
|
|
||||||
else if (format == Theorafile.th_pixel_fmt.TH_PF_444)
|
|
||||||
{
|
|
||||||
UVWidth = Width;
|
|
||||||
UVHeight = Height;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new NotSupportedException("Unrecognized YUV format!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IntPtr Read(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr datasource) => (IntPtr) SDL2.SDL.SDL_RWread(datasource, ptr, size, nmemb);
|
|
||||||
private static int Seek(IntPtr datasource, long offset, Theorafile.SeekWhence whence) => (int) SDL2.SDL.SDL_RWseek(datasource, offset, (int) whence);
|
|
||||||
private static int Close(IntPtr datasource) => (int) SDL2.SDL.SDL_RWclose(datasource);
|
|
||||||
|
|
||||||
private static Theorafile.tf_callbacks callbacks = new Theorafile.tf_callbacks
|
|
||||||
{
|
|
||||||
read_func = Read,
|
|
||||||
seek_func = Seek,
|
|
||||||
close_func = Close
|
|
||||||
};
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!disposed)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
// dispose managed state (managed objects)
|
|
||||||
}
|
|
||||||
|
|
||||||
// free unmanaged resources (unmanaged objects)
|
|
||||||
Theorafile.tf_close(ref Handle);
|
|
||||||
NativeMemory.Free(videoData);
|
|
||||||
|
|
||||||
disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~Video()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using MoonWorks.Graphics;
|
||||||
|
|
||||||
|
namespace MoonWorks.Video
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This class takes in a filename for AV1 data in .obu (open bitstream unit) format
|
||||||
|
/// </summary>
|
||||||
|
public unsafe class VideoAV1 : GraphicsResource
|
||||||
|
{
|
||||||
|
public string Filename { get; }
|
||||||
|
|
||||||
|
// "double buffering" so we can loop without a stutter
|
||||||
|
internal VideoAV1Stream StreamA { get; }
|
||||||
|
internal VideoAV1Stream StreamB { get; }
|
||||||
|
|
||||||
|
public int Width => width;
|
||||||
|
public int Height => height;
|
||||||
|
public double FramesPerSecond { get; set; }
|
||||||
|
public Dav1dfile.PixelLayout PixelLayout => pixelLayout;
|
||||||
|
public int UVWidth { get; }
|
||||||
|
public int UVHeight { get; }
|
||||||
|
|
||||||
|
private int width;
|
||||||
|
private int height;
|
||||||
|
private Dav1dfile.PixelLayout pixelLayout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens an AV1 file so it can be loaded by VideoPlayer. You must also provide a playback framerate.
|
||||||
|
/// </summary>
|
||||||
|
public VideoAV1(GraphicsDevice device, string filename, double framesPerSecond) : base(device)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filename))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Video file not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Dav1dfile.df_fopen(filename, out var handle) == 0)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to open video file!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Dav1dfile.df_videoinfo(handle, out width, out height, out pixelLayout);
|
||||||
|
Dav1dfile.df_close(handle);
|
||||||
|
|
||||||
|
if (pixelLayout == Dav1dfile.PixelLayout.I420)
|
||||||
|
{
|
||||||
|
UVWidth = Width / 2;
|
||||||
|
UVHeight = Height / 2;
|
||||||
|
}
|
||||||
|
else if (pixelLayout == Dav1dfile.PixelLayout.I422)
|
||||||
|
{
|
||||||
|
UVWidth = Width / 2;
|
||||||
|
UVHeight = Height;
|
||||||
|
}
|
||||||
|
else if (pixelLayout == Dav1dfile.PixelLayout.I444)
|
||||||
|
{
|
||||||
|
UVWidth = width;
|
||||||
|
UVHeight = height;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Unrecognized YUV format!");
|
||||||
|
}
|
||||||
|
|
||||||
|
FramesPerSecond = framesPerSecond;
|
||||||
|
|
||||||
|
Filename = filename;
|
||||||
|
|
||||||
|
StreamA = new VideoAV1Stream(device, this);
|
||||||
|
StreamB = new VideoAV1Stream(device, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: if you call this while a VideoPlayer is playing the stream, your program will explode
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
StreamA.Dispose();
|
||||||
|
StreamB.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
using System;
|
||||||
|
using MoonWorks.Graphics;
|
||||||
|
|
||||||
|
namespace MoonWorks.Video
|
||||||
|
{
|
||||||
|
internal class VideoAV1Stream : GraphicsResource
|
||||||
|
{
|
||||||
|
public IntPtr Handle => handle;
|
||||||
|
IntPtr handle;
|
||||||
|
|
||||||
|
public bool Ended => Dav1dfile.df_eos(Handle) == 1;
|
||||||
|
|
||||||
|
public IntPtr yDataHandle;
|
||||||
|
public IntPtr uDataHandle;
|
||||||
|
public IntPtr vDataHandle;
|
||||||
|
public uint yDataLength;
|
||||||
|
public uint uvDataLength;
|
||||||
|
public uint yStride;
|
||||||
|
public uint uvStride;
|
||||||
|
|
||||||
|
public bool FrameDataUpdated { get; set; }
|
||||||
|
|
||||||
|
public VideoAV1Stream(GraphicsDevice device, VideoAV1 video) : base(device)
|
||||||
|
{
|
||||||
|
if (Dav1dfile.df_fopen(video.Filename, out handle) == 0)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to open video file!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
Dav1dfile.df_reset(Handle);
|
||||||
|
ReadNextFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReadNextFrame()
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
if (!Ended)
|
||||||
|
{
|
||||||
|
if (Dav1dfile.df_readvideo(
|
||||||
|
Handle,
|
||||||
|
1,
|
||||||
|
out var yDataHandle,
|
||||||
|
out var uDataHandle,
|
||||||
|
out var vDataHandle,
|
||||||
|
out var yDataLength,
|
||||||
|
out var uvDataLength,
|
||||||
|
out var yStride,
|
||||||
|
out var uvStride) == 1
|
||||||
|
) {
|
||||||
|
this.yDataHandle = yDataHandle;
|
||||||
|
this.uDataHandle = uDataHandle;
|
||||||
|
this.vDataHandle = vDataHandle;
|
||||||
|
this.yDataLength = yDataLength;
|
||||||
|
this.uvDataLength = uvDataLength;
|
||||||
|
this.yStride = yStride;
|
||||||
|
this.uvStride = uvStride;
|
||||||
|
|
||||||
|
FrameDataUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
Dav1dfile.df_close(Handle);
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,30 +1,26 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Threading.Tasks;
|
||||||
using MoonWorks.Audio;
|
|
||||||
using MoonWorks.Graphics;
|
using MoonWorks.Graphics;
|
||||||
|
|
||||||
namespace MoonWorks.Video
|
namespace MoonWorks.Video
|
||||||
{
|
{
|
||||||
public unsafe class VideoPlayer : IDisposable
|
/// <summary>
|
||||||
|
/// A structure for continuous decoding of AV1 videos and rendering them into a texture.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe class VideoPlayer : GraphicsResource
|
||||||
{
|
{
|
||||||
public Texture RenderTexture { get; private set; } = null;
|
public Texture RenderTexture { get; private set; } = null;
|
||||||
public VideoState State { get; private set; } = VideoState.Stopped;
|
public VideoState State { get; private set; } = VideoState.Stopped;
|
||||||
public bool Loop { get; set; }
|
public bool Loop { get; set; }
|
||||||
public float Volume {
|
|
||||||
get => volume;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
volume = value;
|
|
||||||
if (audioStream != null)
|
|
||||||
{
|
|
||||||
audioStream.Volume = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public float PlaybackSpeed { get; set; } = 1;
|
public float PlaybackSpeed { get; set; } = 1;
|
||||||
|
|
||||||
private Video Video = null;
|
private VideoAV1 Video = null;
|
||||||
|
private VideoAV1Stream CurrentStream = null;
|
||||||
|
|
||||||
|
private Task ReadNextFrameTask;
|
||||||
|
private Task ResetStreamATask;
|
||||||
|
private Task ResetStreamBTask;
|
||||||
|
|
||||||
private GraphicsDevice GraphicsDevice;
|
private GraphicsDevice GraphicsDevice;
|
||||||
private Texture yTexture = null;
|
private Texture yTexture = null;
|
||||||
|
@ -32,36 +28,26 @@ namespace MoonWorks.Video
|
||||||
private Texture vTexture = null;
|
private Texture vTexture = null;
|
||||||
private Sampler LinearSampler;
|
private Sampler LinearSampler;
|
||||||
|
|
||||||
private void* yuvData = null;
|
|
||||||
private int yuvDataLength = 0;
|
|
||||||
|
|
||||||
private int currentFrame;
|
private int currentFrame;
|
||||||
|
|
||||||
private AudioDevice AudioDevice;
|
|
||||||
private StreamingSoundTheora audioStream = null;
|
|
||||||
private float volume = 1.0f;
|
|
||||||
|
|
||||||
private Stopwatch timer;
|
private Stopwatch timer;
|
||||||
private double lastTimestamp;
|
private double lastTimestamp;
|
||||||
private double timeElapsed;
|
private double timeElapsed;
|
||||||
|
|
||||||
private bool disposed;
|
public VideoPlayer(GraphicsDevice device) : base(device)
|
||||||
|
|
||||||
public VideoPlayer(GraphicsDevice graphicsDevice, AudioDevice audioDevice)
|
|
||||||
{
|
{
|
||||||
GraphicsDevice = graphicsDevice;
|
GraphicsDevice = device;
|
||||||
if (GraphicsDevice.VideoPipeline == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Missing video shaders!");
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioDevice = audioDevice;
|
LinearSampler = new Sampler(device, SamplerCreateInfo.LinearClamp);
|
||||||
LinearSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp);
|
|
||||||
|
|
||||||
timer = new Stopwatch();
|
timer = new Stopwatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Load(Video video)
|
/// <summary>
|
||||||
|
/// Prepares a VideoAV1 for decoding and rendering.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="video"></param>
|
||||||
|
public void Load(VideoAV1 video)
|
||||||
{
|
{
|
||||||
if (Video != video)
|
if (Video != video)
|
||||||
{
|
{
|
||||||
|
@ -111,25 +97,19 @@ namespace MoonWorks.Video
|
||||||
vTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
|
vTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
var newDataLength = (
|
|
||||||
(video.Width * video.Height) +
|
|
||||||
(video.UVWidth * video.UVHeight * 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newDataLength != yuvDataLength)
|
|
||||||
{
|
|
||||||
yuvData = NativeMemory.Realloc(yuvData, (nuint) newDataLength);
|
|
||||||
yuvDataLength = newDataLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
Video = video;
|
Video = video;
|
||||||
|
|
||||||
InitializeTheoraStream();
|
InitializeDav1dStream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts playing back and decoding the loaded video.
|
||||||
|
/// </summary>
|
||||||
public void Play()
|
public void Play()
|
||||||
{
|
{
|
||||||
|
if (Video == null) { return; }
|
||||||
|
|
||||||
if (State == VideoState.Playing)
|
if (State == VideoState.Playing)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -137,16 +117,16 @@ namespace MoonWorks.Video
|
||||||
|
|
||||||
timer.Start();
|
timer.Start();
|
||||||
|
|
||||||
if (audioStream != null)
|
|
||||||
{
|
|
||||||
audioStream.Play();
|
|
||||||
}
|
|
||||||
|
|
||||||
State = VideoState.Playing;
|
State = VideoState.Playing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pauses playback and decoding of the currently playing video.
|
||||||
|
/// </summary>
|
||||||
public void Pause()
|
public void Pause()
|
||||||
{
|
{
|
||||||
|
if (Video == null) { return; }
|
||||||
|
|
||||||
if (State != VideoState.Playing)
|
if (State != VideoState.Playing)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -154,16 +134,16 @@ namespace MoonWorks.Video
|
||||||
|
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
|
|
||||||
if (audioStream != null)
|
|
||||||
{
|
|
||||||
audioStream.Pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
State = VideoState.Paused;
|
State = VideoState.Paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops and resets decoding of the currently playing video.
|
||||||
|
/// </summary>
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
|
if (Video == null) { return; }
|
||||||
|
|
||||||
if (State == VideoState.Stopped)
|
if (State == VideoState.Stopped)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -172,20 +152,28 @@ namespace MoonWorks.Video
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
timer.Reset();
|
timer.Reset();
|
||||||
|
|
||||||
Theorafile.tf_reset(Video.Handle);
|
|
||||||
lastTimestamp = 0;
|
lastTimestamp = 0;
|
||||||
timeElapsed = 0;
|
timeElapsed = 0;
|
||||||
|
|
||||||
if (audioStream != null)
|
InitializeDav1dStream();
|
||||||
{
|
|
||||||
audioStream.StopImmediate();
|
|
||||||
audioStream.Dispose();
|
|
||||||
audioStream = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
State = VideoState.Stopped;
|
State = VideoState.Stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unloads the currently playing video.
|
||||||
|
/// </summary>
|
||||||
|
public void Unload()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
ResetStreamATask?.Wait();
|
||||||
|
ResetStreamBTask?.Wait();
|
||||||
|
Video = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders the video data into RenderTexture.
|
||||||
|
/// </summary>
|
||||||
public void Render()
|
public void Render()
|
||||||
{
|
{
|
||||||
if (Video == null || State == VideoState.Stopped)
|
if (Video == null || State == VideoState.Stopped)
|
||||||
|
@ -199,37 +187,39 @@ namespace MoonWorks.Video
|
||||||
int thisFrame = ((int) (timeElapsed / (1000.0 / Video.FramesPerSecond)));
|
int thisFrame = ((int) (timeElapsed / (1000.0 / Video.FramesPerSecond)));
|
||||||
if (thisFrame > currentFrame)
|
if (thisFrame > currentFrame)
|
||||||
{
|
{
|
||||||
if (Theorafile.tf_readvideo(
|
if (CurrentStream.FrameDataUpdated)
|
||||||
Video.Handle,
|
{
|
||||||
(IntPtr) yuvData,
|
|
||||||
thisFrame - currentFrame
|
|
||||||
) == 1 || currentFrame == -1) {
|
|
||||||
UpdateRenderTexture();
|
UpdateRenderTexture();
|
||||||
|
CurrentStream.FrameDataUpdated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentFrame = thisFrame;
|
currentFrame = thisFrame;
|
||||||
|
ReadNextFrameTask = Task.Run(CurrentStream.ReadNextFrame);
|
||||||
|
ReadNextFrameTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ended = Theorafile.tf_eos(Video.Handle) == 1;
|
if (CurrentStream.Ended)
|
||||||
if (ended)
|
|
||||||
{
|
{
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
timer.Reset();
|
timer.Reset();
|
||||||
|
|
||||||
if (audioStream != null)
|
var task = Task.Run(CurrentStream.Reset);
|
||||||
{
|
task.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
|
||||||
audioStream.Stop();
|
|
||||||
audioStream.Dispose();
|
|
||||||
audioStream = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Theorafile.tf_reset(Video.Handle);
|
if (CurrentStream == Video.StreamA)
|
||||||
|
{
|
||||||
|
ResetStreamATask = task;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ResetStreamBTask = task;
|
||||||
|
}
|
||||||
|
|
||||||
if (Loop)
|
if (Loop)
|
||||||
{
|
{
|
||||||
// Start over!
|
// Start over on the next stream!
|
||||||
InitializeTheoraStream();
|
CurrentStream = (CurrentStream == Video.StreamA) ? Video.StreamB : Video.StreamA;
|
||||||
|
currentFrame = -1;
|
||||||
timer.Start();
|
timer.Start();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -241,32 +231,40 @@ namespace MoonWorks.Video
|
||||||
|
|
||||||
private void UpdateRenderTexture()
|
private void UpdateRenderTexture()
|
||||||
{
|
{
|
||||||
var commandBuffer = GraphicsDevice.AcquireCommandBuffer();
|
lock (CurrentStream)
|
||||||
|
{
|
||||||
|
var commandBuffer = GraphicsDevice.AcquireCommandBuffer();
|
||||||
|
|
||||||
commandBuffer.SetTextureDataYUV(
|
commandBuffer.SetTextureDataYUV(
|
||||||
yTexture,
|
yTexture,
|
||||||
uTexture,
|
uTexture,
|
||||||
vTexture,
|
vTexture,
|
||||||
(IntPtr) yuvData,
|
CurrentStream.yDataHandle,
|
||||||
(uint) yuvDataLength
|
CurrentStream.uDataHandle,
|
||||||
);
|
CurrentStream.vDataHandle,
|
||||||
|
CurrentStream.yDataLength,
|
||||||
|
CurrentStream.uvDataLength,
|
||||||
|
CurrentStream.yStride,
|
||||||
|
CurrentStream.uvStride
|
||||||
|
);
|
||||||
|
|
||||||
commandBuffer.BeginRenderPass(
|
commandBuffer.BeginRenderPass(
|
||||||
new ColorAttachmentInfo(RenderTexture, Color.Black)
|
new ColorAttachmentInfo(RenderTexture, Color.Black)
|
||||||
);
|
);
|
||||||
|
|
||||||
commandBuffer.BindGraphicsPipeline(GraphicsDevice.VideoPipeline);
|
commandBuffer.BindGraphicsPipeline(GraphicsDevice.VideoPipeline);
|
||||||
commandBuffer.BindFragmentSamplers(
|
commandBuffer.BindFragmentSamplers(
|
||||||
new TextureSamplerBinding(yTexture, LinearSampler),
|
new TextureSamplerBinding(yTexture, LinearSampler),
|
||||||
new TextureSamplerBinding(uTexture, LinearSampler),
|
new TextureSamplerBinding(uTexture, LinearSampler),
|
||||||
new TextureSamplerBinding(vTexture, LinearSampler)
|
new TextureSamplerBinding(vTexture, LinearSampler)
|
||||||
);
|
);
|
||||||
|
|
||||||
commandBuffer.DrawPrimitives(0, 1, 0, 0);
|
commandBuffer.DrawPrimitives(0, 1, 0, 0);
|
||||||
|
|
||||||
commandBuffer.EndRenderPass();
|
commandBuffer.EndRenderPass();
|
||||||
|
|
||||||
GraphicsDevice.Submit(commandBuffer);
|
GraphicsDevice.Submit(commandBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Texture CreateRenderTexture(GraphicsDevice graphicsDevice, int width, int height)
|
private static Texture CreateRenderTexture(GraphicsDevice graphicsDevice, int width, int height)
|
||||||
|
@ -291,53 +289,42 @@ namespace MoonWorks.Video
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeTheoraStream()
|
private void InitializeDav1dStream()
|
||||||
{
|
{
|
||||||
// Grab the first video frame ASAP.
|
ReadNextFrameTask?.Wait();
|
||||||
while (Theorafile.tf_readvideo(Video.Handle, (IntPtr) yuvData, 1) == 0);
|
|
||||||
|
|
||||||
// Grab the first bit of audio. We're trying to start the decoding ASAP.
|
ResetStreamATask = Task.Run(Video.StreamA.Reset);
|
||||||
if (AudioDevice != null && Theorafile.tf_hasaudio(Video.Handle) == 1)
|
ResetStreamATask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
|
||||||
{
|
ResetStreamBTask = Task.Run(Video.StreamB.Reset);
|
||||||
int channels, sampleRate;
|
ResetStreamBTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
|
||||||
Theorafile.tf_audioinfo(Video.Handle, out channels, out sampleRate);
|
|
||||||
audioStream = new StreamingSoundTheora(AudioDevice, Video.Handle, channels, (uint) sampleRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
CurrentStream = Video.StreamA;
|
||||||
currentFrame = -1;
|
currentFrame = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
private static void HandleTaskException(Task task)
|
||||||
{
|
{
|
||||||
if (!disposed)
|
if (task.Exception.InnerException is not TaskCanceledException)
|
||||||
{
|
{
|
||||||
if (disposing)
|
throw task.Exception;
|
||||||
{
|
|
||||||
// dispose managed state (managed objects)
|
|
||||||
RenderTexture.Dispose();
|
|
||||||
yTexture.Dispose();
|
|
||||||
uTexture.Dispose();
|
|
||||||
vTexture.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
|
||||||
NativeMemory.Free(yuvData);
|
|
||||||
|
|
||||||
disposed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
~VideoPlayer()
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
if (!IsDisposed)
|
||||||
Dispose(disposing: false);
|
{
|
||||||
}
|
if (disposing)
|
||||||
|
{
|
||||||
|
Unload();
|
||||||
|
|
||||||
public void Dispose()
|
RenderTexture?.Dispose();
|
||||||
{
|
yTexture?.Dispose();
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
uTexture?.Dispose();
|
||||||
Dispose(disposing: true);
|
vTexture?.Dispose();
|
||||||
GC.SuppressFinalize(this);
|
}
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace MoonWorks.Video
|
||||||
|
{
|
||||||
|
public enum VideoState
|
||||||
|
{
|
||||||
|
Playing,
|
||||||
|
Paused,
|
||||||
|
Stopped
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,13 +5,18 @@ using SDL2;
|
||||||
|
|
||||||
namespace MoonWorks
|
namespace MoonWorks
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a window in the client operating system. <br/>
|
||||||
|
/// Every Game has a MainWindow automatically. <br/>
|
||||||
|
/// You can create additional Windows if you desire. They must be Claimed by the GraphicsDevice to be rendered to.
|
||||||
|
/// </summary>
|
||||||
public class Window : IDisposable
|
public class Window : IDisposable
|
||||||
{
|
{
|
||||||
internal IntPtr Handle { get; }
|
internal IntPtr Handle { get; }
|
||||||
public ScreenMode ScreenMode { get; private set; }
|
public ScreenMode ScreenMode { get; private set; }
|
||||||
public uint Width { get; private set; }
|
public uint Width { get; private set; }
|
||||||
public uint Height { get; private set; }
|
public uint Height { get; private set; }
|
||||||
internal Texture SwapchainTexture { get; set; } = null;
|
internal Texture SwapchainTexture { get; set; }
|
||||||
|
|
||||||
public bool Claimed { get; internal set; }
|
public bool Claimed { get; internal set; }
|
||||||
public MoonWorks.Graphics.TextureFormat SwapchainFormat { get; internal set; }
|
public MoonWorks.Graphics.TextureFormat SwapchainFormat { get; internal set; }
|
||||||
|
@ -45,21 +50,28 @@ namespace MoonWorks
|
||||||
|
|
||||||
ScreenMode = windowCreateInfo.ScreenMode;
|
ScreenMode = windowCreateInfo.ScreenMode;
|
||||||
|
|
||||||
|
SDL.SDL_GetDesktopDisplayMode(0, out var displayMode);
|
||||||
|
|
||||||
Handle = SDL.SDL_CreateWindow(
|
Handle = SDL.SDL_CreateWindow(
|
||||||
windowCreateInfo.WindowTitle,
|
windowCreateInfo.WindowTitle,
|
||||||
SDL.SDL_WINDOWPOS_UNDEFINED,
|
SDL.SDL_WINDOWPOS_CENTERED,
|
||||||
SDL.SDL_WINDOWPOS_UNDEFINED,
|
SDL.SDL_WINDOWPOS_CENTERED,
|
||||||
(int) windowCreateInfo.WindowWidth,
|
windowCreateInfo.ScreenMode == ScreenMode.Windowed ? (int) windowCreateInfo.WindowWidth : displayMode.w,
|
||||||
(int) windowCreateInfo.WindowHeight,
|
windowCreateInfo.ScreenMode == ScreenMode.Windowed ? (int) windowCreateInfo.WindowHeight : displayMode.h,
|
||||||
flags
|
flags
|
||||||
);
|
);
|
||||||
|
|
||||||
Width = windowCreateInfo.WindowWidth;
|
/* Requested size might be different in fullscreen, so let's just get the area */
|
||||||
Height = windowCreateInfo.WindowHeight;
|
SDL.SDL_GetWindowSize(Handle, out var width, out var height);
|
||||||
|
Width = (uint) width;
|
||||||
|
Height = (uint) height;
|
||||||
|
|
||||||
idLookup.Add(SDL.SDL_GetWindowID(Handle), this);
|
idLookup.Add(SDL.SDL_GetWindowID(Handle), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the ScreenMode of this window.
|
||||||
|
/// </summary>
|
||||||
public void SetScreenMode(ScreenMode screenMode)
|
public void SetScreenMode(ScreenMode screenMode)
|
||||||
{
|
{
|
||||||
SDL.SDL_WindowFlags windowFlag = 0;
|
SDL.SDL_WindowFlags windowFlag = 0;
|
||||||
|
@ -73,13 +85,18 @@ namespace MoonWorks
|
||||||
windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
|
windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScreenMode = screenMode;
|
|
||||||
|
|
||||||
SDL.SDL_SetWindowFullscreen(Handle, (uint) windowFlag);
|
SDL.SDL_SetWindowFullscreen(Handle, (uint) windowFlag);
|
||||||
|
|
||||||
|
if (screenMode == ScreenMode.Windowed)
|
||||||
|
{
|
||||||
|
SDL.SDL_SetWindowPosition(Handle, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenMode = screenMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resizes the window.
|
/// Resizes the window. <br/>
|
||||||
/// Note that you are responsible for recreating any graphics resources that need to change as a result of the size change.
|
/// Note that you are responsible for recreating any graphics resources that need to change as a result of the size change.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="width"></param>
|
/// <param name="width"></param>
|
||||||
|
@ -89,6 +106,11 @@ namespace MoonWorks
|
||||||
SDL.SDL_SetWindowSize(Handle, (int) width, (int) height);
|
SDL.SDL_SetWindowSize(Handle, (int) width, (int) height);
|
||||||
Width = width;
|
Width = width;
|
||||||
Height = height;
|
Height = height;
|
||||||
|
|
||||||
|
if (ScreenMode == ScreenMode.Windowed)
|
||||||
|
{
|
||||||
|
SDL.SDL_SetWindowPosition(Handle, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Window Lookup(uint windowID)
|
internal static Window Lookup(uint windowID)
|
||||||
|
@ -112,6 +134,9 @@ namespace MoonWorks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// You can specify a method to run when the window size changes.
|
||||||
|
/// </summary>
|
||||||
public void RegisterSizeChangeCallback(System.Action<uint, uint> sizeChangeCallback)
|
public void RegisterSizeChangeCallback(System.Action<uint, uint> sizeChangeCallback)
|
||||||
{
|
{
|
||||||
SizeChangeCallback = sizeChangeCallback;
|
SizeChangeCallback = sizeChangeCallback;
|
||||||
|
|
|
@ -1,13 +1,37 @@
|
||||||
namespace MoonWorks
|
namespace MoonWorks
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All the information required for window creation.
|
||||||
|
/// </summary>
|
||||||
public struct WindowCreateInfo
|
public struct WindowCreateInfo
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the window that will be displayed in the operating system.
|
||||||
|
/// </summary>
|
||||||
public string WindowTitle;
|
public string WindowTitle;
|
||||||
|
/// <summary>
|
||||||
|
/// The width of the window.
|
||||||
|
/// </summary>
|
||||||
public uint WindowWidth;
|
public uint WindowWidth;
|
||||||
|
/// <summary>
|
||||||
|
/// The height of the window.
|
||||||
|
/// </summary>
|
||||||
public uint WindowHeight;
|
public uint WindowHeight;
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies if the window will be created in windowed mode or a fullscreen mode.
|
||||||
|
/// </summary>
|
||||||
public ScreenMode ScreenMode;
|
public ScreenMode ScreenMode;
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the presentation mode for the window. Roughly equivalent to V-Sync.
|
||||||
|
/// </summary>
|
||||||
public PresentMode PresentMode;
|
public PresentMode PresentMode;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the window can be resized using the operating system's window dragging feature.
|
||||||
|
/// </summary>
|
||||||
public bool SystemResizable;
|
public bool SystemResizable;
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies if the window will open at the maximum desktop resolution.
|
||||||
|
/// </summary>
|
||||||
public bool StartMaximized;
|
public bool StartMaximized;
|
||||||
|
|
||||||
public WindowCreateInfo(
|
public WindowCreateInfo(
|
||||||
|
|
Loading…
Reference in New Issue