From 468d641e671fc53cdca596091d663689fa3c9be5 Mon Sep 17 00:00:00 2001 From: ook3d <47336113+ook3D@users.noreply.github.com> Date: Sat, 31 Jan 2026 13:23:25 -0500 Subject: [PATCH] add parallax support --- CodeWalker.Shaders/BasicPS.hlsl | 42 ++- CodeWalker.Shaders/BasicPS.hlsli | 5 + CodeWalker.Shaders/BasicPS_Deferred.hlsl | 27 +- CodeWalker.Shaders/Common.hlsli | 157 +++++++++++ CodeWalker.Shaders/Shadowmap.hlsli | 5 +- CodeWalker.Shaders/TerrainPS.hlsl | 243 ++++++++++++++++-- CodeWalker.Shaders/TerrainPS.hlsli | 10 +- CodeWalker.Shaders/TerrainPS_Deferred.hlsl | 210 +++++++++++++-- CodeWalker.Shaders/TerrainVS.hlsli | 2 + CodeWalker.Shaders/TerrainVS_PNCCT.hlsl | 2 + CodeWalker.Shaders/TerrainVS_PNCCTT.hlsl | 2 + CodeWalker.Shaders/TerrainVS_PNCCTTTX.hlsl | 2 + CodeWalker.Shaders/TerrainVS_PNCCTTX.hlsl | 2 + CodeWalker.Shaders/TerrainVS_PNCCTX.hlsl | 2 + CodeWalker.Shaders/TerrainVS_PNCTTTX.hlsl | 2 + CodeWalker.Shaders/TerrainVS_PNCTTX.hlsl | 2 + CodeWalker/Rendering/Renderable.cs | 40 +++ CodeWalker/Rendering/Shaders/BasicShader.cs | 34 +++ CodeWalker/Rendering/Shaders/TerrainShader.cs | 38 ++- Shaders/BasicPS.cso | Bin 11084 -> 22068 bytes Shaders/BasicPS_Deferred.cso | Bin 6744 -> 16224 bytes Shaders/TerrainPS.cso | Bin 13328 -> 26136 bytes Shaders/TerrainPS_Deferred.cso | Bin 9300 -> 18080 bytes Shaders/TerrainVS_PNCCT.cso | Bin 4912 -> 5064 bytes Shaders/TerrainVS_PNCCTT.cso | Bin 4936 -> 5088 bytes Shaders/TerrainVS_PNCCTTTX.cso | Bin 5508 -> 5648 bytes Shaders/TerrainVS_PNCCTTX.cso | Bin 5484 -> 5636 bytes Shaders/TerrainVS_PNCCTX.cso | Bin 5460 -> 5612 bytes Shaders/TerrainVS_PNCTTTX.cso | Bin 5484 -> 5624 bytes Shaders/TerrainVS_PNCTTX.cso | Bin 5460 -> 5612 bytes 30 files changed, 777 insertions(+), 50 deletions(-) diff --git a/CodeWalker.Shaders/BasicPS.hlsl b/CodeWalker.Shaders/BasicPS.hlsl index 1dfe4e83a..5567a1dfe 100644 --- a/CodeWalker.Shaders/BasicPS.hlsl +++ b/CodeWalker.Shaders/BasicPS.hlsl @@ -3,15 +3,43 @@ float4 main(VS_OUTPUT input) : SV_TARGET { + // Calculate parallax offset if height mapping is enabled + float2 parallaxTexOffset = float2(0, 0); + float parallaxSelfShadow = 1.0; + if (EnableHeightMap && RenderMode == 0) + { + float3 viewDir = -normalize(input.CamRelPos); // Negate to get direction FROM surface TO camera + float3 norm0 = normalize(input.Normal); + float3 tang0 = normalize(input.Tangent.xyz); + float3 bitang0 = normalize(input.Bitangent.xyz); + parallaxTexOffset = ParallaxOffset( + Heightmap, TextureSS, input.Texcoord0, + viewDir, norm0, tang0, bitang0, + heightScale, heightBias); + + // Parallax self-shadow, transform light dir to tangent space and trace + float3 tanLightDir; + tanLightDir.x = dot(tang0, GlobalLights.LightDir.xyz); + tanLightDir.y = dot(bitang0, GlobalLights.LightDir.xyz); + tanLightDir.z = dot(norm0, GlobalLights.LightDir.xyz); + float shadowAmount = TraceSelfShadow(Heightmap, TextureSS, + input.Texcoord0 + parallaxTexOffset, + tanLightDir, 1.0, heightScale); + parallaxSelfShadow = 1.0 - shadowAmount * PARALLAX_SELF_SHADOW_AMOUNT; + } + + // Apply parallax offset to base texture coordinates + float2 texc0 = input.Texcoord0 + parallaxTexOffset; + float4 c = float4(0.5, 0.5, 0.5, 1); if (RenderMode == 0) c = float4(1, 1, 1, 1); if (EnableTexture > 0) { - float2 texc = input.Texcoord0; + float2 texc = texc0; if (RenderMode >= 5) { - if (RenderSamplerCoord == 2) texc = input.Texcoord1; - else if (RenderSamplerCoord == 3) texc = input.Texcoord2; + if (RenderSamplerCoord == 2) texc = input.Texcoord1 + parallaxTexOffset; + else if (RenderSamplerCoord == 3) texc = input.Texcoord2 + parallaxTexOffset; } c = Colourmap.Sample(TextureSS, texc); @@ -80,8 +108,8 @@ float4 main(VS_OUTPUT input) : SV_TARGET if (RenderMode == 0) { - float4 nv = Bumpmap.Sample(TextureSS, input.Texcoord0); //sample r1.xyzw, v2.xyxx, t3.xyzw, s3 (BumpSampler) - float4 sv = Specmap.Sample(TextureSS, input.Texcoord0); //sample r2.xyzw, v2.xyxx, t4.xyzw, s4 (SpecSampler) + float4 nv = Bumpmap.Sample(TextureSS, texc0); //sample r1.xyzw, v2.xyxx, t3.xyzw, s3 (BumpSampler) + float4 sv = Specmap.Sample(TextureSS, texc0); //sample r2.xyzw, v2.xyxx, t4.xyzw, s4 (SpecSampler) float2 nmv = nv.xy; @@ -92,7 +120,7 @@ float4 main(VS_OUTPUT input) : SV_TARGET if (EnableDetailMap) { //detail normalmapp - r0.xy = input.Texcoord0 * detailSettings.zw; //mul r0.xy, v2.xyxx, detailSettings.zwzz + r0.xy = texc0 * detailSettings.zw; //mul r0.xy, v2.xyxx, detailSettings.zwzz r0.zw = r0.xy * 3.17; //mul r0.zw, r0.xxxy, l(0.000000, 0.000000, 3.170000, 3.170000) r0.xy = Detailmap.Sample(TextureSS, r0.xy).xy - 0.5; //sample r1.xyzw, r0.xyxx, t2.xyzw, s2 (DetailSampler) //mad r0.xy, r1.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), l(-1.000000, -1.000000, 0.000000, 0.000000) r0.zw = Detailmap.Sample(TextureSS, r0.zw).xy - 0.5; //sample r1.xyzw, r0.zwzz, t2.xyzw, s2 (DetailSampler) //mad r0.zw, r1.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) //r0.zw = r0.zw*0.5; //mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000) @@ -161,7 +189,7 @@ float4 main(VS_OUTPUT input) : SV_TARGET float4 fc = c; - c.rgb = FullLighting(c.rgb, spec, norm, input.Colour0, GlobalLights, EnableShadows, input.Shadows.x, input.LightShadow); + c.rgb = FullLighting(c.rgb, spec, norm, input.Colour0, GlobalLights, EnableShadows, input.Shadows.x, input.LightShadow, parallaxSelfShadow); if (IsEmissive==1) diff --git a/CodeWalker.Shaders/BasicPS.hlsli b/CodeWalker.Shaders/BasicPS.hlsli index 6b6b55964..c42fd9bda 100644 --- a/CodeWalker.Shaders/BasicPS.hlsli +++ b/CodeWalker.Shaders/BasicPS.hlsli @@ -6,6 +6,7 @@ Texture2D Specmap : register(t3); Texture2D Detailmap : register(t4); Texture2D Colourmap2 : register(t5); Texture2D TintPalette : register(t6); +Texture2D Heightmap : register(t7); SamplerState TextureSS : register(s0); @@ -39,6 +40,10 @@ cbuffer PSGeomVars : register(b2) float wetnessMultiplier; uint SpecOnly; float4 TextureAlphaMask; + uint EnableHeightMap; + float heightScale; + float heightBias; + float Pad0; } diff --git a/CodeWalker.Shaders/BasicPS_Deferred.hlsl b/CodeWalker.Shaders/BasicPS_Deferred.hlsl index bf6c7c395..d9bca27c1 100644 --- a/CodeWalker.Shaders/BasicPS_Deferred.hlsl +++ b/CodeWalker.Shaders/BasicPS_Deferred.hlsl @@ -3,17 +3,32 @@ PS_OUTPUT main(VS_OUTPUT input) { + // Calculate parallax offset if height mapping is enabled + float2 parallaxTexOffset = float2(0, 0); + if (EnableHeightMap && RenderMode == 0) + { + float3 viewDir = -normalize(input.CamRelPos); // Negate to get direction FROM surface TO camera + parallaxTexOffset = ParallaxOffset( + Heightmap, TextureSS, input.Texcoord0, + viewDir, normalize(input.Normal), + normalize(input.Tangent.xyz), normalize(input.Bitangent.xyz), + heightScale, heightBias); + } + + // Apply parallax offset to base texture coordinates + float2 texc0 = input.Texcoord0 + parallaxTexOffset; + float4 c = float4(0.5, 0.5, 0.5, 1); if (RenderMode == 0) c = float4(1, 1, 1, 1); if (EnableTexture > 0) { - float2 texc = input.Texcoord0; + float2 texc = texc0; if (RenderMode >= 5) { if (RenderSamplerCoord == 2) - texc = input.Texcoord1; + texc = input.Texcoord1 + parallaxTexOffset; else if (RenderSamplerCoord == 3) - texc = input.Texcoord2; + texc = input.Texcoord2 + parallaxTexOffset; } c = Colourmap.Sample(TextureSS, texc); @@ -85,8 +100,8 @@ PS_OUTPUT main(VS_OUTPUT input) if (RenderMode == 0) { - float4 nv = Bumpmap.Sample(TextureSS, input.Texcoord0); - float4 sv = Specmap.Sample(TextureSS, input.Texcoord0); + float4 nv = Bumpmap.Sample(TextureSS, texc0); + float4 sv = Specmap.Sample(TextureSS, texc0); float2 nmv = nv.xy; @@ -97,7 +112,7 @@ PS_OUTPUT main(VS_OUTPUT input) if (EnableDetailMap) { //detail normalmapp - r0.xy = input.Texcoord0 * detailSettings.zw; + r0.xy = texc0 * detailSettings.zw; r0.zw = r0.xy * 3.17; r0.xy = Detailmap.Sample(TextureSS, r0.xy).xy - 0.5; r0.zw = Detailmap.Sample(TextureSS, r0.zw).xy - 0.5; diff --git a/CodeWalker.Shaders/Common.hlsli b/CodeWalker.Shaders/Common.hlsli index 8f4747e2a..726fb2082 100644 --- a/CodeWalker.Shaders/Common.hlsli +++ b/CodeWalker.Shaders/Common.hlsli @@ -119,6 +119,163 @@ float3 NormalMap(float2 nmv, float bumpinezz, float3 norm, float3 tang, float3 b } +// POM constants +#define POM_MIN_STEPS 3 +#define POM_MAX_STEPS 16 +#define POM_VDOTN_BLEND_FACTOR 0.25f +#define POM_HEIGHT_SCALE 0.1f // Global parallax strength multiplier (1.0 = full, 0.5 = half) + +// Distance-based POM fade constants (reduces noise at steep angles/distance) +#define POM_DISTANCE_START 5.0f // Distance where fade begins +#define POM_DISTANCE_END 50.0f // Distance where POM is fully disabled + +// Binary search refinement for more precise intersection (reduces ring artifacts at close range) +#define POM_BINARY_SEARCH_STEPS 5 // Number of binary search iterations after linear search +#define POM_CLOSE_DISTANCE 2.0f // Distance threshold for close-range step boost +#define POM_CLOSE_STEP_MULTIPLIER 2.0f // Step multiplier when very close to surface + +// Distance fade lookup table (5 control points for smooth non-linear falloff) +// Based on GTA V pomWeights table +#define NUM_POM_CTRL_POINTS 5 +static const float pomWeights[NUM_POM_CTRL_POINTS] = { + 1.0f, // Full quality at close range + 0.9f, + 0.5f, // 50% at mid distance + 0.1f, + 0.0f // Disabled at far distance +}; + +// Compute smooth distance-based fade for POM steps +float ComputePOMDistanceFade(float distanceBlend) +{ + if (distanceBlend >= 1.0f) + return 0.0f; + + // Find the nearest control points and interpolate + int startPoint = clamp(int(distanceBlend * (NUM_POM_CTRL_POINTS - 1)), 0, NUM_POM_CTRL_POINTS - 2); + int endPoint = startPoint + 1; + + float t = distanceBlend * (NUM_POM_CTRL_POINTS - 1) - float(startPoint); + return lerp(pomWeights[startPoint], pomWeights[endPoint], t); +} + +// Performs relief mapping by tracing through the height field +float TraceHeight(Texture2D heightMapSampler, SamplerState samplerState, float2 texCoords, float2 direction, float2 bias, int maxNumberOfSteps) +{ + if (maxNumberOfSteps == 0) + { + return 0.0f; + } + + float heightStep = 1.0f / float(maxNumberOfSteps); + float2 offsetPerStep = direction * heightStep; + + float currentBound = 1.0f; + float previousBound = currentBound; + + float2 texCoordOffset = bias; + + // Use derivatives for proper mip selection to avoid aliasing + float2 ddx0 = ddx(texCoords.xy); + float2 ddy0 = ddy(texCoords.xy); + + float currentHeight = heightMapSampler.SampleGrad(samplerState, texCoords.xy, ddx0, ddy0).r + 1e-6f; + float previousHeight = currentHeight; + + [unroll(POM_MAX_STEPS)] + for (int s = 0; s < maxNumberOfSteps; ++s) + { + if (currentHeight < currentBound) + { + previousBound = currentBound; + previousHeight = currentHeight; + + currentBound -= heightStep; + texCoordOffset += offsetPerStep; + currentHeight = heightMapSampler.SampleGrad(samplerState, texCoords + texCoordOffset, ddx0, ddy0).r; + } + else + { + break; + } + } + + // Interpolate between the two points to find a more precise height + float currentDelta = currentBound - currentHeight; + float previousDelta = previousBound - previousHeight; + float denominator = previousDelta - currentDelta; + + float finalHeight = currentHeight; + + if (denominator > 0) + { + finalHeight = ((currentBound * previousDelta) - (previousBound * currentDelta)) / denominator; + } + + return clamp(finalHeight, 0.0, 1.0f); +} + +// Parallax self-shadow: traces through heightmap in light direction to find occlusion +// Returns shadow factor (0 = fully lit, 1 = fully in shadow) +float TraceSelfShadow(Texture2D heightMapSampler, SamplerState samplerState, float2 texCoords, float3 tanLightDir, float edgeWeight, float hScale) +{ + float2 inXY = (tanLightDir.xy * hScale * edgeWeight) / max(tanLightDir.z, 0.01f); + + // Sample base height at current (displaced) position + float sh0 = heightMapSampler.SampleLevel(samplerState, texCoords, 0).r; + + // Trace 7 samples along light direction with increasing weight for closer occlusion + float shA = (heightMapSampler.SampleLevel(samplerState, texCoords + inXY * 0.88, 0).r - sh0 - 0.88) * 1; + float sh9 = (heightMapSampler.SampleLevel(samplerState, texCoords + inXY * 0.77, 0).r - sh0 - 0.77) * 2; + float sh8 = (heightMapSampler.SampleLevel(samplerState, texCoords + inXY * 0.66, 0).r - sh0 - 0.66) * 4; + float sh7 = (heightMapSampler.SampleLevel(samplerState, texCoords + inXY * 0.55, 0).r - sh0 - 0.55) * 6; + float sh6 = (heightMapSampler.SampleLevel(samplerState, texCoords + inXY * 0.44, 0).r - sh0 - 0.44) * 8; + float sh5 = (heightMapSampler.SampleLevel(samplerState, texCoords + inXY * 0.33, 0).r - sh0 - 0.33) * 10; + float sh4 = (heightMapSampler.SampleLevel(samplerState, texCoords + inXY * 0.22, 0).r - sh0 - 0.22) * 12; + + float finalHeight = max(max(max(max(max(max(shA, sh9), sh8), sh7), sh6), sh5), sh4); + return saturate(finalHeight); +} + +#define PARALLAX_SELF_SHADOW_AMOUNT 0.95f + +// Calculate parallax texture coordinate offset +float2 ParallaxOffset(Texture2D heightMapSampler, SamplerState samplerState, float2 texCoords, + float3 viewDir, float3 normal, float3 tangent, float3 bitangent, + float inHeightScale, float inHeightBias) +{ + // Transform view direction to tangent space + float3 tanEyePos; + tanEyePos.x = dot(tangent.xyz, viewDir.xyz); + tanEyePos.y = dot(bitangent.xyz, viewDir.xyz); + tanEyePos.z = dot(normal.xyz, viewDir.xyz); + tanEyePos = normalize(tanEyePos); + + // Clamp Z to avoid division issues at grazing angles + float zLimit = 0.1f; + float clampedZ = max(zLimit, tanEyePos.z); + + // Calculate view-dependent step count for quality/performance balance + float VdotN = abs(dot(normalize(viewDir.xyz), normalize(normal.xyz))); + float numberOfSteps = lerp(POM_MAX_STEPS, POM_MIN_STEPS, VdotN); + + // Apply global scale based on view angle for smooth falloff + float globalScale = saturate(numberOfSteps - 1.0f) * saturate(VdotN / POM_VDOTN_BLEND_FACTOR); + + // Calculate max parallax offset and bias offset + float2 maxParallaxOffset = (-tanEyePos.xy / clampedZ) * inHeightScale * globalScale; + float2 heightBiasOffset = (tanEyePos.xy / clampedZ) * inHeightBias * globalScale; + + // Trace through height field + float height = TraceHeight(heightMapSampler, samplerState, texCoords, maxParallaxOffset, heightBiasOffset, (int)numberOfSteps); + + // Calculate final texture coordinate offset + float2 texCoordOffset = heightBiasOffset + (maxParallaxOffset * (1.0f - height)); + + return texCoordOffset; +} + + float3 BasicLighting(float4 lightcolour, float4 ambcolour, float pclit) diff --git a/CodeWalker.Shaders/Shadowmap.hlsli b/CodeWalker.Shaders/Shadowmap.hlsli index 1dba4edb1..92c61557a 100644 --- a/CodeWalker.Shaders/Shadowmap.hlsli +++ b/CodeWalker.Shaders/Shadowmap.hlsli @@ -225,7 +225,7 @@ float ShadowAmount(float4 shadowcoord, float shadowdepth)//, inout float4 colour -float3 FullLighting(float3 diff, float3 spec, float3 norm, float4 vc0, uniform ShaderGlobalLightParams globalLights, uint enableShadows, float shadowdepth, float4 shadowcoord) +float3 FullLighting(float3 diff, float3 spec, float3 norm, float4 vc0, uniform ShaderGlobalLightParams globalLights, uint enableShadows, float shadowdepth, float4 shadowcoord, float selfShadow = 1.0) { float lf = saturate(dot(norm, globalLights.LightDir.xyz)); @@ -241,6 +241,9 @@ float3 FullLighting(float3 diff, float3 spec, float3 norm, float4 vc0, uniform S } } + // Apply parallax self-shadow to cascade shadow + shadowlit *= selfShadow; + lf *= shadowlit; float3 speclit = spec*shadowlit; diff --git a/CodeWalker.Shaders/TerrainPS.hlsl b/CodeWalker.Shaders/TerrainPS.hlsl index d11a6cdcf..91f1b21d0 100644 --- a/CodeWalker.Shaders/TerrainPS.hlsl +++ b/CodeWalker.Shaders/TerrainPS.hlsl @@ -1,6 +1,193 @@ #include "TerrainPS.hlsli" +// Sample and blend terrain height maps based on layer weights +// Returns inverted height (1.0 - sample) for correct POM ray marching +float BlendTerrainHeight(float4 layerBlends, float2 texCoord) +{ + float result = 0.0f; + result += layerBlends.x * Heightmap0.SampleLevel(TextureSS, texCoord, 0).r; + result += layerBlends.y * Heightmap1.SampleLevel(TextureSS, texCoord, 0).r; + result += layerBlends.z * Heightmap2.SampleLevel(TextureSS, texCoord, 0).r; + result += layerBlends.w * Heightmap3.SampleLevel(TextureSS, texCoord, 0).r; + // Invert height: GTA V height maps use 1.0=raised, 0.0=base + // POM ray march expects 0.0=raised (hit early), 1.0=base (hit late) + return 1.0f - result; +} + +// Get blended height scale and bias based on layer weights +float2 GetBlendedScaleBias(float4 layerBlends) +{ + float2 result = float2(0.0f, 0.0f); + result += layerBlends.x * float2(heightScale.x, heightBias.x); + result += layerBlends.y * float2(heightScale.y, heightBias.y); + result += layerBlends.z * float2(heightScale.z, heightBias.z); + result += layerBlends.w * float2(heightScale.w, heightBias.w); + return result; +} + +// Calculate terrain parallax offset using blended heights +// Includes distance-based fade and vertex edge weight to reduce noise at steep angles and mesh edges +float2 CalculateTerrainParallaxOffset(float4 layerBlends, float2 texCoord, float3 viewDir, float3 normal, float3 tangent, float3 bitangent, float viewDistance, float2 edgeWeightData) +{ + // Get blended scale and bias + float2 scaleBias = GetBlendedScaleBias(layerBlends); + float hScale = scaleBias.x * POM_HEIGHT_SCALE; // Apply global strength multiplier + float hBias = scaleBias.y * POM_HEIGHT_SCALE; + + if (hScale == 0.0f) + return float2(0.0f, 0.0f); + + // Vertex edge weight from mesh data + // edgeWeightData.x: 0 = full POM, 1 = no POM (at mesh edges) + // edgeWeightData.y: controls dynamic zLimit adjustment + float vertexEdgeWeight = 1.0f - saturate(edgeWeightData.x); + + // Early out if vertex says no POM at this point + if (vertexEdgeWeight <= 0.0f) + return float2(0.0f, 0.0f); + + // Transform view direction to tangent space + float3 tanEyePos; + tanEyePos.x = dot(tangent.xyz, viewDir.xyz); + tanEyePos.y = dot(bitangent.xyz, viewDir.xyz); + tanEyePos.z = dot(normal.xyz, viewDir.xyz); + tanEyePos = normalize(tanEyePos); + + // Dynamic zLimit from vertex edge weight + // Higher edgeWeightData.y = lower zLimit = allow steeper angles + float zLimit = 1.0f - clamp(edgeWeightData.y, 0.1f, 1.0f); + zLimit = max(zLimit, 0.1f); // Ensure minimum zLimit + float clampedZ = max(zLimit, tanEyePos.z); + + // Calculate view-dependent step count + float VdotN = abs(dot(normalize(viewDir.xyz), normalize(normal.xyz))); + float numberOfSteps = lerp(POM_MAX_STEPS, POM_MIN_STEPS, VdotN); + + // Close-range step boost - increase precision when very close to surface (reduces ring artifacts) + float closeBoost = saturate(1.0f - viewDistance / POM_CLOSE_DISTANCE); + numberOfSteps *= lerp(1.0f, POM_CLOSE_STEP_MULTIPLIER, closeBoost); + + // Distance-based fade - reduces noise at steep angles/far distances + float distanceBlend = saturate((viewDistance - POM_DISTANCE_START) / (POM_DISTANCE_END - POM_DISTANCE_START)); + float distanceFade = ComputePOMDistanceFade(distanceBlend); + + // Reduce steps over distance - artifacts become less noticeable at distance + numberOfSteps *= distanceFade; + + // Early out if steps reduced to nearly zero + if (numberOfSteps < 1.0f) + return float2(0.0f, 0.0f); + + // Calculate weight distance blend for smooth fade-out near zero steps + float scaleOutRange = (POM_DISTANCE_END - POM_DISTANCE_START) * 0.35f; + float weightDistanceBlend = saturate(((viewDistance - POM_DISTANCE_START) - (POM_DISTANCE_END - POM_DISTANCE_START) + scaleOutRange) / scaleOutRange); + + // Combined edge weight: vertex edge * view angle fade * step count fade * distance fade + float edgeWeight = vertexEdgeWeight * (1.0f - weightDistanceBlend); + edgeWeight *= saturate(numberOfSteps - 1.0f) * saturate(VdotN / POM_VDOTN_BLEND_FACTOR); + + // Apply combined scale + float globalScale = edgeWeight; + + float2 maxParallaxOffset = (-tanEyePos.xy / clampedZ) * hScale * globalScale; + float2 heightBiasOffset = (tanEyePos.xy / clampedZ) * hBias * globalScale; + + float heightStep = 1.0f / max(numberOfSteps, 1.0f); + float2 offsetPerStep = maxParallaxOffset * heightStep; + + float currentHeight = 1.0f; + float previousHeight = currentHeight; + + float2 texCoordOffset = heightBiasOffset; + + float terrainHeight = BlendTerrainHeight(layerBlends, texCoord) + 1e-6f; + float previousTerrainHeight = terrainHeight; + + // Ray march through the height field + int maxSteps = (int)numberOfSteps; + for (int i = 0; i < maxSteps; ++i) + { + if (terrainHeight < currentHeight) + { + previousHeight = currentHeight; + previousTerrainHeight = terrainHeight; + + currentHeight -= heightStep; + texCoordOffset += offsetPerStep; + terrainHeight = BlendTerrainHeight(layerBlends, texCoord + texCoordOffset); + } + else + { + break; + } + } + + // Binary search refinement for more precise intersection (reduces ring artifacts at close range) + float2 prevOffset = texCoordOffset - offsetPerStep; + float2 currOffset = texCoordOffset; + float prevHeight = previousHeight; + float currHeight = currentHeight; + + [unroll(POM_BINARY_SEARCH_STEPS)] + for (int j = 0; j < POM_BINARY_SEARCH_STEPS; ++j) + { + float2 midOffset = (prevOffset + currOffset) * 0.5f; + float midHeight = (prevHeight + currHeight) * 0.5f; + float midTerrainHeight = BlendTerrainHeight(layerBlends, texCoord + midOffset); + + if (midTerrainHeight < midHeight) + { + // Intersection is in second half + prevOffset = midOffset; + prevHeight = midHeight; + } + else + { + // Intersection is in first half + currOffset = midOffset; + currHeight = midHeight; + } + } + + // Final interpolation between the refined bracket + float finalTerrainHeight = BlendTerrainHeight(layerBlends, texCoord + currOffset); + float currentDelta = currHeight - finalTerrainHeight; + float previousDelta = prevHeight - BlendTerrainHeight(layerBlends, texCoord + prevOffset); + float denominator = previousDelta - currentDelta; + + float refinedHeight = 1.0f; + if (abs(denominator) > 1e-6f) + { + refinedHeight = (currHeight * previousDelta - prevHeight * currentDelta) / denominator; + } + else + { + refinedHeight = 1.0f - (currOffset.x / maxParallaxOffset.x); + } + + return heightBiasOffset + (maxParallaxOffset * (1.0f - saturate(refinedHeight))); +} + +// Terrain-specific self-shadow using blended heightmaps +float TraceTerrainSelfShadow(float4 layerBlends, float2 texCoords, float3 tanLightDir, float edgeWeight, float hScale) +{ + float2 inXY = (tanLightDir.xy * hScale * edgeWeight) / max(tanLightDir.z, 0.01f); + + float sh0 = 1.0f - BlendTerrainHeight(layerBlends, texCoords); + + float shA = (1.0f - BlendTerrainHeight(layerBlends, texCoords + inXY * 0.88) - sh0 - 0.88) * 1; + float sh9 = (1.0f - BlendTerrainHeight(layerBlends, texCoords + inXY * 0.77) - sh0 - 0.77) * 2; + float sh8 = (1.0f - BlendTerrainHeight(layerBlends, texCoords + inXY * 0.66) - sh0 - 0.66) * 4; + float sh7 = (1.0f - BlendTerrainHeight(layerBlends, texCoords + inXY * 0.55) - sh0 - 0.55) * 6; + float sh6 = (1.0f - BlendTerrainHeight(layerBlends, texCoords + inXY * 0.44) - sh0 - 0.44) * 8; + float sh5 = (1.0f - BlendTerrainHeight(layerBlends, texCoords + inXY * 0.33) - sh0 - 0.33) * 10; + float sh4 = (1.0f - BlendTerrainHeight(layerBlends, texCoords + inXY * 0.22) - sh0 - 0.22) * 12; + + float finalHeight = max(max(max(max(max(max(shA, sh9), sh8), sh7), sh6), sh5), sh4); + return saturate(finalHeight); +} + float4 main(VS_OUTPUT input) : SV_TARGET { float4 vc0 = input.Colour0; @@ -16,24 +203,46 @@ float4 main(VS_OUTPUT input) : SV_TARGET float2 sc4 = tc0; float2 scm = tc1; - ////switch (ShaderName) - ////{ - //// case 3965214311: //terrain_cb_w_4lyr_cm_pxm_tnt vt: PNCTTTX_3 //vb_35_beache - //// case 4186046662: //terrain_cb_w_4lyr_cm_pxm vt: PNCTTTX_3 //cs6_08_struct08 - //// //vc1 = vc0; - //// //sc1 = tc0*25; - //// //sc2 = sc1; - //// //sc3 = sc1; - //// //sc4 = sc1; - //// //scm = tc0; - //// break; - ////} + // Calculate layer blend weights from vertex colors (4-layer blending) + // Layer weights: x=(1-g)*(1-b), y=(1-g)*b, z=g*(1-b), w=g*b + float4 layerBlends; + layerBlends.x = (1.0f - vc1.g) * (1.0f - vc1.b); + layerBlends.y = (1.0f - vc1.g) * vc1.b; + layerBlends.z = vc1.g * (1.0f - vc1.b); + layerBlends.w = vc1.g * vc1.b; + + // Calculate single parallax offset using blended heights + float parallaxSelfShadow = 1.0; + if (EnableHeightMap && RenderMode == 0) + { + float3 viewDir = -normalize(input.CamRelPos); // Negate to get direction FROM surface TO camera + float3 norm = normalize(input.Normal); + float3 tang = normalize(input.Tangent.xyz); + float3 bitang = normalize(input.Bitangent.xyz); + + // Calculate single offset from blended height values (with distance fade and edge weight) + float2 parallaxOffset = CalculateTerrainParallaxOffset(layerBlends, tc0, viewDir, norm, tang, bitang, input.ViewDistance, input.EdgeWeight); + + // Apply same offset to all texture coordinates + sc0 += parallaxOffset; + sc1 += parallaxOffset; + sc2 += parallaxOffset; + sc3 += parallaxOffset; + sc4 += parallaxOffset; + + // Parallax self-shadow, transform light dir to tangent space and trace blended heights + float3 tanLightDir; + tanLightDir.x = dot(tang, GlobalLights.LightDir.xyz); + tanLightDir.y = dot(bitang, GlobalLights.LightDir.xyz); + tanLightDir.z = dot(norm, GlobalLights.LightDir.xyz); + float2 blendedScaleBias = GetBlendedScaleBias(layerBlends); + float blendedHScale = blendedScaleBias.x * POM_HEIGHT_SCALE; + float edgeWeight = 1.0f - saturate(input.EdgeWeight.x); + float shadowAmount = TraceTerrainSelfShadow(layerBlends, sc0, tanLightDir, edgeWeight, blendedHScale); + parallaxSelfShadow = 1.0 - shadowAmount * PARALLAX_SELF_SHADOW_AMOUNT; + } float4 bc0 = float4(0.5, 0.5, 0.5, 1); - //if (EnableVertexColour) - //{ - // bc0 = vc0; - //} if (RenderMode == 8) //direct texture - choose texcoords { @@ -214,7 +423,7 @@ float4 main(VS_OUTPUT input) : SV_TARGET float3 spec = 0; - tv.rgb = FullLighting(tv.rgb, spec, norm, vc0, GlobalLights, EnableShadows, input.Shadows.x, input.LightShadow); + tv.rgb = FullLighting(tv.rgb, spec, norm, vc0, GlobalLights, EnableShadows, input.Shadows.x, input.LightShadow, parallaxSelfShadow); return float4(tv.rgb, saturate(tv.a)); diff --git a/CodeWalker.Shaders/TerrainPS.hlsli b/CodeWalker.Shaders/TerrainPS.hlsli index b8cefdd4a..d9503c340 100644 --- a/CodeWalker.Shaders/TerrainPS.hlsli +++ b/CodeWalker.Shaders/TerrainPS.hlsli @@ -11,6 +11,10 @@ Texture2D Normalmap1 : register(t8); Texture2D Normalmap2 : register(t9); Texture2D Normalmap3 : register(t10); Texture2D Normalmap4 : register(t11); +Texture2D Heightmap0 : register(t12); +Texture2D Heightmap1 : register(t13); +Texture2D Heightmap2 : register(t14); +Texture2D Heightmap3 : register(t15); SamplerState TextureSS : register(s0); @@ -35,7 +39,9 @@ cbuffer PSEntityVars : register(b2) uint EnableTint; uint EnableVertexColour; float bumpiness; - uint Pad102; + uint EnableHeightMap; + float4 heightScale; // x=layer0, y=layer1, z=layer2, w=layer3 + float4 heightBias; // x=layer0, y=layer1, z=layer2, w=layer3 } @@ -54,6 +60,8 @@ struct VS_OUTPUT float4 Tangent : TEXCOORD6; float4 Bitangent : TEXCOORD7; float3 CamRelPos : TEXCOORD8; + float ViewDistance : TEXCOORD9; // Distance from camera for POM fade + float2 EdgeWeight : TEXCOORD10; // x=POM edge fade, y=zLimit adjustment (GTA V style) }; struct PS_OUTPUT diff --git a/CodeWalker.Shaders/TerrainPS_Deferred.hlsl b/CodeWalker.Shaders/TerrainPS_Deferred.hlsl index aeb1ab3a6..766c58c3f 100644 --- a/CodeWalker.Shaders/TerrainPS_Deferred.hlsl +++ b/CodeWalker.Shaders/TerrainPS_Deferred.hlsl @@ -1,6 +1,174 @@ #include "TerrainPS.hlsli" +// Sample and blend terrain height maps based on layer weights +// Returns inverted height (1.0 - sample) for correct POM ray marching +float BlendTerrainHeight(float4 layerBlends, float2 texCoord) +{ + float result = 0.0f; + result += layerBlends.x * Heightmap0.SampleLevel(TextureSS, texCoord, 0).r; + result += layerBlends.y * Heightmap1.SampleLevel(TextureSS, texCoord, 0).r; + result += layerBlends.z * Heightmap2.SampleLevel(TextureSS, texCoord, 0).r; + result += layerBlends.w * Heightmap3.SampleLevel(TextureSS, texCoord, 0).r; + // height maps use 1.0=raised, 0.0=base + // POM ray march expects 0.0=raised (hit early), 1.0=base (hit late) + return 1.0f - result; +} + +// Get blended height scale and bias based on layer weights +float2 GetBlendedScaleBias(float4 layerBlends) +{ + float2 result = float2(0.0f, 0.0f); + result += layerBlends.x * float2(heightScale.x, heightBias.x); + result += layerBlends.y * float2(heightScale.y, heightBias.y); + result += layerBlends.z * float2(heightScale.z, heightBias.z); + result += layerBlends.w * float2(heightScale.w, heightBias.w); + return result; +} + +// Calculate terrain parallax offset using blended heights +// Includes distance-based fade and vertex edge weight to reduce noise at steep angles and mesh edges +float2 CalculateTerrainParallaxOffset(float4 layerBlends, float2 texCoord, float3 viewDir, float3 normal, float3 tangent, float3 bitangent, float viewDistance, float2 edgeWeightData) +{ + // Get blended scale and bias + float2 scaleBias = GetBlendedScaleBias(layerBlends); + float hScale = scaleBias.x * POM_HEIGHT_SCALE; // Apply global strength multiplier + float hBias = scaleBias.y * POM_HEIGHT_SCALE; + + if (hScale == 0.0f) + return float2(0.0f, 0.0f); + + // Vertex edge weight from mesh data + // edgeWeightData.x: 0 = full POM, 1 = no POM (at mesh edges) + // edgeWeightData.y: controls dynamic zLimit adjustment + float vertexEdgeWeight = 1.0f - saturate(edgeWeightData.x); + + // Early out if vertex says no POM at this point + if (vertexEdgeWeight <= 0.0f) + return float2(0.0f, 0.0f); + + // Transform view direction to tangent space + float3 tanEyePos; + tanEyePos.x = dot(tangent.xyz, viewDir.xyz); + tanEyePos.y = dot(bitangent.xyz, viewDir.xyz); + tanEyePos.z = dot(normal.xyz, viewDir.xyz); + tanEyePos = normalize(tanEyePos); + + // Dynamic zLimit from vertex edge weight (GTA V style) + // Higher edgeWeightData.y = lower zLimit = allow steeper angles + float zLimit = 1.0f - clamp(edgeWeightData.y, 0.1f, 1.0f); + zLimit = max(zLimit, 0.1f); // Ensure minimum zLimit + float clampedZ = max(zLimit, tanEyePos.z); + + // Calculate view-dependent step count + float VdotN = abs(dot(normalize(viewDir.xyz), normalize(normal.xyz))); + float numberOfSteps = lerp(POM_MAX_STEPS, POM_MIN_STEPS, VdotN); + + // Close-range step boost - increase precision when very close to surface (reduces ring artifacts) + float closeBoost = saturate(1.0f - viewDistance / POM_CLOSE_DISTANCE); + numberOfSteps *= lerp(1.0f, POM_CLOSE_STEP_MULTIPLIER, closeBoost); + + // Distance-based fade (GTA V style) - reduces noise at steep angles/far distances + float distanceBlend = saturate((viewDistance - POM_DISTANCE_START) / (POM_DISTANCE_END - POM_DISTANCE_START)); + float distanceFade = ComputePOMDistanceFade(distanceBlend); + + // Reduce steps over distance - artifacts become less noticeable at distance + numberOfSteps *= distanceFade; + + // Early out if steps reduced to nearly zero + if (numberOfSteps < 1.0f) + return float2(0.0f, 0.0f); + + // Calculate weight distance blend for smooth fade-out near zero steps + float scaleOutRange = (POM_DISTANCE_END - POM_DISTANCE_START) * 0.35f; + float weightDistanceBlend = saturate(((viewDistance - POM_DISTANCE_START) - (POM_DISTANCE_END - POM_DISTANCE_START) + scaleOutRange) / scaleOutRange); + + // Combined edge weight: vertex edge * view angle fade * step count fade * distance fade + float edgeWeight = vertexEdgeWeight * (1.0f - weightDistanceBlend); + edgeWeight *= saturate(numberOfSteps - 1.0f) * saturate(VdotN / POM_VDOTN_BLEND_FACTOR); + + // Apply combined scale + float globalScale = edgeWeight; + + float2 maxParallaxOffset = (-tanEyePos.xy / clampedZ) * hScale * globalScale; + float2 heightBiasOffset = (tanEyePos.xy / clampedZ) * hBias * globalScale; + + float heightStep = 1.0f / max(numberOfSteps, 1.0f); + float2 offsetPerStep = maxParallaxOffset * heightStep; + + float currentHeight = 1.0f; + float previousHeight = currentHeight; + + float2 texCoordOffset = heightBiasOffset; + + float terrainHeight = BlendTerrainHeight(layerBlends, texCoord) + 1e-6f; + float previousTerrainHeight = terrainHeight; + + // Ray march through the height field + int maxSteps = (int)numberOfSteps; + for (int i = 0; i < maxSteps; ++i) + { + if (terrainHeight < currentHeight) + { + previousHeight = currentHeight; + previousTerrainHeight = terrainHeight; + + currentHeight -= heightStep; + texCoordOffset += offsetPerStep; + terrainHeight = BlendTerrainHeight(layerBlends, texCoord + texCoordOffset); + } + else + { + break; + } + } + + // Binary search refinement for more precise intersection (reduces ring artifacts at close range) + float2 prevOffset = texCoordOffset - offsetPerStep; + float2 currOffset = texCoordOffset; + float prevHeight = previousHeight; + float currHeight = currentHeight; + + [unroll(POM_BINARY_SEARCH_STEPS)] + for (int j = 0; j < POM_BINARY_SEARCH_STEPS; ++j) + { + float2 midOffset = (prevOffset + currOffset) * 0.5f; + float midHeight = (prevHeight + currHeight) * 0.5f; + float midTerrainHeight = BlendTerrainHeight(layerBlends, texCoord + midOffset); + + if (midTerrainHeight < midHeight) + { + // Intersection is in second half + prevOffset = midOffset; + prevHeight = midHeight; + } + else + { + // Intersection is in first half + currOffset = midOffset; + currHeight = midHeight; + } + } + + // Final interpolation between the refined bracket + float finalTerrainHeight = BlendTerrainHeight(layerBlends, texCoord + currOffset); + float currentDelta = currHeight - finalTerrainHeight; + float previousDelta = prevHeight - BlendTerrainHeight(layerBlends, texCoord + prevOffset); + float denominator = previousDelta - currentDelta; + + float refinedHeight = 1.0f; + if (abs(denominator) > 1e-6f) + { + refinedHeight = (currHeight * previousDelta - prevHeight * currentDelta) / denominator; + } + else + { + refinedHeight = 1.0f - (currOffset.x / maxParallaxOffset.x); + } + + return heightBiasOffset + (maxParallaxOffset * (1.0f - saturate(refinedHeight))); +} + PS_OUTPUT main(VS_OUTPUT input) { float4 vc0 = input.Colour0; @@ -16,24 +184,34 @@ PS_OUTPUT main(VS_OUTPUT input) float2 sc4 = tc0; float2 scm = tc1; - ////switch (ShaderName) - ////{ - //// case 3965214311: //terrain_cb_w_4lyr_cm_pxm_tnt vt: PNCTTTX_3 //vb_35_beache - //// case 4186046662: //terrain_cb_w_4lyr_cm_pxm vt: PNCTTTX_3 //cs6_08_struct08 - //// //vc1 = vc0; - //// //sc1 = tc0*25; - //// //sc2 = sc1; - //// //sc3 = sc1; - //// //sc4 = sc1; - //// //scm = tc0; - //// break; - ////} + // Calculate layer blend weights from vertex colors (4-layer blending) + // Layer weights: x=(1-g)*(1-b), y=(1-g)*b, z=g*(1-b), w=g*b + float4 layerBlends; + layerBlends.x = (1.0f - vc1.g) * (1.0f - vc1.b); + layerBlends.y = (1.0f - vc1.g) * vc1.b; + layerBlends.z = vc1.g * (1.0f - vc1.b); + layerBlends.w = vc1.g * vc1.b; + + // Calculate single parallax offset using blended heights (GTA V approach) + if (EnableHeightMap && RenderMode == 0) + { + float3 viewDir = -normalize(input.CamRelPos); // Negate to get direction FROM surface TO camera + float3 norm = normalize(input.Normal); + float3 tang = normalize(input.Tangent.xyz); + float3 bitang = normalize(input.Bitangent.xyz); + + // Calculate single offset from blended height values (with distance fade and edge weight) + float2 parallaxOffset = CalculateTerrainParallaxOffset(layerBlends, tc0, viewDir, norm, tang, bitang, input.ViewDistance, input.EdgeWeight); + + // Apply same offset to all texture coordinates + sc0 += parallaxOffset; + sc1 += parallaxOffset; + sc2 += parallaxOffset; + sc3 += parallaxOffset; + sc4 += parallaxOffset; + } float4 bc0 = float4(0.5, 0.5, 0.5, 1); - //if (EnableVertexColour) - //{ - // bc0 = vc0; - //} if (RenderMode == 8) //direct texture - choose texcoords { diff --git a/CodeWalker.Shaders/TerrainVS.hlsli b/CodeWalker.Shaders/TerrainVS.hlsli index 454760035..d69409643 100644 --- a/CodeWalker.Shaders/TerrainVS.hlsli +++ b/CodeWalker.Shaders/TerrainVS.hlsli @@ -46,6 +46,8 @@ struct VS_OUTPUT float4 Tangent : TEXCOORD6; float4 Bitangent : TEXCOORD7; float3 CamRelPos : TEXCOORD8; + float ViewDistance : TEXCOORD9; // Distance from camera for POM fade + float2 EdgeWeight : TEXCOORD10; // x=POM edge fade, y=zLimit adjustment (GTA V style) }; Texture2D TintPalette : register(t0); diff --git a/CodeWalker.Shaders/TerrainVS_PNCCT.hlsl b/CodeWalker.Shaders/TerrainVS_PNCCT.hlsl index af7ee7b2f..711e84c1b 100644 --- a/CodeWalker.Shaders/TerrainVS_PNCCT.hlsl +++ b/CodeWalker.Shaders/TerrainVS_PNCCT.hlsl @@ -35,5 +35,7 @@ VS_OUTPUT main(VS_INPUT input) output.Tint = tnt; output.Tangent = float4(btang, 0); output.Bitangent = float4(cross(btang, bnorm), 0); + output.ViewDistance = length(opos); + output.EdgeWeight = float2(0, 0.9); // Default: full POM, zLimit=0.1 return output; } diff --git a/CodeWalker.Shaders/TerrainVS_PNCCTT.hlsl b/CodeWalker.Shaders/TerrainVS_PNCCTT.hlsl index a850e0bb4..5f9d9c36c 100644 --- a/CodeWalker.Shaders/TerrainVS_PNCCTT.hlsl +++ b/CodeWalker.Shaders/TerrainVS_PNCCTT.hlsl @@ -36,5 +36,7 @@ VS_OUTPUT main(VS_INPUT input) output.Tint = tnt; output.Tangent = float4(btang, 0); output.Bitangent = float4(cross(btang, bnorm), 0); + output.ViewDistance = length(opos); + output.EdgeWeight = float2(0, 0.9); // Default: full POM, zLimit=0.1 return output; } diff --git a/CodeWalker.Shaders/TerrainVS_PNCCTTTX.hlsl b/CodeWalker.Shaders/TerrainVS_PNCCTTTX.hlsl index 585a06c00..2edadfa13 100644 --- a/CodeWalker.Shaders/TerrainVS_PNCCTTTX.hlsl +++ b/CodeWalker.Shaders/TerrainVS_PNCCTTTX.hlsl @@ -38,5 +38,7 @@ VS_OUTPUT main(VS_INPUT input) output.Tangent = float4(btang, input.Tangent.w); output.Bitangent = float4(cross(btang, bnorm) * input.Tangent.w, 0); output.Tint = tnt; + output.ViewDistance = length(opos); + output.EdgeWeight = input.Texcoord2; // Edge weight from vertex data (GTA V style) return output; } diff --git a/CodeWalker.Shaders/TerrainVS_PNCCTTX.hlsl b/CodeWalker.Shaders/TerrainVS_PNCCTTX.hlsl index 15907a35e..f1b1ac32c 100644 --- a/CodeWalker.Shaders/TerrainVS_PNCCTTX.hlsl +++ b/CodeWalker.Shaders/TerrainVS_PNCCTTX.hlsl @@ -37,5 +37,7 @@ VS_OUTPUT main(VS_INPUT input) output.Tangent = float4(btang, input.Tangent.w); output.Bitangent = float4(cross(btang, bnorm) * input.Tangent.w, 0); output.Tint = tnt; + output.ViewDistance = length(opos); + output.EdgeWeight = float2(0, 0.9); // Default: full POM, zLimit=0.1 return output; } diff --git a/CodeWalker.Shaders/TerrainVS_PNCCTX.hlsl b/CodeWalker.Shaders/TerrainVS_PNCCTX.hlsl index 66810197b..6df483a89 100644 --- a/CodeWalker.Shaders/TerrainVS_PNCCTX.hlsl +++ b/CodeWalker.Shaders/TerrainVS_PNCCTX.hlsl @@ -36,5 +36,7 @@ VS_OUTPUT main(VS_INPUT input) output.Tangent = float4(btang, input.Tangent.w); output.Bitangent = float4(cross(btang, bnorm) * input.Tangent.w, 0); output.Tint = tnt; + output.ViewDistance = length(opos); + output.EdgeWeight = float2(0, 0.9); // Default: full POM, zLimit=0.1 return output; } diff --git a/CodeWalker.Shaders/TerrainVS_PNCTTTX.hlsl b/CodeWalker.Shaders/TerrainVS_PNCTTTX.hlsl index d1e1b561b..540cb5eea 100644 --- a/CodeWalker.Shaders/TerrainVS_PNCTTTX.hlsl +++ b/CodeWalker.Shaders/TerrainVS_PNCTTTX.hlsl @@ -37,5 +37,7 @@ VS_OUTPUT main(VS_INPUT input) output.Tangent = float4(btang, input.Tangent.w); output.Bitangent = float4(cross(btang, bnorm) * input.Tangent.w, 0); output.Tint = tnt; + output.ViewDistance = length(opos); + output.EdgeWeight = input.Texcoord2; // Edge weight from vertex data (GTA V style) return output; } diff --git a/CodeWalker.Shaders/TerrainVS_PNCTTX.hlsl b/CodeWalker.Shaders/TerrainVS_PNCTTX.hlsl index b44b868e7..94ef8250f 100644 --- a/CodeWalker.Shaders/TerrainVS_PNCTTX.hlsl +++ b/CodeWalker.Shaders/TerrainVS_PNCTTX.hlsl @@ -36,5 +36,7 @@ VS_OUTPUT main(VS_INPUT input) output.Tangent = float4(btang, input.Tangent.w); output.Bitangent = float4(cross(btang, bnorm) * input.Tangent.w, 0); output.Tint = tnt; + output.ViewDistance = length(opos); + output.EdgeWeight = float2(0, 0.9); // Default: full POM, zLimit=0.1 return output; } diff --git a/CodeWalker/Rendering/Renderable.cs b/CodeWalker/Rendering/Renderable.cs index f27eba1e8..367fce0dd 100644 --- a/CodeWalker/Rendering/Renderable.cs +++ b/CodeWalker/Rendering/Renderable.cs @@ -845,6 +845,16 @@ public class RenderableGeometry public float RippleSpeed { get; set; } = 1.0f; public float RippleScale { get; set; } = 1.0f; public float RippleBumpiness { get; set; } = 1.0f; + public float heightScale { get; set; } = 0.03f; + public float heightBias { get; set; } = 0.015f; + public float heightScale0 { get; set; } = 0.03f; + public float heightScale1 { get; set; } = 0.03f; + public float heightScale2 { get; set; } = 0.03f; + public float heightScale3 { get; set; } = 0.03f; + public float heightBias0 { get; set; } = 0.015f; + public float heightBias1 { get; set; } = 0.015f; + public float heightBias2 { get; set; } = 0.015f; + public float heightBias3 { get; set; } = 0.015f; public Vector4 WindGlobalParams { get; set; } = Vector4.Zero; public Vector4 WindOverrideParams { get; set; } = Vector4.One; public Vector4 globalAnimUV0 { get; set; } = new Vector4(1.0f, 0.0f, 0.0f, 0.0f); @@ -1041,6 +1051,36 @@ public void Init(DrawableGeometry dgeom) case ShaderParamNames.RippleBumpiness: RippleBumpiness = ((Vector4)param.Data).X; break; + case ShaderParamNames.heightScale: + heightScale = ((Vector4)param.Data).X; + break; + case ShaderParamNames.heightBias: + heightBias = ((Vector4)param.Data).X; + break; + case ShaderParamNames.heightScale0: + heightScale0 = ((Vector4)param.Data).X; + break; + case ShaderParamNames.heightScale1: + heightScale1 = ((Vector4)param.Data).X; + break; + case ShaderParamNames.heightScale2: + heightScale2 = ((Vector4)param.Data).X; + break; + case ShaderParamNames.heightScale3: + heightScale3 = ((Vector4)param.Data).X; + break; + case ShaderParamNames.heightBias0: + heightBias0 = ((Vector4)param.Data).X; + break; + case ShaderParamNames.heightBias1: + heightBias1 = ((Vector4)param.Data).X; + break; + case ShaderParamNames.heightBias2: + heightBias2 = ((Vector4)param.Data).X; + break; + case ShaderParamNames.heightBias3: + heightBias3 = ((Vector4)param.Data).X; + break; case ShaderParamNames.globalAnimUV0: globalAnimUV0 = (Vector4)param.Data; globalAnimUVEnable = true; diff --git a/CodeWalker/Rendering/Shaders/BasicShader.cs b/CodeWalker/Rendering/Shaders/BasicShader.cs index b01914ea8..4a411b984 100644 --- a/CodeWalker/Rendering/Shaders/BasicShader.cs +++ b/CodeWalker/Rendering/Shaders/BasicShader.cs @@ -76,6 +76,10 @@ public struct BasicShaderPSGeomVars public float wetnessMultiplier; public uint SpecOnly; public Vector4 TextureAlphaMask; + public uint EnableHeightMap; + public float heightScale; + public float heightBias; + public float Pad0; } public struct BasicShaderInstGlobalMatrix { @@ -609,6 +613,7 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) RenderableTexture bumptex = null; RenderableTexture spectex = null; RenderableTexture detltex = null; + RenderableTexture heighttex = null; bool isdistmap = false; float tntpalind = 0.0f; @@ -664,6 +669,8 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) texture2 = itex; break; case ShaderParamNames.heightSampler: + heighttex = itex; + break; case ShaderParamNames.EnvironmentSampler: //case MetaName.SnowSampler0: //case MetaName.SnowSampler1: @@ -704,6 +711,7 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) bool usespec = ((spectex != null) && (spectex.ShaderResourceView != null)); bool usedetl = ((detltex != null) && (detltex.ShaderResourceView != null)); bool usetint = ((tintpal != null) && (tintpal.ShaderResourceView != null)); + bool useheight = ((heighttex != null) && (heighttex.ShaderResourceView != null)); uint tintflag = 0; if (usetint) tintflag = 1; @@ -785,6 +793,28 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) PSGeomVars.Vars.wetnessMultiplier = geom.wetnessMultiplier; PSGeomVars.Vars.SpecOnly = geom.SpecOnly ? 1u : 0u; PSGeomVars.Vars.TextureAlphaMask = textureAlphaMask; + // Disable POM for wind-displaced geometry - wind moves vertices without + // updating the tangent frame, causing POM to displace in the wrong direction + if (windflag != 0) + { + useheight = false; + } + PSGeomVars.Vars.EnableHeightMap = useheight ? 1u : 0u; + + // DPM shaders store heightScale/heightBias calibrated for tessellation vertex + // displacement (default 0.4 / -0.5, range -10 to 10). POM needs much smaller + // positive values (default 0.03 / 0.015, range 0.01-0.05). Detect tessellation- + // range values and convert them to POM range to prevent excessive UV warping. + float hs = geom.heightScale; + float hb = geom.heightBias; + if (Math.Abs(hs) > 0.1f || hb < 0.0f) + { + hs = Math.Abs(hs) * 0.075f; // DPM(0.4) -> POM(0.03) + hb = hs * 0.5f; // POM convention: bias = scale/2 + } + PSGeomVars.Vars.heightScale = hs; + PSGeomVars.Vars.heightBias = hb; + PSGeomVars.Vars.Pad0 = 0.0f; PSGeomVars.Update(context); PSGeomVars.SetPSCBuffer(context, 2); @@ -828,6 +858,10 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) { tintpal.SetPSResource(context, 6); } + if (useheight) + { + heighttex.SetPSResource(context, 7); + } if (geom.BoneTransforms != null) diff --git a/CodeWalker/Rendering/Shaders/TerrainShader.cs b/CodeWalker/Rendering/Shaders/TerrainShader.cs index 1e894193a..93bf383c3 100644 --- a/CodeWalker/Rendering/Shaders/TerrainShader.cs +++ b/CodeWalker/Rendering/Shaders/TerrainShader.cs @@ -63,7 +63,9 @@ public struct TerrainShaderPSGeomVars public uint EnableTint; public uint EnableVertexColour; public float bumpiness; - public uint Pad102; + public uint EnableHeightMap; + public Vector4 heightScale; // x=layer0, y=layer1, z=layer2, w=layer3 + public Vector4 heightBias; // x=layer0, y=layer1, z=layer2, w=layer3 } public class TerrainShader : Shader, IDisposable @@ -372,6 +374,10 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) RenderableTexture normals2 = null; RenderableTexture normals3 = null; RenderableTexture normals4 = null; + RenderableTexture heightmap0 = null; + RenderableTexture heightmap1 = null; + RenderableTexture heightmap2 = null; + RenderableTexture heightmap3 = null; float tntpalind = 0.0f; bool usevc = true; @@ -432,6 +438,18 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) tntpalind = (VSEntityVars.Vars.TintPaletteIndex + 0.5f) / tintpal.Key.Height; } break; + case ShaderParamNames.heightMapSamplerLayer0: + heightmap0 = itex; + break; + case ShaderParamNames.heightMapSamplerLayer1: + heightmap1 = itex; + break; + case ShaderParamNames.heightMapSamplerLayer2: + heightmap2 = itex; + break; + case ShaderParamNames.heightMapSamplerLayer3: + heightmap3 = itex; + break; } } @@ -531,6 +549,11 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) bool usemask = ((texturemask != null) && (texturemask.ShaderResourceView != null)); bool usetint = ((tintpal != null) && (tintpal.ShaderResourceView != null)); bool usenm = (((normals0 != null) && (normals0.ShaderResourceView != null)) || ((normals1 != null) && (normals1.ShaderResourceView != null))); + bool useheight0 = ((heightmap0 != null) && (heightmap0.ShaderResourceView != null)); + bool useheight1 = ((heightmap1 != null) && (heightmap1.ShaderResourceView != null)); + bool useheight2 = ((heightmap2 != null) && (heightmap2.ShaderResourceView != null)); + bool useheight3 = ((heightmap3 != null) && (heightmap3.ShaderResourceView != null)); + bool useheight = useheight0 || useheight1 || useheight2 || useheight3; float bumpiness = 1.0f; @@ -550,7 +573,10 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) PSGeomVars.Vars.ShaderName = geom.DrawableGeom.Shader.Name.Hash; PSGeomVars.Vars.EnableTint = usetint ? 1u : 0u; PSGeomVars.Vars.EnableVertexColour = usevc ? 1u : 0u; - PSGeomVars.Vars.bumpiness = bumpiness;// + PSGeomVars.Vars.bumpiness = bumpiness; + PSGeomVars.Vars.EnableHeightMap = useheight ? 1u : 0u; + PSGeomVars.Vars.heightScale = new Vector4(geom.heightScale0, geom.heightScale1, geom.heightScale2, geom.heightScale3); + PSGeomVars.Vars.heightBias = new Vector4(geom.heightBias0, geom.heightBias1, geom.heightBias2, geom.heightBias3); PSGeomVars.Update(context); PSGeomVars.SetPSCBuffer(context, 2); @@ -575,6 +601,10 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) if (normals2 != null) normals2.SetPSResource(context, 9); if (normals3 != null) normals3.SetPSResource(context, 10); if (normals4 != null) normals4.SetPSResource(context, 11); + if (useheight0) heightmap0.SetPSResource(context, 12); + if (useheight1) heightmap1.SetPSResource(context, 13); + if (useheight2) heightmap2.SetPSResource(context, 14); + if (useheight3) heightmap3.SetPSResource(context, 15); } @@ -604,6 +634,10 @@ public override void UnbindResources(DeviceContext context) context.PixelShader.SetShaderResource(9, null); context.PixelShader.SetShaderResource(10, null); context.PixelShader.SetShaderResource(11, null); + context.PixelShader.SetShaderResource(12, null); + context.PixelShader.SetShaderResource(13, null); + context.PixelShader.SetShaderResource(14, null); + context.PixelShader.SetShaderResource(15, null); context.VertexShader.Set(null); context.PixelShader.Set(null); } diff --git a/Shaders/BasicPS.cso b/Shaders/BasicPS.cso index 266b52deb5f53e40a388db7d88c3ba7cf2d97607..c8752c11fcdf6bd3c42af6fe2e4614c5cc5a77d9 100644 GIT binary patch literal 22068 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≦3SJdJ#;@MF6hUKvvlKeC-$*|ANb!6xWa*98B=d^EE6osGFpcvw%* zn)gdo64}gtfYZ>yIJOym>>V;>kN)O-&s4rVk!t&lW)W0jyMj#c>Bz~McjjJ@NbIj zV19L!XP46fZ?6tPt9$z%b>OeX)^Z5hg{>)4S9TD?Gc~ZYJAFSmZifK-TqV^^aa{c_o!3c8tXWNNVJm0lFTw7C zD?_Yr9QOAezL`zt+o|=z$8fF^)#=f&{*>&-ALLfjyk~3;UE_h*;;=p3H|Vk$agJDL zduzP1uBanZBf*Z)PnOgj^qn`~te0=dR-SCcH|+gde~wjBO;+Pfyj>;LpB@Z5u$8sg z!Tn}Eb{Gx2;lU1c^=nKGj@OwT(5O-Ll~jLvbXHQJA_u%}iDG|2a@UEl@D0t>ENA>4 zXzt~yx4qr3@V&G`JE#{5eGJX?f|cpzu16mF{JNmuY!gZLtn!#`;4u&H z3Li7ACx*!DPHscF%d95M$m2%QCNYR>xPoiF}+aUKh(+Z?YD2Uz<#?FVbVQN{VFP3O%v!#z*ZX#>msz z^zT9D<`a1gZAx~5^LF426C>t^S5i%!<1e3Ym6UEp`uXy9r{d4~Vy(Rq8TX~mwMM?O zo!Vi3l7rB=6Y2GPrg)c_Uh73J-i)fJ{>^A|rGLlgC1%9*ivENaKt~_vkvNji-q%p$ zh{s<<0~=5s<=u}>ey+QNHSn+=S=1GBY#_|JJK&{$ZVqxdBc3<(ld3mH+Z1@P>Ac2X zmh`}Hwh@1w(;@T4?sBpC_Za&6Qs;hq?%X}dF+T1XtKqXz$rpla;tZ;_dAZ*wo2HTJnaU zMmIbAQs;j2#*wib!(PR{XjLgcIWJlMErkz7%z~~^i;8>Fa>o3M*BWMTYVuC>B6dcv zq|Ti@^XNAF#O9LdTv)D9hwup-K6PpPcaUcm3tOutMNVvwa--m5w;~?yl#>Co+_Si{ zo;D&=bgvb$s-&Q68+kvhy(6Eo+wv>kJASim5uN+*4(S~ayhFqJc_(i8!TP)P89(zx z`V0QRvt78wNxACpZOeONh;G(yipJCkVSZoXrCz|p9x>k>=`q>VL3q#?+r(Xuzu0ZO zV4NARKgc9@qr$k$E2;kUG}f>`CI|9XiNqH62D{i3=&>z{{D9sMt{(Z=LBmH|Yk5Eq zufBf66gU6w^!BR0gt!ghFL5)yilxVhpJC*t=>TIbcF=2o3){`+&k8rax@QX8FSQn1 z;iume_5c}|^NoDASQl?w$sl&zMZ`$|#O7-P7`iR)_cor3^{9O zit)#_FYa~KA?b(LbXb3DkRQ{)Mnar6sV9ciKf~}d&P~p)@~XH4q2sIN0d!lVp|1nb zEhe%3>^DA}uH0vj4Qvj2#b2CH&do<;+jyX1FSf-o(0_&XbqE~adU?%u_8ptxGaY6- z_F|WIXL&`QV!!b!4h65}J$ZnA z{69YyC$rCLH!?qV8QVW2x$G_LV%@pl&MCe{+{$c}{EK{Nzmcc>@^=9_z+00Xu)4oV zWAn{0?gGQCMj*#9s}V6~u_&t%=#A+nNAQ*A^A_3ab2X}QRwLL`bXhDoi$#8}B>8@c z9-l?8Gsv`fvQL+)5!l3<|E5M@JNEs*YXtVcml|PtA@={NM%cdC-jVyZFBVVsjNHFo z9W^38XT;2EgoZ`_Yi)7vp~I(r@$Xs2=tHLYfi7_FY5PVr)_z=cr$WCRO}XuDL_Tuq z&4#;@{0)dM-VZb|;^6Jkzm0I-W$T8vF!qDDDh)cnHRug9zle$Hp$~b!!+MwGZ!TyS zn^nG*9oS$p%x2CgHuyWDN3Z3%(aoNI$>(i_YjT0f1mppMC)u0 z@8xU^arD2nLcP)oZK%FW*=YM>J-p=2`QShQ?hMU-8!!*CSi@s+fOjDLH!KZ(9Ml(k zZ@-0jo3@iYANaj#@q^cR*rStyhkT;ltF5likIi>AzE+F)_gxzQ9Un~ZN{?*_n7WG^ Vzqx4GYHhOy%5(qkkcO4@{|4+is%`)P diff --git a/Shaders/BasicPS_Deferred.cso b/Shaders/BasicPS_Deferred.cso index dd7ffd0938abe0624ba37afe2c73a71eb5b8f39c..3e459d4f58638193645e8196e9a896cd1396c076 100644 GIT binary patch literal 16224 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 literal 6744 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 u0cJj4P)W)w6D#~b20C~{> diff --git a/Shaders/TerrainPS.cso b/Shaders/TerrainPS.cso index 4629d7d17a36de106e27b247ff6c85d6801246cb..4fcee40d1115d91bf27c8e5ba36fe566a646cef2 100644 GIT binary patch literal 26136 zcmeI4dx%`ueaG+4%B$5%jg+ll-Nw#Xsa;z(MOry_s=Cr>wOUC;uWDtXw4w1x8rj=O zyJB}$JHZX>b)BFP8vhYYAqg1c2!m6TsA+MEgB_(UrZ~lQsr!dRASH-GLrGO9kP>je zpL5Um&Y7z_BPq38Lv_%dbMEi`I*)s2N8<;_ww(I$KYZf4udn_5&;ESz=8j$TgtJS|xSGMG>w=ywbC)}56IUaD@ zkj-_v0&g60zb?G~1sj&Ae@jhW;FTHK3l(_tga4ZfJo2Cc|7ryuy~e*$fyWNx&sE@w zm+}8mfgeclKd-${| z1izyKzcRt^7rv??pXMjsDS^4;LlgU|4Q%|D&T)F_)8V=?+Z>2?U(ibtKctJ!2eV5S1RCJ zn#V)o?46lY==yabF>l5lm})(kJ^Spl{lYIv@H;8bfDEJYso88xbMA2SNNdm0qjRnK zIWbwL^BV~!4>ymsdf>z4>w(`gb8>nfo5*UPlPSC!5Ezy;~-B&di*coNvy~ z`)2F(k!);c_DE}XZ}Z5JspLBa@2HLP@#MnKl{;Rs! zgI-YoiY{vHdvRRX_}}Art;U&R0!>51tK)dD#<#`seHydW^o5hjVAc?P>vZ$bu~xpS z5-?WKv{v@Drd3V5W{$M7_Hc(_55@b*=J6B9TC-bbRCeOu%Z95~&hN(r2Udnp3obJ* zfd7%;_+6&|oZ!Tu4F96woVm*IKNI}=3i!7K=X_SCe_8N)1^gcc|FsBLm$`1w34Uur z@3rd3g5QzAO`qw&L9XzDdEvcfh2V_}c%GlQbFYo*inr@JZ$~?C*LU9D*?GIGsSP_9 zo^RW`nkT|cZq4p)9&ed=N-I%Lb)Ypn-+Cz4{h^b`PfSg>=H?cy?xP=IDI=)_u3tx9^Y>#}EeOm5r#1ua#ai>Qke zaoM8tEC%;*glib!+%8q^KU#tl+L8)-&Zso#fiIB~=+Q+3E|u9z!GI%^2Amk!9(ZZMk!d)*G~g<`0uC#@fHTMNYx2Oy_U@V7v46*&-P!Iv`*v;KnQhs#bI-nP|JH+B_Uzd=uDQKOxuK>d zBG^?|?}jWIxsj~>oA+(ox?en#+sF59KbY6{@U>a?K}|+=Y73h}7e9_3J=$?EaL4_3 zUwP%XQ~L7wBZJwJi9e|a+aUai!pl94IQX6%5#Qg%{ri}O8V}}U{5p8O(sCk)-+OQS z9$b2lob(?5(|dB8-lH?UCzt6xHm3LFH@zok={@;O@5y6&&vQw7&p9`}Cr{iTT9x(B zu38o1#`wI(_+~hMd5lktM@54_jL#-x;P?f6EEx}LOpRI&zIu$G9^<3O_{SKT_|{Us z&e!w2JfNwTkFw>XVI|G7C$3qwuvv82TWjbe)K|O4eqCiS7HKyu)Ps-s*%xTDtgb0C zD?E8V@W;AS!{%B;A8Nq4XzG_*)|x1DDDu8PThedXhZ4Gxgbo?tupJvFBJSvj9FDlf z2wNlnfsMpoelG=wEfd3?{kTfoxDAm8J@|!;pRVkrFUukq$fWNji#kYUL0^_dE|Epw zOBQvM%7R`t4-QKrwd7b1E#x_i@flp*1&?j;?-QQ<4fN$PKJe*I{-=yz67i>UJp5`) z&k9d&2872BdgQ4UxYO^+Vf4_Zb`yV^YprZ|8DG|GnbuDpom*n!+GSn*P2t8OEXASU z^$7i4_3SFs4aIpSaF`e1#T(|3WiyA?MZMVH*U*QWbs!r*z&Zbn$vgAcz4H&w^!P(v z5?k`0@=4Y*d=0Q#ZWDA9$+(NmL1dDrLT19#VWUvNvKS$73p<5%PQVI2L<(T@~{AN|7JE_&uEaYQFHj$?)2jiFdyaULx< z@6h9qhd5fs(BrR$_09KHG9@eM6>p(?SRuYE`p}MdmR%!w*>A@lx*O!9ZLv=0ir86~ zWiUUW^E_b=s<@S^w!7scN%Map++O;~6Us{|kD z91GV<X>&?05?D@o1x!n^NsX@*+DQ&MB#BTEEx`5X;0}kJ5 z&|m|80qSzb{(8~4+6wZ$1O*%tf(C?ZV)=)n8vH#u}*9zyD z9y*?5Tw9(m z*3d`(&LMs8aKZXUuHa*RVUA!Q@u41bzq3uUaRu_NyPJHkcWIt&&!yB~O53acu#Fg2 z)gL%~qCw+&0p887UiAlGRsE&%(zxJfxB6R@J@BRV2e0F;VaNWwSpew!8u}y ztK8>y?OAs>`Q_RR&uj8M@Lgo>xxk_sf8=R-@@mx^0>vC$ZN?x!z zvHfyt4_@1+;V)Es$v7%j%tS8ik>(OhVVbFK0(I89p2W__+1$U)Jy*}jg z$ZO1yV(<0wN{wErQSagDdQod=0+rV3E9OvfHrj{Wv{CJSc`#XhW!n){b?{)4yM z`41a>zOt^eU6*tItCAONPHcat&VO>H@_dMY%xksqUXAAkpUDQU?tCACt$t>`*z+L1 zrnJ3s@^-uvOLg_i3A)mGiciD&{9{cf_XwZ+3j$K@vmKm4k2^+YFr<+~f z@??2wo-RdRw>(*1nx|BrObqX$u~jx-FyAf*FC9gmtNdq9IM%(+NyIj#?KOY8Q=*9YDAK$3k%Dof%Zu9UvKe=@i*LPo85w8D@dAM4>P`kvcJP+AtT-Ue< z4fi9l_dG1m`EL2PJor;xmfKBUw|pbd`L%u6VtE=Cd#zw0pV0R;^pUTZ%MuwyzEfHe zhVKR8L$3KhdG&J}Z7(rN>+|iHhvdaN(^#>O8Mj6>H0*L-;OS;px4c+hnwLwF*DWuW zr+HIppMb>~M>;F>lE^6XlF~}XQqy3!^K$g)xs9G*_|Un}{b1vB+kUX|J3o0QT;=)Y zIu3jn+2=TKvd@{e+j=?{Z6im{BfR9%xHOO0zvR&QsXnDlLx-Latw>(BBRJ-N-Ngz-DQ+fp5=Fw*Dc?c zM}BG8mC6$+%tLU}5%S46-RCSL#XH|TL#Ty$$iQ~OfAw?Q!t-Im4@s2W*z9LOVAw|< z;HOVZt>1N;4m~NIZ6x-Y&M3T}sK?&}kn<-#QxCRjnAI9Ws`sT~ zwpIHIwwC7wdi)Mm?}5W}AT$G_MK1H0`&vUE?eCTJOu4YhGQGAE2cFmI%QB(iJ%MF% zUu)>2i_AeG(&t6uS(b^c(_a5&Cp@)=K1wp{F-}3|Vm-u(*wh;B7{yvI;^cRgArJBi zU1j~y5T_tl`nj()^ik69IPqTjoPIwMVj!8sHIW(KTj3{hLgwj#{QLLQ*XA)z<#^pB z-q0p;R-B185u0G!U@oWDfUx6xog#2i%h;l!A|Fe`?5@6sZ8!`4Skek?$qzE{M!feuYC0PHMF*UYIKnI zTg(^Ma^?W_dOH60g}ougkw!S{7 z+?$`A1^Z*JQvC(4Kd+HOzi4egG{Hu}LQE6?Qye)<`BVMor%&yNpEDTmD$!TiuX|{f z-#+J~{^2)o@%Sr`tmWscAy5A``uP(g4bO7wvkmdL4%7yDWQ`(@PsP~$^!<6=cs>!w z(TJm_s8ieVdd#Ol%g>4inR@qG><_hu07c^+TdwQ8XgmCgwd0(HdvAwPXGePtj8|DL?SEW{P}>7M%Ihn&G{x^_!o(bl9(aFz?|vZX(u9p+Dvj{m>w* zuwOW8$Nb>Px#xY*uY2o1BYs+8zt%2r=1*&;uwVAjB&(>IBaeQM|LP&s$(y@R-6Hgr zI*GM#rB2#)^5v7uKkGXAo$sIerpIS*82qBgcbxs^n;!qu$#4AKm0IfDXU@ktU(DJ6 zr)y6C?|u2bu9L4k{ln{3C&h1nwZDYDgFU9!(1-Sm{9@c*yIFf#SHqb}apc_LwL9HA ziuF}A<-H^2=iFibu=a|cy(8uK+Uq>Bu3Ep>UU2E&QO>p3{to-Ku6Nrz3jLz7{j;%- z3j2lYws)lbw%`1P{c&HZ+B*vSBTc$@-2eLrf8;#=ZMOI`h3iTlV;vFWP>1mhkmljt zl1H5_IL^Q8_WSgF$+<3_+n#T#yN+`maPQaPig=4Zo!iW5WR}l`@R%Q5QTLJ7YlG)D z`igT^r19KlE*JVWZ_>HVxawR8-25H>m*#fxU$FAI(EguM-O&pBbuI)~p4;dv>`%^x z*QqA>y%z73C=N*r-`{8$egh7S--6SR>%Go!)W`cN8a~*DA_mvz^s-?k^CHC1IvatF z=V4!2t)aX2J@D|iDD|XPM;3IaMs4&p!kM=Cof+`5-P8bf(_fC=v3A}uyUBxpAE#kf zPkdZc;Uj;8=ol99m0!rgKb{kY6_UEv;`53E@Yg@1J2hfeYe)gTE^yQaGDlb9nV~$J{3#H=7M;O2ApXnKg1f?>Bn@Z z=2)YLIIyO$#`s+(_l_Sh-dh60CYtHsN#A)w2h4QD{7j4!F%NcW&Le9=eOV{63@htI z7QVtuj_qTR73XU`iDh{X;xjUwzoG_WZpaa_#kb*3KQ;lQF0hSv&d`-*36u@Q-`F_c z=Hq!6KKN6<;*2BR;h8wvlF}6CsAykVHvS_Ue)C};vdJyDYQGiB&gBaq%+~OT9_C$F5=8s@mP-SjEP5h*S(n2jR1>dVkw&NZVFobq!AbHS0{QbJJpxkE-VN1T8YG##pn|`0-xGammgz>t4BU|(D2c`8=?$i zj||xs{vRCtWlk0Lw&OMcj<{K`;um1V&oKXg5;<+|GjkT-`P*abx6QvLxNYIPJ==~v zcv4>DY%}XHdXS;Mt8dilsN@j~)-HG^qTlqu`EB>=2(vH54ccqfIfsE)b%AZeIlN~c zkzfcc7c=Ko&{>BBo zW0KlWzN_YkU;{;sM7j9KytygXp5siP#(@9Rfp`MPH{zMn+II48I@wg%ZhxE$cZ9s*zNJue*7KJjkTT4*|I9{Pb3c@Fq~3>wE;!%%l2 zMxwPZ@Gwt^acVo6*BPIP9ij0}Wfd;mpUH#@w(>_*_@iDfXLIaVTg#FQ^0T z=l{VcPS{{sUMIM}S-4aWFw52domQMTQr&r6^8b&g>{|z2h3~`w9jdRD|DrLu;4H!( zI&dmq_eT;N3}Y{_U0yd*TUjIQhv^)P@)}Xp!a_aR0^U3thO+{)H$?k1w~Cy5jTno5 zI2O!BVi9UF$}P`DaMtZOLF2gy-g7af{YBP@HPOER%QXTU@Z(a}2-k(k|3_bdF|wX zQ_~B_`JLLLVI$2pfWv;8Z9q5eu6lkx4A0*>TZCst!7Pja-wZ$H$MRY6V6V^6j5&ML z42!hkZ?^JL9{g>m`rd!yAUAEb_GJmlF9c8_&SF&-)c-zx2IM13i zKWh+=f-XLrfW!AH+(0`9;aNSzAkzDsp2+TMBhSg$_*#sK)-6GlY+bf-U|H@Tb`*4a z#-)M7M#)$a%4a9FT(vSoAHYyF1`#0~O*Z7ADBAn>vJBp{})N1uS^IWG( WwOH9$W5;!o1Gg~^KA;O+ulC==^4Gus literal 13328 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 a>EZ7W;>9;Jy2=LmD~_E)+*3rBtp5efUn#}_ diff --git a/Shaders/TerrainVS_PNCCT.cso b/Shaders/TerrainVS_PNCCT.cso index 97924d34333014dda3315c9c9cf0aa587e10dcd8..cdba70a932a6462c50f104f335526612b6800efb 100644 GIT binary patch delta 492 zcmXw#y-Nc@5XC2FlDmrsCU=1#C|dXdN(6&8e#S%tqS%;1>~mNsq_7cUW2!5xCYJsW zks#RE2O))pt%ZVxm9-%F-reQF><;fYJ3G7Gx&C}<_oPz&{=GhXeczjT`q)2eZ9HEk zM5OPDq_D3b&P|cLCDOG;4!wucmpR)(%@`0|X|f*THWFqLYw4y{7;ndvfYdScPR$OcaVVjYy7y!e<|fKBkX*{>436Ns+QcbYsyj zEb%PgM3JJMm@w#PXhQTI?v9??3w8$`${4fJyS2SUqM~=1s@(uCO=rdFy=6Yr#Voqq z2v(?OWW!Gw?gw~R1*IHnjWzn1dr@^KwdGE%@*<;*1@e7=JIX}1@H5Bto%$t=8e;h1 UJn}^yxa%nwnV~svN`KtsKj$_(OaK4? delta 329 zcmX@1zCq2%CBn(M?aPkWVM1}2ek{!E^_>@1#K^$FU?9xEzzU>IfH;SpfkA|WfuVtu zfnkf#Mx$r!swqGW-yC^fgecwOt$3^mkfr=f+Rq)Oh62h4W66} zl?91_07#Yvh(WTEljj0uBY}Eh+Cj1)2k`(Ik(1AIh%-h`{tHzD5(fd08jy=1Cg^fP zOaQtb3_!A6Kn#(MPfk1N4*w&=&8_dpU&}C*R?+*sQ?)fNipY(DBLQ!jhB! g2uV!#5atnd2I>U44g`dNSaWi!ur;IL?R@Vq51fJTzjjJaq7c#4 zBU+HSC+kWh+R}+82GOPW5d3K+)L4QfOE-L^S5}7T%9N9cqb4}wYoU=G=LMix=#ThL zXgo(5fpN|xiup~i?1HM2fzVe#1eLtJDAhclUqsh$b|>zEK=D#I#=LFJ9A&Chpha0T zdTvyF+>8taDrTu;_w<5Iy|y&!c3LNicsFFI-uT{FQi7Hn;+CDZ?DJ`cs4Q`lKixyc I<#n(A51-IG7ytkO delta 371 zcmaE$enQR2CBn&hn%U>oxAtz7nf~Rq(SnA&3=BFP3=A_k z85qt8Z8ZAF&YJ>M!wZxGiZVD%w&f6)^a1ID0%jl$;<5v!gD2+#WrKlSm;#V22=fDR z@Z`Bb*+`faR0d=*GYUj8JiHNR> z%wgY0+!sYUHV!3`(Ku+>>m#Hrz@YiSbckC%Ii;*?2DV@8Wt4z4hhSyTyk(RQMOr|R z#^wh!M(wPel=HYa#AZeL0jYvRk0uaRvW-&1E18JYhy&U3_!BAO{1W1+%h(WT!llMYp zK_Vakl4Sv6kZk1Sw?NrQpkA1EkSxeOJU~X|WLZve#>mOOoDel2aS#Bh0l5fbLM>Dl z=y)&y$#MZPM0PEw_~bjB8k`2)Kpy~YIkTCU%bam?0Jp{FDcl!0CQlJLGC4<7VseZq rkE|Wg8NnfrAtgZFAwUeX4;VxY7L(VCTC)lPSIg6@ diff --git a/Shaders/TerrainVS_PNCCTTX.cso b/Shaders/TerrainVS_PNCCTTX.cso index 77641ede1c2df473db4c09f45f590bb9321dbfe5..375fe6590f079257d719e2defb52a7ba277644df 100644 GIT binary patch delta 474 zcmXw$zb`{k6vt10>AP*ki}tkN218Blpb?8>u=yLL zi^Zt18TvO62_~@_e82DYeUtOfz2|ezJ@?%ARcg<eXMZ*`I0jK7U@?<@5W9 zh{W?E3HU|WNm69T68SJ9m(GJbX${{ZX9NgQIn_g}FxF(%Krwi3lfWn&*CXmut z-_j@0PKXACjbj;+j(%H(1=1)Y6zhiQNfF15e1-9NnpH&;oXK-2Qr?suJGwidn93Jv z3Kq}{Q^@mVMkrD=Lr1+aC3A-cWsLIPeKxa^NJZaLRdWpNPT7*_PN)0Uyp7361z%_@ zYWtTD{WhMpf>aKXVvhc1d-|Fy>SJzViH)LVd?3&B)`OME8eVU&zEM9zU>_C^hMy$M RYMhls7OR-noN+yJtbcV4J4OHi delta 354 zcmZqCd81|I65-_RAiV1Bnbw5j^Ah`)RO+=wGBPkQB1Okva(3M~_BPaWE zLd*oI1ObpN(Ctv!T265>kY146K>#Gn4aDpmlMiufaT);G4nW7A+5DALn{l!Zx5efH z?h71~3q%f24i=T1Y#}NkYX|gAaEN0_2~b-I5Q7{H0wO?cF?pe=HKWkvyQ1m(P`iOqt`|;}G-_Lp9 z5Rri|GK#!_I4&VuB2P-B?caq(Yvu|yV?c1F&3cGwBwc0fOdnXmL?@>Nq$YwpJNhE0 zbSTmSg0!n2K)cvG+)m1Y94xx}%L*E#m?#x#5m6|E!e<|fKBmumR#n!Ml@uvEWGC*Z zV4iz;#hv3st Q_-(SRzr7WsqU-+bKjbkwIsgCw delta 345 zcmaE(eMQU2CBn(s>SzA6b55UJcJE3*aF(T$laYaeAw-mcffY!b0C5gGkmg`uSis4^ za6@FH5gUhU3Q!F%Pzor@-~dt&1R#nT%wk~R2hu*1V>!engQ2n@36Lxk5QAicC-*{S zK_Vakl4Sv6kZk1Sy+GMWpkA1EkSxeSJU~X|1Yf8u z(Dh&dlH~$oh-@vV_~bR58k`2)Kpy~YIkWjLr#$0i4sMIhKHL{LCi{q-pKL8EIaxte qLe>uGjNlN*kP@Ko5Fm!xCk$j;Ozsr5W)%Xmm?vKpm7e@Slmh@#DKJ$4 diff --git a/Shaders/TerrainVS_PNCTTTX.cso b/Shaders/TerrainVS_PNCTTTX.cso index 293ade2b8f1cc6fd5d9b98597822908a0ac201f0..342b7a4c0bf68a5122c173c20d1f176c50e74495 100644 GIT binary patch delta 454 zcmXw#F-rqM5QXOwV|FhWJWVc#h?>SGQN%+mw2_OXQBfPU5p3)P{Qp|Z7{;$k4ZAh&}6NR}Ij**PX3;?&|a0J0r`jy<#aE2lQ&WF2mc%>~>S qI3^c}T$mgpD#6G$IagF%Fa)RpHjpr62hA3zWUp$^V2o|B{pZtmm!?z`{4m-kwKCRp1Io44h^gQNS8r}3`Z`2IZJ zH$1J5i))Cj}VvZJH2$huP6*s-~_`)4-Tty&E-Q7@>Z&o9Ylam@J9YGm39ky@W1h z!53OLOUWY)@+R&TqLf3~tkEx(SAFMJ^|d>;z=O;NK2Q`zE9pvP885TlT5X-e*g^1V RDEu~A)W7bMQPFj8`X6}!IdA{~ delta 345 zcmaE(eMQU2CBn)1&pG*7k1G$}K67bF&|F>DN=60-h7eH(238<#0>nA&K$?SrVF4!t z!wr#*Mr<6aDL^&6Kq;Uog9AuC5P&FVFpGhKA4vO5j^z-S42H^rBtWuEKn#)%p4>0obrs5Ik+u0`*2_2nCv5RezLWwVZF}YLJnpFtMVxD|aRC@9QQ4RpV05NX>