
// uniform ivec2 atlasSize;
/*
float BilinearHeightSample(in vec2 coord)
{
    ivec2 tileOffset = ivec2(tileOffset * tileScale);
    coord = coord * atlasSize - 0.5;
    ivec2 i = ivec2(coord);

    vec4 sh = vec4(
        texelFetch(normals, (i + ivec2(0, 1)) % atlasSize + tileOffset, 0).a,
        texelFetch(normals, (i + ivec2(1, 1)) % atlasSize + tileOffset, 0).a,
        texelFetch(normals, (i + ivec2(1, 0)) % atlasSize + tileOffset, 0).a,
        texelFetch(normals, (i + ivec2(0, 0)) % atlasSize + tileOffset, 0).a
    );

    sh += step(sh, vec4(1e-3));
    vec2 fpc = fract(coord);

    sh.xy = mix(sh.wx, sh.zy, fpc.x);
    return mix(sh.x, sh.y, fpc.y);
}
*/
vec3 CalculateParallax(in vec3 tangentViewVector, in mat2 texGrad, in float dither) {
    vec3 offsetCoord = vec3(tileCoord, 1.0);
    vec3 stepSize = vec3(tangentViewVector.xy, -1.0) * rcp(PARALLAX_SAMPLES);
    stepSize.xy *= PARALLAX_DEPTH * rcp(-tangentViewVector.z);
    stepSize *= 2.0 / PARALLAX_SAMPLES;

    int currRefinements = 0;
    offsetCoord += stepSize * dither;
    for (uint i = 1u; i < PARALLAX_SAMPLES; ++i) {
        offsetCoord += stepSize * i;

        #ifdef SMOOTH_PARALLAX
            float sampleHeight = BilinearHeightSample(OffsetCoord(offsetCoord.xy));
        #else
            float sampleHeight = textureGrad(normals, OffsetCoord(offsetCoord.xy), texGrad[0], texGrad[1]).a;
        #endif

        #ifdef PARALLAX_REFINEMENT
            if (sampleHeight > offsetCoord.z) {
                if (currRefinements >= PARALLAX_REFINEMENT_STEPS) break;
                offsetCoord -= stepSize * i;
                stepSize *= 0.5;
                ++currRefinements;
            }
        #endif
    }

    return offsetCoord;
}




#if PARALLAX_MODE > 1

#include "/lib/OtherCommon/TemporalNoise.glsl"

float BilinearHeightSample(vec2 coord, ivec2 tilesBaseTexel, float textureResolution, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	ivec2 texelX = ivec2(floor(coord));
	ivec2 texelW = texelX + 1;

	vec2 f = coord - vec2(texelX);

	#ifndef PROGRAM_TERRAIN
		if (isBlock){
			texelX = tilesBaseTexel + (texelX & iTextureResolution);
			texelW = tilesBaseTexel + (texelW & iTextureResolution);
		}
	#else
		texelX = tilesBaseTexel + (texelX & iTextureResolution);
		texelW = tilesBaseTexel + (texelW & iTextureResolution);
	#endif

	vec4 sh = vec4(texelFetch(normals, texelX, 0).a,
				   texelFetch(normals, ivec2(texelW.x, texelX.y), 0).a,
				   texelFetch(normals, ivec2(texelX.x, texelW.y), 0).a,
				   texelFetch(normals, texelW, 0).a);

	sh += saturate(1.0 - sh * 1e20);

	return mix(mix(sh.x, sh.y, f.x),
			   mix(sh.z, sh.w, f.x),
			   f.y);
}

vec3 HeightBasedNormal(vec2 coord, ivec2 tilesBaseTexel, float textureResolution, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	coord -= 0.5;

	ivec2 texelX = ivec2(floor(coord));
	ivec2 texelW = texelX + 1;

	vec2 f = coord - vec2(texelX);

	#ifndef PROGRAM_TERRAIN
		if (isBlock){
			texelX = tilesBaseTexel + (texelX & iTextureResolution);
			texelW = tilesBaseTexel + (texelW & iTextureResolution);
		}
	#else
		texelX = tilesBaseTexel + (texelX & iTextureResolution);
		texelW = tilesBaseTexel + (texelW & iTextureResolution);
	#endif

	vec4 sh = vec4(texelFetch(normals, texelX, 0).a,
				   texelFetch(normals, ivec2(texelW.x, texelX.y), 0).a,
				   texelFetch(normals, ivec2(texelX.x, texelW.y), 0).a,
				   texelFetch(normals, texelW, 0).a);

	#if PARALLAX_MODE == 2
		sh.w = sh.y + sh.z -sh.x - sh.w;
		return vec3(sh.w * f.yx + (sh.x - sh.yz), (8.0 / PARALLAX_DEPTH) / textureResolution);
	#else
		const float eps = 0.01;
		f -= 0.5;

		float dX = mix(sh.x - sh.y, sh.z - sh.w, saturate(f.y * 1e20)) * saturate((eps - abs(f.x)) * 1e20);
		float dY = mix(sh.x - sh.z, sh.y - sh.w, saturate(f.x * 1e20)) * saturate((eps - abs(f.y)) * 1e20);
		
		return vec3(dX, dY, step(abs(dX) + abs(dY), 0.0));
	#endif
}

