opti : shaders rework test
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user