216 lines
5.3 KiB
HLSL
216 lines
5.3 KiB
HLSL
#ifndef MASK_INCLUDED
|
|
#define MASK_INCLUDED
|
|
|
|
/*
|
|
MASK - MEDIUM COMPATIBLE VERSION
|
|
|
|
Goal:
|
|
Keep the original portal / projection behavior while avoiding unnecessary work
|
|
in cases where the mask is not supposed to affect the object.
|
|
|
|
Important compatibility point:
|
|
In the original shader, when all three masks are active, MaskID 0 is affected:
|
|
Alpha = (MaskID <= 1) ? 1 - mask : mask;
|
|
|
|
The previous Medium version incorrectly skipped MaskID 0 entirely.
|
|
That could break portal logic.
|
|
|
|
Changes from the original version:
|
|
- Preserves the original Alpha logic.
|
|
- Keeps rounded rectangle SDF to avoid changing portal shape.
|
|
- Adds safer early returns only when they do not change original behavior.
|
|
- Samples only active masks outside the "all three active" special case.
|
|
*/
|
|
|
|
float ProjectionMask(
|
|
float3 PositionWS,
|
|
float3 CamPositionWS,
|
|
float3 RectPositionWS,
|
|
float3 RectNormalWS,
|
|
float3 RectUpWS,
|
|
float2 RectSize,
|
|
float Oval
|
|
)
|
|
{
|
|
/*
|
|
Build the local projection frame.
|
|
*/
|
|
float3 N = normalize(RectNormalWS);
|
|
float3 U = normalize(RectUpWS);
|
|
float3 R = normalize(cross(N, U));
|
|
|
|
/*
|
|
Recompute U to guarantee an orthogonal frame.
|
|
*/
|
|
U = normalize(cross(R, N));
|
|
|
|
/*
|
|
Ray from camera to fragment.
|
|
*/
|
|
float3 camToFrag = PositionWS - CamPositionWS;
|
|
float distToFrag = length(camToFrag);
|
|
|
|
if (distToFrag <= 1e-5)
|
|
return 0.0;
|
|
|
|
float3 rayDir = camToFrag / distToFrag;
|
|
|
|
/*
|
|
Ray-plane intersection.
|
|
*/
|
|
float denom = dot(rayDir, N);
|
|
|
|
if (abs(denom) < 1e-6)
|
|
return 0.0;
|
|
|
|
float t = dot(RectPositionWS - CamPositionWS, N) / denom;
|
|
|
|
/*
|
|
Reject intersections behind the camera or behind the current fragment.
|
|
*/
|
|
if (t < 0.0 || t > distToFrag + 1e-4)
|
|
return 0.0;
|
|
|
|
/*
|
|
Compute hit point on the projection plane.
|
|
*/
|
|
float3 hitPoint = CamPositionWS + rayDir * t;
|
|
|
|
/*
|
|
Convert hit point to rectangle local coordinates.
|
|
*/
|
|
float3 localVec = hitPoint - RectPositionWS;
|
|
float localU = dot(localVec, R);
|
|
float localV = dot(localVec, U);
|
|
|
|
float halfW = RectSize.x * 0.5;
|
|
float halfH = RectSize.y * 0.5;
|
|
|
|
if (halfW <= 1e-5 || halfH <= 1e-5)
|
|
return 0.0;
|
|
|
|
/*
|
|
Original rounded rectangle SDF preserved.
|
|
|
|
The previous Medium version replaced this with a hard rectangle / ellipse.
|
|
That was cheaper, but it could change the portal shape.
|
|
*/
|
|
float radius = saturate(Oval) * min(halfW, halfH);
|
|
|
|
float2 uv = float2(localU, localV);
|
|
float2 ext = float2(halfW, halfH);
|
|
float2 q = abs(uv) - ext + radius;
|
|
|
|
float sdf = length(max(q, 0.0)) - radius;
|
|
|
|
return sdf <= 0.0 ? 1.0 : 0.0;
|
|
}
|
|
|
|
float SampleProjectionMask(float3 PositionWS, float4x4 M)
|
|
{
|
|
/*
|
|
M[0].w is used as the active flag.
|
|
*/
|
|
if (M[0].w <= 0.5)
|
|
return 0.0;
|
|
|
|
return ProjectionMask(
|
|
PositionWS,
|
|
M[0].xyz, // Camera world position
|
|
M[1].xyz, // Rectangle world position
|
|
M[3].xyz, // Rectangle normal in world space
|
|
M[2].xyz, // Rectangle up vector in world space
|
|
float2(M[2].w, M[3].w), // Rectangle size
|
|
M[1].w // Rounded shape amount
|
|
);
|
|
}
|
|
|
|
void Mask_float
|
|
(
|
|
float3 PositionWS,
|
|
float MaskID,
|
|
float4x4 Mask0,
|
|
float4x4 Mask1,
|
|
float4x4 Mask2,
|
|
out float Alpha
|
|
)
|
|
{
|
|
float active0 = Mask0[0].w > 0.5 ? 1.0 : 0.0;
|
|
float active1 = Mask1[0].w > 0.5 ? 1.0 : 0.0;
|
|
float active2 = Mask2[0].w > 0.5 ? 1.0 : 0.0;
|
|
|
|
float activeCount = active0 + active1 + active2;
|
|
|
|
/*
|
|
No active mask case.
|
|
|
|
Preserves original behavior:
|
|
- MaskID 1 is hidden.
|
|
- Other IDs remain visible.
|
|
*/
|
|
if (activeCount <= 0.0)
|
|
{
|
|
Alpha = (MaskID == 1) ? 0.0 : 1.0;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
Original special case:
|
|
when all three masks are active, MaskID 0 is also affected by the mask.
|
|
|
|
This is probably what your portal depends on.
|
|
*/
|
|
if (active0 > 0.5 && active1 > 0.5 && active2 > 0.5)
|
|
{
|
|
float mask = 0.0;
|
|
|
|
mask += SampleProjectionMask(PositionWS, Mask0);
|
|
mask += SampleProjectionMask(PositionWS, Mask1);
|
|
mask += SampleProjectionMask(PositionWS, Mask2);
|
|
|
|
mask = saturate(mask);
|
|
|
|
Alpha = (MaskID <= 1.0) ? (1.0 - mask) : mask;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
Safe optimization:
|
|
In the original shader, when not all three masks are active,
|
|
MaskID 0 is always fully visible.
|
|
|
|
So only here it is safe to skip all projection work for MaskID 0.
|
|
*/
|
|
if (MaskID == 0)
|
|
{
|
|
Alpha = 1.0;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
Partial active mask case.
|
|
|
|
Only sample masks that are active.
|
|
*/
|
|
float maskPartial = 0.0;
|
|
|
|
if (active0 > 0.5)
|
|
maskPartial += SampleProjectionMask(PositionWS, Mask0);
|
|
|
|
if (active1 > 0.5)
|
|
maskPartial += SampleProjectionMask(PositionWS, Mask1);
|
|
|
|
if (active2 > 0.5)
|
|
maskPartial += SampleProjectionMask(PositionWS, Mask2);
|
|
|
|
maskPartial = saturate(maskPartial);
|
|
|
|
/*
|
|
Original convention preserved:
|
|
- MaskID 1 is visible outside the mask.
|
|
- MaskID > 1 is visible inside the mask.
|
|
*/
|
|
Alpha = (MaskID == 1.0) ? (1.0 - maskPartial) : maskPartial;
|
|
}
|
|
|
|
#endif |