Seed HLSL-shaped math library
This commit is contained in:
commit
38b79a8d89
12 changed files with 366 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
**/bin/
|
||||
**/obj/
|
||||
artifacts/
|
||||
*.user
|
||||
*.suo
|
||||
.vs/
|
||||
.idea/
|
||||
8
CultMath.slnx
Normal file
8
CultMath.slnx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/CultMath/CultMath.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/CultMath.Tests/CultMath.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
43
README.md
Normal file
43
README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# CultMath
|
||||
|
||||
Clean-room C# math shaped for people who think in HLSL.
|
||||
|
||||
CultMath provides lowercase vector types and shader-style intrinsics so
|
||||
math-heavy renderer code can move between C# and HLSL without turning every
|
||||
line into a translation exercise. It is not a Unity.Mathematics clone. It is a
|
||||
small, Aquarium-born library built from public shader semantics and plain .NET.
|
||||
|
||||
## Current Surface
|
||||
|
||||
- `float2`, `float3`, `float4`
|
||||
- component-wise arithmetic
|
||||
- scalar-to-vector splats
|
||||
- swizzles for common lanes
|
||||
- conversions to and from `System.Numerics`
|
||||
- `math` intrinsics: `radians`, `degrees`, `abs`, `floor`, `ceil`, `frac`,
|
||||
`min`, `max`, `clamp`, `saturate`, `lerp`, `step`, `smoothstep`, `dot`,
|
||||
`cross`, `length`, `distance`, `normalize`, and `reflect`
|
||||
|
||||
## Example
|
||||
|
||||
```csharp
|
||||
using CultMath;
|
||||
|
||||
float3 normal = math.normalize(new float3(0.25f, 1.0f, -0.1f));
|
||||
float rim = math.saturate(1.0f - math.dot(normal, new float3(0.0f, 0.0f, -1.0f)));
|
||||
float glow = math.smoothstep(0.2f, 1.0f, rim);
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
dotnet build CultMath.slnx
|
||||
dotnet test CultMath.slnx
|
||||
```
|
||||
|
||||
## Doctrine
|
||||
|
||||
The goal is mechanical sympathy between CPU-side authoring and GPU-side shader
|
||||
math. If an intrinsic exists here, it should behave like the shader concept
|
||||
unless .NET numeric rules make that impossible. Weird deviations get tests and
|
||||
documentation, not folklore.
|
||||
21
docs/design.md
Normal file
21
docs/design.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Design
|
||||
|
||||
CultMath exists to make renderer math readable in both places it needs to live:
|
||||
C# host code and HLSL shader code.
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep type names HLSL-shaped: `float2`, `float3`, `float4`, and `math`.
|
||||
- Implement public shader semantics directly; do not copy Unity.Mathematics.
|
||||
- Prefer small immutable value types.
|
||||
- Prefer component-wise overloads over clever generic machinery until the API
|
||||
earns that complexity.
|
||||
- Keep the first surface boring and exact. Noise, matrices, quaternions, and
|
||||
packing helpers come later.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Compiling C# to shaders.
|
||||
- Replacing `System.Numerics` for general .NET work.
|
||||
- Recreating every Unity.Mathematics type because a spreadsheet somewhere says
|
||||
“coverage.” That way lies decorative obesity.
|
||||
6
global.json
Normal file
6
global.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "10.0.202",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
21
src/CultMath/CultMath.csproj
Normal file
21
src/CultMath/CultMath.csproj
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>CultMath</RootNamespace>
|
||||
<AssemblyName>CultMath</AssemblyName>
|
||||
<PackageId>CultMath</PackageId>
|
||||
<Description>Clean-room C# math shaped for HLSL-style renderer code.</Description>
|
||||
<Authors>GameCult</Authors>
|
||||
<RepositoryUrl>https://github.com/GameCult/CultMath</RepositoryUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<NoWarn>$(NoWarn);CS8981</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
26
src/CultMath/float2.cs
Normal file
26
src/CultMath/float2.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
namespace CultMath;
|
||||
|
||||
public readonly record struct float2(float x, float y)
|
||||
{
|
||||
public static readonly float2 zero = new(0.0f, 0.0f);
|
||||
public static readonly float2 one = new(1.0f, 1.0f);
|
||||
|
||||
public float this[int index] => index switch
|
||||
{
|
||||
0 => x,
|
||||
1 => y,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public static implicit operator float2(float value) => new(value, value);
|
||||
public static explicit operator System.Numerics.Vector2(float2 value) => new(value.x, value.y);
|
||||
public static explicit operator float2(System.Numerics.Vector2 value) => new(value.X, value.Y);
|
||||
|
||||
public static float2 operator +(float2 value) => value;
|
||||
public static float2 operator -(float2 value) => new(-value.x, -value.y);
|
||||
public static float2 operator +(float2 left, float2 right) => new(left.x + right.x, left.y + right.y);
|
||||
public static float2 operator -(float2 left, float2 right) => new(left.x - right.x, left.y - right.y);
|
||||
public static float2 operator *(float2 left, float2 right) => new(left.x * right.x, left.y * right.y);
|
||||
public static float2 operator /(float2 left, float2 right) => new(left.x / right.x, left.y / right.y);
|
||||
public static float2 operator %(float2 left, float2 right) => new(left.x % right.x, left.y % right.y);
|
||||
}
|
||||
31
src/CultMath/float3.cs
Normal file
31
src/CultMath/float3.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
namespace CultMath;
|
||||
|
||||
public readonly record struct float3(float x, float y, float z)
|
||||
{
|
||||
public static readonly float3 zero = new(0.0f, 0.0f, 0.0f);
|
||||
public static readonly float3 one = new(1.0f, 1.0f, 1.0f);
|
||||
|
||||
public float2 xy => new(x, y);
|
||||
public float2 xz => new(x, z);
|
||||
public float2 yz => new(y, z);
|
||||
|
||||
public float this[int index] => index switch
|
||||
{
|
||||
0 => x,
|
||||
1 => y,
|
||||
2 => z,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public static implicit operator float3(float value) => new(value, value, value);
|
||||
public static explicit operator System.Numerics.Vector3(float3 value) => new(value.x, value.y, value.z);
|
||||
public static explicit operator float3(System.Numerics.Vector3 value) => new(value.X, value.Y, value.Z);
|
||||
|
||||
public static float3 operator +(float3 value) => value;
|
||||
public static float3 operator -(float3 value) => new(-value.x, -value.y, -value.z);
|
||||
public static float3 operator +(float3 left, float3 right) => new(left.x + right.x, left.y + right.y, left.z + right.z);
|
||||
public static float3 operator -(float3 left, float3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z);
|
||||
public static float3 operator *(float3 left, float3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z);
|
||||
public static float3 operator /(float3 left, float3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z);
|
||||
public static float3 operator %(float3 left, float3 right) => new(left.x % right.x, left.y % right.y, left.z % right.z);
|
||||
}
|
||||
31
src/CultMath/float4.cs
Normal file
31
src/CultMath/float4.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
namespace CultMath;
|
||||
|
||||
public readonly record struct float4(float x, float y, float z, float w)
|
||||
{
|
||||
public static readonly float4 zero = new(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
public static readonly float4 one = new(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
public float2 xy => new(x, y);
|
||||
public float3 xyz => new(x, y, z);
|
||||
|
||||
public float this[int index] => index switch
|
||||
{
|
||||
0 => x,
|
||||
1 => y,
|
||||
2 => z,
|
||||
3 => w,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public static implicit operator float4(float value) => new(value, value, value, value);
|
||||
public static explicit operator System.Numerics.Vector4(float4 value) => new(value.x, value.y, value.z, value.w);
|
||||
public static explicit operator float4(System.Numerics.Vector4 value) => new(value.X, value.Y, value.Z, value.W);
|
||||
|
||||
public static float4 operator +(float4 value) => value;
|
||||
public static float4 operator -(float4 value) => new(-value.x, -value.y, -value.z, -value.w);
|
||||
public static float4 operator +(float4 left, float4 right) => new(left.x + right.x, left.y + right.y, left.z + right.z, left.w + right.w);
|
||||
public static float4 operator -(float4 left, float4 right) => new(left.x - right.x, left.y - right.y, left.z - right.z, left.w - right.w);
|
||||
public static float4 operator *(float4 left, float4 right) => new(left.x * right.x, left.y * right.y, left.z * right.z, left.w * right.w);
|
||||
public static float4 operator /(float4 left, float4 right) => new(left.x / right.x, left.y / right.y, left.z / right.z, left.w / right.w);
|
||||
public static float4 operator %(float4 left, float4 right) => new(left.x % right.x, left.y % right.y, left.z % right.z, left.w % right.w);
|
||||
}
|
||||
114
src/CultMath/math.cs
Normal file
114
src/CultMath/math.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
namespace CultMath;
|
||||
|
||||
public static class math
|
||||
{
|
||||
public const float PI = MathF.PI;
|
||||
public const float TAU = MathF.PI * 2.0f;
|
||||
public const float HALF_PI = MathF.PI * 0.5f;
|
||||
|
||||
public static float radians(float degrees) => degrees * (PI / 180.0f);
|
||||
public static float degrees(float radians) => radians * (180.0f / PI);
|
||||
|
||||
public static float abs(float value) => MathF.Abs(value);
|
||||
public static float2 abs(float2 value) => new(abs(value.x), abs(value.y));
|
||||
public static float3 abs(float3 value) => new(abs(value.x), abs(value.y), abs(value.z));
|
||||
public static float4 abs(float4 value) => new(abs(value.x), abs(value.y), abs(value.z), abs(value.w));
|
||||
|
||||
public static float floor(float value) => MathF.Floor(value);
|
||||
public static float2 floor(float2 value) => new(floor(value.x), floor(value.y));
|
||||
public static float3 floor(float3 value) => new(floor(value.x), floor(value.y), floor(value.z));
|
||||
public static float4 floor(float4 value) => new(floor(value.x), floor(value.y), floor(value.z), floor(value.w));
|
||||
|
||||
public static float ceil(float value) => MathF.Ceiling(value);
|
||||
public static float2 ceil(float2 value) => new(ceil(value.x), ceil(value.y));
|
||||
public static float3 ceil(float3 value) => new(ceil(value.x), ceil(value.y), ceil(value.z));
|
||||
public static float4 ceil(float4 value) => new(ceil(value.x), ceil(value.y), ceil(value.z), ceil(value.w));
|
||||
|
||||
public static float frac(float value) => value - floor(value);
|
||||
public static float2 frac(float2 value) => value - floor(value);
|
||||
public static float3 frac(float3 value) => value - floor(value);
|
||||
public static float4 frac(float4 value) => value - floor(value);
|
||||
|
||||
public static float min(float left, float right) => MathF.Min(left, right);
|
||||
public static float2 min(float2 left, float2 right) => new(min(left.x, right.x), min(left.y, right.y));
|
||||
public static float3 min(float3 left, float3 right) => new(min(left.x, right.x), min(left.y, right.y), min(left.z, right.z));
|
||||
public static float4 min(float4 left, float4 right) => new(min(left.x, right.x), min(left.y, right.y), min(left.z, right.z), min(left.w, right.w));
|
||||
|
||||
public static float max(float left, float right) => MathF.Max(left, right);
|
||||
public static float2 max(float2 left, float2 right) => new(max(left.x, right.x), max(left.y, right.y));
|
||||
public static float3 max(float3 left, float3 right) => new(max(left.x, right.x), max(left.y, right.y), max(left.z, right.z));
|
||||
public static float4 max(float4 left, float4 right) => new(max(left.x, right.x), max(left.y, right.y), max(left.z, right.z), max(left.w, right.w));
|
||||
|
||||
public static float clamp(float value, float minimum, float maximum) => min(max(value, minimum), maximum);
|
||||
public static float2 clamp(float2 value, float2 minimum, float2 maximum) => min(max(value, minimum), maximum);
|
||||
public static float3 clamp(float3 value, float3 minimum, float3 maximum) => min(max(value, minimum), maximum);
|
||||
public static float4 clamp(float4 value, float4 minimum, float4 maximum) => min(max(value, minimum), maximum);
|
||||
|
||||
public static float saturate(float value) => clamp(value, 0.0f, 1.0f);
|
||||
public static float2 saturate(float2 value) => clamp(value, 0.0f, 1.0f);
|
||||
public static float3 saturate(float3 value) => clamp(value, 0.0f, 1.0f);
|
||||
public static float4 saturate(float4 value) => clamp(value, 0.0f, 1.0f);
|
||||
|
||||
public static float lerp(float start, float end, float amount) => start + (end - start) * amount;
|
||||
public static float2 lerp(float2 start, float2 end, float2 amount) => start + (end - start) * amount;
|
||||
public static float3 lerp(float3 start, float3 end, float3 amount) => start + (end - start) * amount;
|
||||
public static float4 lerp(float4 start, float4 end, float4 amount) => start + (end - start) * amount;
|
||||
|
||||
public static float step(float edge, float value) => value < edge ? 0.0f : 1.0f;
|
||||
public static float2 step(float2 edge, float2 value) => new(step(edge.x, value.x), step(edge.y, value.y));
|
||||
public static float3 step(float3 edge, float3 value) => new(step(edge.x, value.x), step(edge.y, value.y), step(edge.z, value.z));
|
||||
public static float4 step(float4 edge, float4 value) => new(step(edge.x, value.x), step(edge.y, value.y), step(edge.z, value.z), step(edge.w, value.w));
|
||||
|
||||
public static float smoothstep(float minimum, float maximum, float value)
|
||||
{
|
||||
var t = saturate((value - minimum) / (maximum - minimum));
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
public static float2 smoothstep(float2 minimum, float2 maximum, float2 value)
|
||||
{
|
||||
var t = saturate((value - minimum) / (maximum - minimum));
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
public static float3 smoothstep(float3 minimum, float3 maximum, float3 value)
|
||||
{
|
||||
var t = saturate((value - minimum) / (maximum - minimum));
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
public static float4 smoothstep(float4 minimum, float4 maximum, float4 value)
|
||||
{
|
||||
var t = saturate((value - minimum) / (maximum - minimum));
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
public static float dot(float2 left, float2 right) => left.x * right.x + left.y * right.y;
|
||||
public static float dot(float3 left, float3 right) => left.x * right.x + left.y * right.y + left.z * right.z;
|
||||
public static float dot(float4 left, float4 right) => left.x * right.x + left.y * right.y + left.z * right.z + left.w * right.w;
|
||||
|
||||
public static float3 cross(float3 left, float3 right) =>
|
||||
new(
|
||||
left.y * right.z - left.z * right.y,
|
||||
left.z * right.x - left.x * right.z,
|
||||
left.x * right.y - left.y * right.x);
|
||||
|
||||
public static float lengthsq(float2 value) => dot(value, value);
|
||||
public static float lengthsq(float3 value) => dot(value, value);
|
||||
public static float lengthsq(float4 value) => dot(value, value);
|
||||
|
||||
public static float length(float2 value) => MathF.Sqrt(lengthsq(value));
|
||||
public static float length(float3 value) => MathF.Sqrt(lengthsq(value));
|
||||
public static float length(float4 value) => MathF.Sqrt(lengthsq(value));
|
||||
|
||||
public static float distance(float2 left, float2 right) => length(left - right);
|
||||
public static float distance(float3 left, float3 right) => length(left - right);
|
||||
public static float distance(float4 left, float4 right) => length(left - right);
|
||||
|
||||
public static float2 normalize(float2 value) => value / MathF.Max(length(value), 1.0e-20f);
|
||||
public static float3 normalize(float3 value) => value / MathF.Max(length(value), 1.0e-20f);
|
||||
public static float4 normalize(float4 value) => value / MathF.Max(length(value), 1.0e-20f);
|
||||
|
||||
public static float2 reflect(float2 incident, float2 normal) => incident - 2.0f * dot(normal, incident) * normal;
|
||||
public static float3 reflect(float3 incident, float3 normal) => incident - 2.0f * dot(normal, incident) * normal;
|
||||
}
|
||||
20
tests/CultMath.Tests/CultMath.Tests.csproj
Normal file
20
tests/CultMath.Tests/CultMath.Tests.csproj
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\CultMath\CultMath.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
38
tests/CultMath.Tests/MathTests.cs
Normal file
38
tests/CultMath.Tests/MathTests.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using CultMath;
|
||||
using Xunit;
|
||||
|
||||
namespace CultMath.Tests;
|
||||
|
||||
public sealed class MathTests
|
||||
{
|
||||
[Fact]
|
||||
public void FracMatchesShaderStylePositiveAndNegativeValues()
|
||||
{
|
||||
Assert.Equal(0.25f, math.frac(1.25f));
|
||||
Assert.Equal(0.75f, math.frac(-1.25f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VectorOperationsAreComponentWise()
|
||||
{
|
||||
var value = new float3(1.0f, 2.0f, 3.0f) * new float3(4.0f, 5.0f, 6.0f);
|
||||
|
||||
Assert.Equal(new float3(4.0f, 10.0f, 18.0f), value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeProducesUnitLength()
|
||||
{
|
||||
var value = math.normalize(new float3(3.0f, 4.0f, 0.0f));
|
||||
|
||||
Assert.Equal(1.0f, math.length(value), precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmoothstepUsesHermiteRamp()
|
||||
{
|
||||
Assert.Equal(0.0f, math.smoothstep(0.0f, 1.0f, -1.0f));
|
||||
Assert.Equal(0.5f, math.smoothstep(0.0f, 1.0f, 0.5f), precision: 5);
|
||||
Assert.Equal(1.0f, math.smoothstep(0.0f, 1.0f, 2.0f));
|
||||
}
|
||||
}
|
||||
Reference in a new issue