opti : shaders rework test

This commit is contained in:
2026-07-07 19:49:54 +02:00
parent a5b7c571eb
commit 06da5b858a
31 changed files with 1813 additions and 361 deletions
@@ -1,31 +1,68 @@
#ifndef ENKI_BILAL_INCLUDED
#define ENKI_BILAL_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
/*
ENKI BILLAL - MEDIUM VERSION WITH SECOND DETAIL LAYER
Goal:
Keep the optimized stylized render while allowing DetailTexB to be tested again.
Changes from the original version:
- Removed custom shadow sampling.
In the original shader, shadowAtten was calculated but never used.
This was pure GPU overhead.
- Removed Lighting.hlsl include.
GetMainLight / GetAdditionalLight are no longer used here.
- Uses the optimized SampleProjectedDetailLOD from Details.hlsl.
- DetailTexA is active when DetailIntensityA > 0.
- DetailTexB is active when DetailIntensityB > 0.
- Both details are limited by camera distance.
- World/Object conversions are computed only once if at least one detail layer is used.
- Simple fog is preserved.
Cost with the new Details.hlsl:
- DetailTexA only: 6 texture samples.
- DetailTexA + DetailTexB: 12 texture samples.
This is still much cheaper than the original setup.
*/
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Assets/_Content/Shaders/Include/Noises.hlsl"
#include "Assets/_Content/Shaders/Include/Details.hlsl"
#include "Assets/_Content/Shaders/Include/Utilities.hlsl"
//#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/AmbientOcclusion.hlsl"
/*
Maximum distance for projected details.
Lower value = better GPU performance.
Higher value = details visible farther from the camera.
Recommended range:
- 12 to 18 for dense scenes.
- 25 to 30 if details disappear too aggressively in depth.
*/
#define ENKI_DETAIL_MAX_DISTANCE 18.0
/*
Fade range used to avoid a hard cutoff.
Detail starts fading at:
ENKI_DETAIL_MAX_DISTANCE - ENKI_DETAIL_FADE_DISTANCE.
*/
#define ENKI_DETAIL_FADE_DISTANCE 4.0
/*
Safety stub.
If another include or older graph version still calls GetShadow,
the shader will compile and return "no custom shadow".
In this Medium version, custom shadow sampling is intentionally disabled.
*/
float GetShadow(float3 PositionWS)
{
float4 shadowCoord = TransformWorldToShadowCoord(PositionWS);
Light mainLight = GetMainLight(shadowCoord);
float shadow = 1 - mainLight.shadowAttenuation;
#if defined(_ADDITIONAL_LIGHTS)
uint lightsCount = GetAdditionalLightsCount();
[loop]
for (uint i = 0; i < lightsCount; i++)
{
Light light = GetAdditionalLight(i, PositionWS);
shadow = max(shadow, 1 - light.shadowAttenuation);
}
#endif
return saturate(shadow);
return 0.0;
}
void EnkiBillal_float
@@ -49,64 +86,99 @@ void EnkiBillal_float
out float Alpha
)
{
// Common
float depth = length(PositionWS - _WorldSpaceCameraPos.xyz);
float3 pivotWS = TransformObjectToWorld(float3(0, 0, 0));
float3 scale = GetScale(pivotWS);
float3 positionOS = TransformWorldToObject(PositionWS) * scale;
float3 normalOS = normalize(TransformWorldToObjectDir(NormalWS));
//float ao = GetScreenSpaceAmbientOcclusion(ScreenUV).directAmbientOcclusion;
// Shadow
float shadow = GetShadow(PositionWS);
float shadowAtten = Remap(1, 0, .6, 1, shadow);
// Detail
float4 detailA = SampleProjectedDetailLOD
(
DetailTexA,
positionOS,
normalOS,
ProjectionsBlend,
DetailSizeA,
depth
);
float4 detailB = SampleProjectedDetailLOD
(
DetailTexB,
positionOS,
normalOS,
ProjectionsBlend,
DetailSizeB,
depth
);
float detailMaskA = smoothstep(0, .8, detailA.a);
float detailMaskB = smoothstep(0, .8, detailB.a);
// Fog
// Output
/*
Base output.
If no extra effect is applied, this function remains very cheap.
*/
Output = Color.rgb;
Output = lerp(Output.rgb, detailA.rgb, detailMaskA * DetailIntensityA);
Output = lerp(Output.rgb, detailB.rgb, detailMaskB * DetailIntensityB);
Alpha = Color.a;
/*
Camera-to-fragment distance.
Used for projected detail distance limiting and optional fog.
*/
float depth = length(PositionWS - _WorldSpaceCameraPos.xyz);
/*
Distance fade for projected details.
detailFade is:
- 1 near the camera.
- 0 beyond ENKI_DETAIL_MAX_DISTANCE.
*/
float detailFadeStart = ENKI_DETAIL_MAX_DISTANCE - ENKI_DETAIL_FADE_DISTANCE;
float detailFade = 1.0 - smoothstep(detailFadeStart, ENKI_DETAIL_MAX_DISTANCE, depth);
float detailWeightA = DetailIntensityA * detailFade;
float detailWeightB = DetailIntensityB * detailFade;
/*
Only compute object-space data if at least one detail layer is actually used.
*/
if (detailWeightA > 0.001 || detailWeightB > 0.001)
{
float3 pivotWS = TransformObjectToWorld(float3(0.0, 0.0, 0.0));
float3 scale = GetScale(pivotWS);
float3 positionOS = TransformWorldToObject(PositionWS) * scale;
float3 normalOS = normalize(TransformWorldToObjectDir(NormalWS));
/*
Detail layer A.
*/
if (detailWeightA > 0.001)
{
float4 detailA = SampleProjectedDetailLOD
(
DetailTexA,
positionOS,
normalOS,
ProjectionsBlend,
DetailSizeA,
depth
);
float detailMaskA = smoothstep(0.0, 0.8, detailA.a);
Output = lerp(Output, detailA.rgb, detailMaskA * detailWeightA);
}
/*
Detail layer B.
This is intentionally restored for testing.
If performance drops too much, set DetailIntensityB to 0
or remove this block again.
*/
if (detailWeightB > 0.001)
{
float4 detailB = SampleProjectedDetailLOD
(
DetailTexB,
positionOS,
normalOS,
ProjectionsBlend,
DetailSizeB,
depth
);
float detailMaskB = smoothstep(0.0, 0.8, detailB.a);
Output = lerp(Output, detailB.rgb, detailMaskB * detailWeightB);
}
}
/*
Simple fog is preserved.
If fog becomes a measurable cost later, it can be moved to a cheaper global pass
or replaced with a simpler approximation.
*/
if (FogUse)
{
Output = lerp(Output.rgb, FogColor.rgb, smoothstep(FogMin, FogMax, depth) * FogColor.a);
float fogFactor = smoothstep(FogMin, FogMax, depth) * FogColor.a;
Output = lerp(Output, FogColor.rgb, fogFactor);
}
Alpha = Color.a;
}
#endif
@@ -1,6 +1,27 @@
#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,
@@ -8,51 +29,78 @@ float ProjectionMask(
float3 RectNormalWS,
float3 RectUpWS,
float2 RectSize,
float Oval)
float Oval
)
{
// Build the rectangle local frame
/*
Build the local projection frame.
*/
float3 N = normalize(RectNormalWS);
float3 U = normalize(RectUpWS);
float3 R = normalize(cross(N, U)); // Right axis
U = normalize(cross(R, N)); // Recompute Up orthogonally in case RectUpWS is not perfectly perpendicular to RectNormalWS
float3 R = normalize(cross(N, U));
// Ray from camera toward the fragment
float3 rayDir = normalize(PositionWS - CamPositionWS);
/*
Recompute U to guarantee an orthogonal frame.
*/
U = normalize(cross(R, N));
// Ray-plane intersection
// Plane equation: dot(P - RectPositionWS, N) = 0
/*
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);
// Ray is parallel to the plane or facing away — no hit
if (abs(denom) < 1e-6)
return 0;
return 0.0;
float t = dot(RectPositionWS - CamPositionWS, N) / denom;
// Intersection must be in front of the camera (t > 0)
// and not beyond the fragment (t <= distance from camera to fragment)
float tFrag = dot(PositionWS - CamPositionWS, rayDir);
if (t < 0.0 || t > tFrag + 1e-4)
return 0;
/*
Reject intersections behind the camera or behind the current fragment.
*/
if (t < 0.0 || t > distToFrag + 1e-4)
return 0.0;
// Compute the hit point on the plane
/*
Compute hit point on the projection plane.
*/
float3 hitPoint = CamPositionWS + rayDir * t;
// Project hit point into the rectangle local space
/*
Convert hit point to rectangle local coordinates.
*/
float3 localVec = hitPoint - RectPositionWS;
float localU = dot(localVec, R); // horizontal axis
float localV = dot(localVec, U); // vertical axis
float localU = dot(localVec, R);
float localV = dot(localVec, U);
float halfW = RectSize.x * 0.5;
float halfH = RectSize.y * 0.5;
// Corner radius: 0 = rectangle, min(halfW, halfH) = oval
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);
// Rounded rectangle SDF
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;
@@ -60,17 +108,20 @@ float ProjectionMask(
float SampleProjectionMask(float3 PositionWS, float4x4 M)
{
if (M[0].w == 0)
return 0;
/*
M[0].w is used as the active flag.
*/
if (M[0].w <= 0.5)
return 0.0;
return ProjectionMask(
PositionWS,
M[0].xyz, // CamPositionWS
M[1].xyz, // RectPositionWS
M[3].xyz, // RectNormalWS
M[2].xyz, // RectUpWS
float2(M[2].w, M[3].w), // RectSize
M[1].w // Shape
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
);
}
@@ -84,40 +135,82 @@ void Mask_float
out float Alpha
)
{
Alpha = 1;
if (Mask0[0].w == 1 && Mask1[0].w == 1 && Mask2[0].w == 1)
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)
{
float mask = SampleProjectionMask(PositionWS, Mask0);
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) ? 1 - mask : mask;
Alpha = (MaskID <= 1.0) ? (1.0 - mask) : mask;
return;
}
else if (MaskID == 0)
/*
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;
}
else
{
float Active = Mask0[0].w + Mask1[0].w + Mask2[0].w;
if (Active > 0)
{
float mask = saturate(
SampleProjectionMask(PositionWS, Mask0) +
SampleProjectionMask(PositionWS, Mask1) +
SampleProjectionMask(PositionWS, Mask2)
);
Alpha = (MaskID == 1) ? 1 - mask : mask;
}
else
{
Alpha = (MaskID == 1) ? 0 : 1;
}
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
#endif
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b83c226d7219d244b50aa3f10545374
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,112 @@
#ifndef ENKI_BILAL_INCLUDED
#define ENKI_BILAL_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Assets/_Content/Shaders/Include/Noises.hlsl"
#include "Assets/_Content/Shaders/Include/Details.hlsl"
#include "Assets/_Content/Shaders/Include/Utilities.hlsl"
//#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/AmbientOcclusion.hlsl"
float GetShadow(float3 PositionWS)
{
float4 shadowCoord = TransformWorldToShadowCoord(PositionWS);
Light mainLight = GetMainLight(shadowCoord);
float shadow = 1 - mainLight.shadowAttenuation;
#if defined(_ADDITIONAL_LIGHTS)
uint lightsCount = GetAdditionalLightsCount();
[loop]
for (uint i = 0; i < lightsCount; i++)
{
Light light = GetAdditionalLight(i, PositionWS);
shadow = max(shadow, 1 - light.shadowAttenuation);
}
#endif
return saturate(shadow);
}
void EnkiBillal_float
(
float4 Color,
UnityTexture2D DetailTexA,
UnityTexture2D DetailTexB,
float2 ScreenUV,
float3 PositionWS,
float3 NormalWS,
float ProjectionsBlend,
float DetailIntensityA,
float DetailIntensityB,
float DetailSizeA,
float DetailSizeB,
uint FogUse,
float4 FogColor,
float FogMin,
float FogMax,
out float3 Output,
out float Alpha
)
{
// Common
float depth = length(PositionWS - _WorldSpaceCameraPos.xyz);
float3 pivotWS = TransformObjectToWorld(float3(0, 0, 0));
float3 scale = GetScale(pivotWS);
float3 positionOS = TransformWorldToObject(PositionWS) * scale;
float3 normalOS = normalize(TransformWorldToObjectDir(NormalWS));
//float ao = GetScreenSpaceAmbientOcclusion(ScreenUV).directAmbientOcclusion;
// Shadow
float shadow = GetShadow(PositionWS);
float shadowAtten = Remap(1, 0, .6, 1, shadow);
// Detail
float4 detailA = SampleProjectedDetailLOD
(
DetailTexA,
positionOS,
normalOS,
ProjectionsBlend,
DetailSizeA,
depth
);
float4 detailB = SampleProjectedDetailLOD
(
DetailTexB,
positionOS,
normalOS,
ProjectionsBlend,
DetailSizeB,
depth
);
float detailMaskA = smoothstep(0, .8, detailA.a);
float detailMaskB = smoothstep(0, .8, detailB.a);
// Fog
// Output
Output = Color.rgb;
Output = lerp(Output.rgb, detailA.rgb, detailMaskA * DetailIntensityA);
Output = lerp(Output.rgb, detailB.rgb, detailMaskB * DetailIntensityB);
if (FogUse)
{
Output = lerp(Output.rgb, FogColor.rgb, smoothstep(FogMin, FogMax, depth) * FogColor.a);
}
Alpha = Color.a;
}
#endif
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c4f3908c63ccbf2429b0628cdc9a2e23
ShaderIncludeImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,123 @@
#ifndef MASK_INCLUDED
#define MASK_INCLUDED
float ProjectionMask(
float3 PositionWS,
float3 CamPositionWS,
float3 RectPositionWS,
float3 RectNormalWS,
float3 RectUpWS,
float2 RectSize,
float Oval)
{
// Build the rectangle local frame
float3 N = normalize(RectNormalWS);
float3 U = normalize(RectUpWS);
float3 R = normalize(cross(N, U)); // Right axis
U = normalize(cross(R, N)); // Recompute Up orthogonally in case RectUpWS is not perfectly perpendicular to RectNormalWS
// Ray from camera toward the fragment
float3 rayDir = normalize(PositionWS - CamPositionWS);
// Ray-plane intersection
// Plane equation: dot(P - RectPositionWS, N) = 0
float denom = dot(rayDir, N);
// Ray is parallel to the plane or facing away — no hit
if (abs(denom) < 1e-6)
return 0;
float t = dot(RectPositionWS - CamPositionWS, N) / denom;
// Intersection must be in front of the camera (t > 0)
// and not beyond the fragment (t <= distance from camera to fragment)
float tFrag = dot(PositionWS - CamPositionWS, rayDir);
if (t < 0.0 || t > tFrag + 1e-4)
return 0;
// Compute the hit point on the plane
float3 hitPoint = CamPositionWS + rayDir * t;
// Project hit point into the rectangle local space
float3 localVec = hitPoint - RectPositionWS;
float localU = dot(localVec, R); // horizontal axis
float localV = dot(localVec, U); // vertical axis
float halfW = RectSize.x * 0.5;
float halfH = RectSize.y * 0.5;
// Corner radius: 0 = rectangle, min(halfW, halfH) = oval
float radius = saturate(Oval) * min(halfW, halfH);
// Rounded rectangle SDF
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)
{
if (M[0].w == 0)
return 0;
return ProjectionMask(
PositionWS,
M[0].xyz, // CamPositionWS
M[1].xyz, // RectPositionWS
M[3].xyz, // RectNormalWS
M[2].xyz, // RectUpWS
float2(M[2].w, M[3].w), // RectSize
M[1].w // Shape
);
}
void Mask_float
(
float3 PositionWS,
float MaskID,
float4x4 Mask0,
float4x4 Mask1,
float4x4 Mask2,
out float Alpha
)
{
Alpha = 1;
if (Mask0[0].w == 1 && Mask1[0].w == 1 && Mask2[0].w == 1)
{
float mask = SampleProjectionMask(PositionWS, Mask0);
mask += SampleProjectionMask(PositionWS, Mask1);
mask += SampleProjectionMask(PositionWS, Mask2);
mask = saturate(mask);
Alpha = (MaskID <= 1) ? 1 - mask : mask;
}
else if (MaskID == 0)
{
Alpha = 1;
}
else
{
float Active = Mask0[0].w + Mask1[0].w + Mask2[0].w;
if (Active > 0)
{
float mask = saturate(
SampleProjectionMask(PositionWS, Mask0) +
SampleProjectionMask(PositionWS, Mask1) +
SampleProjectionMask(PositionWS, Mask2)
);
Alpha = (MaskID == 1) ? 1 - mask : mask;
}
else
{
Alpha = (MaskID == 1) ? 0 : 1;
}
}
}
#endif
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5e676c47e68d7e141bf1ab708fbdebca
ShaderIncludeImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
#ifndef STRIPES_INCLUDED
#define STRIPES_INCLUDED
// Returns a value in [0, 1)
float VanDerCorput(uint index)
{
// Reverse bits of the integer and scale to [0,1]
return (float) reversebits(index) * 2.3283064365386963e-10;
}
void Stripes_float(
float2 UV,
float Density,
float Count,
float Thickness,
float Offset,
float DistortionAmplitude,
float DistortionFrequency,
out float Stripes
)
{
float density = saturate(Density);
float offset = frac(Offset);
float distortionAmplitude = DistortionAmplitude * .1;
uint stripeCount = max((uint) round(Count), 1u);
float2 uv = UV;
uv.x += offset;
float2 uvA = uv;
float2 uvB = uv;
uvA.x = frac(uvA.x + cos((uv.y + offset) * sin(uv.x) * DistortionFrequency + offset * 7.8) * distortionAmplitude);
uvB.x = frac(uvB.x + sin((uv.y + offset) * cos(uv.x) * DistortionFrequency + offset * 5.3) * distortionAmplitude);
float stripDistanceY = abs(saturate(uv.y) - .5);
float stripeMax = 0;
for (uint i = 0u; i < stripeCount; i++)
{
float vdc = VanDerCorput(i);
bool usesSecondDistortion = vdc >= .5;
float thickness = vdc < .3 ? 1 : (vdc < .6 ? 1.8 : 2.6);
thickness *= saturate(Thickness);
float wrappedX = usesSecondDistortion ? uvB.x : uvA.x;
float localProgress = saturate(density * stripeCount - vdc * stripeCount);
float stripeCenterX = frac((i + .5) / stripeCount);
float stripDistanceX = abs(wrappedX - stripeCenterX);
stripDistanceX = min(stripDistanceX, 1.0 - stripDistanceX);
float stepX = (1. / stripeCount) * localProgress * thickness * .5;
float stepY = localProgress * .5 - .01;
float softEdgeX = .01;
float softEdgeY = .05;
float widthMask = 1 - smoothstep(stepX - softEdgeX, stepX, stripDistanceX);
float heightMask = 1 - smoothstep(stepY - softEdgeY, stepY, stripDistanceY);
float stripeMask = widthMask * heightMask;
stripeMax = max(stripeMax, stripeMask);
}
Stripes = stripeMax;
}
#endif
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fa3b413e6fe24314f874cc5f0bce969d
ShaderIncludeImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,11 +1,89 @@
#ifndef STRIPES_INCLUDED
#define STRIPES_INCLUDED
// Returns a value in [0, 1)
/*
STRIPES - MEDIUM VERSION
Goal:
Reduce the GPU cost of the procedural stripe pattern.
Changes from the original version:
- Removed the dynamic loop over the full stripe count.
Original behavior: evaluate every stripe from 0 to Count.
New behavior: evaluate only 6 nearby stripe candidates.
- Clamped Count to 32 to prevent expensive high-count cases.
- Kept Van der Corput distribution to preserve stable pseudo-random variation.
- Kept the two distortion paths to preserve part of the organic look.
- Visual result is an approximation of the original, but much cheaper.
*/
float VanDerCorput(uint index)
{
// Reverse bits of the integer and scale to [0,1]
return (float) reversebits(index) * 2.3283064365386963e-10;
return (float)reversebits(index) * 2.3283064365386963e-10;
}
int WrapIndex(int x, int count)
{
int r = x % count;
return r < 0 ? r + count : r;
}
float EvalStripeCandidate(
uint index,
uint stripeCount,
float wrappedXA,
float wrappedXB,
float stripDistanceY,
float density,
float thicknessInput
)
{
float vdc = VanDerCorput(index);
/*
Same progressive reveal logic as the original shader.
Original:
saturate(density * stripeCount - vdc * stripeCount)
Equivalent simplified form:
saturate((density - vdc) * stripeCount)
*/
float localProgress = saturate((density - vdc) * stripeCount);
if (localProgress <= 0.0001)
return 0.0;
/*
Keep the original two distortion paths.
Each stripe chooses one path based on its pseudo-random value.
*/
float wrappedX = vdc >= 0.5 ? wrappedXB : wrappedXA;
/*
Keep the original thickness variation.
This prevents the stripes from looking too uniform.
*/
float thickness = vdc < 0.3 ? 1.0 : (vdc < 0.6 ? 1.8 : 2.6);
thickness *= saturate(thicknessInput);
float stripeCenterX = ((float)index + 0.5) / (float)stripeCount;
float stripDistanceX = abs(wrappedX - stripeCenterX);
stripDistanceX = min(stripDistanceX, 1.0 - stripDistanceX);
float stepX = (1.0 / (float)stripeCount) * localProgress * thickness * 0.5;
float stepY = localProgress * 0.5 - 0.01;
/*
Soft edges are kept from the original shader.
If more performance is needed, these smoothstep calls can be replaced
with step calls, but the result will look harsher.
*/
float widthMask = 1.0 - smoothstep(stepX - 0.01, stepX, stripDistanceX);
float heightMask = 1.0 - smoothstep(stepY - 0.05, stepY, stripDistanceY);
return widthMask * heightMask;
}
void Stripes_float(
@@ -21,48 +99,78 @@ void Stripes_float(
{
float density = saturate(Density);
float offset = frac(Offset);
float distortionAmplitude = DistortionAmplitude * .1;
uint stripeCount = max((uint) round(Count), 1u);
/*
Intentional clamp.
Above 32 stripes, the cost increases while the visual gain is usually limited,
especially on distant objects or in a strongly stylized render.
*/
uint stripeCount = max((uint)round(Count), 1u);
stripeCount = min(stripeCount, 32u);
float distortionAmplitude = DistortionAmplitude * 0.1;
float2 uv = UV;
uv.x += offset;
float2 uvA = uv;
float2 uvB = uv;
uvA.x = frac(uvA.x + cos((uv.y + offset) * sin(uv.x) * DistortionFrequency + offset * 7.8) * distortionAmplitude);
uvB.x = frac(uvB.x + sin((uv.y + offset) * cos(uv.x) * DistortionFrequency + offset * 5.3) * distortionAmplitude);
float stripDistanceY = abs(saturate(uv.y) - .5);
uv.x = frac(uv.x + offset);
float stripeMax = 0;
for (uint i = 0u; i < stripeCount; i++)
/*
The two distortion paths are computed once and reused for all nearby candidates.
*/
float wrappedXA = frac(
uv.x + cos((uv.y + offset) * sin(uv.x) * DistortionFrequency + offset * 7.8)
* distortionAmplitude
);
float wrappedXB = frac(
uv.x + sin((uv.y + offset) * cos(uv.x) * DistortionFrequency + offset * 5.3)
* distortionAmplitude
);
float stripDistanceY = abs(saturate(uv.y) - 0.5);
int sc = (int)stripeCount;
/*
Find the nearest stripe indices for both distortion paths.
Original shader tested every stripe.
Medium version tests only base - 1, base, base + 1 for A and B.
*/
int baseA = (int)floor(wrappedXA * sc);
int baseB = (int)floor(wrappedXB * sc);
float stripeMax = 0.0;
[unroll]
for (int k = -1; k <= 1; k++)
{
float vdc = VanDerCorput(i);
bool usesSecondDistortion = vdc >= .5;
uint idxA = (uint)WrapIndex(baseA + k, sc);
uint idxB = (uint)WrapIndex(baseB + k, sc);
float thickness = vdc < .3 ? 1 : (vdc < .6 ? 1.8 : 2.6);
thickness *= saturate(Thickness);
float wrappedX = usesSecondDistortion ? uvB.x : uvA.x;
stripeMax = max(
stripeMax,
EvalStripeCandidate(
idxA,
stripeCount,
wrappedXA,
wrappedXB,
stripDistanceY,
density,
Thickness
)
);
float localProgress = saturate(density * stripeCount - vdc * stripeCount);
float stripeCenterX = frac((i + .5) / stripeCount);
float stripDistanceX = abs(wrappedX - stripeCenterX);
stripDistanceX = min(stripDistanceX, 1.0 - stripDistanceX);
float stepX = (1. / stripeCount) * localProgress * thickness * .5;
float stepY = localProgress * .5 - .01;
float softEdgeX = .01;
float softEdgeY = .05;
float widthMask = 1 - smoothstep(stepX - softEdgeX, stepX, stripDistanceX);
float heightMask = 1 - smoothstep(stepY - softEdgeY, stepY, stripDistanceY);
float stripeMask = widthMask * heightMask;
stripeMax = max(stripeMax, stripeMask);
stripeMax = max(
stripeMax,
EvalStripeCandidate(
idxB,
stripeCount,
wrappedXA,
wrappedXB,
stripDistanceY,
density,
Thickness
)
);
}
Stripes = stripeMax;