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;
+264 -135
View File
@@ -3,6 +3,46 @@
#include "Assets/_Content/Shaders/Include/Utilities.hlsl"
/*
DETAILS - MEDIUM LOD VERSION
Goal:
Keep the large performance gain from the optimized triplanar version,
while restoring the old distance-based "detail upscale" behavior with smoother transitions.
Main changes compared to the original version:
- Replaced the 7-direction projection system with standard 3-axis triplanar mapping.
- Removed the expensive 2x2 texture bombing by default.
- Restored manual distance-based detail scaling.
- Reduced the LOD ratio from 4.0 to 2.0 to make transitions less visible.
- Added stable spatial jitter to avoid the whole screen switching LOD at the same distance.
- Kept the public function signature SampleProjectedDetailLOD(...)
so existing Shader Graph nodes do not break.
Original cost per detail layer:
7 projections * 2 LOD levels * 4 bombing samples = 56 texture samples.
This version:
3 projections * 2 LOD levels = 6 texture samples.
If EnkiBillal samples both DetailTexA and DetailTexB:
6 * 2 = 12 texture samples.
Still much cheaper than the original 112 samples.
*/
#define DETAILS_USE_LIGHT_BOMBING 0
/*
Distance-based detail LOD.
This restores the old idea:
- close camera -> smaller, sharper details;
- far camera -> larger, more readable details.
Difference from the original:
lodRatio is reduced from 4.0 to 2.0.
This makes each transition less visible.
*/
void GetDetailLOD
(
float Depth,
@@ -13,81 +53,140 @@ void GetDetailLOD
out float LODBlend
)
{
const float lodRatio = 4;
const float lodSize = DetailSizeA / lodRatio * 2;
const float lodRatio = 2.0;
const float baseScale = 1.0;
float dist = max(Depth, 1e-5);
float lod = log(dist) / log(lodRatio) + LODJitter; // log2(dist);
float lod = log(dist) / log(lodRatio) + LODJitter;
float lodFloor = floor(lod);
float lodFrac = lod - lodFloor;
LODBlend = lodFrac * lodFrac * (3 - 2 * lodFrac);
/*
Quintic smootherstep.
This is smoother than classic smoothstep and helps hide the LOD blend.
*/
float t = saturate(lodFrac);
LODBlend = t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
DetailSizeAA = lodSize * pow(lodRatio, lodFloor);
DetailSizeAB = lodSize * pow(lodRatio, lodFloor + 1);
DetailSizeAA = DetailSizeA * baseScale * pow(lodRatio, lodFloor);
DetailSizeAB = DetailSizeA * baseScale * pow(lodRatio, lodFloor + 1.0);
}
/*
Default fast texture sample.
Uses normal GPU mipmapping.
*/
float4 SampleDetailTextureFast
(
UnityTexture2D Tex,
float2 UV
)
{
return Tex.tex.Sample(Tex.samplerstate, UV);
}
/*
Optional lightweight texture bombing.
Disabled by default.
Compared to the original SampleBombedTexture:
- original: 4 texture samples + 4 rotations + 4 weights;
- this version: 1 texture sample with stable per-cell offset and rotation.
Warning:
This can create visible cell transitions because neighboring cells are not blended.
*/
float4 SampleDetailTextureLightBombed
(
UnityTexture2D Tex,
float2 UV
)
{
float2 cell = floor(UV);
float2 randomOffset = (Hash2D(cell) - 0.5) * 0.35;
float randomAngle = Hash1From2D(cell) * TWO_PI;
float2 sampleUV = UV + randomOffset;
sampleUV = RotateUV
(
sampleUV,
cell + 0.5,
randomAngle
);
return Tex.tex.Sample(Tex.samplerstate, sampleUV);
}
/*
Compatibility function.
Existing code can still call SampleBombedTexture.
In Medium mode, it routes to:
- direct sampling by default;
- optional one-sample light bombing if enabled.
*/
float4 SampleBombedTexture
(
UnityTexture2D Tex,
float2 UV
)
{
const float cellBlend = 4;
float2 cell = floor(UV);
float2 local = frac(UV);
float4 result = 0;
float weightSum = 0;
[unroll]
for (int y = 0; y < 2; y++)
{
[unroll]
for (int x = 0; x < 2; x++)
{
float2 currentCell = cell + float2(x, y);
float2 randomOffset = (Hash2D(currentCell) - .5) * .35;
float randomAngle = Hash1From2D(currentCell) * TWO_PI;
float2 center = float2(x, y) + randomOffset;
float2 delta = local - center;
float weight =
pow(saturate(1 - abs(delta.x)), cellBlend) *
pow(saturate(1 - abs(delta.y)), cellBlend);
float2 sampleUV = UV + randomOffset;
sampleUV = RotateUV
(
sampleUV,
currentCell + .5,
randomAngle
);
result += Tex.tex.Sample
(
Tex.samplerstate,
sampleUV
) * weight;
weightSum += weight;
}
}
return result / max(weightSum, 1e-5);
#if DETAILS_USE_LIGHT_BOMBING
return SampleDetailTextureLightBombed(Tex, UV);
#else
return SampleDetailTextureFast(Tex, UV);
#endif
}
/*
Triplanar weights.
ProjectionBlend keeps the same artistic role:
- low value -> softer blend between axes;
- high value -> sharper projection selection.
The original shader used up to 32.
This version uses 16 by default to reduce overly sharp transitions.
*/
float3 GetTriplanarWeights
(
float3 NormalOS,
float ProjectionBlend
)
{
float3 n = abs(normalize(NormalOS));
float sharpness = lerp(2.0, 16.0, saturate(ProjectionBlend));
float3 w = pow(saturate(n), sharpness);
float sumW = max(w.x + w.y + w.z, 1e-5);
return w / sumW;
}
/*
Compatibility structure.
Kept because old code or Shader Graph nodes may still expect this type.
The optimized SampleProjectedDetailLOD below does not use the old 7-projection path.
*/
struct ProjectionData
{
float2 uv[7];
float mask[7];
};
/*
Compatibility fallback.
Fills only the first three entries using standard triplanar projections.
Entries 3..6 are disabled.
*/
ProjectionData GetProjections
(
float3 PositionOS,
@@ -97,57 +196,72 @@ ProjectionData GetProjections
{
ProjectionData data;
float3 normal = normalize(NormalOS);
float3 w = GetTriplanarWeights(NormalOS, ProjectionBlend);
float3 projectionNormals[7] =
{
normalize(float3(1, 1, 1)),
normalize(float3(-1, 1, 1)),
normalize(float3(1, -1, 1)),
normalize(float3(-1, -1, 1)),
float3(1, 0, 0),
float3(0, 1, 0),
float3(0, 0, 1)
};
/*
Standard triplanar UVs:
- X projection samples the YZ plane.
- Y projection samples the XZ plane.
- Z projection samples the XY plane.
*/
data.uv[0] = PositionOS.zy;
data.uv[1] = PositionOS.xz;
data.uv[2] = PositionOS.xy;
float sharpness = lerp(2, 32, saturate(ProjectionBlend));
float maskSum = 0;
data.mask[0] = w.x;
data.mask[1] = w.y;
data.mask[2] = w.z;
[unroll]
for (int i = 0; i < 7; i++)
for (int i = 3; i < 7; i++)
{
float3 n = projectionNormals[i];
float d = abs(dot(normal, n));
data.mask[i] = pow(saturate(d), sharpness);
maskSum += data.mask[i];
float3 up = abs(n.y) < .999
? float3(0, 1, 0)
: float3(1, 0, 0);
float3 tangent = normalize(cross(up, n));
float3 bitangent = normalize(cross(n, tangent));
data.uv[i] = float2
(
dot(PositionOS, tangent),
dot(PositionOS, bitangent)
);
data.uv[i] = 0.0;
data.mask[i] = 0.0;
}
maskSum = max(maskSum, 1e-5);
[unroll]
for (int i = 0; i < 7; i++)
data.mask[i] /= maskSum;
return data;
}
/*
Samples one triplanar axis with two distance-based detail sizes.
This restores the "detail upscale with distance" effect without returning
to the original 7-projection and 2x2 bombing cost.
*/
float4 SampleDetailAxisLOD
(
UnityTexture2D DetailTex,
float2 BaseUV,
float DetailSizeAA,
float DetailSizeAB,
float LODBlend
)
{
float4 detailA = SampleBombedTexture
(
DetailTex,
BaseUV / max(DetailSizeAA, 1e-5)
);
float4 detailB = SampleBombedTexture
(
DetailTex,
BaseUV / max(DetailSizeAB, 1e-5)
);
return lerp(detailA, detailB, LODBlend);
}
/*
Optimized projected detail sampling with smoother distance LOD.
Cost:
3 triplanar axes * 2 LOD samples = 6 texture samples.
The old version also added per-projection jitter.
This version uses one stable object-space jitter for the whole sample.
That keeps the transition less synchronized without reintroducing too much cost.
*/
float4 SampleProjectedDetailLOD
(
UnityTexture2D DetailTex,
@@ -158,57 +272,72 @@ float4 SampleProjectedDetailLOD
float Depth
)
{
const float lodJitterStrength = .8;
const float lodJitterSize = 1.5;
float detailSize = max(DetailSizeA, 1e-5);
float4 detail = 0;
float totalMask = 0;
float3 weights = GetTriplanarWeights(NormalOS, ProjectionBlend);
ProjectionData projections = GetProjections
/*
Stable spatial jitter.
This prevents the whole screen from switching detail scale at exactly
the same camera distance.
Keep this value low.
Too much jitter makes the transition patchy.
*/
float lodJitter = (Hash1From2D(floor(PositionOS.xz * 0.75)) - 0.5) * 0.35;
float DetailSizeAA;
float DetailSizeAB;
float lodBlend;
GetDetailLOD
(
PositionOS,
NormalOS,
ProjectionBlend
Depth,
detailSize,
lodJitter,
DetailSizeAA,
DetailSizeAB,
lodBlend
);
[unroll]
for (int i = 0; i < 7; i++)
{
float mask = projections.mask[i];
/*
Triplanar base UVs in object space.
*/
float2 uvX = PositionOS.zy;
float2 uvY = PositionOS.xz;
float2 uvZ = PositionOS.xy;
float lodJitter = (Hash1From2D(floor(projections.uv[i] * lodJitterSize) + i * 17.13) - .5) * lodJitterStrength;
float4 sampleX = SampleDetailAxisLOD
(
DetailTex,
uvX,
DetailSizeAA,
DetailSizeAB,
lodBlend
);
float DetailSizeAA;
float DetailSizeAB;
float lodBlend;
float4 sampleY = SampleDetailAxisLOD
(
DetailTex,
uvY,
DetailSizeAA,
DetailSizeAB,
lodBlend
);
GetDetailLOD
(
Depth,
DetailSizeA,
lodJitter,
DetailSizeAA,
DetailSizeAB,
lodBlend
);
float4 sampleZ = SampleDetailAxisLOD
(
DetailTex,
uvZ,
DetailSizeAA,
DetailSizeAB,
lodBlend
);
float4 detailA = SampleBombedTexture
(
DetailTex,
projections.uv[i] / DetailSizeAA
);
float4 detailB = SampleBombedTexture
(
DetailTex,
projections.uv[i] / DetailSizeAB
);
detail += lerp(detailA, detailB, lodBlend) * mask;
totalMask += mask;
}
return detail / max(totalMask, 1e-5);
return sampleX * weights.x
+ sampleY * weights.y
+ sampleZ * weights.z;
}
#endif
#endif
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e3d6911aa39004b4a88bf6731817967c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,214 @@
#ifndef DETAILS_HLSL
#define DETAILS_HLSL
#include "Assets/_Content/Shaders/Include/Utilities.hlsl"
void GetDetailLOD
(
float Depth,
float DetailSizeA,
float LODJitter,
out float DetailSizeAA,
out float DetailSizeAB,
out float LODBlend
)
{
const float lodRatio = 4;
const float lodSize = DetailSizeA / lodRatio * 2;
float dist = max(Depth, 1e-5);
float lod = log(dist) / log(lodRatio) + LODJitter; // log2(dist);
float lodFloor = floor(lod);
float lodFrac = lod - lodFloor;
LODBlend = lodFrac * lodFrac * (3 - 2 * lodFrac);
DetailSizeAA = lodSize * pow(lodRatio, lodFloor);
DetailSizeAB = lodSize * pow(lodRatio, lodFloor + 1);
}
float4 SampleBombedTexture
(
UnityTexture2D Tex,
float2 UV
)
{
const float cellBlend = 4;
float2 cell = floor(UV);
float2 local = frac(UV);
float4 result = 0;
float weightSum = 0;
[unroll]
for (int y = 0; y < 2; y++)
{
[unroll]
for (int x = 0; x < 2; x++)
{
float2 currentCell = cell + float2(x, y);
float2 randomOffset = (Hash2D(currentCell) - .5) * .35;
float randomAngle = Hash1From2D(currentCell) * TWO_PI;
float2 center = float2(x, y) + randomOffset;
float2 delta = local - center;
float weight =
pow(saturate(1 - abs(delta.x)), cellBlend) *
pow(saturate(1 - abs(delta.y)), cellBlend);
float2 sampleUV = UV + randomOffset;
sampleUV = RotateUV
(
sampleUV,
currentCell + .5,
randomAngle
);
result += Tex.tex.Sample
(
Tex.samplerstate,
sampleUV
) * weight;
weightSum += weight;
}
}
return result / max(weightSum, 1e-5);
}
struct ProjectionData
{
float2 uv[7];
float mask[7];
};
ProjectionData GetProjections
(
float3 PositionOS,
float3 NormalOS,
float ProjectionBlend
)
{
ProjectionData data;
float3 normal = normalize(NormalOS);
float3 projectionNormals[7] =
{
normalize(float3(1, 1, 1)),
normalize(float3(-1, 1, 1)),
normalize(float3(1, -1, 1)),
normalize(float3(-1, -1, 1)),
float3(1, 0, 0),
float3(0, 1, 0),
float3(0, 0, 1)
};
float sharpness = lerp(2, 32, saturate(ProjectionBlend));
float maskSum = 0;
[unroll]
for (int i = 0; i < 7; i++)
{
float3 n = projectionNormals[i];
float d = abs(dot(normal, n));
data.mask[i] = pow(saturate(d), sharpness);
maskSum += data.mask[i];
float3 up = abs(n.y) < .999
? float3(0, 1, 0)
: float3(1, 0, 0);
float3 tangent = normalize(cross(up, n));
float3 bitangent = normalize(cross(n, tangent));
data.uv[i] = float2
(
dot(PositionOS, tangent),
dot(PositionOS, bitangent)
);
}
maskSum = max(maskSum, 1e-5);
[unroll]
for (int i = 0; i < 7; i++)
data.mask[i] /= maskSum;
return data;
}
float4 SampleProjectedDetailLOD
(
UnityTexture2D DetailTex,
float3 PositionOS,
float3 NormalOS,
float ProjectionBlend,
float DetailSizeA,
float Depth
)
{
const float lodJitterStrength = .8;
const float lodJitterSize = 1.5;
float4 detail = 0;
float totalMask = 0;
ProjectionData projections = GetProjections
(
PositionOS,
NormalOS,
ProjectionBlend
);
[unroll]
for (int i = 0; i < 7; i++)
{
float mask = projections.mask[i];
float lodJitter = (Hash1From2D(floor(projections.uv[i] * lodJitterSize) + i * 17.13) - .5) * lodJitterStrength;
float DetailSizeAA;
float DetailSizeAB;
float lodBlend;
GetDetailLOD
(
Depth,
DetailSizeA,
lodJitter,
DetailSizeAA,
DetailSizeAB,
lodBlend
);
float4 detailA = SampleBombedTexture
(
DetailTex,
projections.uv[i] / DetailSizeAA
);
float4 detailB = SampleBombedTexture
(
DetailTex,
projections.uv[i] / DetailSizeAB
);
detail += lerp(detailA, detailB, lodBlend) * mask;
totalMask += mask;
}
return detail / max(totalMask, 1e-5);
}
#endif
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a4dfd3fe541362f4f8143c626427c71f
ShaderIncludeImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: