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"]
|
||||
path = lib/WellspringCS
|
||||
url = https://gitea.moonside.games/MoonsideGames/WellspringCS.git
|
||||
[submodule "lib/Theorafile"]
|
||||
path = lib/Theorafile
|
||||
url = https://github.com/FNA-XNA/Theorafile.git
|
||||
[submodule "lib/dav1dfile"]
|
||||
path = lib/dav1dfile
|
||||
url = https://github.com/MoonsideGames/dav1dfile.git
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0</TargetFrameworks>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>11</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
@ -15,13 +14,29 @@
|
|||
<Compile Include="lib\FAudio\csharp\FAudio.cs" />
|
||||
<Compile Include="lib\RefreshCS\src\Refresh.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\dav1dfile\csharp\dav1dfile.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="MoonWorks.dll.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</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>
|
||||
|
|
|
@ -5,18 +5,18 @@
|
|||
<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="osx" target="libRefresh.0.dylib"/>
|
||||
<dllmap dll="Refresh" os="linux,freebsd,netbsd" target="libRefresh.so.0"/>
|
||||
<dllmap dll="Refresh" os="osx" target="libRefresh.1.dylib"/>
|
||||
<dllmap dll="Refresh" os="linux,freebsd,netbsd" target="libRefresh.so.1"/>
|
||||
|
||||
<dllmap dll="FAudio" os="windows" target="FAudio.dll"/>
|
||||
<dllmap dll="FAudio" os="osx" target="libFAudio.0.dylib"/>
|
||||
<dllmap dll="FAudio" os="linux,freebsd,netbsd" target="libFAudio.so.0"/>
|
||||
|
||||
<dllmap dll="Wellspring" os="windows" target="Wellspring.dll"/>
|
||||
<dllmap dll="Wellspring" os="osx" target="libWellspring.0.dylib"/>
|
||||
<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.0"/>
|
||||
<dllmap dll="Wellspring" os="osx" target="libWellspring.1.dylib"/>
|
||||
<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.1"/>
|
||||
|
||||
<dllmap dll="Theorafile" os="windows" target="libtheorafile.dll"/>
|
||||
<dllmap dll="Theorafile" os="osx" target="libtheorafile.dylib"/>
|
||||
<dllmap dll="Theorafile" os="linux,freebsd,netbsd" target="libtheorafile.so"/>
|
||||
<dllmap dll="dav1dfile" os="windows" target="dav1dfile.dll"/>
|
||||
<dllmap dll="dav1dfile" os="osx" target="libdav1dfile.1.dylib"/>
|
||||
<dllmap dll="dav1dfile" os="linux,freebsd,netbsd,openbsd" target="libdav1dfile.so.1"/>
|
||||
</configuration>
|
||||
|
|
|
@ -12,9 +12,13 @@ MoonWorks uses strictly Free Open Source Software. It will never have any kind o
|
|||
|
||||
## Documentation
|
||||
|
||||
API Reference: https://moonside.games/docs/moonworksapi/
|
||||
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
* [FAudio](https://github.com/FNA-XNA/FAudio) - Audio
|
||||
* [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
|
||||
|
||||
|
|
|
@ -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.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// AudioDevice manages all audio-related concerns.
|
||||
/// </summary>
|
||||
public class AudioDevice : IDisposable
|
||||
{
|
||||
public IntPtr Handle { get; }
|
||||
public byte[] Handle3D { get; }
|
||||
public IntPtr MasteringVoice { 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 DopplerScale = 1f;
|
||||
public float SpeedOfSound = 343.5f;
|
||||
|
||||
private float masteringVolume = 1f;
|
||||
public float MasteringVolume
|
||||
private readonly HashSet<GCHandle> resourceHandles = new HashSet<GCHandle>();
|
||||
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;
|
||||
set
|
||||
{
|
||||
masteringVolume = value;
|
||||
FAudio.FAudioVoice_SetVolume(MasteringVoice, masteringVolume, 0);
|
||||
}
|
||||
}
|
||||
UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step);
|
||||
|
||||
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);
|
||||
Handle = handle;
|
||||
|
||||
|
@ -80,25 +92,24 @@ namespace MoonWorks.Audio
|
|||
}
|
||||
|
||||
/* Init Mastering Voice */
|
||||
IntPtr masteringVoice;
|
||||
|
||||
if (FAudio.FAudio_CreateMasteringVoice(
|
||||
var result = FAudio.FAudio_CreateMasteringVoice(
|
||||
Handle,
|
||||
out masteringVoice,
|
||||
out trueMasteringVoice,
|
||||
FAudio.FAUDIO_DEFAULT_CHANNELS,
|
||||
FAudio.FAUDIO_DEFAULT_SAMPLERATE,
|
||||
0,
|
||||
i,
|
||||
IntPtr.Zero
|
||||
) != 0)
|
||||
);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
Logger.LogError("No mastering voice found!");
|
||||
Handle = IntPtr.Zero;
|
||||
FAudio.FAudio_Release(Handle);
|
||||
Logger.LogError("Failed to create a mastering voice!");
|
||||
Logger.LogError("Audio device creation failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
MasteringVoice = masteringVoice;
|
||||
fauxMasteringVoice = SubmixVoice.CreateFauxMasteringVoice(this);
|
||||
|
||||
/* Init 3D Audio */
|
||||
|
||||
|
@ -109,169 +120,224 @@ namespace MoonWorks.Audio
|
|||
Handle3D
|
||||
);
|
||||
|
||||
/* Init reverb */
|
||||
AudioTweenManager = new AudioTweenManager();
|
||||
VoicePool = new SourceVoicePool(this);
|
||||
|
||||
IntPtr reverbVoice;
|
||||
WakeSignal = new AutoResetEvent(true);
|
||||
|
||||
IntPtr reverb;
|
||||
FAudio.FAudioCreateReverb(out reverb, 0);
|
||||
Thread = new Thread(ThreadMain);
|
||||
Thread.IsBackground = true;
|
||||
Thread.Start();
|
||||
|
||||
IntPtr chainPtr;
|
||||
chainPtr = Marshal.AllocHGlobal(
|
||||
Marshal.SizeOf<FAudio.FAudioEffectChain>()
|
||||
Running = true;
|
||||
|
||||
TickStopwatch.Start();
|
||||
previousTickTime = 0;
|
||||
}
|
||||
|
||||
private void ThreadMain()
|
||||
{
|
||||
while (Running)
|
||||
{
|
||||
lock (StateLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
ThreadMainTick();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
);
|
||||
|
||||
FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr;
|
||||
reverbChain->EffectCount = 1;
|
||||
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()
|
||||
internal void ClearTweens(
|
||||
Voice voice,
|
||||
AudioTweenProperty property
|
||||
) {
|
||||
lock (StateLock)
|
||||
{
|
||||
for (var i = streamingSounds.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var weakReference = streamingSounds[i];
|
||||
if (weakReference.TryGetTarget(out var streamingSound))
|
||||
{
|
||||
streamingSound.Update();
|
||||
AudioTweenManager.ClearTweens(voice, property);
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
internal void WakeThread()
|
||||
{
|
||||
streamingSounds.RemoveAt(i);
|
||||
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)
|
||||
{
|
||||
resourceHandles.Remove(resourceReference);
|
||||
|
||||
internal void AddResourceReference(WeakReference<AudioResource> resourceReference)
|
||||
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
|
||||
{
|
||||
lock (resources)
|
||||
{
|
||||
resources.Add(resourceReference);
|
||||
updatingSourceVoices.Remove(updatableVoice);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveResourceReference(WeakReference<AudioResource> resourceReference)
|
||||
{
|
||||
lock (resources)
|
||||
{
|
||||
resources.Remove(resourceReference);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
Running = false;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
for (var i = resources.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var weakReference = resources[i];
|
||||
Thread.Join();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
resources.Clear();
|
||||
|
||||
resourceHandles.Clear();
|
||||
}
|
||||
|
||||
FAudio.FAudioVoice_DestroyVoice(ReverbVoice);
|
||||
FAudio.FAudioVoice_DestroyVoice(MasteringVoice);
|
||||
FAudio.FAudio_Release(Handle);
|
||||
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||
~AudioDevice()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
|
|
|
@ -4,6 +4,9 @@ using MoonWorks.Math.Float;
|
|||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// An emitter for 3D spatial audio.
|
||||
/// </summary>
|
||||
public class AudioEmitter : AudioResource
|
||||
{
|
||||
internal FAudio.F3DAUDIO_EMITTER emitterData;
|
||||
|
@ -129,7 +132,5 @@ namespace MoonWorks.Audio
|
|||
emitterData.pReverbCurve = IntPtr.Zero;
|
||||
emitterData.CurveDistanceScaler = 1.0f;
|
||||
}
|
||||
|
||||
protected override void Destroy() { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ using MoonWorks.Math.Float;
|
|||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// A listener for 3D spatial audio. Usually attached to a camera.
|
||||
/// </summary>
|
||||
public class AudioListener : AudioResource
|
||||
{
|
||||
internal FAudio.F3DAUDIO_LISTENER listenerData;
|
||||
|
@ -91,7 +94,5 @@ namespace MoonWorks.Audio
|
|||
/* Unexposed variables, defaults based on XNA behavior */
|
||||
listenerData.pCone = IntPtr.Zero;
|
||||
}
|
||||
|
||||
protected override void Destroy() { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
|
@ -8,28 +9,24 @@ namespace MoonWorks.Audio
|
|||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
private WeakReference<AudioResource> selfReference;
|
||||
private GCHandle SelfReference;
|
||||
|
||||
public AudioResource(AudioDevice device)
|
||||
protected AudioResource(AudioDevice device)
|
||||
{
|
||||
Device = device;
|
||||
|
||||
selfReference = new WeakReference<AudioResource>(this);
|
||||
Device.AddResourceReference(selfReference);
|
||||
SelfReference = GCHandle.Alloc(this, GCHandleType.Weak);
|
||||
Device.AddResourceReference(SelfReference);
|
||||
}
|
||||
|
||||
protected abstract void Destroy();
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
Destroy();
|
||||
|
||||
if (selfReference != null)
|
||||
if (disposing)
|
||||
{
|
||||
Device.RemoveResourceReference(selfReference);
|
||||
selfReference = null;
|
||||
Device.RemoveResourceReference(SelfReference);
|
||||
SelfReference.Free();
|
||||
}
|
||||
|
||||
IsDisposed = true;
|
||||
|
@ -38,8 +35,12 @@ namespace MoonWorks.Audio
|
|||
|
||||
~AudioResource()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: false);
|
||||
#if DEBUG
|
||||
// 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()
|
||||
|
|
|
@ -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.Runtime.InteropServices;
|
||||
using MoonWorks.Graphics;
|
||||
using MoonWorks.Graphics.PackedVector;
|
||||
|
||||
namespace MoonWorks
|
||||
{
|
||||
/// <summary>
|
||||
/// Conversion utilities for interop.
|
||||
/// </summary>
|
||||
public static class Conversions
|
||||
{
|
||||
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
|
||||
{
|
||||
/// <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,
|
||||
/// <summary>
|
||||
/// The game will render no more than the specified frames per second.
|
||||
/// </summary>
|
||||
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 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 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.Graphics;
|
||||
using MoonWorks.Input;
|
||||
|
@ -9,6 +8,12 @@ using System.Diagnostics;
|
|||
|
||||
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 TimeSpan MAX_DELTA_TIME = TimeSpan.FromMilliseconds(100);
|
||||
|
@ -33,8 +38,18 @@ namespace MoonWorks
|
|||
public AudioDevice AudioDevice { get; }
|
||||
public Inputs Inputs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This Window is automatically created when your Game is instantiated.
|
||||
/// </summary>
|
||||
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(
|
||||
WindowCreateInfo windowCreateInfo,
|
||||
FrameLimiterSettings frameLimiterSettings,
|
||||
|
@ -42,6 +57,7 @@ namespace MoonWorks
|
|||
bool debugMode = false
|
||||
)
|
||||
{
|
||||
Logger.LogInfo("Initializing frame limiter...");
|
||||
Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep);
|
||||
gameTimer = Stopwatch.StartNew();
|
||||
|
||||
|
@ -52,21 +68,25 @@ namespace MoonWorks
|
|||
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)
|
||||
{
|
||||
System.Console.WriteLine("Failed to initialize SDL!");
|
||||
Logger.LogError("Failed to initialize SDL!");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Initialize();
|
||||
|
||||
Logger.LogInfo("Initializing input...");
|
||||
Inputs = new Inputs();
|
||||
|
||||
Logger.LogInfo("Initializing graphics device...");
|
||||
GraphicsDevice = new GraphicsDevice(
|
||||
Backend.Vulkan,
|
||||
debugMode
|
||||
);
|
||||
|
||||
Logger.LogInfo("Initializing main window...");
|
||||
MainWindow = new Window(windowCreateInfo, GraphicsDevice.WindowFlags | SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN);
|
||||
|
||||
if (!GraphicsDevice.ClaimWindow(MainWindow, windowCreateInfo.PresentMode))
|
||||
|
@ -74,9 +94,13 @@ namespace MoonWorks
|
|||
throw new System.SystemException("Could not claim window!");
|
||||
}
|
||||
|
||||
Logger.LogInfo("Initializing audio thread...");
|
||||
AudioDevice = new AudioDevice();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates the main game loop. Call this once from your Program.Main method.
|
||||
/// </summary>
|
||||
public void Run()
|
||||
{
|
||||
MainWindow.Show();
|
||||
|
@ -86,15 +110,29 @@ namespace MoonWorks
|
|||
Tick();
|
||||
}
|
||||
|
||||
Logger.LogInfo("Starting shutdown sequence...");
|
||||
|
||||
Logger.LogInfo("Cleaning up game...");
|
||||
Destroy();
|
||||
|
||||
AudioDevice.Dispose();
|
||||
Logger.LogInfo("Unclaiming window...");
|
||||
GraphicsDevice.UnclaimWindow(MainWindow);
|
||||
|
||||
Logger.LogInfo("Disposing window...");
|
||||
MainWindow.Dispose();
|
||||
|
||||
Logger.LogInfo("Disposing graphics device...");
|
||||
GraphicsDevice.Dispose();
|
||||
|
||||
Logger.LogInfo("Closing audio thread...");
|
||||
AudioDevice.Dispose();
|
||||
|
||||
SDL.SDL_Quit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the frame limiter settings.
|
||||
/// </summary>
|
||||
public void SetFrameLimiter(FrameLimiterSettings settings)
|
||||
{
|
||||
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);
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// You can optionally override this to perform cleanup tasks before the game quits.
|
||||
/// </summary>
|
||||
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) {}
|
||||
|
||||
/* Required to distinguish between multiple files dropped at once
|
||||
* vs multiple files dropped one at a time.
|
||||
*
|
||||
* Called once for every multi-file drop.
|
||||
*/
|
||||
/// <summary>
|
||||
/// Required to distinguish between multiple files dropped at once
|
||||
/// vs multiple files dropped one at a time.
|
||||
/// Called once for every multi-file drop.
|
||||
/// </summary>
|
||||
protected virtual void DropBegin() {}
|
||||
protected virtual void DropComplete() {}
|
||||
|
||||
|
@ -168,9 +230,8 @@ namespace MoonWorks
|
|||
while (accumulatedUpdateTime >= Timestep)
|
||||
{
|
||||
Inputs.Update();
|
||||
AudioDevice.Update();
|
||||
|
||||
Update(Timestep);
|
||||
AudioDevice.WakeThread();
|
||||
|
||||
accumulatedUpdateTime -= Timestep;
|
||||
}
|
||||
|
@ -282,17 +343,27 @@ namespace MoonWorks
|
|||
var index = evt.cdevice.which;
|
||||
if (SDL.SDL_IsGameController(index) == SDL.SDL_bool.SDL_TRUE)
|
||||
{
|
||||
System.Console.WriteLine($"New controller detected!");
|
||||
Logger.LogInfo("New controller detected!");
|
||||
Inputs.AddGamepad(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleControllerRemoved(SDL.SDL_Event evt)
|
||||
{
|
||||
System.Console.WriteLine($"Controller removal detected!");
|
||||
Logger.LogInfo("Controller removal detected!");
|
||||
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()
|
||||
{
|
||||
long currentTicks = gameTimer.Elapsed.Ticks;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// A buffer-offset pair to be used when binding vertex buffers.
|
||||
/// </summary>
|
||||
public struct BufferBinding
|
||||
{
|
||||
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 Texture Texture;
|
||||
|
|
|
@ -20,6 +20,7 @@ using System.Diagnostics;
|
|||
using System.Text;
|
||||
using MoonWorks.Math;
|
||||
using MoonWorks.Math.Float;
|
||||
using MoonWorks.Graphics.PackedVector;
|
||||
#endregion
|
||||
|
||||
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
|
||||
|
||||
#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.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using WellspringCS;
|
||||
|
||||
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;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
fixed (byte* pByte = &bytes[0])
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Texture.Dispose();
|
||||
}
|
||||
|
||||
Wellspring.Wellspring_DestroyFont(Handle);
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
~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);
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FontRange
|
||||
{
|
||||
public uint FirstCodepoint;
|
||||
public uint NumChars;
|
||||
public byte OversampleH;
|
||||
public byte OversampleV;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Vertex
|
||||
public struct Vertex : IVertexType
|
||||
{
|
||||
public Vector3 Position;
|
||||
public Vector2 TexCoord;
|
||||
public Color Color;
|
||||
|
||||
public static VertexElementFormat[] Formats { get; } = new VertexElementFormat[]
|
||||
{
|
||||
VertexElementFormat.Vector3,
|
||||
VertexElementFormat.Vector2,
|
||||
VertexElementFormat.Color
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,75 +1,87 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using WellspringCS;
|
||||
|
||||
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; }
|
||||
public IntPtr Handle { get; }
|
||||
|
||||
public Buffer VertexBuffer { get; protected set; } = null;
|
||||
public Buffer IndexBuffer { get; protected set; } = null;
|
||||
public Texture Texture { 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();
|
||||
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);
|
||||
Texture = packer.Texture;
|
||||
Wellspring.Wellspring_StartTextBatch(Handle, font.Handle);
|
||||
CurrentFont = font;
|
||||
PrimitiveCount = 0;
|
||||
}
|
||||
|
||||
public unsafe void Draw(
|
||||
// Add text with size and color to the batch
|
||||
public unsafe bool Add(
|
||||
string text,
|
||||
float x,
|
||||
float y,
|
||||
float depth,
|
||||
int pixelSize,
|
||||
Color color,
|
||||
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment verticalAlignment = VerticalAlignment.Baseline
|
||||
) {
|
||||
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 (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,
|
||||
x,
|
||||
y,
|
||||
depth,
|
||||
pixelSize,
|
||||
new Wellspring.Color { R = color.R, G = color.G, B = color.B, A = color.A },
|
||||
(Wellspring.HorizontalAlignment) horizontalAlignment,
|
||||
(Wellspring.VerticalAlignment) verticalAlignment,
|
||||
(IntPtr) bytes,
|
||||
(IntPtr) StringBytes,
|
||||
(uint) byteCount
|
||||
);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
throw new System.ArgumentException("Could not decode string!");
|
||||
}
|
||||
Logger.LogWarn("Could not decode string: " + text);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Call this after you have made all the Draw calls you want.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call this after you have made all the Add calls you want, but before beginning a render pass.
|
||||
public unsafe void UploadBufferData(CommandBuffer commandBuffer)
|
||||
{
|
||||
Wellspring.Wellspring_GetBufferData(
|
||||
|
@ -81,24 +93,16 @@ namespace MoonWorks.Graphics.Font
|
|||
out uint indexDataLengthInBytes
|
||||
);
|
||||
|
||||
if (VertexBuffer == null)
|
||||
{
|
||||
VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
|
||||
}
|
||||
else if (VertexBuffer.Size < vertexDataLengthInBytes)
|
||||
if (VertexBuffer.Size < vertexDataLengthInBytes)
|
||||
{
|
||||
VertexBuffer.Dispose();
|
||||
VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
|
||||
}
|
||||
|
||||
if (IndexBuffer == null)
|
||||
{
|
||||
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes);
|
||||
}
|
||||
else if (IndexBuffer.Size < indexDataLengthInBytes)
|
||||
if (IndexBuffer.Size < indexDataLengthInBytes)
|
||||
{
|
||||
IndexBuffer.Dispose();
|
||||
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes);
|
||||
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, vertexDataLengthInBytes);
|
||||
}
|
||||
|
||||
if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0)
|
||||
|
@ -107,7 +111,41 @@ namespace MoonWorks.Graphics.Font
|
|||
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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using MoonWorks.Video;
|
||||
using RefreshCS;
|
||||
using WellspringCS;
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// GraphicsDevice manages all graphics-related concerns.
|
||||
/// </summary>
|
||||
public class GraphicsDevice : IDisposable
|
||||
{
|
||||
public IntPtr Handle { get; }
|
||||
|
@ -16,11 +22,22 @@ namespace MoonWorks.Graphics
|
|||
// Built-in video pipeline
|
||||
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; }
|
||||
|
||||
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,
|
||||
bool debugMode
|
||||
) {
|
||||
|
@ -35,14 +52,56 @@ namespace MoonWorks.Graphics
|
|||
Conversions.BoolToByte(debugMode)
|
||||
);
|
||||
|
||||
// Check for optional video shaders
|
||||
string basePath = SDL2.SDL.SDL_GetBasePath();
|
||||
string videoVertPath = Path.Combine(basePath, "video_fullscreen.refresh");
|
||||
string videoFragPath = Path.Combine(basePath, "video_yuv2rgba.refresh");
|
||||
// TODO: check for CreateDevice fail
|
||||
|
||||
// Check for replacement stock shaders
|
||||
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))
|
||||
{
|
||||
ShaderModule videoVertShader = new ShaderModule(this, videoVertPath);
|
||||
ShaderModule videoFragShader = new ShaderModule(this, videoFragPath);
|
||||
videoVertShader = new ShaderModule(this, videoVertPath);
|
||||
videoFragShader = new ShaderModule(this, videoFragPath);
|
||||
}
|
||||
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,
|
||||
|
@ -71,11 +130,31 @@ namespace MoonWorks.Graphics
|
|||
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)
|
||||
{
|
||||
if (window.Claimed)
|
||||
{
|
||||
Logger.LogError("Window already claimed!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var success = Conversions.ByteToBool(
|
||||
Refresh.Refresh_ClaimWindow(
|
||||
Handle,
|
||||
|
@ -90,24 +169,46 @@ namespace MoonWorks.Graphics
|
|||
window.SwapchainFormat = GetSwapchainFormat(window);
|
||||
if (window.SwapchainTexture == null)
|
||||
{
|
||||
window.SwapchainTexture = new Texture(this, IntPtr.Zero, window.SwapchainFormat, 0, 0);
|
||||
window.SwapchainTexture = new Texture(this, window.SwapchainFormat);
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unclaims a window, making it unavailable for presenting and freeing associated resources.
|
||||
/// </summary>
|
||||
public void UnclaimWindow(Window window)
|
||||
{
|
||||
if (window.Claimed)
|
||||
{
|
||||
Refresh.Refresh_UnclaimWindow(
|
||||
Handle,
|
||||
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)
|
||||
{
|
||||
if (!window.Claimed)
|
||||
{
|
||||
Logger.LogError("Cannot set present mode on unclaimed window!");
|
||||
return;
|
||||
}
|
||||
|
||||
Refresh.Refresh_SetSwapchainPresentMode(
|
||||
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()
|
||||
{
|
||||
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];
|
||||
|
||||
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)
|
||||
#if DEBUG
|
||||
if (commandBuffer.Submitted)
|
||||
{
|
||||
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(
|
||||
Handle,
|
||||
(uint) commandBuffers.Length,
|
||||
(IntPtr) commandBufferPtrs
|
||||
commandBuffer.Handle
|
||||
);
|
||||
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
return (TextureFormat) Refresh.Refresh_GetSwapchainFormat(Handle, window.Handle);
|
||||
}
|
||||
|
||||
internal void AddResourceReference(WeakReference<GraphicsResource> resourceReference)
|
||||
internal void AddResourceReference(GCHandle resourceReference)
|
||||
{
|
||||
lock (resources)
|
||||
{
|
||||
|
@ -221,7 +425,7 @@ namespace MoonWorks.Graphics
|
|||
}
|
||||
}
|
||||
|
||||
internal void RemoveResourceReference(WeakReference<GraphicsResource> resourceReference)
|
||||
internal void RemoveResourceReference(GCHandle resourceReference)
|
||||
{
|
||||
lock (resources)
|
||||
{
|
||||
|
@ -237,19 +441,28 @@ namespace MoonWorks.Graphics
|
|||
{
|
||||
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.TryGetTarget(out var target))
|
||||
if (resource.Target is VideoPlayer player)
|
||||
{
|
||||
target.Dispose();
|
||||
player.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose everything else
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
if (resource.Target is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
resources.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
Refresh.Refresh_DestroyDevice(Handle);
|
||||
}
|
||||
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
|
|
@ -1,37 +1,33 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
{
|
||||
// TODO: give this a Name property for debugging use
|
||||
public abstract class GraphicsResource : IDisposable
|
||||
{
|
||||
public GraphicsDevice Device { get; }
|
||||
public IntPtr Handle { get; internal set; }
|
||||
|
||||
private GCHandle SelfReference;
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
protected abstract Action<IntPtr, IntPtr> QueueDestroyFunction { get; }
|
||||
|
||||
private WeakReference<GraphicsResource> selfReference;
|
||||
|
||||
public GraphicsResource(GraphicsDevice device, bool trackResource = true)
|
||||
protected GraphicsResource(GraphicsDevice device)
|
||||
{
|
||||
Device = device;
|
||||
|
||||
if (trackResource)
|
||||
{
|
||||
selfReference = new WeakReference<GraphicsResource>(this);
|
||||
Device.AddResourceReference(selfReference);
|
||||
}
|
||||
SelfReference = GCHandle.Alloc(this, GCHandleType.Weak);
|
||||
Device.AddResourceReference(SelfReference);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (selfReference != null)
|
||||
if (disposing)
|
||||
{
|
||||
QueueDestroyFunction(Device.Handle, Handle);
|
||||
Device.RemoveResourceReference(selfReference);
|
||||
selfReference = null;
|
||||
Device.RemoveResourceReference(SelfReference);
|
||||
SelfReference.Free();
|
||||
}
|
||||
|
||||
IsDisposed = true;
|
||||
|
@ -40,8 +36,13 @@ namespace MoonWorks.Graphics
|
|||
|
||||
~GraphicsResource()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: false);
|
||||
#if DEBUG
|
||||
// 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()
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// Can be defined on your struct type to enable simplified vertex input state definition.
|
||||
/// </summary>
|
||||
public interface IVertexType
|
||||
{
|
||||
/// <summary>
|
||||
/// An ordered list of the types in your vertex struct.
|
||||
/// </summary>
|
||||
static abstract VertexElementFormat[] Formats { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// Packed vector type containing unsigned normalized values, ranging from 0 to 1, using
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// 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;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
public struct HalfSingle : IPackedVector<ushort>, IEquatable<HalfSingle>, IPackedVector
|
||||
{
|
||||
|
|
|
@ -19,7 +19,7 @@ using System;
|
|||
using System.Runtime.InteropServices;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
internal static class HalfTypeHelper
|
||||
{
|
||||
|
|
|
@ -19,7 +19,7 @@ using System;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
public struct HalfVector2 : IPackedVector<uint>, IPackedVector, IEquatable<HalfVector2>
|
||||
{
|
||||
|
|
|
@ -19,7 +19,7 @@ using System;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// Packed vector type containing four 16-bit floating-point values.
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
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
|
||||
public interface IPackedVector
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
public struct NormalizedByte2 : IPackedVector<ushort>, IEquatable<NormalizedByte2>
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
public struct NormalizedByte4 : IPackedVector<uint>, IEquatable<NormalizedByte4>
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
public struct NormalizedShort2 : IPackedVector<uint>, IEquatable<NormalizedShort2>
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
public struct NormalizedShort4 : IPackedVector<ulong>, IEquatable<NormalizedShort4>
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
public struct Short2 : IPackedVector<uint>, IEquatable<Short2>
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@ using MoonWorks.Math;
|
|||
using MoonWorks.Math.Float;
|
||||
#endregion
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
namespace MoonWorks.Graphics.PackedVector
|
||||
{
|
||||
/// <summary>
|
||||
/// Packed vector type containing four 16-bit signed integer values.
|
||||
|
|
|
@ -2,11 +2,31 @@
|
|||
|
||||
namespace MoonWorks
|
||||
{
|
||||
/// <summary>
|
||||
/// Presentation mode for a window.
|
||||
/// </summary>
|
||||
public enum PresentMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Does not wait for v-blank to update the window. Can cause visible tearing.
|
||||
/// </summary>
|
||||
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,
|
||||
/// <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,
|
||||
/// <summary>
|
||||
/// Usually waits for v-blank, but if v-blank has passed since last update will update immediately.
|
||||
/// May cause visible tearing.
|
||||
/// </summary>
|
||||
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 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
|
||||
{
|
||||
Binding = binding,
|
||||
InputRate = VertexInputRate.Vertex,
|
||||
InputRate = inputRate,
|
||||
Stride = (uint) Marshal.SizeOf<T>()
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace MoonWorks.Graphics
|
|||
/// <summary>
|
||||
/// Buffers are generic data containers that can be used by the GPU.
|
||||
/// </summary>
|
||||
public class Buffer : GraphicsResource
|
||||
public class Buffer : RefreshResource
|
||||
{
|
||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyBuffer;
|
||||
|
||||
|
@ -58,17 +58,29 @@ 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 is called first.
|
||||
/// 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.
|
||||
/// </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>
|
||||
public unsafe void GetData<T>(
|
||||
T[] data,
|
||||
Span<T> data,
|
||||
uint dataLengthInBytes
|
||||
) 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(
|
||||
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)
|
||||
{
|
||||
return new BufferBinding(b, 0);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
using RefreshCS;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public class GraphicsPipeline : GraphicsResource
|
||||
public class GraphicsPipeline : RefreshResource
|
||||
{
|
||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyGraphicsPipeline;
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace MoonWorks.Graphics
|
|||
/// <summary>
|
||||
/// A sampler specifies how a texture will be sampled in a shader.
|
||||
/// </summary>
|
||||
public class Sampler : GraphicsResource
|
||||
public class Sampler : RefreshResource
|
||||
{
|
||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroySampler;
|
||||
|
||||
|
|
|
@ -1,42 +1,42 @@
|
|||
using RefreshCS;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// Shader modules expect input in Refresh bytecode format.
|
||||
/// </summary>
|
||||
public class ShaderModule : GraphicsResource
|
||||
public class ShaderModule : RefreshResource
|
||||
{
|
||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyShaderModule;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe ShaderModule(GraphicsDevice device, Stream stream) : base(device)
|
||||
{
|
||||
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];
|
||||
stream.Read(bytecode, 0, (int) stream.Length);
|
||||
var bytecodeBuffer = NativeMemory.Alloc((nuint) stream.Length);
|
||||
var bytecodeSpan = new Span<byte>(bytecodeBuffer, (int) stream.Length);
|
||||
stream.ReadExactly(bytecodeSpan);
|
||||
|
||||
fixed (byte* ptr = bytecode)
|
||||
{
|
||||
Refresh.ShaderModuleCreateInfo shaderModuleCreateInfo;
|
||||
shaderModuleCreateInfo.codeSize = (UIntPtr) bytecode.Length;
|
||||
shaderModuleCreateInfo.byteCode = (IntPtr) ptr;
|
||||
shaderModuleCreateInfo.codeSize = (nuint) stream.Length;
|
||||
shaderModuleCreateInfo.byteCode = (nint) bytecodeBuffer;
|
||||
|
||||
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.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using RefreshCS;
|
||||
|
||||
namespace MoonWorks.Graphics
|
||||
|
@ -7,7 +8,7 @@ namespace MoonWorks.Graphics
|
|||
/// <summary>
|
||||
/// A container for pixel data.
|
||||
/// </summary>
|
||||
public class Texture : GraphicsResource
|
||||
public class Texture : RefreshResource
|
||||
{
|
||||
public uint Width { get; internal set; }
|
||||
public uint Height { get; internal set; }
|
||||
|
@ -17,28 +18,24 @@ namespace MoonWorks.Graphics
|
|||
public uint LevelCount { get; }
|
||||
public SampleCount SampleCount { get; }
|
||||
public TextureUsageFlags UsageFlags { get; }
|
||||
public uint Size { get; }
|
||||
|
||||
// FIXME: this allocates a delegate instance
|
||||
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyTexture;
|
||||
|
||||
/// <summary>
|
||||
/// Loads a PNG from a file path.
|
||||
/// NOTE: You can queue as many of these as you want on to a command buffer but it MUST be submitted!
|
||||
/// Creates a 2D Texture using PNG or QOI data from raw byte data.
|
||||
/// </summary>
|
||||
/// <param name="device"></param>
|
||||
/// <param name="commandBuffer"></param>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
public static Texture LoadPNG(GraphicsDevice device, CommandBuffer commandBuffer, string filePath)
|
||||
{
|
||||
var pixels = Refresh.Refresh_Image_Load(
|
||||
filePath,
|
||||
out var width,
|
||||
out var height,
|
||||
out var channels
|
||||
);
|
||||
public static unsafe Texture FromImageBytes(
|
||||
GraphicsDevice device,
|
||||
CommandBuffer commandBuffer,
|
||||
Span<byte> data
|
||||
) {
|
||||
Texture texture;
|
||||
|
||||
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.Width = (uint) width;
|
||||
|
@ -50,34 +47,101 @@ namespace MoonWorks.Graphics
|
|||
textureCreateInfo.SampleCount = SampleCount.One;
|
||||
textureCreateInfo.UsageFlags = TextureUsageFlags.Sampler;
|
||||
|
||||
var texture = new Texture(device, textureCreateInfo);
|
||||
commandBuffer.SetTextureData(texture, pixels, byteCount);
|
||||
texture = new Texture(device, textureCreateInfo);
|
||||
commandBuffer.SetTextureData(texture, pixels, (uint) len);
|
||||
|
||||
Refresh.Refresh_Image_Free(pixels);
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public unsafe static void SavePNG(string path, int width, int height, TextureFormat format, byte[] pixels)
|
||||
{
|
||||
if (format != TextureFormat.R8G8B8A8 && format != TextureFormat.B8G8R8A8)
|
||||
{
|
||||
throw new ArgumentException("Texture format must be RGBA8 or BGRA8!", "format");
|
||||
public static unsafe Texture FromImageStream(
|
||||
GraphicsDevice device,
|
||||
CommandBuffer commandBuffer,
|
||||
Stream stream
|
||||
) {
|
||||
var length = stream.Length;
|
||||
var buffer = NativeMemory.Alloc((nuint) length);
|
||||
var span = new Span<byte>(buffer, (int) length);
|
||||
stream.ReadExactly(span);
|
||||
|
||||
var texture = FromImageBytes(device, commandBuffer, span);
|
||||
|
||||
NativeMemory.Free((void*) buffer);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
fixed (byte* ptr = &pixels[0])
|
||||
/// <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)
|
||||
{
|
||||
using (var reader = new BinaryReader(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);
|
||||
Texture texture;
|
||||
int faces;
|
||||
ParseDDS(reader, out var format, out var width, out var height, out var levels, out var isCube);
|
||||
|
@ -100,22 +164,20 @@ namespace MoonWorks.Graphics
|
|||
var levelWidth = width >> j;
|
||||
var levelHeight = height >> j;
|
||||
|
||||
var pixels = reader.ReadBytes(
|
||||
Texture.CalculateDDSLevelSize(
|
||||
levelWidth,
|
||||
levelHeight,
|
||||
format
|
||||
)
|
||||
);
|
||||
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, pixels);
|
||||
commandBuffer.SetTextureData(textureSlice, (nint) byteBuffer, (uint) levelSize);
|
||||
|
||||
NativeMemory.Free(byteBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 2D texture.
|
||||
|
@ -235,6 +297,7 @@ namespace MoonWorks.Graphics
|
|||
LevelCount = textureCreateInfo.LevelCount;
|
||||
SampleCount = textureCreateInfo.SampleCount;
|
||||
UsageFlags = textureCreateInfo.UsageFlags;
|
||||
Size = Width * Height * BytesPerPixel(Format) / BlockSizeSquared(Format);
|
||||
}
|
||||
|
||||
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.
|
||||
internal Texture(
|
||||
GraphicsDevice device,
|
||||
IntPtr handle,
|
||||
TextureFormat format,
|
||||
uint width,
|
||||
uint height
|
||||
) : base(device, false)
|
||||
TextureFormat format
|
||||
) : base(device)
|
||||
{
|
||||
Handle = handle;
|
||||
Handle = IntPtr.Zero;
|
||||
|
||||
Format = format;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
Depth = 1;
|
||||
IsCube = false;
|
||||
LevelCount = 1;
|
||||
SampleCount = SampleCount.One;
|
||||
UsageFlags = TextureUsageFlags.ColorTarget;
|
||||
Size = Width * Height * BytesPerPixel(Format) / BlockSizeSquared(Format);
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines how color blending will be performed in a GraphicsPipeline.
|
||||
/// </summary>
|
||||
public struct ColorAttachmentBlendState
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
|||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// Information that the pipeline needs about a shader.
|
||||
/// Information that the compute pipeline needs about a compute shader.
|
||||
/// </summary>
|
||||
public struct ComputeShaderInfo
|
||||
{
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// All of the information that is used to create a GraphicsPipeline.
|
||||
/// </summary>
|
||||
public struct GraphicsPipelineCreateInfo
|
||||
{
|
||||
public DepthStencilState DepthStencilState;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// Information that the pipeline needs about a shader.
|
||||
/// Information that the pipeline needs about a graphics shader.
|
||||
/// </summary>
|
||||
public struct GraphicsShaderInfo
|
||||
{
|
||||
|
|
|
@ -2,21 +2,60 @@
|
|||
|
||||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// All of the information that is used to create a sampler.
|
||||
/// </summary>
|
||||
public struct SamplerCreateInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Minification filter mode. Used when the image is downscaled.
|
||||
/// </summary>
|
||||
public Filter MinFilter;
|
||||
/// <summary>
|
||||
/// Magnification filter mode. Used when the image is upscaled.
|
||||
/// </summary>
|
||||
public Filter MagFilter;
|
||||
/// <summary>
|
||||
/// Filter mode applied to mipmap lookups.
|
||||
/// </summary>
|
||||
public SamplerMipmapMode MipmapMode;
|
||||
/// <summary>
|
||||
/// Horizontal addressing mode.
|
||||
/// </summary>
|
||||
public SamplerAddressMode AddressModeU;
|
||||
/// <summary>
|
||||
/// Vertical addressing mode.
|
||||
/// </summary>
|
||||
public SamplerAddressMode AddressModeV;
|
||||
/// <summary>
|
||||
/// Depth addressing mode.
|
||||
/// </summary>
|
||||
public SamplerAddressMode AddressModeW;
|
||||
/// <summary>
|
||||
/// Bias value added to mipmap level of detail calculation.
|
||||
/// </summary>
|
||||
public float MipLodBias;
|
||||
/// <summary>
|
||||
/// Enables anisotropic filtering.
|
||||
/// </summary>
|
||||
public bool AnisotropyEnable;
|
||||
/// <summary>
|
||||
/// Maximum anisotropy value.
|
||||
/// </summary>
|
||||
public float MaxAnisotropy;
|
||||
public bool CompareEnable;
|
||||
public CompareOp CompareOp;
|
||||
/// <summary>
|
||||
/// Clamps the LOD value to a specified minimum.
|
||||
/// </summary>
|
||||
public float MinLod;
|
||||
/// <summary>
|
||||
/// Clamps the LOD value to a specified maximum.
|
||||
/// </summary>
|
||||
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 static readonly SamplerCreateInfo AnisotropicClamp = new SamplerCreateInfo
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// All of the information that is used to create a texture.
|
||||
/// </summary>
|
||||
public struct TextureCreateInfo
|
||||
{
|
||||
public uint Width;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace MoonWorks.Graphics
|
||||
{
|
||||
/// <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>
|
||||
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 Level { get; }
|
||||
|
||||
public uint Size => (uint) (Rectangle.W * Rectangle.H * Texture.BytesPerPixel(Texture.Format) / Texture.BlockSizeSquared(Texture.Format));
|
||||
|
||||
public TextureSlice(Texture texture)
|
||||
{
|
||||
Texture = texture;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
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 VertexBinding VertexBinding { get; }
|
||||
|
@ -12,9 +14,9 @@ namespace MoonWorks.Graphics
|
|||
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];
|
||||
uint offset = 0;
|
||||
|
||||
|
@ -25,7 +27,7 @@ namespace MoonWorks.Graphics
|
|||
attributes[i] = new VertexAttribute
|
||||
{
|
||||
Binding = bindingIndex,
|
||||
Location = i,
|
||||
Location = locationOffset + i,
|
||||
Format = format,
|
||||
Offset = offset
|
||||
};
|
||||
|
|
|
@ -3,6 +3,9 @@ using SDL2;
|
|||
|
||||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a specific joystick direction on a gamepad.
|
||||
/// </summary>
|
||||
public class Axis
|
||||
{
|
||||
public Gamepad Parent { get; }
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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
|
||||
{
|
||||
LeftX_Left,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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
|
||||
{
|
||||
LeftX,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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
|
||||
{
|
||||
A,
|
||||
|
|
|
@ -1,14 +1,40 @@
|
|||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Container for the current state of a binary input.
|
||||
/// </summary>
|
||||
public struct ButtonState
|
||||
{
|
||||
public ButtonStatus ButtonStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the button was pressed this frame.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <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;
|
||||
|
||||
/// <summary>
|
||||
/// True if the button was let go this frame.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// True if the button was either idle or released this frame.
|
||||
/// </summary>
|
||||
public bool IsUp => ButtonStatus == ButtonStatus.Idle || ButtonStatus == ButtonStatus.Released;
|
||||
|
||||
public ButtonState(ButtonStatus buttonStatus)
|
||||
|
@ -43,7 +69,7 @@
|
|||
}
|
||||
|
||||
/// <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>
|
||||
public static ButtonState operator |(ButtonState a, ButtonState b)
|
||||
{
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the current status of a binary input.
|
||||
/// </summary>
|
||||
public enum ButtonStatus
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
namespace MoonWorks.Input
|
||||
{
|
||||
public enum DeviceKind
|
||||
{
|
||||
None,
|
||||
Keyboard,
|
||||
Mouse,
|
||||
Gamepad,
|
||||
}
|
||||
}
|
|
@ -5,6 +5,12 @@ using SDL2;
|
|||
|
||||
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
|
||||
{
|
||||
internal IntPtr Handle;
|
||||
|
@ -51,7 +57,14 @@ namespace MoonWorks.Input
|
|||
|
||||
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; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
private Dictionary<SDL.SDL_GameControllerButton, GamepadButton> EnumToButton;
|
||||
|
@ -100,13 +113,13 @@ namespace MoonWorks.Input
|
|||
|
||||
LeftXLeft = new AxisButton(LeftX, false);
|
||||
LeftXRight = new AxisButton(LeftX, true);
|
||||
LeftYUp = new AxisButton(LeftY, false);
|
||||
LeftYDown = new AxisButton(LeftY, true);
|
||||
LeftYUp = new AxisButton(LeftY, true);
|
||||
LeftYDown = new AxisButton(LeftY, false);
|
||||
|
||||
RightXLeft = new AxisButton(RightX, false);
|
||||
RightXRight = new AxisButton(RightX, true);
|
||||
RightYUp = new AxisButton(RightY, false);
|
||||
RightYDown = new AxisButton(RightY, true);
|
||||
RightYUp = new AxisButton(RightY, true);
|
||||
RightYDown = new AxisButton(RightY, false);
|
||||
|
||||
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);
|
||||
|
@ -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()
|
||||
{
|
||||
AnyPressed = false;
|
||||
|
@ -253,16 +280,25 @@ namespace MoonWorks.Input
|
|||
) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains a gamepad button object given a button code.
|
||||
/// </summary>
|
||||
public GamepadButton Button(GamepadButtonCode buttonCode)
|
||||
{
|
||||
return EnumToButton[(SDL.SDL_GameControllerButton) buttonCode];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains an axis button object given a button code.
|
||||
/// </summary>
|
||||
public AxisButton Button(AxisButtonCode axisButtonCode)
|
||||
{
|
||||
return AxisButtonCodeToAxisButton[axisButtonCode];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains a trigger button object given a button code.
|
||||
/// </summary>
|
||||
public TriggerButton Button(TriggerCode triggerCode)
|
||||
{
|
||||
return TriggerCodeToTriggerButton[triggerCode];
|
||||
|
|
|
@ -3,31 +3,65 @@ using System;
|
|||
|
||||
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 const int MAX_GAMEPADS = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The reference to the Keyboard input abstraction.
|
||||
/// </summary>
|
||||
public Keyboard Keyboard { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The reference to the Mouse input abstraction.
|
||||
/// </summary>
|
||||
public Mouse Mouse { get; }
|
||||
|
||||
Gamepad[] gamepads;
|
||||
Gamepad[] Gamepads;
|
||||
|
||||
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; }
|
||||
|
||||
/// <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 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()
|
||||
{
|
||||
Keyboard = new Keyboard();
|
||||
Mouse = new Mouse();
|
||||
|
||||
gamepads = new Gamepad[MAX_GAMEPADS];
|
||||
Gamepads = new Gamepad[MAX_GAMEPADS];
|
||||
|
||||
// initialize dummy controllers
|
||||
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;
|
||||
}
|
||||
|
||||
foreach (var gamepad in gamepads)
|
||||
foreach (var gamepad in Gamepads)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (slot < 0 || slot >= MAX_GAMEPADS)
|
||||
|
@ -72,13 +111,19 @@ namespace MoonWorks.Input
|
|||
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)
|
||||
{
|
||||
return gamepads[slot];
|
||||
return Gamepads[slot];
|
||||
}
|
||||
|
||||
internal void AddGamepad(int index)
|
||||
|
@ -87,24 +132,40 @@ namespace MoonWorks.Input
|
|||
{
|
||||
if (!GamepadExists(slot))
|
||||
{
|
||||
gamepads[slot].Handle = SDL.SDL_GameControllerOpen(index);
|
||||
System.Console.WriteLine($"Gamepad added to slot {slot}!");
|
||||
var openResult = SDL.SDL_GameControllerOpen(index);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
System.Console.WriteLine("Too many gamepads already!");
|
||||
Logger.LogInfo("Too many gamepads already!");
|
||||
}
|
||||
|
||||
internal void RemoveGamepad(int joystickInstanceID)
|
||||
{
|
||||
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);
|
||||
gamepads[slot].Handle = IntPtr.Zero;
|
||||
System.Console.WriteLine($"Removing gamepad from slot {slot}!");
|
||||
SDL.SDL_GameControllerClose(Gamepads[slot].Handle);
|
||||
Gamepads[slot].Unregister();
|
||||
Logger.LogInfo($"Removing gamepad from slot {slot}!");
|
||||
OnGamepadDisconnected(slot);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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
|
||||
{
|
||||
Unknown = 0,
|
||||
|
|
|
@ -4,12 +4,22 @@ using SDL2;
|
|||
|
||||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// The keyboard input device abstraction.
|
||||
/// </summary>
|
||||
public class Keyboard
|
||||
{
|
||||
/// <summary>
|
||||
/// True if any button on the keyboard is active. Useful for input remapping.
|
||||
/// </summary>
|
||||
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 IntPtr State { get; private set; }
|
||||
internal IntPtr State { get; private set; }
|
||||
|
||||
private KeyCode[] KeyCodes;
|
||||
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)
|
||||
{
|
||||
return Keys[(int) keycode].IsPressed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the button was pressed this frame and the previous frame.
|
||||
/// </summary>
|
||||
public bool IsHeld(KeyCode keycode)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return Keys[(int) keycode].IsDown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the button was let go this frame.
|
||||
/// </summary>
|
||||
public bool IsReleased(KeyCode keycode)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return Keys[(int) keycode].IsIdle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the button was either idle or released this frame.
|
||||
/// </summary>
|
||||
public bool IsUp(KeyCode keycode)
|
||||
{
|
||||
return Keys[(int) keycode].IsUp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a reference to a keyboard button object using a key code.
|
||||
/// </summary>
|
||||
public KeyboardButton Button(KeyCode keycode)
|
||||
{
|
||||
return Keys[(int) keycode];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state of a keyboard button from a key code.
|
||||
/// </summary>
|
||||
public ButtonState ButtonState(KeyCode keycode)
|
||||
{
|
||||
return Keys[(int) keycode].State;
|
||||
|
|
|
@ -3,6 +3,9 @@ using SDL2;
|
|||
|
||||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// The mouse input device abstraction.
|
||||
/// </summary>
|
||||
public class Mouse
|
||||
{
|
||||
public MouseButton LeftButton { get; }
|
||||
|
@ -21,12 +24,23 @@ namespace MoonWorks.Input
|
|||
internal int WheelRaw;
|
||||
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; }
|
||||
|
||||
/// <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 uint ButtonMask { get; private set; }
|
||||
internal uint ButtonMask { get; private set; }
|
||||
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
public Mouse()
|
||||
internal Mouse()
|
||||
{
|
||||
LeftButton = new MouseButton(this, MouseButtonCode.Left, SDL.SDL_BUTTON_LMASK);
|
||||
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)
|
||||
{
|
||||
return CodeToButton[buttonCode].State;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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
|
||||
{
|
||||
Left,
|
||||
|
|
|
@ -3,6 +3,9 @@ using SDL2;
|
|||
|
||||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a trigger input on a gamepad.
|
||||
/// </summary>
|
||||
public class Trigger
|
||||
{
|
||||
public Gamepad Parent { get; }
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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
|
||||
{
|
||||
Left = 4,
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// VirtualButtons map inputs to binary inputs, like a trigger threshold or joystick axis threshold.
|
||||
/// </summary>
|
||||
public abstract class VirtualButton
|
||||
{
|
||||
public ButtonState State { get; protected set; }
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
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 Axis Parent { get; }
|
||||
public AxisButtonCode Code { get; }
|
||||
|
||||
private float threshold = 0.9f;
|
||||
private float threshold = 0.5f;
|
||||
public float Threshold
|
||||
{
|
||||
get => threshold;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A dummy button that can never be pressed. Used for the dummy gamepad.
|
||||
/// </summary>
|
||||
public class EmptyButton : VirtualButton
|
||||
{
|
||||
internal override bool CheckPressed()
|
||||
|
|
|
@ -2,6 +2,9 @@ using SDL2;
|
|||
|
||||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A virtual button corresponding to a gamepad button.
|
||||
/// </summary>
|
||||
public class GamepadButton : VirtualButton
|
||||
{
|
||||
public Gamepad Parent { get; }
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonWorks.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A virtual button corresponding to a keyboard button.
|
||||
/// </summary>
|
||||
public class KeyboardButton : VirtualButton
|
||||
{
|
||||
Keyboard Parent;
|
||||
KeyCode KeyCode;
|
||||
public KeyCode KeyCode { get; }
|
||||
|
||||
internal KeyboardButton(Keyboard parent, KeyCode keyCode)
|
||||
{
|
||||
|
@ -13,9 +14,9 @@ namespace MoonWorks.Input
|
|||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// A virtual button corresponding to a mouse button.
|
||||
/// </summary>
|
||||
public class MouseButton : VirtualButton
|
||||
{
|
||||
Mouse Parent;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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 Trigger Parent { get; }
|
||||
|
|
|
@ -5,9 +5,9 @@ namespace MoonWorks
|
|||
{
|
||||
public static class Logger
|
||||
{
|
||||
public static Action<string> LogInfo;
|
||||
public static Action<string> LogWarn;
|
||||
public static Action<string> LogError;
|
||||
public static Action<string> LogInfo = LogInfoDefault;
|
||||
public static Action<string> LogWarn = LogWarnDefault;
|
||||
public static Action<string> LogError = LogErrorDefault;
|
||||
|
||||
private static RefreshCS.Refresh.Refresh_LogFunc LogInfoFunc = RefreshLogInfo;
|
||||
private static RefreshCS.Refresh.Refresh_LogFunc LogWarnFunc = RefreshLogWarn;
|
||||
|
@ -15,19 +15,6 @@ namespace MoonWorks
|
|||
|
||||
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(
|
||||
LogInfoFunc,
|
||||
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)
|
||||
{
|
||||
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>
|
||||
{
|
||||
private readonly long RawValue;
|
||||
public long RawValue { get; }
|
||||
|
||||
const long MAX_VALUE = long.MaxValue;
|
||||
const long MIN_VALUE = long.MinValue;
|
||||
|
@ -17,6 +17,9 @@ namespace MoonWorks.Math.Fixed
|
|||
const long PI_TIMES_2 = 0x6487ED511;
|
||||
const long PI = 0x3243F6A88;
|
||||
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 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 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);
|
||||
static readonly Fix64 LutInterval = (Fix64)(LUT_SIZE - 1) / PiOver2;
|
||||
|
||||
|
@ -52,6 +59,11 @@ namespace MoonWorks.Math.Fixed
|
|||
return new Fix64(numerator) / new Fix64(denominator);
|
||||
}
|
||||
|
||||
public static Fix64 FromRawValue(long value)
|
||||
{
|
||||
return new Fix64(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fractional component of this Fix64 value.
|
||||
/// </summary>
|
||||
|
@ -60,30 +72,6 @@ namespace MoonWorks.Math.Fixed
|
|||
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>
|
||||
/// Returns an int indicating the sign of a Fix64 number.
|
||||
/// </summary>
|
||||
|
@ -212,7 +200,145 @@ namespace MoonWorks.Math.Fixed
|
|||
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>
|
||||
/// Returns the square root of the given Fix64 value.
|
||||
|
|
|
@ -996,8 +996,8 @@ namespace MoonWorks.Math.Fixed
|
|||
z = Vector3.Normalize(forward);
|
||||
Vector3.Cross(ref forward, ref up, out x);
|
||||
Vector3.Cross(ref x, ref forward, out y);
|
||||
x.Normalize();
|
||||
y.Normalize();
|
||||
x = Vector3.Normalize(x);
|
||||
y = Vector3.Normalize(y);
|
||||
|
||||
result = new Matrix4x4();
|
||||
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>
|
||||
/// 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"/>]}
|
||||
|
@ -759,12 +742,16 @@ namespace MoonWorks.Math.Fixed
|
|||
/// <param name="result">The unit length quaternion an output parameter.</param>
|
||||
public static void Normalize(ref Quaternion quaternion, out Quaternion result)
|
||||
{
|
||||
Fix64 num = Fix64.One / (Fix64.Sqrt(
|
||||
(quaternion.X * quaternion.X) +
|
||||
(quaternion.Y * quaternion.Y) +
|
||||
(quaternion.Z * quaternion.Z) +
|
||||
(quaternion.W * quaternion.W)
|
||||
));
|
||||
Fix64 lengthSquared = (quaternion.X * quaternion.X) + (quaternion.Y * quaternion.Y) +
|
||||
(quaternion.Z * quaternion.Z) + (quaternion.W * quaternion.W);
|
||||
|
||||
if (lengthSquared == Fix64.Zero)
|
||||
{
|
||||
result = Identity;
|
||||
return;
|
||||
}
|
||||
|
||||
Fix64 num = Fix64.One / Fix64.Sqrt(lengthSquared);
|
||||
result.X = quaternion.X * num;
|
||||
result.Y = quaternion.Y * 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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Turns this <see cref="Vector2"/> to an angle in radians.
|
||||
/// </summary>
|
||||
|
@ -423,7 +413,14 @@ namespace MoonWorks.Math.Fixed
|
|||
/// <returns>Unit vector.</returns>
|
||||
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.Y *= val;
|
||||
return value;
|
||||
|
|
|
@ -309,21 +309,6 @@ namespace MoonWorks.Math.Fixed
|
|||
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>
|
||||
/// 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"/>]}
|
||||
|
@ -733,11 +718,14 @@ namespace MoonWorks.Math.Fixed
|
|||
/// <returns>Unit vector.</returns>
|
||||
public static Vector3 Normalize(Vector3 value)
|
||||
{
|
||||
Fix64 factor = Fix64.One / Fix64.Sqrt(
|
||||
(value.X * value.X) +
|
||||
(value.Y * value.Y) +
|
||||
(value.Z * value.Z)
|
||||
);
|
||||
Fix64 lengthSquared = (value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z);
|
||||
|
||||
if (lengthSquared == Fix64.Zero)
|
||||
{
|
||||
return Zero;
|
||||
}
|
||||
|
||||
Fix64 factor = Fix64.One / Fix64.Sqrt(lengthSquared);
|
||||
return new Vector3(
|
||||
value.X * factor,
|
||||
value.Y * factor,
|
||||
|
|
|
@ -611,7 +611,7 @@ namespace MoonWorks.Math.Float
|
|||
);
|
||||
}
|
||||
Vector3.Cross(ref cameraUpVector, ref vector, out vector3);
|
||||
vector3.Normalize();
|
||||
vector3 = Vector3.Normalize(vector3);
|
||||
Vector3.Cross(ref vector, ref vector3, out vector2);
|
||||
result.M11 = vector3.X;
|
||||
result.M12 = vector3.Y;
|
||||
|
@ -730,16 +730,16 @@ namespace MoonWorks.Math.Float
|
|||
Vector3.Forward;
|
||||
}
|
||||
Vector3.Cross(ref rotateAxis, ref vector, out vector3);
|
||||
vector3.Normalize();
|
||||
vector3 = Vector3.Normalize(vector3);
|
||||
Vector3.Cross(ref vector3, ref rotateAxis, out vector);
|
||||
vector.Normalize();
|
||||
vector = Vector3.Normalize(vector);
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3.Cross(ref rotateAxis, ref vector2, out vector3);
|
||||
vector3.Normalize();
|
||||
vector3 = Vector3.Normalize(vector3);
|
||||
Vector3.Cross(ref vector3, ref vector4, out vector);
|
||||
vector.Normalize();
|
||||
vector = Vector3.Normalize(vector);
|
||||
}
|
||||
|
||||
result.M11 = vector3.X;
|
||||
|
@ -1701,8 +1701,8 @@ namespace MoonWorks.Math.Float
|
|||
Vector3.Normalize(ref forward, out z);
|
||||
Vector3.Cross(ref forward, ref up, out x);
|
||||
Vector3.Cross(ref x, ref forward, out y);
|
||||
x.Normalize();
|
||||
y.Normalize();
|
||||
x = Vector3.Normalize(x);
|
||||
y = Vector3.Normalize(y);
|
||||
|
||||
result = new Matrix4x4();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Turns this <see cref="Vector2"/> to an angle in radians.
|
||||
/// </summary>
|
||||
|
@ -717,7 +707,14 @@ namespace MoonWorks.Math.Float
|
|||
/// <returns>Unit vector.</returns>
|
||||
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.Y *= val;
|
||||
return value;
|
||||
|
|
|
@ -302,21 +302,6 @@ namespace MoonWorks.Math.Float
|
|||
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>
|
||||
/// 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"/>]}
|
||||
|
@ -900,11 +885,14 @@ namespace MoonWorks.Math.Float
|
|||
/// <returns>Unit vector.</returns>
|
||||
public static Vector3 Normalize(Vector3 value)
|
||||
{
|
||||
float factor = 1.0f / (float) System.Math.Sqrt(
|
||||
(value.X * value.X) +
|
||||
(value.Y * value.Y) +
|
||||
(value.Z * value.Z)
|
||||
);
|
||||
float lengthSquared = (value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z);
|
||||
|
||||
if (lengthSquared == 0f)
|
||||
{
|
||||
return Zero;
|
||||
}
|
||||
|
||||
float factor = 1.0f / System.MathF.Sqrt(lengthSquared);
|
||||
return new Vector3(
|
||||
value.X * factor,
|
||||
value.Y * factor,
|
||||
|
|
|
@ -267,23 +267,6 @@ namespace MoonWorks.Math.Float
|
|||
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()
|
||||
{
|
||||
return (
|
||||
|
@ -853,12 +836,15 @@ namespace MoonWorks.Math.Float
|
|||
/// <returns>Unit vector.</returns>
|
||||
public static Vector4 Normalize(Vector4 vector)
|
||||
{
|
||||
float factor = 1.0f / (float) System.Math.Sqrt(
|
||||
(vector.X * vector.X) +
|
||||
(vector.Y * vector.Y) +
|
||||
(vector.Z * vector.Z) +
|
||||
(vector.W * vector.W)
|
||||
);
|
||||
var lengthSquared = (vector.X * vector.X) + (vector.Y * vector.Y) +
|
||||
(vector.Z * vector.Z) + (vector.W * vector.W);
|
||||
|
||||
if (lengthSquared == 0)
|
||||
{
|
||||
return Zero;
|
||||
}
|
||||
|
||||
float factor = 1.0f / System.MathF.Sqrt(lengthSquared);
|
||||
return new Vector4(
|
||||
vector.X * factor,
|
||||
vector.Y * factor,
|
||||
|
@ -870,16 +856,20 @@ namespace MoonWorks.Math.Float
|
|||
/// <summary>
|
||||
/// Creates a new <see cref="Vector4"/> that contains a normalized values from another vector.
|
||||
/// </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>
|
||||
public static void Normalize(ref Vector4 vector, out Vector4 result)
|
||||
{
|
||||
float factor = 1.0f / (float) System.Math.Sqrt(
|
||||
(vector.X * vector.X) +
|
||||
(vector.Y * vector.Y) +
|
||||
(vector.Z * vector.Z) +
|
||||
(vector.W * vector.W)
|
||||
);
|
||||
float lengthSquared = (vector.X * vector.X) + (vector.Y * vector.Y) +
|
||||
(vector.Z * vector.Z) + (vector.W * vector.W);
|
||||
|
||||
if (lengthSquared == 0)
|
||||
{
|
||||
result = Zero;
|
||||
return;
|
||||
}
|
||||
|
||||
float factor = 1.0f / System.MathF.Sqrt(lengthSquared);
|
||||
result.X = vector.X * factor;
|
||||
result.Y = vector.Y * factor;
|
||||
result.Z = vector.Z * factor;
|
||||
|
|
|
@ -160,6 +160,26 @@ namespace MoonWorks.Math
|
|||
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>
|
||||
/// Calculates the absolute value of the difference of two values.
|
||||
/// </summary>
|
||||
|
@ -282,7 +302,12 @@ namespace MoonWorks.Math
|
|||
|
||||
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>
|
||||
|
@ -395,27 +420,6 @@ namespace MoonWorks.Math
|
|||
|
||||
#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)
|
||||
{
|
||||
return System.Math.Abs(floatA - floatB) < MachineEpsilonFloat;
|
||||
|
|
|
@ -97,16 +97,12 @@ namespace MoonWorks
|
|||
|
||||
// Get the path to the assembly
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
string assemblyPath = "";
|
||||
if (assembly.Location != null)
|
||||
{
|
||||
assemblyPath = Path.GetDirectoryName(assembly.Location);
|
||||
}
|
||||
string assemblyPath = System.AppContext.BaseDirectory;
|
||||
|
||||
// Locate the config file
|
||||
string xmlPath = Path.Combine(
|
||||
assemblyPath,
|
||||
assembly.GetName().Name + ".dll.config"
|
||||
"MoonWorks.dll.config"
|
||||
);
|
||||
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.Runtime.InteropServices;
|
||||
using MoonWorks.Audio;
|
||||
using System.Threading.Tasks;
|
||||
using MoonWorks.Graphics;
|
||||
|
||||
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 VideoState State { get; private set; } = VideoState.Stopped;
|
||||
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;
|
||||
|
||||
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 Texture yTexture = null;
|
||||
|
@ -32,36 +28,26 @@ namespace MoonWorks.Video
|
|||
private Texture vTexture = null;
|
||||
private Sampler LinearSampler;
|
||||
|
||||
private void* yuvData = null;
|
||||
private int yuvDataLength = 0;
|
||||
|
||||
private int currentFrame;
|
||||
|
||||
private AudioDevice AudioDevice;
|
||||
private StreamingSoundTheora audioStream = null;
|
||||
private float volume = 1.0f;
|
||||
|
||||
private Stopwatch timer;
|
||||
private double lastTimestamp;
|
||||
private double timeElapsed;
|
||||
|
||||
private bool disposed;
|
||||
|
||||
public VideoPlayer(GraphicsDevice graphicsDevice, AudioDevice audioDevice)
|
||||
public VideoPlayer(GraphicsDevice device) : base(device)
|
||||
{
|
||||
GraphicsDevice = graphicsDevice;
|
||||
if (GraphicsDevice.VideoPipeline == null)
|
||||
{
|
||||
throw new InvalidOperationException("Missing video shaders!");
|
||||
}
|
||||
GraphicsDevice = device;
|
||||
|
||||
AudioDevice = audioDevice;
|
||||
LinearSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp);
|
||||
LinearSampler = new Sampler(device, SamplerCreateInfo.LinearClamp);
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -111,25 +97,19 @@ namespace MoonWorks.Video
|
|||
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;
|
||||
|
||||
InitializeTheoraStream();
|
||||
InitializeDav1dStream();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts playing back and decoding the loaded video.
|
||||
/// </summary>
|
||||
public void Play()
|
||||
{
|
||||
if (Video == null) { return; }
|
||||
|
||||
if (State == VideoState.Playing)
|
||||
{
|
||||
return;
|
||||
|
@ -137,16 +117,16 @@ namespace MoonWorks.Video
|
|||
|
||||
timer.Start();
|
||||
|
||||
if (audioStream != null)
|
||||
{
|
||||
audioStream.Play();
|
||||
}
|
||||
|
||||
State = VideoState.Playing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses playback and decoding of the currently playing video.
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
if (Video == null) { return; }
|
||||
|
||||
if (State != VideoState.Playing)
|
||||
{
|
||||
return;
|
||||
|
@ -154,16 +134,16 @@ namespace MoonWorks.Video
|
|||
|
||||
timer.Stop();
|
||||
|
||||
if (audioStream != null)
|
||||
{
|
||||
audioStream.Pause();
|
||||
}
|
||||
|
||||
State = VideoState.Paused;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops and resets decoding of the currently playing video.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (Video == null) { return; }
|
||||
|
||||
if (State == VideoState.Stopped)
|
||||
{
|
||||
return;
|
||||
|
@ -172,20 +152,28 @@ namespace MoonWorks.Video
|
|||
timer.Stop();
|
||||
timer.Reset();
|
||||
|
||||
Theorafile.tf_reset(Video.Handle);
|
||||
lastTimestamp = 0;
|
||||
timeElapsed = 0;
|
||||
|
||||
if (audioStream != null)
|
||||
{
|
||||
audioStream.StopImmediate();
|
||||
audioStream.Dispose();
|
||||
audioStream = null;
|
||||
}
|
||||
InitializeDav1dStream();
|
||||
|
||||
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()
|
||||
{
|
||||
if (Video == null || State == VideoState.Stopped)
|
||||
|
@ -199,37 +187,39 @@ namespace MoonWorks.Video
|
|||
int thisFrame = ((int) (timeElapsed / (1000.0 / Video.FramesPerSecond)));
|
||||
if (thisFrame > currentFrame)
|
||||
{
|
||||
if (Theorafile.tf_readvideo(
|
||||
Video.Handle,
|
||||
(IntPtr) yuvData,
|
||||
thisFrame - currentFrame
|
||||
) == 1 || currentFrame == -1) {
|
||||
if (CurrentStream.FrameDataUpdated)
|
||||
{
|
||||
UpdateRenderTexture();
|
||||
CurrentStream.FrameDataUpdated = false;
|
||||
}
|
||||
|
||||
currentFrame = thisFrame;
|
||||
ReadNextFrameTask = Task.Run(CurrentStream.ReadNextFrame);
|
||||
ReadNextFrameTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
bool ended = Theorafile.tf_eos(Video.Handle) == 1;
|
||||
if (ended)
|
||||
if (CurrentStream.Ended)
|
||||
{
|
||||
timer.Stop();
|
||||
timer.Reset();
|
||||
|
||||
if (audioStream != null)
|
||||
{
|
||||
audioStream.Stop();
|
||||
audioStream.Dispose();
|
||||
audioStream = null;
|
||||
}
|
||||
var task = Task.Run(CurrentStream.Reset);
|
||||
task.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
Theorafile.tf_reset(Video.Handle);
|
||||
if (CurrentStream == Video.StreamA)
|
||||
{
|
||||
ResetStreamATask = task;
|
||||
}
|
||||
else
|
||||
{
|
||||
ResetStreamBTask = task;
|
||||
}
|
||||
|
||||
if (Loop)
|
||||
{
|
||||
// Start over!
|
||||
InitializeTheoraStream();
|
||||
|
||||
// Start over on the next stream!
|
||||
CurrentStream = (CurrentStream == Video.StreamA) ? Video.StreamB : Video.StreamA;
|
||||
currentFrame = -1;
|
||||
timer.Start();
|
||||
}
|
||||
else
|
||||
|
@ -240,6 +230,8 @@ namespace MoonWorks.Video
|
|||
}
|
||||
|
||||
private void UpdateRenderTexture()
|
||||
{
|
||||
lock (CurrentStream)
|
||||
{
|
||||
var commandBuffer = GraphicsDevice.AcquireCommandBuffer();
|
||||
|
||||
|
@ -247,8 +239,13 @@ namespace MoonWorks.Video
|
|||
yTexture,
|
||||
uTexture,
|
||||
vTexture,
|
||||
(IntPtr) yuvData,
|
||||
(uint) yuvDataLength
|
||||
CurrentStream.yDataHandle,
|
||||
CurrentStream.uDataHandle,
|
||||
CurrentStream.vDataHandle,
|
||||
CurrentStream.yDataLength,
|
||||
CurrentStream.uvDataLength,
|
||||
CurrentStream.yStride,
|
||||
CurrentStream.uvStride
|
||||
);
|
||||
|
||||
commandBuffer.BeginRenderPass(
|
||||
|
@ -268,6 +265,7 @@ namespace MoonWorks.Video
|
|||
|
||||
GraphicsDevice.Submit(commandBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
while (Theorafile.tf_readvideo(Video.Handle, (IntPtr) yuvData, 1) == 0);
|
||||
ReadNextFrameTask?.Wait();
|
||||
|
||||
// Grab the first bit of audio. We're trying to start the decoding ASAP.
|
||||
if (AudioDevice != null && Theorafile.tf_hasaudio(Video.Handle) == 1)
|
||||
{
|
||||
int channels, sampleRate;
|
||||
Theorafile.tf_audioinfo(Video.Handle, out channels, out sampleRate);
|
||||
audioStream = new StreamingSoundTheora(AudioDevice, Video.Handle, channels, (uint) sampleRate);
|
||||
}
|
||||
ResetStreamATask = Task.Run(Video.StreamA.Reset);
|
||||
ResetStreamATask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
|
||||
ResetStreamBTask = Task.Run(Video.StreamB.Reset);
|
||||
ResetStreamBTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
CurrentStream = Video.StreamA;
|
||||
currentFrame = -1;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private static void HandleTaskException(Task task)
|
||||
{
|
||||
if (!disposed)
|
||||
if (task.Exception.InnerException is not TaskCanceledException)
|
||||
{
|
||||
throw task.Exception;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
RenderTexture.Dispose();
|
||||
yTexture.Dispose();
|
||||
uTexture.Dispose();
|
||||
vTexture.Dispose();
|
||||
}
|
||||
Unload();
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
NativeMemory.Free(yuvData);
|
||||
|
||||
disposed = true;
|
||||
RenderTexture?.Dispose();
|
||||
yTexture?.Dispose();
|
||||
uTexture?.Dispose();
|
||||
vTexture?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
~VideoPlayer()
|
||||
{
|
||||
// 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);
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
namespace MoonWorks.Video
|
||||
{
|
||||
public enum VideoState
|
||||
{
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped
|
||||
}
|
||||
}
|
|
@ -5,13 +5,18 @@ using SDL2;
|
|||
|
||||
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
|
||||
{
|
||||
internal IntPtr Handle { get; }
|
||||
public ScreenMode ScreenMode { get; private set; }
|
||||
public uint Width { 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 MoonWorks.Graphics.TextureFormat SwapchainFormat { get; internal set; }
|
||||
|
@ -45,21 +50,28 @@ namespace MoonWorks
|
|||
|
||||
ScreenMode = windowCreateInfo.ScreenMode;
|
||||
|
||||
SDL.SDL_GetDesktopDisplayMode(0, out var displayMode);
|
||||
|
||||
Handle = SDL.SDL_CreateWindow(
|
||||
windowCreateInfo.WindowTitle,
|
||||
SDL.SDL_WINDOWPOS_UNDEFINED,
|
||||
SDL.SDL_WINDOWPOS_UNDEFINED,
|
||||
(int) windowCreateInfo.WindowWidth,
|
||||
(int) windowCreateInfo.WindowHeight,
|
||||
SDL.SDL_WINDOWPOS_CENTERED,
|
||||
SDL.SDL_WINDOWPOS_CENTERED,
|
||||
windowCreateInfo.ScreenMode == ScreenMode.Windowed ? (int) windowCreateInfo.WindowWidth : displayMode.w,
|
||||
windowCreateInfo.ScreenMode == ScreenMode.Windowed ? (int) windowCreateInfo.WindowHeight : displayMode.h,
|
||||
flags
|
||||
);
|
||||
|
||||
Width = windowCreateInfo.WindowWidth;
|
||||
Height = windowCreateInfo.WindowHeight;
|
||||
/* Requested size might be different in fullscreen, so let's just get the area */
|
||||
SDL.SDL_GetWindowSize(Handle, out var width, out var height);
|
||||
Width = (uint) width;
|
||||
Height = (uint) height;
|
||||
|
||||
idLookup.Add(SDL.SDL_GetWindowID(Handle), this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the ScreenMode of this window.
|
||||
/// </summary>
|
||||
public void SetScreenMode(ScreenMode screenMode)
|
||||
{
|
||||
SDL.SDL_WindowFlags windowFlag = 0;
|
||||
|
@ -73,13 +85,18 @@ namespace MoonWorks
|
|||
windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
}
|
||||
|
||||
ScreenMode = screenMode;
|
||||
|
||||
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>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="width"></param>
|
||||
|
@ -89,6 +106,11 @@ namespace MoonWorks
|
|||
SDL.SDL_SetWindowSize(Handle, (int) width, (int) height);
|
||||
Width = width;
|
||||
Height = height;
|
||||
|
||||
if (ScreenMode == ScreenMode.Windowed)
|
||||
{
|
||||
SDL.SDL_SetWindowPosition(Handle, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
SizeChangeCallback = sizeChangeCallback;
|
||||
|
|
|
@ -1,13 +1,37 @@
|
|||
namespace MoonWorks
|
||||
{
|
||||
/// <summary>
|
||||
/// All the information required for window creation.
|
||||
/// </summary>
|
||||
public struct WindowCreateInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the window that will be displayed in the operating system.
|
||||
/// </summary>
|
||||
public string WindowTitle;
|
||||
/// <summary>
|
||||
/// The width of the window.
|
||||
/// </summary>
|
||||
public uint WindowWidth;
|
||||
/// <summary>
|
||||
/// The height of the window.
|
||||
/// </summary>
|
||||
public uint WindowHeight;
|
||||
/// <summary>
|
||||
/// Specifies if the window will be created in windowed mode or a fullscreen mode.
|
||||
/// </summary>
|
||||
public ScreenMode ScreenMode;
|
||||
/// <summary>
|
||||
/// Specifies the presentation mode for the window. Roughly equivalent to V-Sync.
|
||||
/// </summary>
|
||||
public PresentMode PresentMode;
|
||||
/// <summary>
|
||||
/// Whether the window can be resized using the operating system's window dragging feature.
|
||||
/// </summary>
|
||||
public bool SystemResizable;
|
||||
/// <summary>
|
||||
/// Specifies if the window will open at the maximum desktop resolution.
|
||||
/// </summary>
|
||||
public bool StartMaximized;
|
||||
|
||||
public WindowCreateInfo(
|
||||
|
|
Loading…
Reference in New Issue