ivec2 ParallaxOcclusionMapping(vec2 coord, mat3 tbnMat, vec3 shadowVector, vec2 duv1, vec2 duv2, inout vec3 normalTex, inout float parallaxShadow){
	#ifndef PROGRAM_TERRAIN
		const float textureResolution = ENTITIES_TEXTURE_RESOLUTION;
		ivec2 atlasSize = textureSize(tex, 0);

		bool isBlock = textureSize(gaux4, 0) == atlasSize;
	#endif

	vec2 atlasTiles = vec2(atlasSize) / textureResolution;

	#ifdef PARALLAX_FADE
		vec2 duvMax = max(abs(duv1), abs(duv2)) * atlasTiles;
		float fade = saturate(pow(max(duvMax.x, duvMax.y), -0.1) * (1.0 / 1.4));
	#endif

	vec2 tilesBaseCoord = floor(coord * atlasTiles);
	ivec2 tilesBaseTexel = ivec2(tilesBaseCoord * textureResolution);
	int iTextureResolution = int(textureResolution) - 1;

	vec3 parallaxTexelCoord = vec3(coord * atlasSize, 1.0);

	float sampleHeight = BilinearHeightSample(parallaxTexelCoord.xy - 0.5, tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);

	if (sampleHeight > 0.0 && sampleHeight < 1.0){
		parallaxTexelCoord.xy -= 0.5;

		vec3 viewVector = normalize(viewPos) * tbnMat;
		viewVector /= -viewVector.z;

		vec3 stepDir = viewVector / PARALLAX_QUALITY;
		stepDir.xy *= textureResolution * PARALLAX_DEPTH * 0.25;

		float stepLength = 2.0 / PARALLAX_QUALITY;

		for (int i = 0; i < PARALLAX_QUALITY; i++, stepLength += 2.0 / PARALLAX_QUALITY){
			parallaxTexelCoord += stepDir * stepLength;

			sampleHeight = BilinearHeightSample(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);

			if (sampleHeight > parallaxTexelCoord.z) break;
		}

		for (int i = 0; i < PARALLAX_MAX_REFINEMENTS; i++){
			if (sampleHeight > parallaxTexelCoord.z){
				parallaxTexelCoord -= stepDir * stepLength;
				stepLength *= 0.5;
			}

			parallaxTexelCoord += stepDir * stepLength;

			sampleHeight = BilinearHeightSample(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
		}

		if (sampleHeight <= parallaxTexelCoord.z) parallaxTexelCoord += stepDir * stepLength * 2.0;


		#ifdef PARALLAX_SHADOW

			if(parallaxShadow > 0.0){
				shadowVector = shadowVector * tbnMat;
				shadowVector /= shadowVector.z * 0.9 + 0.1;

				vec3 shadowTexelCoord = parallaxTexelCoord;

				vec3 stepSize = shadowVector / (PARALLAX_SHADOW_QUALITY);
				stepSize.xy *= textureResolution * PARALLAX_DEPTH * 0.25;

				#ifdef TAA
					shadowTexelCoord += stepSize * BlueNoiseTemproal().x;
				#else
					shadowTexelCoord += stepSize;
				#endif

				for (int i = 0; i < PARALLAX_SHADOW_QUALITY; i++, shadowTexelCoord += stepSize){	
					if (shadowTexelCoord.z > 1.0) break;

					sampleHeight = BilinearHeightSample(shadowTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
					#ifndef PROGRAM_TERRAIN
					, isBlock
					#endif
					);

					float diff = shadowTexelCoord.z - sampleHeight;
					parallaxShadow *= saturate(diff * 40.0 + 1.0);

					if(parallaxShadow < 0.003) break;
				}
			}
			#ifdef PARALLAX_FADE
				parallaxShadow = mix(1.0, parallaxShadow, fade);
			#endif
		#endif
		
		parallaxTexelCoord.xy += 0.5;

		#if PARALLAX_BASED_NORMAL == 2
			normalTex = HeightBasedNormal(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
		#endif

		parallaxTexelCoord.xy = (fract(parallaxTexelCoord.xy / textureResolution) + tilesBaseCoord) * textureResolution;
	}

	ivec2 parallaxTexel = ivec2(parallaxTexelCoord.xy);

	#if PARALLAX_BASED_NORMAL < 2
		normalTex = DecodeNormalTex(texelFetch(normals, parallaxTexel, 0).rgb);
	#else
		#ifdef PARALLAX_FADE
			normalTex = mix(vec3(0.0, 0.0, 1.0), normalTex, fade);
		#endif
	#endif

	return parallaxTexel;
}

// ============================================================
// PARALLAX_MODE == 3 : labPBR 1.1 Style
// Улучшен: secant-интерполяция + более точный binary refinement.
// Отражения для стиля 3: только если material.hasReflections == true.
// ============================================================

#elif PARALLAX_MODE == 3

ivec2 ParallaxOcclusionMapping(vec2 coord, mat3 tbnMat, vec3 shadowVector, vec2 duv1, vec2 duv2, inout vec3 normalTex, inout float parallaxShadow){
	#ifndef PROGRAM_TERRAIN
		const float textureResolution = ENTITIES_TEXTURE_RESOLUTION;
		ivec2 atlasSize = textureSize(tex, 0);
		bool isBlock = textureSize(gaux4, 0) == atlasSize;
	#else
		ivec2 atlasSize = textureSize(tex, 0);
	#endif

	vec2 atlasTiles   = vec2(atlasSize) / textureResolution;
	vec2 tilesBaseCoord = floor(coord * atlasTiles);
	ivec2 tilesBaseTexel = ivec2(tilesBaseCoord * textureResolution);
	int iTextureResolution = int(textureResolution) - 1;

	#ifdef PARALLAX_FADE
		vec2 duvMax = max(abs(duv1), abs(duv2)) * atlasTiles;
		float fade  = saturate(pow(max(duvMax.x, duvMax.y), -0.1) * (1.0 / 1.4));
	#endif

	vec3 parallaxTexelCoord = vec3(coord * vec2(atlasSize), 1.0);
	parallaxTexelCoord.xy  -= 0.5;

	float sampleHeight = BilinearHeightSample(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);

	if (sampleHeight > 0.0 && sampleHeight < 1.0){

		vec3 viewVector = normalize(viewPos) * tbnMat;
		viewVector /= -viewVector.z;

		// labPBR 1.1: шаг пропорционален глубине слоя
		float invQ   = 1.0 / float(PARALLAX_QUALITY);
		vec3 stepDir = viewVector * invQ;
		stepDir.xy  *= textureResolution * PARALLAX_DEPTH * 0.25;

		float prevHeight = sampleHeight;
		vec3  prevCoord  = parallaxTexelCoord;

		for (int i = 0; i < PARALLAX_QUALITY; i++){
			parallaxTexelCoord += stepDir;

			sampleHeight = BilinearHeightSample(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);

			if (sampleHeight >= parallaxTexelCoord.z) break;

			prevHeight = sampleHeight;
			prevCoord  = parallaxTexelCoord;
		}

		// Secant interpolation — точное место пересечения (labPBR 1.1)
		float afterDepth  = sampleHeight   - parallaxTexelCoord.z;
		float beforeDepth = prevHeight     - prevCoord.z;
		float denom = afterDepth - beforeDepth;
		float weight = (abs(denom) > 1e-6) ? (afterDepth / denom) : 0.5;
		parallaxTexelCoord = mix(parallaxTexelCoord, prevCoord, clamp(weight, 0.0, 1.0));

		// Улучшенный binary refinement поверх secant
		// Используем stepDir масштабированный на invQ*0.5 для первого шага
		float stepLen = invQ * 0.5;
		vec3 refDir = stepDir;
		for (int i = 0; i < PARALLAX_MAX_REFINEMENTS; i++){
			sampleHeight = BilinearHeightSample(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);

			float diff = sampleHeight - parallaxTexelCoord.z;
			// Уточняем в нужном направлении
			parallaxTexelCoord += refDir * (diff > 0.0 ? -stepLen : stepLen);
			stepLen *= 0.5;
		}

		#ifdef PARALLAX_SHADOW
			if (parallaxShadow > 0.0){
				shadowVector = shadowVector * tbnMat;
				shadowVector /= shadowVector.z * 0.9 + 0.1;

				vec3 shadowTexelCoord = parallaxTexelCoord;
				vec3 shadowStep = shadowVector / float(PARALLAX_SHADOW_QUALITY);
				shadowStep.xy  *= textureResolution * PARALLAX_DEPTH * 0.25;

				#ifdef TAA
					shadowTexelCoord += shadowStep * BlueNoiseTemproal().x;
				#else
					shadowTexelCoord += shadowStep;
				#endif

				// Улучшенные мягкие параллакс-тени для стиля 3
				float penumbra = 1.0;
				for (int i = 0; i < PARALLAX_SHADOW_QUALITY; i++, shadowTexelCoord += shadowStep){
					if (shadowTexelCoord.z > 1.0) break;

					float sh = BilinearHeightSample(shadowTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
					#ifndef PROGRAM_TERRAIN
					, isBlock
					#endif
					);

					float diff = shadowTexelCoord.z - sh;
					// Используем smoothstep вместо жёсткого clamp — мягкая penumbra
					penumbra = min(penumbra, smoothstep(-0.03, 0.06, -diff));
					if (penumbra < 0.003) break;
				}
				parallaxShadow *= penumbra;
			}
			#ifdef PARALLAX_FADE
				parallaxShadow = mix(1.0, parallaxShadow, fade);
			#endif
		#endif

		parallaxTexelCoord.xy += 0.5;

		#if PARALLAX_BASED_NORMAL == 2
			// Улучшенные нормали для стиля 3: 5-точечный cross-Sobel
			// вместо 2-точечного — более гладкий результат
			ivec2 baseTexel = ivec2(parallaxTexelCoord.xy);

			float hC  = BilinearHeightSample(parallaxTexelCoord.xy,              tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
			float hPX = BilinearHeightSample(parallaxTexelCoord.xy + vec2(1,0),  tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
			float hNX = BilinearHeightSample(parallaxTexelCoord.xy + vec2(-1,0), tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
			float hPY = BilinearHeightSample(parallaxTexelCoord.xy + vec2(0,1),  tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
			float hNY = BilinearHeightSample(parallaxTexelCoord.xy + vec2(0,-1), tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);

			float dX = hNX - hPX;
			float dY = hNY - hPY;
			float scale = (4.0 / PARALLAX_DEPTH) / textureResolution;
			vec3 heightNormal = normalize(vec3(dX * scale, dY * scale, 1.0));

			// Blend с текстурной нормалью
			vec3 texNormal = DecodeNormalTex(texelFetch(normals, ivec2(parallaxTexelCoord.xy), 0).rgb);
			float edgeness = 1.0 - abs(dot(texNormal, vec3(0.0, 0.0, 1.0)));
			normalTex = normalize(mix(texNormal, heightNormal, smoothstep(0.1, 0.4, edgeness)));

			#ifdef PARALLAX_FADE
				normalTex = mix(vec3(0.0, 0.0, 1.0), normalTex, fade);
			#endif
		#endif

		parallaxTexelCoord.xy = (fract(parallaxTexelCoord.xy / textureResolution) + tilesBaseCoord) * textureResolution;
	} else {
		parallaxTexelCoord.xy += 0.5;
	}

	ivec2 parallaxTexel = ivec2(parallaxTexelCoord.xy);

	#if PARALLAX_BASED_NORMAL < 2
		normalTex = DecodeNormalTex(texelFetch(normals, parallaxTexel, 0).rgb);
	#endif

	return parallaxTexel;
}

// ============================================================
// PARALLAX_MODE == 4 : Ultra-Kappa Multi-Layer POM
//
// Полностью переписан. Реализует:
//   1. Многоуровневый (multi-layer) марчинг — грубый проход крупным
//      шагом, потом мелкий проход вблизи поверхности (два уровня LOD).
//   2. Catmull-Rom bicubic сэмплинг высот на каждом шаге.
//   3. Угловое закругление (Corner Rounding) — плавное скругление
//      переходов между тайлами, блоки выглядят «мягкими».
//   4. Secant + binary refinement с bicubic — точное пересечение.
//   5. 7-точечный Sobel по bicubic высотам — очень гладкие нормали.
//   6. Адаптивные мягкие параллакс-тени (soft penumbra).
//   7. Отражения только если material.hasReflections == true.
// ============================================================

#elif PARALLAX_MODE == 4

// ---- Вспомогательные функции ----

// Catmull-Rom вес (Mitchell-Netravali B=0 C=0.5)
float UltraCRWeight(float x){
	x = abs(x);
	if (x < 1.0) return ((1.5*x - 2.5)*x*x + 1.0);
	if (x < 2.0) return ((-0.5*x + 2.5)*x - 4.0)*x + 2.0;
	return 0.0;
}

// Получить один тексель высоты с tile-wrapping
float UltraTexelHeight(ivec2 tap, ivec2 tilesBaseTexel, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	#ifndef PROGRAM_TERRAIN
		if (isBlock)
	#endif
	tap = tilesBaseTexel + (tap & iTextureResolution);

	float h = texelFetch(normals, tap, 0).a;
	h += saturate(1.0 - h * 1e20); // 0 → 1.0 (flat)
	return h;
}

// Bicubic Catmull-Rom сэмпл высоты — максимально гладкий
float UltraBicubicHeight(vec2 coord, ivec2 tilesBaseTexel, float textureResolution, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	vec2  tc   = coord - 0.5;
	ivec2 base = ivec2(floor(tc));
	vec2  f    = tc - vec2(base);

	float wx[4], wy[4];
	for (int k = 0; k < 4; k++){
		wx[k] = UltraCRWeight(float(k-1) - f.x);
		wy[k] = UltraCRWeight(float(k-1) - f.y);
	}

	float result = 0.0;
	for (int j = 0; j < 4; j++){
		for (int i = 0; i < 4; i++){
			ivec2 tap = base + ivec2(i-1, j-1);
			float h = UltraTexelHeight(tap, tilesBaseTexel, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
			result += h * wx[i] * wy[j];
		}
	}
	return clamp(result, 0.0, 1.0);
}

// Corner Rounding — скругление углов тайлов
// strength: доля тайла, на которой идёт скругление (0.2 = 20% от края)
float UltraCornerRound(vec2 coord, float textureResolution, float strength){
	vec2 local = fract(coord / textureResolution) * textureResolution;
	vec2 edgeDist = min(local, textureResolution - local) / (textureResolution * 0.5);
	float edgeFactor = min(edgeDist.x, edgeDist.y);
	return smoothstep(0.0, strength, edgeFactor);
}

// Высота с corner rounding (блоки плавно стыкуются)
float UltraRoundedHeight(vec2 coord, ivec2 tilesBaseTexel, float textureResolution, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	float h = UltraBicubicHeight(coord, tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);

	#ifndef ULTRA_CORNER_RADIUS
		#define ULTRA_CORNER_RADIUS 0.20
	#endif

	float roundFactor = UltraCornerRound(coord, textureResolution, ULTRA_CORNER_RADIUS);
	return mix(1.0, h, roundFactor);
}

// 7-точечный Sobel по bicubic высотам — очень гладкие нормали
// Использует диагонали (+крест) для более точного градиента
vec3 UltraSmoothNormal(vec2 coord, ivec2 tilesBaseTexel, float textureResolution, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	// Крест + диагонали (7 точек: центр + 6 соседей)
	float hC  = UltraRoundedHeight(coord,              tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hPX = UltraRoundedHeight(coord + vec2(1,0),  tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hNX = UltraRoundedHeight(coord + vec2(-1,0), tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hPY = UltraRoundedHeight(coord + vec2(0,1),  tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hNY = UltraRoundedHeight(coord + vec2(0,-1), tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	// Диагонали для полного Sobel 3x3
	float hPXPY = UltraRoundedHeight(coord + vec2(1,1),   tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hNXNY = UltraRoundedHeight(coord + vec2(-1,-1), tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hPXNY = UltraRoundedHeight(coord + vec2(1,-1),  tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hNXPY = UltraRoundedHeight(coord + vec2(-1,1),  tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);

	// Собель 3x3 с весами: ось × 2, диагональ × 1
	float dX = (hNX - hPX) * 2.0 + (hNXPY - hPXPY) + (hNXNY - hPXNY);
	float dY = (hNY - hPY) * 2.0 + (hNXPY - hNXNY) + (hPXPY - hPXNY);

	float scale = (2.5 / PARALLAX_DEPTH) / textureResolution;
	return normalize(vec3(dX * scale, dY * scale, 1.0));
}

// ---- Основная функция ----

ivec2 ParallaxOcclusionMapping(vec2 coord, mat3 tbnMat, vec3 shadowVector, vec2 duv1, vec2 duv2, inout vec3 normalTex, inout float parallaxShadow){
	#ifndef PROGRAM_TERRAIN
		const float textureResolution = ENTITIES_TEXTURE_RESOLUTION;
		ivec2 atlasSize = textureSize(tex, 0);
		bool isBlock = textureSize(gaux4, 0) == atlasSize;
	#else
		ivec2 atlasSize = textureSize(tex, 0);
	#endif

	vec2 atlasTiles      = vec2(atlasSize) / textureResolution;
	vec2 tilesBaseCoord  = floor(coord * atlasTiles);
	ivec2 tilesBaseTexel = ivec2(tilesBaseCoord * textureResolution);
	int iTextureResolution = int(textureResolution) - 1;

	#ifdef PARALLAX_FADE
		vec2 duvMax = max(abs(duv1), abs(duv2)) * atlasTiles;
		float fade  = saturate(pow(max(duvMax.x, duvMax.y), -0.1) * (1.0 / 1.4));
	#endif

	vec3 parallaxTexelCoord = vec3(coord * vec2(atlasSize), 1.0);
	parallaxTexelCoord.xy  -= 0.5;

	// Начальный сэмпл
	float sampleHeight = UltraRoundedHeight(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);

	if (sampleHeight > 0.0 && sampleHeight < 1.0){

		vec3 viewVector = normalize(viewPos) * tbnMat;
		viewVector /= -viewVector.z;

		vec3 baseStep = viewVector;
		baseStep.xy  *= textureResolution * PARALLAX_DEPTH * 0.25;

		// ============================================================
		// MULTI-LAYER MARCHING
		// Уровень 1 (грубый): PARALLAX_QUALITY/2 крупных шагов.
		//   Быстро находим примерную зону пересечения.
		// Уровень 2 (мелкий): PARALLAX_QUALITY/2 мелких шагов в зоне.
		//   Точно локализуем поверхность.
		// ============================================================

		int coarseSteps = PARALLAX_QUALITY / 2;
		int fineSteps   = PARALLAX_QUALITY - coarseSteps;

		float t     = 0.0;
		float prevT = 0.0;
		float prevH = sampleHeight;
		vec3  curCoord  = parallaxTexelCoord;
		vec3  prevCoord = parallaxTexelCoord;

		// ---- Уровень 1: грубый марш (адаптивный шаг) ----
		bool hitCoarse = false;
		float coarseInvQ = 1.0 / float(coarseSteps);

		for (int i = 0; i < coarseSteps; i++){
			float gap = curCoord.z - sampleHeight;
			if (gap <= 0.0){ hitCoarse = true; break; }

			// Адаптивный dt: пропорционален зазору, от 0.3/Q до 2.0/Q
			float dt = clamp(gap * 1.2, 0.3 * coarseInvQ, 2.0 * coarseInvQ);
			prevT    = t;
			prevH    = sampleHeight;
			prevCoord = curCoord;
			t       += dt;

			curCoord = parallaxTexelCoord + baseStep * t;

			sampleHeight = UltraRoundedHeight(curCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
		}

		if (hitCoarse){
			// Уже внутри — откатываемся назад
			curCoord = prevCoord;
			t        = prevT;
			sampleHeight = prevH;
		}

		// ---- Уровень 2: мелкий марш в зоне пересечения ----
		// Диапазон: от prevT до prevT + 2*(t-prevT), мелкими шагами
		float zoneStart = prevT;
		float zoneSize  = max(t - prevT, coarseInvQ) * 1.5;
		float fineInvQ  = 1.0 / float(fineSteps);
		float fineDt    = zoneSize * fineInvQ;

		float fineT  = zoneStart;
		float fPrevT = zoneStart;
		float fPrevH = sampleHeight;
		vec3  fPrevCoord = curCoord;

		for (int i = 0; i < fineSteps; i++){
			fPrevT    = fineT;
			fPrevH    = sampleHeight;
			fPrevCoord = curCoord;
			fineT    += fineDt;

			curCoord = parallaxTexelCoord + baseStep * fineT;

			sampleHeight = UltraRoundedHeight(curCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);

			if (sampleHeight >= curCoord.z) break;
		}

		// ---- Secant interpolation (точное пересечение) ----
		float afterDepth  = sampleHeight - curCoord.z;
		float beforeDepth = fPrevH       - fPrevCoord.z;
		float denom = afterDepth - beforeDepth;
		float w = (abs(denom) > 1e-6) ? (afterDepth / denom) : 0.5;
		parallaxTexelCoord = mix(curCoord, fPrevCoord, clamp(w, 0.0, 1.0));

		// ---- Binary refinement с bicubic ----
		float stepLen = fineDt * 0.5;
		for (int i = 0; i < PARALLAX_MAX_REFINEMENTS; i++){
			sampleHeight = UltraRoundedHeight(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
			float diff = sampleHeight - parallaxTexelCoord.z;
			parallaxTexelCoord += baseStep * (diff > 0.0 ? -stepLen : stepLen);
			stepLen *= 0.5;
		}

		// ---- Мягкие параллакс-тени (soft penumbra) ----
		#ifdef PARALLAX_SHADOW
			if (parallaxShadow > 0.0){
				shadowVector = shadowVector * tbnMat;
				shadowVector /= shadowVector.z * 0.9 + 0.1;

				vec3 shadowTexelCoord = parallaxTexelCoord;
				vec3 shadowBase = shadowVector;
				shadowBase.xy  *= textureResolution * PARALLAX_DEPTH * 0.25;

				vec3 shadowStep = shadowBase / float(PARALLAX_SHADOW_QUALITY);

				#ifdef TAA
					shadowTexelCoord += shadowStep * BlueNoiseTemproal().x;
				#else
					shadowTexelCoord += shadowStep;
				#endif

				float penumbra = 1.0;
				float minRatio = 1.0;  // для PCSS-like оценки мягкости

				for (int i = 0; i < PARALLAX_SHADOW_QUALITY; i++, shadowTexelCoord += shadowStep){
					if (shadowTexelCoord.z > 1.0) break;

					float sh = UltraRoundedHeight(shadowTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
					#ifndef PROGRAM_TERRAIN
					, isBlock
					#endif
					);

					float diff = shadowTexelCoord.z - sh;
					// Мягкая тень с учётом расстояния до блокатора
					float shadowRatio = diff / (shadowTexelCoord.z + 0.001);
					minRatio = min(minRatio, shadowRatio);

					// Ширина penumbra зависит от расстояния до блокатора
					float penumbraWidth = max(0.02, diff * 0.3);
					penumbra = min(penumbra, smoothstep(-penumbraWidth, penumbraWidth, -diff));

					if (penumbra < 0.003) break;
				}
				parallaxShadow *= penumbra;
			}
			#ifdef PARALLAX_FADE
				parallaxShadow = mix(1.0, parallaxShadow, fade);
			#endif
		#endif

		parallaxTexelCoord.xy += 0.5;

		// ---- Нормали: 9-точечный Sobel по bicubic высотам ----
		#if PARALLAX_BASED_NORMAL >= 2
			vec3 ultraNormal = UltraSmoothNormal(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);

			// Blend с текстурной нормалью: плавный переход
			vec3 texNormal = DecodeNormalTex(texelFetch(normals, ivec2(parallaxTexelCoord.xy), 0).rgb);
			float edgeness = 1.0 - abs(dot(texNormal, vec3(0.0, 0.0, 1.0)));
			// Более широкий диапазон blend — очень плавный переход
			normalTex = normalize(mix(texNormal, ultraNormal, smoothstep(0.03, 0.55, edgeness)));

			#ifdef PARALLAX_FADE
				normalTex = mix(vec3(0.0, 0.0, 1.0), normalTex, fade);
			#endif
		#endif

		parallaxTexelCoord.xy = (fract(parallaxTexelCoord.xy / textureResolution) + tilesBaseCoord) * textureResolution;
	} else {
		parallaxTexelCoord.xy += 0.5;
	}

	ivec2 parallaxTexel = ivec2(parallaxTexelCoord.xy);

	#if PARALLAX_BASED_NORMAL < 2
		normalTex = DecodeNormalTex(texelFetch(normals, parallaxTexel, 0).rgb);
	#endif

	return parallaxTexel;
}

// ============================================================
// PARALLAX_MODE == 5 : Kappa PBR Style
//
// Полноценная реализация Kappa-стиля POM:
//   1. Catmull-Rom bicubic сэмплинг высот — вся поверхность гладкая.
//   2. Corner Rounding — плавная стыковка между тайлами.
//   3. Адаптивный шаг во время марша.
//   4. Secant + binary refinement поверх bicubic.
//   5. 5-точечный Sobel + blend с текстурной нормалью.
//   6. Soft penumbra параллакс-тени.
//   Отражения только если material.hasReflections == true.
// ============================================================

#elif PARALLAX_MODE == 5

// ---- Вспомогательные функции ----

// Catmull-Rom сплайновый вес (Mitchell-Netravali B=0 C=0.5)
float KappaCRWeight(float x){
	x = abs(x);
	if (x < 1.0) return ((1.5*x - 2.5)*x*x + 1.0);
	if (x < 2.0) return ((-0.5*x + 2.5)*x - 4.0)*x + 2.0;
	return 0.0;
}

// Получить один тексель высоты с tile-wrapping
float KappaTexelHeight(ivec2 tap, ivec2 tilesBaseTexel, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	#ifndef PROGRAM_TERRAIN
		if (isBlock)
	#endif
	tap = tilesBaseTexel + (tap & iTextureResolution);

	float h = texelFetch(normals, tap, 0).a;
	// Если h == 0 — нет данных (flat), заменяем на 1.0
	h += saturate(1.0 - h * 1e20);
	return h;
}

// Bicubic (Catmull-Rom) сэмпл высоты — гладкий по всей поверхности
float KappaBicubicHeight(vec2 coord, ivec2 tilesBaseTexel, float textureResolution, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	vec2  tc   = coord - 0.5;
	ivec2 base = ivec2(floor(tc));
	vec2  f    = tc - vec2(base);

	// Предвычислим веса по X и Y (4 штуки каждый)
	float wx[4], wy[4];
	for (int k = 0; k < 4; k++){
		wx[k] = KappaCRWeight(float(k-1) - f.x);
		wy[k] = KappaCRWeight(float(k-1) - f.y);
	}

	float result = 0.0;
	// Catmull-Rom: сумма весов = 1.0 по построению, нормировка не нужна
	for (int j = 0; j < 4; j++){
		for (int i = 0; i < 4; i++){
			ivec2 tap = base + ivec2(i-1, j-1);
			float h = KappaTexelHeight(tap, tilesBaseTexel, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
			result += h * wx[i] * wy[j];
		}
	}

	// Catmull-Rom может выйти за [0,1] на резких перепадах — clamp обязателен
	return clamp(result, 0.0, 1.0);
}

// Угловое смягчение высоты (Corner Rounding).
float KappaCornerRound(vec2 coord, float textureResolution, float strength){
	// coord в пикселях внутри тайла [0..textureResolution]
	vec2 local = fract(coord / textureResolution) * textureResolution;
	// расстояние от края тайла (0 = на краю, 1 = в центре)
	vec2 edgeDist = min(local, textureResolution - local) / (textureResolution * 0.5);
	float edgeFactor = min(edgeDist.x, edgeDist.y);
	// плавный «подъём» высоты у края
	return smoothstep(0.0, strength, edgeFactor);
}

// Применяет corner rounding к высоте: у края высота подтягивается к 1.0 (flat)
float KappaRoundedHeight(vec2 coord, ivec2 tilesBaseTexel, float textureResolution, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	float h = KappaBicubicHeight(coord, tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);

	#ifndef KAPPA_CORNER_RADIUS
		#define KAPPA_CORNER_RADIUS 0.15
	#endif

	float roundFactor = KappaCornerRound(coord, textureResolution, KAPPA_CORNER_RADIUS);
	// Интерполируем: 0 = flat (1.0), 1 = оригинальная высота
	return mix(1.0, h, roundFactor);
}

// 5-точечный Sobel по bicubic высотам — более гладкий градиент нормалей
vec3 KappaSmoothNormal(vec2 coord, ivec2 tilesBaseTexel, float textureResolution, int iTextureResolution
#ifndef PROGRAM_TERRAIN
, bool isBlock
#endif
){
	// Сэмплируем 5 точек крестом
	float hC  = KappaRoundedHeight(coord,              tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hPX = KappaRoundedHeight(coord + vec2(1,0),  tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hNX = KappaRoundedHeight(coord + vec2(-1,0), tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hPY = KappaRoundedHeight(coord + vec2(0,1),  tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);
	float hNY = KappaRoundedHeight(coord + vec2(0,-1), tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);

	float dX = hNX - hPX;
	float dY = hNY - hPY;

	// Масштаб нормали: больше PARALLAX_DEPTH → крупнее детали → мягче нормаль
	float scale = (3.0 / PARALLAX_DEPTH) / textureResolution;
	return normalize(vec3(dX * scale, dY * scale, 1.0));
}

// ---- Основная функция ----

ivec2 ParallaxOcclusionMapping(vec2 coord, mat3 tbnMat, vec3 shadowVector, vec2 duv1, vec2 duv2, inout vec3 normalTex, inout float parallaxShadow){
	#ifndef PROGRAM_TERRAIN
		const float textureResolution = ENTITIES_TEXTURE_RESOLUTION;
		ivec2 atlasSize = textureSize(tex, 0);
		bool isBlock = textureSize(gaux4, 0) == atlasSize;
	#else
		ivec2 atlasSize = textureSize(tex, 0);
	#endif

	vec2 atlasTiles      = vec2(atlasSize) / textureResolution;
	vec2 tilesBaseCoord  = floor(coord * atlasTiles);
	ivec2 tilesBaseTexel = ivec2(tilesBaseCoord * textureResolution);
	int iTextureResolution = int(textureResolution) - 1;

	#ifdef PARALLAX_FADE
		vec2 duvMax = max(abs(duv1), abs(duv2)) * atlasTiles;
		float fade  = saturate(pow(max(duvMax.x, duvMax.y), -0.1) * (1.0 / 1.4));
	#endif

	vec3 parallaxTexelCoord = vec3(coord * vec2(atlasSize), 1.0);
	parallaxTexelCoord.xy  -= 0.5;

	// Начальный сэмпл с corner rounding
	float sampleHeight = KappaRoundedHeight(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
	#ifndef PROGRAM_TERRAIN
	, isBlock
	#endif
	);

	if (sampleHeight > 0.0 && sampleHeight < 1.0){

		vec3 viewVector = normalize(viewPos) * tbnMat;
		viewVector /= -viewVector.z;

		float invQ   = 1.0 / float(PARALLAX_QUALITY);
		vec3 baseStep = viewVector;
		baseStep.xy  *= textureResolution * PARALLAX_DEPTH * 0.25;

		float prevH     = sampleHeight;
		vec3  prevCoord = parallaxTexelCoord;

		// Марш с адаптивным шагом
		float t     = 0.0;
		float prevT = 0.0;

		for (int i = 0; i < PARALLAX_QUALITY; i++){
			float gap = parallaxTexelCoord.z - sampleHeight;
			if (gap <= 0.0) break;

			// Адаптивный dt: от 0.3/Q до 1.0/Q
			float dt = clamp(gap * 0.8, 0.3 * invQ, invQ);
			prevT     = t;
			prevH     = sampleHeight;
			prevCoord = parallaxTexelCoord;
			t        += dt;

			parallaxTexelCoord = vec3(coord * vec2(atlasSize), 1.0) - vec3(0.5, 0.5, 0.0);
			parallaxTexelCoord += baseStep * t;

			sampleHeight = KappaRoundedHeight(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
		}

		// Secant interpolation между двумя последними позициями
		float afterDepth  = sampleHeight - parallaxTexelCoord.z;
		float beforeDepth = prevH        - prevCoord.z;
		float denom = afterDepth - beforeDepth;
		float weight = (abs(denom) > 1e-6) ? (afterDepth / denom) : 0.5;
		parallaxTexelCoord = mix(parallaxTexelCoord, prevCoord, clamp(weight, 0.0, 1.0));

		// Binary refinement с bicubic (+ corner rounding)
		float stepLen = (t - prevT) * 0.5;
		for (int i = 0; i < PARALLAX_MAX_REFINEMENTS; i++){
			sampleHeight = KappaRoundedHeight(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);
			float diff = sampleHeight - parallaxTexelCoord.z;
			parallaxTexelCoord += baseStep * (diff > 0.0 ? -stepLen : stepLen);
			stepLen *= 0.5;
		}

		// ---- Параллакс-тени ----
		#ifdef PARALLAX_SHADOW
			if (parallaxShadow > 0.0){
				shadowVector = shadowVector * tbnMat;
				shadowVector /= shadowVector.z * 0.9 + 0.1;

				vec3 shadowTexelCoord = parallaxTexelCoord;
				vec3 shadowBase = shadowVector;
				shadowBase.xy  *= textureResolution * PARALLAX_DEPTH * 0.25;

				vec3 shadowStep = shadowBase / float(PARALLAX_SHADOW_QUALITY);

				#ifdef TAA
					shadowTexelCoord += shadowStep * BlueNoiseTemproal().x;
				#else
					shadowTexelCoord += shadowStep;
				#endif

				// Soft penumbra через smoothstep — плавная тень без резких краёв
				float penumbra = 1.0;
				for (int i = 0; i < PARALLAX_SHADOW_QUALITY; i++, shadowTexelCoord += shadowStep){
					if (shadowTexelCoord.z > 1.0) break;

					float sh = KappaRoundedHeight(shadowTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
					#ifndef PROGRAM_TERRAIN
					, isBlock
					#endif
					);

					float diff = shadowTexelCoord.z - sh;
					// smoothstep шире чем в mode 3/4 — даёт более мягкую тень
					penumbra = min(penumbra, smoothstep(-0.05, 0.05, -diff));
					if (penumbra < 0.003) break;
				}
				parallaxShadow *= penumbra;
			}
			#ifdef PARALLAX_FADE
				parallaxShadow = mix(1.0, parallaxShadow, fade);
			#endif
		#endif

		parallaxTexelCoord.xy += 0.5;

		// ---- Нормали ----
		#if PARALLAX_BASED_NORMAL >= 2
			// Smooth Kappa-нормаль через 5-точечный Sobel по bicubic высотам
			vec3 kappaSmooth = KappaSmoothNormal(parallaxTexelCoord.xy, tilesBaseTexel, textureResolution, iTextureResolution
			#ifndef PROGRAM_TERRAIN
			, isBlock
			#endif
			);

			// Blend с текстурной нормалью
			vec3 texNormal = DecodeNormalTex(texelFetch(normals, ivec2(parallaxTexelCoord.xy), 0).rgb);
			float edgeness = 1.0 - abs(dot(texNormal, vec3(0.0, 0.0, 1.0)));
			// Более широкий smoothstep чем раньше — более мягкий переход
			normalTex = normalize(mix(texNormal, kappaSmooth, smoothstep(0.05, 0.5, edgeness)));

			#ifdef PARALLAX_FADE
				normalTex = mix(vec3(0.0, 0.0, 1.0), normalTex, fade);
			#endif
		#endif

		parallaxTexelCoord.xy = (fract(parallaxTexelCoord.xy / textureResolution) + tilesBaseCoord) * textureResolution;
	} else {
		parallaxTexelCoord.xy += 0.5;
	}

	ivec2 parallaxTexel = ivec2(parallaxTexelCoord.xy);

	#if PARALLAX_BASED_NORMAL < 2
		normalTex = DecodeNormalTex(texelFetch(normals, parallaxTexel, 0).rgb);
	#endif

	return parallaxTexel;
}

#elif PARALLAX_MODE == 1

ivec2 ParallaxOcclusionMapping(vec2 coord, mat3 tbnMat, vec3 shadowVector, vec2 duv1, vec2 duv2, inout vec3 hitNormal, inout float parallaxShadow){
	#ifndef PROGRAM_TERRAIN
		const float textureResolution = ENTITIES_TEXTURE_RESOLUTION;
		ivec2 atlasSize = textureSize(tex, 0);

		bool isBlock = textureSize(gaux4, 0) == atlasSize;
	#endif

	vec2 atlasTiles = vec2(atlasSize) / textureResolution;

	vec2 pixelCoord = coord * vec2(atlasSize);
	ivec2 parallaxTexel = ivec2(floor(pixelCoord));
	int iTextureResolution = int(textureResolution) - 1;

	float sampleHeight = texelFetch(normals, parallaxTexel, 0).a;
	sampleHeight += saturate(1.0 - sampleHeight * 1e20);

	bool exit = false;

	if (sampleHeight > 0.0 && sampleHeight < 1.0){
		#ifdef PARALLAX_FADE
			vec2 duvMax = max(abs(duv1), abs(duv2)) * atlasTiles;
			float fade = saturate(pow(max(duvMax.x, duvMax.y), -0.4) * 0.2);
		#endif

		ivec2 tilesPixelBase = ivec2(floor(coord * atlasTiles) * textureResolution);

		vec3 viewVector = normalize(viewPos) * tbnMat;
		vec3 stepDir = normalize(vec3(viewVector.xy * PARALLAX_DEPTH * textureResolution * 0.25, -viewVector.z));

		vec2 ardir = abs(1.0 / stepDir.xy);
		ivec2 sdir = (floatBitsToInt(stepDir.xy) >> 31) * 2 + 1;

		vec2 totalStep = (vec2(sdir) * (0.5 - (pixelCoord - vec2(parallaxTexel))) + 0.5) * ardir;

		float stepLength = 0.0;
		float stepHeight = 1.0;
		float prevStepHeight = 1.0;
		ivec2 stepNext = ivec2(0);

		for (int i = 0; i < PARALLAX_QUALITY; i++){
			stepLength = min(totalStep.x, totalStep.y);
			stepHeight = 1.0 - stepLength * stepDir.z;
			if (sampleHeight > stepHeight){
				if (sampleHeight > prevStepHeight){
					hitNormal = vec3(-stepNext * sdir, 0.0);
					sampleHeight = prevStepHeight;
				}
				exit = true;
				break;
			}

			stepNext = (floatBitsToInt(vec2(stepLength) - totalStep) >> 31) + 1;
			parallaxTexel += stepNext * sdir;
			totalStep += vec2(stepNext) * ardir;
			prevStepHeight = stepHeight;

			#ifndef PROGRAM_TERRAIN
				if (isBlock)
			#endif
			parallaxTexel = tilesPixelBase + (parallaxTexel & iTextureResolution);
			sampleHeight = texelFetch(normals, parallaxTexel, 0).a;
			sampleHeight += saturate(1.0 - sampleHeight * 1e20);
		}


		#ifdef PARALLAX_SHADOW
			shadowVector = shadowVector * tbnMat;

			float shadow = 1.0;

			if(dot(shadowVector, hitNormal) * parallaxShadow > 0.0 && exit){
				float currHeight = sampleHeight;
				stepLength = (1.0 - currHeight) / stepDir.z;

				vec2 shadowTexelCoord = pixelCoord + stepLength * stepDir.xy;
				ivec2 shadowTexel = ivec2(floor(shadowTexelCoord));

				stepDir = normalize(vec3(shadowVector.xy * PARALLAX_DEPTH * textureResolution * 0.25, shadowVector.z));

				vec2 ardir = abs(1.0 / stepDir.xy);
				ivec2 sdir = (floatBitsToInt(stepDir.xy) >> 31) * 2 + 1;

				totalStep = (vec2(sdir) * (0.5 - (shadowTexelCoord - shadowTexel)) + 0.5) * ardir;
				stepNext = ivec2(0);

				for (int i = 0; i < PARALLAX_SHADOW_QUALITY; i++){
					stepLength = min(totalStep.x, totalStep.y);
					stepHeight = currHeight + stepLength * stepDir.z;

					stepNext = (floatBitsToInt(vec2(stepLength) - totalStep) >> 31) + 1;
					shadowTexel += stepNext * sdir;
					totalStep += vec2(stepNext) * ardir;

					#ifndef PROGRAM_TERRAIN
						if (isBlock)
					#endif
					shadowTexel = tilesPixelBase + (shadowTexel & iTextureResolution);
					sampleHeight = texelFetch(normals, shadowTexel, 0).a;
					sampleHeight += saturate(1.0 - sampleHeight * 1e20);

					if (sampleHeight > stepHeight){
						parallaxShadow = 0.0;
						break;
					}
				}

				#ifdef PARALLAX_FADE
					parallaxShadow *= mix(1.0, shadow, fade);
				#else
					parallaxShadow *= shadow;
				#endif
			}
		#endif

	#if PARALLAX_BASED_NORMAL == 1
		#ifdef PARALLAX_FADE
			hitNormal = mix(DecodeNormalTex(texelFetch(normals, parallaxTexel, 0).rgb), hitNormal, fade * (1.0 - hitNormal.z));
		#else
			if(hitNormal.z == 1.0) hitNormal = DecodeNormalTex(texelFetch(normals, parallaxTexel, 0).rgb);
		#endif
		}else{
			hitNormal = DecodeNormalTex(texelFetch(normals, parallaxTexel, 0).rgb);
	#elif PARALLAX_BASED_NORMAL == 2 && defined PARALLAX_FADE
		hitNormal = mix(vec3(0.0, 0.0, 1.0), hitNormal, fade);
	#endif
	}

	#if PARALLAX_BASED_NORMAL == 0
		hitNormal = DecodeNormalTex(texelFetch(normals, parallaxTexel, 0).rgb);
	#endif

	return parallaxTexel;
}

#endif


#ifdef PARALLAX_SHADOW
    float CalculateParallaxShadow(in vec3 tangentLightVector, in vec3 offsetCoord, in mat2 texGrad, in float dither) {
        float parallaxShadow = 1.0;

        vec3 stepSize = vec3(tangentLightVector.xy, 1.0) * offsetCoord.z * rcp(PARALLAX_SAMPLES);
        stepSize.xy *= PARALLAX_DEPTH * rcp(tangentLightVector.z);
        stepSize *= 2.0 / PARALLAX_SAMPLES;

        offsetCoord += stepSize * dither;
        for (uint i = 1u; i < PARALLAX_SAMPLES; ++i) {
            offsetCoord += stepSize * i;

            #ifdef SMOOTH_PARALLAX
                float sampleHeight = BilinearHeightSample(OffsetCoord(offsetCoord.xy));
            #else
                float sampleHeight = textureGrad(normals, OffsetCoord(offsetCoord.xy), texGrad[0], texGrad[1]).a;
            #endif

            parallaxShadow *= float(offsetCoord.z > sampleHeight);
            if (parallaxShadow < 1e-4) break;
        }

        return 1.0 - parallaxShadow;
    }
#endif

// ============================================================
// REFLECTION GUARD для стилей 3, 4, 5
//
// Использование в шейдере отражений:
//
//   #if PARALLAX_MODE >= 3
//       if (!material.hasReflections) {
//           // Полностью пропускаем отражения для этого блока
//           reflectionColor = vec3(0.0);
//       }
//   #endif
//
// material.hasReflections вычисляется в Material.inc:
//   material.hasReflections = specTex.r + material.isMetal > 5e-3;
//
// Таким образом блоки без specular-текстуры (камень, земля, дерево)
// автоматически не получают отражений в стилях 3, 4 и 5.
// ============================================================
