dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.48k stars 4.76k forks source link

[API Proposal]: Vector2, Vector3, Vector4 deconstruction and reconstruction #90795

Open PavielKraskouski opened 1 year ago

PavielKraskouski commented 1 year ago

Background and motivation

Convenient conversion from a vector to a tuple of its components and vice versa.

API Proposal

namespace System.Numerics;

public struct Vector2
{
    public void Deconstruct(out float x, out float y)
    {
        x = X;
        y = Y;
    }

    public static implicit operator Vector2((float x, float y) xy)
    {
        return new Vector2(xy.x, xy.y);
    }
}

public struct Vector3
{
    public void Deconstruct(out float x, out float y, out float z)
    {
        x = X;
        y = Y;
        z = Z;
    }

    public static implicit operator Vector3((float x, float y, float z) xyz)
    {
        return new Vector3(xyz.x, xyz.y, xyz.z);
    }
}

public struct Vector4
{
    public void Deconstruct(out float x, out float y, out float z, out float w)
    {
        x = X;
        y = Y;
        z = Z;
        w = W;
    }

    public static implicit operator Vector4((float x, float y, float z, float w) xyzw)
    {
        return new Vector4(xyzw.x, xyzw.y, xyzw.z, xyzw.w);
    }
}

API Usage

public Vector3 SampleTexture(Vector2 uv, Texture texture)
{
    // texture sampling code
}

(float metalness, float roughness, float ao) = material.MraoMap == null ?
    (material.Metalness, material.Roughness, 1f) : SampleTexture(uv, material.MraoMap);

Alternative Designs

No response

Risks

No response

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-system-numerics See info in area-owners.md if you want to be subscribed.

Issue Details
### Background and motivation Convenient conversion from a vector to a tuple of its components and vice versa. ### API Proposal ```csharp namespace System.Numerics; public struct Vector2 { public void Deconstruct(out float x, out float y) { x = X; y = Y; } public static implicit operator Vector2((float x, float y) xy) { return new Vector2(xy.x, xy.y); } } public struct Vector3 { public void Deconstruct(out float x, out float y, out float z) { x = X; y = Y; z = Z; } public static implicit operator Vector3((float x, float y, float z) xyz) { return new Vector3(xyz.x, xyz.y, xyz.z); } } public struct Vector4 { public void Deconstruct(out float x, out float y, out float z, out float w) { x = X; y = Y; z = Z; w = W; } public static implicit operator Vector4((float x, float y, float z, float w) xyzw) { return new Vector4(xyzw.x, xyzw.y, xyzw.z, xyzw.w); } } ``` ### API Usage ```csharp public Vector3 SampleTexture(Vector2 uv, Texture texture) { // texture sampling code } (float metalness, float roughness, float ao) = material.MraoMap == null ? (material.Metalness, material.Roughness, 1f) : SampleTexture(uv, material.MraoMap); ``` ### Alternative Designs _No response_ ### Risks _No response_
Author: PavielKraskouski
Assignees: -
Labels: `api-suggestion`, `area-System.Numerics`
Milestone: -
tannergooding commented 1 year ago

While this would be convenient, it is very counter-intuitive to the places that these vector types are typically used.

Vector2/3/4 are types that have SIMD acceleration and which are typically enregistered. That is, unlike a regular struct where the type would be considered to have 2, 3, or 4 distinct fields; SIMD types are typically considered and operated on as 1 individual unit. At runtime, they typically exist in a single register when being utilized and not in memory or promoted to a register per field.

Because of this, accessing the individual fields of these SIMD types can be quite a bit more expensive and thus deconstruction will lead to very poor performance and is effectively an anti-pattern for the type. You instead typically want to treat it like you would any other primitive type; replacing the whole vector at once.

We do not typically provide a way to construct a type from a tuple, particularly not implicitly and I expect that side is a non-starter.

ghost commented 1 year ago

This issue has been marked needs-author-action and may be missing some important information.

PavielKraskouski commented 1 year ago

Then what is better for performance if I still need to extract the values of the vector components, because they are used separately in different formulas: access the fields of the vector directly or write the fields to separate variables for later use?

tannergooding commented 1 year ago

Could you elaborate on the formula being used?

You typically want to try to restructure your data to allow taking advantage of the underlying vectorization where possible. One of the most typical ways to do this would be to utilize "structure of arrays" (SoA) rather than "arrays of structures" (AoS).

However, if you can't restructure your data, there are often some simple transformations you can make to the code to better take advantage of how SIMD works at the hardware level. The perf benefits tend to be well worth it, particularly in the domains these types get used.

PavielKraskouski commented 1 year ago

I am working on a software renderer. All my textures are a one-dimensional Vector3 array. For each type of texture, Vector3 stores different information. For color textures, these are colors in linear space in the range [0, 1], for normal maps, these are normal vectors. I also use MRAO textures that store different coefficients in each Vector3 component (X - metalness, Y - roughness, Z - ambient occlusion). Colors and normal vectors are used as expected using SIMD acceleration. But Vector3 components of MRAO textures cannot be used as a whole. Here is an example of calculating Fresnel reflectance at 0 degrees using metalness (mrao.X):

Vector3 F0 = Vector3.Lerp(new(0.04f), baseColor, mrao.X);

This calculates ambient light reflection, which uses ambient occlusion (mrao.Z).

Vector3 ambient = AmbientLight * baseColor * mrao.Z;

Calculating values of the normal distribution function and the geometric masking-shadowing function using roughness (mrao.Y):

float DTrowbridgeReitz()
{
    float a2 = mrao.Y * mrao.Y * mrao.Y * mrao.Y;
    float denom = NdotH * NdotH * (a2 - 1) + 1;
    return a2 / Max(1e-4f, Pi * denom * denom);
}

float GSmith()
{
    float GSchlickBeckmann(float NdotX)
    {
        float k = mrao.Y * mrao.Y * 0.5f;
        return 1 / Max(1e-4f, NdotX * (1 - k) + k);
    }

    return GSchlickBeckmann(NdotL) * GSchlickBeckmann(NdotV);
}