#version 120

// DRAWING STYLE SYSTEM
#define DRAWING_STYLE 4 // Drawing Style [1 2 3 4]

// Style-dependent settings resolution
#if DRAWING_STYLE == 1
    // Pencil on Paper Style
    #define STYLE_GRAYSCALE_STRENGTH 1.0
	#define STYLE_ADVANCED_GRAIN_SYSTEM 1
	#define STYLE_OUTLINE_STRENGTH 1.5
	#define STYLE_PENCIL_CORE_STRENGTH 0.7
	#define STYLE_KUWAHARA_ENABLED 0
	
	#define STYLE_DISABLE_TEXTURES 0
	#define STYLE_PENCIL_R 0.25
	#define STYLE_PENCIL_G 0.25
	#define STYLE_PENCIL_B 0.30
#elif DRAWING_STYLE == 2
    // Colorful Sketch Style
    #define STYLE_GRAYSCALE_STRENGTH 0.0
	#define STYLE_ADVANCED_GRAIN_SYSTEM 0
	#define STYLE_OUTLINE_STRENGTH 1.5
	#define STYLE_PENCIL_CORE_STRENGTH 0.7
	#define STYLE_KUWAHARA_ENABLED 0
	
	#define STYLE_DISABLE_TEXTURES 0
	#define STYLE_PENCIL_R 0.25
	#define STYLE_PENCIL_G 0.25
	#define STYLE_PENCIL_B 0.30
#elif DRAWING_STYLE == 3
	// Blue Pen Style
	#define STYLE_GRAYSCALE_STRENGTH 1.0
	#define STYLE_ADVANCED_GRAIN_SYSTEM 1
	#define STYLE_OUTLINE_STRENGTH 1.5
	#define STYLE_PENCIL_CORE_STRENGTH 0.7
	#define STYLE_KUWAHARA_ENABLED 0
	
	#define STYLE_DISABLE_TEXTURES 1
	#define STYLE_PENCIL_R 0.05
	#define STYLE_PENCIL_G 0.05
	#define STYLE_PENCIL_B 0.60
#elif DRAWING_STYLE == 4
    // Painterly (Oil Paint) Style - uses Kuwahara filter
    #define STYLE_GRAYSCALE_STRENGTH 0.0
    #define STYLE_ADVANCED_GRAIN_SYSTEM 0
	#define STYLE_OUTLINE_STRENGTH 0.5
	#define STYLE_PENCIL_CORE_STRENGTH 0.4
	#define STYLE_KUWAHARA_ENABLED 1
    
    #define STYLE_DISABLE_TEXTURES 0
    #define STYLE_PENCIL_R 0.25
    #define STYLE_PENCIL_G 0.25
    #define STYLE_PENCIL_B 0.30
#else
    // Fallback to Pencil style
    #define STYLE_GRAYSCALE_STRENGTH 1.0
	#define STYLE_ADVANCED_GRAIN_SYSTEM 0
	#define STYLE_OUTLINE_STRENGTH 1.5
	#define STYLE_PENCIL_CORE_STRENGTH 0.7
	#define STYLE_KUWAHARA_ENABLED 0
	
	#define STYLE_DISABLE_TEXTURES 0
	#define STYLE_PENCIL_R 0.25
	#define STYLE_PENCIL_G 0.25
	#define STYLE_PENCIL_B 0.30
#endif

// Paper Settings
#define PAPER_STYLE 1 // Enable paper style [0 1]
#define DISABLE_TEXTURES STYLE_DISABLE_TEXTURES // Permanently disable all texture rendering [0 1]
#define PAPER_WHITENESS 1.0 // How white the paper is [0.5 0.6 0.7 0.8 0.9 1.0]
#define PAPER_MAX_BRIGHTNESS 0.75 // Maximum paper brightness multiplier [0.7 0.75 0.8 0.85 0.9 0.95 1.0]
#define PAPER_MIN_BRIGHTNESS 0.05 // Minimum paper brightness [0.05 0.1 0.15 0.2 0.25 0.3]
#define PAPER_BRIGHTNESS_SOFTNESS 0.05 // How soft the minimum clamp is [0.02 0.05 0.1 0.15]
#define PAPER_WARMTH 0.2 // How warm/sepia the paper tone is [0.0 0.1 0.15 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0]
#define PAPER_TEXTURE_STRENGTH 0.05 // Paper texture intensity [0.0 0.05 0.08 0.15 0.3]
#define PAPER_TEXTURE_SCALE 64.0 // Paper texture scale [8.0 16.0 32.0 64.0]
#define PAPER_TEXTURE_SPEED 2.0 // Paper texture update rate [0.1 0.5 1.0 2.0 5.0 10.0]
#define PAPER_CRINKLE_STRENGTH 0.03 // Paper crinkle/ridge intensity [0.0 0.02 0.03 0.08 0.15]
#define PAPER_CRINKLE_SCALE 8.0 // Paper crinkle scale [2.0 4.0 8.0 16.0]
#define DARKNESS_THRESHOLD 0.1 // Below this brightness, preserve darkness [0.1 0.15 0.2 0.3 0.4]
#define DARKNESS_SMOOTHNESS 0.05 // How smoothly darkness transitions [0.02 0.05 0.1 0.15]
#define GRAYSCALE_STRENGTH STYLE_GRAYSCALE_STRENGTH // How much to convert to grayscale [0.0 0.125 0.25 0.375 0.5 0.625 0.75 0.875 1.0]	

// MASTER GRAIN SYSTEM TOGGLE
#define ADVANCED_GRAIN_SYSTEM STYLE_ADVANCED_GRAIN_SYSTEM // Enable advanced grain interaction system [0 1] - Set to 0 for original basic paper texture

// Advanced Pencil Grain Interaction Settings (only used when ADVANCED_GRAIN_SYSTEM = 1)
#define PENCIL_GRAIN_ENABLED 1 // Enable pencil-paper grain interaction [0 1]
#define GRAIN_HEIGHT_SCALE 128.0 // Scale of grain height map [64.0 96.0 128.0 192.0 256.0]
#define GRAIN_INTENSITY 1.2 // Overall grain effect strength [0.4 0.6 0.8 1.0 1.2]
#define GRAIN_DIRECTIONAL_BIAS 0.5 // Diagonal bias strength [0.0 0.2 0.3 0.4 0.5]
#define GRAIN_FINE_DETAIL 0.6 // Fine speckle detail strength [0.2 0.4 0.6 0.8 1.0]
#define PRESSURE_SOFTENING 0.4 // How much pressure softens grain effect [0.2 0.3 0.4 0.5 0.6]

// Graphite-on-paper (multi-layer) controls (only used when ADVANCED_GRAIN_SYSTEM = 1)
#define GRAIN_L1_SCALE        192.0   // fine light speckle (higher -> finer) [32.0 64.0 96.0 128.0 192.0 256.0]
#define GRAIN_L2_SCALE         96.0   // darker, sparser flecks (lower -> larger blobs) [32.0 64.0 96.0 128.0 192.0 256.0]

// Pencil Line Settings
#define OUTLINE_STRENGTH STYLE_OUTLINE_STRENGTH // Overall outline intensity [0.0 0.25 0.5 0.8 1.0 1.5]
#define FINAL_THICKNESS_BOOST 2.0 // Overall thickness multiplier [1.0 2.0 4.0 6.0]
#define PENCIL_CORE_STRENGTH STYLE_PENCIL_CORE_STRENGTH // How dark the line center is [0.3 0.4 0.5 0.6 0.7 0.8 0.9]
#define THIN_LINE_FADE_FACTOR 2.0 // How much thinner lines fade [1.0 1.5 2.0 3.0]
#define ENABLE_SOFT_PENCIL_LINE 1 // Soft or solid pencil line [0 1]
#define PENCIL_R STYLE_PENCIL_R //Pencil color red [0.00 0.05 0.10 0.15 0.20 0.25 0.30 0.35 0.40 0.45 0.50 0.55 0.60 0.65 0.70 0.75 0.80 0.85 0.90 0.95 1.00]
#define PENCIL_G STYLE_PENCIL_G //Pencil color blue [0.00 0.05 0.10 0.15 0.20 0.25 0.30 0.35 0.40 0.45 0.50 0.55 0.60 0.65 0.70 0.75 0.80 0.85 0.90 0.95 1.00]
#define PENCIL_B STYLE_PENCIL_B //Pencil color green [0.00 0.05 0.10 0.15 0.20 0.25 0.30 0.35 0.40 0.45 0.50 0.55 0.60 0.65 0.70 0.75 0.80 0.85 0.90 0.95 1.00]

// Pencil Line Variation Effects
#define PENCIL_LINE_VARIATION 1 // Enable pencil line variation [0 1]
#define PENCIL_GRAIN_INTENSITY 0.4 // Pencil grain/texture intensity [0.0 0.2 0.4 0.6 0.8]
#define PENCIL_PRESSURE_VARIATION 0.6 // Simulated pressure variation [0.0 0.3 0.6 0.9]
#define LINE_BLUR_RADIUS 1.5 // Edge blur radius for pencil lines [0.5 1.0 1.5 2.0]

// Color-adaptive pencil settings (DRAWING_STYLE 2 only)
#define COLOR_ADAPTIVE_PENCIL 1 // Make pencil adopt underlying colors [0 1]
#define PENCIL_DARKEN_AMOUNT 0.3 // How much to darken underlying color [0.2 0.3 0.4 0.5 0.6 0.7 0.8]
#define PENCIL_SATURATION_BOOST 2.0 // Saturation multiplier for pencil [1.0 1.1 1.2 1.3 1.5 1.7 2.0]
#define PENCIL_BRIGHTNESS_BIAS 1.0 // Scale effect with brightness (1.0 = more on dark, less on light) [0.0 0.25 0.5 0.75 1.0]

// Bloom Settings
#define BLOOM_ENABLED 1 // Enable bloom effect [0 1]
#define BLOOM_INTENSITY 0.9 // Bloom intensity [0.1 0.3 0.6 0.9 1.2]

// Draw-On Animation Settings
#define DRAW_ON_ENABLED 1 // Enable draw-on effect [0 1]
#define TEXTURE_START_TIME 1.7 // When textures start appearing [5.0 6.0 7.0]
#define TEXTURE_DURATION 0.5// Duration for textures to fully appear [1.0 2.0 3.0]

// Color Grading Controls
#define COLOR_GRADING_ENABLED 1 // Enable color grading system [0 1]
#define VIBRANCE_AMOUNT 0.00 // Vibrance adjustment [-1.00 -0.94 -0.88 -0.81 -0.75 -0.69 -0.62 -0.56 -0.50 -0.44 -0.38 -0.31 -0.25 -0.19 -0.12 -0.06 0.00 0.06 0.12 0.19 0.25 0.31 0.38 0.44 0.50 0.56 0.62 0.69 0.75 0.81 0.88 0.94 1.00]
#define SATURATION_AMOUNT 1.00 // Saturation multiplier [0.00 0.06 0.12 0.19 0.25 0.31 0.38 0.44 0.50 0.56 0.62 0.69 0.75 0.81 0.88 0.94 1.00 1.06 1.12 1.19 1.25 1.31 1.38 1.44 1.50 1.56 1.62 1.69 1.75 1.81 1.88 1.94 2.00]
#define CONTRAST_AMOUNT 1.00 // Contrast adjustment [0.10 0.20 0.30 0.40 0.50 0.60 0.70 0.80 0.90 1.00 1.10 1.20 1.30 1.40 1.50 1.60 1.70 1.80 1.90 2.00 2.10 2.20 2.30 2.40 2.50 2.60 2.70 2.80 2.90 3.00]
#define BRIGHTNESS_FACTOR 0.00 // Brightness adjustment [-1.00 -0.88 -0.75 -0.62 -0.50 -0.38 -0.25 -0.12 0.00 0.12 0.25 0.38 0.50 0.62 0.75 0.88 1.00 1.12 1.25 1.38 1.50 1.62 1.75 1.88 2.00 2.12 2.25 2.38 2.50 2.62 2.75 2.88 3.00]
#define LIFT_AMOUNT 0.00 // Lift (shadows)  [-1.00 -0.94 -0.88 -0.81 -0.75 -0.69 -0.62 -0.56 -0.50 -0.44 -0.38 -0.31 -0.25 -0.19 -0.12 -0.06 0.00 0.06 0.12 0.19 0.25 0.31 0.38 0.44 0.50 0.56 0.62 0.69 0.75 0.81 0.88 0.94 1.00]
#define GAMMA_AMOUNT 1.00 // Gamma (midtones) [0.10 0.20 0.30 0.40 0.50 0.60 0.70 0.80 0.90 1.00 1.10 1.20 1.30 1.40 1.50 1.60 1.70 1.80 1.90 2.00 2.10 2.20 2.30 2.40 2.50 2.60 2.70 2.80 2.90 3.00]
#define GAIN_AMOUNT 1.00 // Gain (highlights) [0.00 0.06 0.12 0.19 0.25 0.31 0.38 0.44 0.50 0.56 0.62 0.69 0.75 0.81 0.88 0.94 1.00 1.06 1.12 1.19 1.25 1.31 1.38 1.44 1.50 1.56 1.62 1.69 1.75 1.81 1.88 1.94 2.00]
#define COLOR_TEMP_AMOUNT 0.00 // Color Temperature: negative = cooler (blue), positive = warmer (orange) [-1.00 -0.94 -0.88 -0.81 -0.75 -0.69 -0.62 -0.56 -0.50 -0.44 -0.38 -0.31 -0.25 -0.19 -0.12 -0.06 0.00 0.06 0.12 0.19 0.25 0.31 0.38 0.44 0.50 0.56 0.62 0.69 0.75 0.81 0.88 0.94 1.00]

// Color Palette Settings
#define PALETTE_ENABLED 1 // Enable color colorPalette quantization [0 1]
// Palette source:
//   0 = Custom 1xN  (place CustomPalette.png in shaders/palettes/ — brute-force, slower)
//   1 = Custom LUT  (place CustomPalette_lut.png in shaders/luts/  — fast, recommended)
//   2 = Faber-Castell Polychromos 120 (built-in LUT)
//   3 = Prismacolor Premier 150       (built-in LUT)
#define PALETTE_BRAND 2 // Palette source [0 1 2 3]
#define PALETTE_CUSTOM_1XN_SIZE 72 // Custom 1xN palette color count [8 12 16 24 32 36 48 60 64 72 120 128 150]

// Derive whether to use the LUT path and the brute-force palette size.
// Built-in brands (2, 3) and Custom LUT (1) always use the fast LUT path.
// Custom 1xN (0) always uses the brute-force path.
#if PALETTE_BRAND == 0
    // Custom 1xN — brute-force
    #define PALETTE_USE_LUT 0
    #define PALETTE_SIZE PALETTE_CUSTOM_1XN_SIZE
#else
    // Custom LUT or built-in brand — fast LUT path
    #define PALETTE_USE_LUT 1
    #define PALETTE_SIZE 64  // unused for LUT path, kept to avoid compile errors
#endif

// LUT size must match the --size used when generating the LUT PNGs (default 64)
#define PALETTE_LUT_SIZE 64

#define PALETTE_STRENGTH 1.0 // How much to snap to colorPalette colors [0.0 0.25 0.5 0.75 1.0]
#define PALETTE_SMOOTHNESS 0.75 // Blending between colorPalette colors [0.0 0.05 0.1 0.15 0.2 0.3 0.5 0.75 1.0]
#define PALETTE_PRESERVE_LUMINANCE 1 // Preserve original brightness [0 1]
#define PALETTE_DITHER_STRENGTH 4.0 // Dithering intensity [0.0 0.25 0.5 0.75 1.0 1.5 2.0 4.0 6.0 8.0]

// Purkinje Shift Settings (scotopic blue-green push at low light)
#define PURKINJE_ENABLED 1 // Enable Purkinje shift at night [0 1]
#define PURKINJE_STRENGTH 0.75 // Overall Purkinje intensity [0.0 0.25 0.5 0.75 1.0 1.5 2.0]
#define PURKINJE_THRESHOLD 0.46 // Sun angle at which shift begins (matches night transition) [0.40 0.43 0.46 0.49 0.52]

// Kuwahara / Painterly Mode Settings (DRAWING_STYLE 4)
#define KUWAHARA_ENABLED STYLE_KUWAHARA_ENABLED // [0 1]
#define KUWAHARA_RADIUS 10 // Brush size for painterly effect [2 3 4 5 6 8 10 12 14 16 24]
#define KUWAHARA_SHARPNESS 8.0 // Edge sharpness of painted strokes [1.0 2.0 4.0 8.0 16.0 32.0]

// White Vignette Settings
#define WHITE_VIGNETTE_ENABLED 0 // Enable white vignette effect [0 1]
#define VIGNETTE_STRENGTH 1.0 // Overall vignette intensity [0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0]
#define VIGNETTE_RADIUS 1.0 // Vignette falloff radius [0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2]
#define VIGNETTE_SOFTNESS 0.6 // Edge softness [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8]

#define SPEED_LINES_ENABLED 0 // Enable radial speed lines [0 1]
#define SPEED_LINE_COUNT 24 // Number of radial lines [8 12 16 20 24 32]
#define SPEED_LINE_INTENSITY 1.2 // How strong the lines are [0.3 0.5 0.8 1.0 1.2]
#define SPEED_LINE_BASE_THICKNESS 0.1 // Base line thickness [0.005 0.01 0.015 0.02 0.03]
#define SPEED_LINE_THICKNESS_VARIATION 4.0 // Random thickness multiplier [1.5 2.0 2.5 3.0 4.0]
#define SPEED_LINE_INNER_RADIUS 0.6 // Inner radius where lines start [0.1 0.15 0.2 0.3]
#define SPEED_LINE_INNER_RADIUS_VARIATION 0.5 // Random variation of inner radius [0.1 0.2 0.3 0.4 0.5]
#define SPEED_LINE_OUTER_RADIUS 2.0 // Outer radius where lines end [1.0 1.2 1.4 1.6]
#define SPEED_LINE_FADE_POWER 0.3 // Fade curve steepness [1.0 1.5 2.0 2.5 3.0]
#define SPEED_LINE_NOISE_SCALE 8.0 // Scale for line variation noise [4.0 6.0 8.0 12.0]
#define SPEED_LINE_NOISE_STRENGTH 0.3 // Strength of line variation [0.1 0.2 0.3 0.4 0.5]
#define SPEED_LINE_WHITE 1 // Use white lines instead of black [0 1]
#define SPEED_LINE_ANIMATION_SPEED 3.0 // How fast lines animate [1.0 2.0 3.0 4.0 5.0]

#define SPEED_LINES_STATIC 0 // Static speedlines (ignore camera speed) [0 1]
#define SPEED_THRESHOLD 0.2 // Distance per frame to show lines [0.02 0.04 0.06 0.08 0.1 0.15 0.2 0.25 0.3 0.35 0.4]
#define SPEED_FADE_IN 0.06 // Speed range for fade-in [0.01 0.02 0.04 0.06]
#define SPEED_INNER_RADIUS_MIN 0.9 // Inner radius at threshold speed [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]
#define SPEED_INNER_RADIUS_MAX 0.08 // Inner radius at max speed [0.0 0.05 0.08 0.1 0.15 0.2]
#define SPEED_MAX_FOR_SCALING 0.3 // Speed where inner radius reaches minimum [0.2 0.3 0.4 0.5 0.6 0.7 0.8]

// ============================================================
// Volumetric Godrays (Crepuscular Rays)
// ============================================================
#define GODRAYS_ENABLED 1 // Enable volumetric godrays [0 1]
#define GODRAY_SAMPLES 48 // Raymarching samples [16 24 32 48 52 64]
#define GODRAY_INTENSITY 0.35 // Overall godray brightness [0. 0.05 0.10 0.15 0.20 0.25 0.30 0.35 0.40 0.45 0.50 0.55 0.6]
#define GODRAY_DENSITY 0.85 // How dense/far the rays reach [0.3 0.5 0.6 0.7 0.8 0.85 0.9 1.0]
#define GODRAY_DECAY 0.96 // How fast rays fade with distance [0.90 0.93 0.95 0.96 0.97 0.98 0.99]
#define GODRAY_WEIGHT 0.6 // Per-sample contribution weight [0.1 0.2 0.3 0.4 0.5 0.6 0.7]
#define GODRAY_WARMTH 0.15 // Warm tint on the rays [0.0 0.05 0.10 0.15 0.20 0.25 0.30]
#define GODRAY_NIGHT_INTENSITY 0.08 // Godray intensity at night [0.0 0.02 0.05 0.08 0.10 0.15]
#define GODRAY_DUAL_TWILIGHT 1 // Enable dual sun+moon godrays at twilight [0 1]
#define GODRAY_MOON_WARMTH -0.10 // Moon ray warmth, negative = cool/blue [−0.30 −0.20 −0.10 0.0 0.10 0.20 0.30]
// ============================================================

// Fun/experiemental effect
#define COLOR_GRID_ENABLED 0 // Enable color assimilation grid effect [0 1]
#define GRID_COLOR_SATURATION 6.5 // Oversaturation of grid lines [1.5 2.0 2.5 3.0 3.5]
#define GRID_COLOR_INTENSITY 8.0 // How much color to apply to grid lines [0.5 0.6 0.7 0.8 0.9 1.0]
#define BASE_DESATURATION 1.0 // How much to desaturate the base image [0.7 0.8 0.85 0.9 0.95 1.0]
#define COLOR_BLEEDING 0.0 // How much grid color bleeds into surrounding areas [0.1 0.2 0.3 0.4 0.5]
#define ADAPTIVE_COLOR_STRENGTH 1 // Make color strength adapt to original image colors [0 1]

const int noiseTextureResolution = 256;

uniform sampler2D colortex0; // Color with outline data in alpha
uniform sampler2D colortex1; // Now contains edge direction in RG and hatching in B
uniform sampler2D colortex2; // Bloom buffer
uniform sampler2D colortex5; // Color palette texture (1xPALETTE_SIZE) - used when PALETTE_USE_LUT=0
uniform sampler2D colortex6; // Palette LUT texture (LUT_SIZE*LUT_SIZE x LUT_SIZE) - used when PALETTE_USE_LUT=1
uniform sampler2D noisetex;
uniform sampler2D depthtex0; // Depth buffer for godray occlusion
uniform float frameTimeCounter;
uniform float viewWidth;
uniform float viewHeight;
uniform float dynamicSpeedInnerRadius;
uniform float rainStrength;
uniform vec3 shadowLightPosition; // Sun/moon position in view space
uniform float sunAngle; // 0=sunrise, 0.25=noon, 0.5=sunset, 0.75=midnight
uniform mat4 gbufferProjection;
varying vec2 texcoord;
varying float vDynamicInnerRadius;

// Simple hash function for noise generation
float hash(vec2 p) {
    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}

// 2D noise function
float noise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    
    return mix(mix(hash(i), hash(i + vec2(1,0)), f.x),
               mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), f.x), f.y);
}

// Ridged noise for paper crinkles
float ridgedNoise(vec2 p) {
    return abs(noise(p) * 2.0 - 1.0);
}

// Generate paper-like texture with crinkles and fine grain (ORIGINAL SYSTEM)
float paperTexture(vec2 uv) {
    // Discrete time steps for random texture changes
    float timeStep = floor(frameTimeCounter * PAPER_TEXTURE_SPEED);
    
    // Create different random seeds for each time step
    float seed1 = hash(vec2(timeStep, 1.0)) * 1000.0;
    float seed2 = hash(vec2(timeStep, 2.0)) * 1000.0;
    float seed3 = hash(vec2(timeStep, 3.0)) * 1000.0;
    float seed4 = hash(vec2(timeStep, 4.0)) * 1000.0;
    float seed5 = hash(vec2(timeStep, 5.0)) * 1000.0;
    
    // Large-scale paper crinkles
    float crinkles = 0.0;
    crinkles += ridgedNoise(uv * PAPER_CRINKLE_SCALE + seed5) * 0.6;
    crinkles += ridgedNoise(uv * PAPER_CRINKLE_SCALE * 2.0 + seed5 * 1.3) * 0.4;
    crinkles *= PAPER_CRINKLE_STRENGTH;
    
    // Fine paper grain texture
    float fineGrain = 0.0;
    fineGrain += noise(uv * PAPER_TEXTURE_SCALE * 2.0 + seed1) * 0.5;
    fineGrain += noise(uv * PAPER_TEXTURE_SCALE + seed2) * 0.3;
    fineGrain += noise(uv * PAPER_TEXTURE_SCALE * 0.5 + seed3) * 0.2;
    fineGrain += noise(uv * vec2(PAPER_TEXTURE_SCALE * 4.0, PAPER_TEXTURE_SCALE * 0.5) + seed4) * 0.1;
    fineGrain = (fineGrain - 0.5) * PAPER_TEXTURE_STRENGTH;
    
    return crinkles + fineGrain;
}

// ADVANCED GRAIN SYSTEM FUNCTIONS (only compiled when ADVANCED_GRAIN_SYSTEM = 1)
#if ADVANCED_GRAIN_SYSTEM == 1

	// Generate grain height map for pencil interaction
	float generateGrainHeightMap(vec2 uv) {
		float timeStep = floor(frameTimeCounter * PAPER_TEXTURE_SPEED);
		float grainSeed = hash(vec2(timeStep, 10.0)) * 1000.0;
		
		// Multi-octave noise for realistic grain peaks and valleys
		float height = 0.0;
		
		// Primary grain structure (medium frequency)
		height += noise(uv * GRAIN_HEIGHT_SCALE + grainSeed) * 0.5;
		height += noise(uv * GRAIN_HEIGHT_SCALE * 2.0 + grainSeed * 1.3) * 0.25;
		
		// Fine detail (high frequency speckles)
		height += noise(uv * GRAIN_HEIGHT_SCALE * 4.0 + grainSeed * 1.7) * 0.15 * GRAIN_FINE_DETAIL;
		height += noise(uv * GRAIN_HEIGHT_SCALE * 8.0 + grainSeed * 2.1) * 0.1 * GRAIN_FINE_DETAIL;
		
		// Organic directional variation (no screen-space gradients)
		if (GRAIN_DIRECTIONAL_BIAS > 0.0) {
			// Per-pixel directional noise instead of screen-space bias
			float dirNoise1 = noise(uv * GRAIN_HEIGHT_SCALE * 0.3 + grainSeed * 0.7);
			float dirNoise2 = noise(uv * GRAIN_HEIGHT_SCALE * 0.6 + grainSeed * 1.1);
			
			// Create organic directional variation
			float directionalHeight = (dirNoise1 - 0.5) * (dirNoise2 - 0.5) * 4.0; // -1 to 1 range
			height += directionalHeight * GRAIN_DIRECTIONAL_BIAS * 0.1;
		}
		
		return clamp(height, 0.0, 1.0);
	}

	// Apply pencil grain interaction to darken colors realistically
	vec3 applyPencilGrainInteraction(vec3 inputColor, vec2 uv) {
		#if PENCIL_GRAIN_ENABLED == 0
		return inputColor;
		#endif

		// Base luminance
		float L = dot(inputColor, vec3(0.299, 0.587, 0.114));
		float darkness = 1.0 - L;
		if (darkness < 0.05) return inputColor;

		// Seeds
		float timeStep = floor(frameTimeCounter * PAPER_TEXTURE_SPEED);
		float seedA = hash(vec2(timeStep, 11.0)) * 1000.0;
		float seedB = hash(vec2(timeStep, 17.0)) * 1000.0;

		// Paper peaks/valleys
		float Hbase = generateGrainHeightMap(uv);

		// -----------------------------
		// Layer 1: fine gray speckle
		float H1 = noise(uv * GRAIN_L1_SCALE + seedA);
		float Hmix1 = mix(H1, Hbase, 0.5);
		float cov1 = smoothstep(0.8, 0.3, Hmix1) * pow(darkness, 1.1);
		float lay1Dark = 0.14 * cov1;

		// Layer 2: sparse dark flecks
		float H2 = noise(uv * GRAIN_L2_SCALE + seedB);
		float Hmix2 = mix(H2, Hbase, 0.7);
		float cov2 = smoothstep(0.9, 0.2, Hmix2) * pow(darkness, 9.0); //to make lighter increase pow
		float lay2Dark = 0.5 * cov2; //to make lighter decrease multiplier

		// -----------------------------
		// Stroke directionality
		vec2 dir = normalize(vec2(0.7, 0.5)); // ~diagonal
		float Dp = noise(uv * GRAIN_L1_SCALE + dir * 3.1 + seedA);
		float Dm = noise(uv * GRAIN_L1_SCALE - dir * 3.1 + seedA);
		float Ddir = clamp(Dp - Dm, -1.0, 1.0); // -1..1 ridge side test

		// Bias graphite layers: one side of hill gets more, one side less
		lay1Dark *= (1.0 + 0.3 * Ddir);
		lay2Dark *= (1.0 + 0.4 * Ddir);

		// -----------------------------
		// Layer 3: forced white specks (negative graphite)
		// strongest on trough side, fades out as darkness rises
		float whiteMask = smoothstep(0.4, 0.8, Hbase); // valleys = high
		float lay3Bright = whiteMask * (1.0 - darkness) * (0.3 + 0.3 * (-Ddir));

		// -----------------------------
		// Combine
		float graphite = clamp(lay1Dark + lay2Dark - lay3Bright, 0.0, 0.9);
		#if DRAWING_STYLE == 2 && COLOR_ADAPTIVE_PENCIL == 1
			float Lout = max(L - graphite * 0.4, 0.0); // Apply with 40% strength
		#else
			float Lout = max(L - graphite * 0.7, 0.0); // Apply with 70% strength
		#endif

		// Add salt specks *after* darkening
		float salt = noise(uv * 256.0 + seedB * 3.3);
		float saltMask = step(0.9, salt); // rare specks
		float lay4Bright = saltMask * (0.4 * (1.0 - darkness));

		// Specks brighten back toward paper (1.0 = white)
		Lout = mix(Lout, 1.0, lay4Bright);

		float safeL = max(L, 1e-4);
		vec3 outColor = inputColor * (Lout / safeL);

		return outColor;
	}

#endif // End of ADVANCED_GRAIN_SYSTEM functions

// Pencil line rendering
float renderPencilLine(float thickness, vec2 uv) {
    if (thickness <= 0.0) return 0.0;
    
    // Use thickness directly from composite stage
    float lineStrength = thickness;
    
    // Time-consistent grain system
    float timeStep = floor(frameTimeCounter * PAPER_TEXTURE_SPEED);
    float grainSeed = hash(vec2(timeStep, 15.0)) * 1000.0;
    
    // Single unified grain system
    // Scale grain UV to 1440p reference so grain density is resolution-independent
    float grainResScale = viewHeight / 1440.0;
    vec2 grainUV = uv * viewWidth * 0.3 / grainResScale;
    float pencilGrain = 0.0;
    pencilGrain += noise(grainUV * 6.0 + grainSeed) * 0.5;
    pencilGrain += noise(grainUV * 12.0 + grainSeed * 1.3) * 0.3;
    pencilGrain += noise(grainUV * 24.0 + grainSeed * 1.7) * 0.2;
    pencilGrain = (pencilGrain - 0.5) * 2.0; // -1 to 1 range
    
    // Thickness-based opacity: thinner lines are more transparent
    float thicknessOpacity = pow(lineStrength, 1.0 / (1.0 + THIN_LINE_FADE_FACTOR));
    
    // Edge fading: simulate pencil pressure variation
    float pressureNoise = noise(uv * viewWidth * 0.1 / grainResScale + grainSeed);
    float pressureVariation = mix(0.6, 1.0, (pressureNoise + 1.0) * 0.5);
    pressureVariation = mix(1.0 - PENCIL_PRESSURE_VARIATION * 0.5, 1.0, pressureVariation);
    
    // Core line strength varies with thickness
    float coreStrength = mix(0.2, PENCIL_CORE_STRENGTH, pow(lineStrength, 0.8));
    
    // Combine effects
    float baseIntensity = coreStrength * thicknessOpacity * pressureVariation;
    
    // Add grain (more visible in thicker lines)
    float grainContribution = pencilGrain * PENCIL_GRAIN_INTENSITY * baseIntensity * lineStrength;
    
    // Final intensity with pressure variation
    float finalIntensity = baseIntensity + grainContribution;
    finalIntensity *= mix(1.0 - PENCIL_PRESSURE_VARIATION * 0.3, 1.0, pressureVariation);
    
    return clamp(finalIntensity, 0.0, 1.0);
}

float renderPencilLineWithDirectionalEdges(float thickness, vec2 uv, vec2 edgeDirection) {
    if (thickness <= 0.0) return 0.0;
    
    // Decode edge direction from 0,1 range back to -1,1 range
    vec2 decodedDirection = edgeDirection * 2.0 - 1.0;
    
    // If we don't have a valid edge direction, fall back to regular rendering
    if (length(decodedDirection) < 0.1) {
        return renderPencilLine(thickness, uv);
    }
    
    // Calculate perpendicular direction (rotate 90 degrees)
    vec2 perpendicular = vec2(-decodedDirection.y, decodedDirection.x);
    
    // Sample along the perpendicular direction to find line boundaries
    vec2 pixelSize = vec2(1.0 / viewWidth, 1.0 / viewHeight);
    float resScale = viewHeight / 1440.0;
    float maxSearchDistance = 20.0 * resScale; // Scale search distance with resolution (reference: 1440p)
    
    // Find line boundaries by sampling outward in both perpendicular directions
    float leftBoundary = 0.0;
    float rightBoundary = 0.0;
    
    // Search left (negative perpendicular direction)
    for (float i = 1.0; i <= maxSearchDistance; i += 1.0) {
        vec2 samplePos = uv + perpendicular * pixelSize * -i;
        float sampleThickness = texture2D(colortex0, samplePos).a;
        if (sampleThickness <= 0.1) {
            leftBoundary = i;
            break;
        }
    }
    if (leftBoundary == 0.0) leftBoundary = maxSearchDistance;
    
    // Search right (positive perpendicular direction)
    for (float i = 1.0; i <= maxSearchDistance; i += 1.0) {
        vec2 samplePos = uv + perpendicular * pixelSize * i;
        float sampleThickness = texture2D(colortex0, samplePos).a;
        if (sampleThickness <= 0.1) {
            rightBoundary = i;
            break;
        }
    }
    if (rightBoundary == 0.0) rightBoundary = maxSearchDistance;
    
    // Calculate line width and center position
    float totalLineWidth = leftBoundary + rightBoundary;
    float distanceFromCenter = min(leftBoundary, rightBoundary);
    
    // Calculate edge falloff - closer to edge = more transparent
    float edgeFalloff = 1.0;
    if (totalLineWidth > 2.0) { // Only apply falloff for lines wide enough
        float edgeDistance = distanceFromCenter / (totalLineWidth * 0.5);
        // Smooth falloff from center (1.0) to edges (0.2)
        edgeFalloff = mix(0.2, 1.0, smoothstep(0.0, 1.0, edgeDistance));
    }
    
    // Time-consistent grain system (same as working renderPencilLine)
    float timeStep = floor(frameTimeCounter * PAPER_TEXTURE_SPEED);
    float grainSeed = hash(vec2(timeStep, 15.0)) * 1000.0;
    
    // Single unified grain system
    float grainResScale = viewHeight / 1440.0;
    vec2 grainUV = uv * viewWidth * 0.3 / grainResScale;
    float pencilGrain = 0.0;
    pencilGrain += noise(grainUV * 6.0 + grainSeed) * 0.5;
    pencilGrain += noise(grainUV * 12.0 + grainSeed * 1.3) * 0.3;
    pencilGrain += noise(grainUV * 24.0 + grainSeed * 1.7) * 0.2;
    pencilGrain = (pencilGrain - 0.5) * 2.0; // -1 to 1 range
    
    // Thickness-based opacity: thinner lines are more transparent
    float thicknessOpacity = pow(thickness, 1.0 / (1.0 + THIN_LINE_FADE_FACTOR));
    
    // Edge fading: simulate pencil pressure variation
    float pressureNoise = noise(uv * viewWidth * 0.1 / grainResScale + grainSeed);
    float pressureVariation = mix(0.6, 1.0, (pressureNoise + 1.0) * 0.5);
    pressureVariation = mix(1.0 - PENCIL_PRESSURE_VARIATION * 0.5, 1.0, pressureVariation);
    
    // Core line strength varies with thickness and edge falloff
    float coreStrength = mix(0.2, PENCIL_CORE_STRENGTH, pow(thickness, 0.8));
    
    // Apply edge falloff to core strength
    coreStrength *= edgeFalloff;
    
    // Combine effects
    float baseIntensity = coreStrength * thicknessOpacity * pressureVariation;
    
    // Add grain (more visible in thicker lines)
    float grainContribution = pencilGrain * PENCIL_GRAIN_INTENSITY * baseIntensity * thickness;
    
    // Final intensity with pressure variation
    float finalIntensity = baseIntensity + grainContribution;
    finalIntensity *= mix(1.0 - PENCIL_PRESSURE_VARIATION * 0.3, 1.0, pressureVariation);
    
    return clamp(finalIntensity, 0.0, 1.0);
}

// Function to convert RGB to HSV
vec3 rgb2hsv(vec3 c) {
    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
    
    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

// Function to convert HSV to RGB
vec3 hsv2rgb(vec3 c) {
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

// Enhanced desaturation that preserves some color information
vec3 smartDesaturate(vec3 color, float amount) {
    vec3 hsv = rgb2hsv(color);
    float originalSaturation = hsv.y;
    
    // Reduce saturation but keep a hint of the original color
    hsv.y = mix(hsv.y, hsv.y * (1.0 - amount), amount);
    
    return hsv2rgb(hsv);
}

// Generate oversaturated color based on original image color
vec3 generateGridColor(vec3 originalColor, vec2 texcoord) {
    vec3 hsv = rgb2hsv(originalColor);
    
    // If the original color has very low saturation, create a synthetic color
    if (hsv.y < 0.1) {
        // Use position-based hue for neutral areas
        float syntheticHue = fract(dot(texcoord, vec2(127.1, 311.7)) * 0.1);
        hsv.x = syntheticHue;
        hsv.y = 0.6; // Moderate base saturation
    }
    
    // Oversaturate the color
    hsv.y = min(hsv.y * GRID_COLOR_SATURATION, 1.0);
    
    // Optionally adjust brightness for better visibility
    hsv.z = clamp(hsv.z * 1.2, 0.3, 0.9);
    
    return hsv2rgb(hsv);
}

// Calculate draw-on progress for different elements
float getDrawOnProgress() {
    #if DRAW_ON_ENABLED == 0 ||  PAPER_STYLE == 0
    return float(1.0); // All elements fully visible
    #endif
    
    float time = frameTimeCounter;
	
    // Texture fade-in (0 to 1)
    float textureProgress = smoothstep(TEXTURE_START_TIME, TEXTURE_START_TIME + TEXTURE_DURATION, time);
    
    return textureProgress;
}

float getEnhancedBrushMask(vec2 texcoord, float progress) {
    return progress;
}

float minOf3(vec3 v) {
    return min(min(v.r, v.g), v.b);
}

float maxOf3(vec3 v) {
    return max(max(v.r, v.g), v.b);
}

// Get index of maximum component (0=r, 1=g, 2=b)
int maxIdx(vec3 color, out float maxVal) {
    maxVal = maxOf3(color);
    if (color.r == maxVal) return 0;
    else if (color.g == maxVal) return 1;
    else return 2;
}

// Get index of minimum component (0=r, 1=g, 2=b)
int minIdx(vec3 color, out float minVal) {
    minVal = minOf3(color);
    if (color.r == minVal) return 0;
    else if (color.g == minVal) return 1;
    else return 2;
}

float luminance(vec3 color) {
    return dot(color, vec3(0.299, 0.587, 0.114));
}

float clamp01(float x) {
    return clamp(x, 0.0, 1.0);
}

float pow2(float x) {
    return x * x;
}

// Find closest color in colorPalette (brute-force, used when PALETTE_USE_LUT=0)
vec3 findClosestPaletteColor(vec3 color, out float distance) {
    vec3 closestColor = color;
    float minDist = 999999.0;
    
    for (int i = 0; i < PALETTE_SIZE; i++) {
        float u = (float(i) + 0.5) / PALETTE_SIZE;
        vec3 colorPaletteColor = texture2D(colortex5, vec2(u, 0.5)).rgb;
        
        vec3 diff = color - colorPaletteColor;
		float dist = diff.r * diff.r * 0.299 +
					 diff.g * diff.g * 0.587 +
					 diff.b * diff.b * 0.114;
        
        if (dist < minDist) {
            minDist = dist;
            closestColor = colorPaletteColor;
        }
    }
    
    distance = minDist;
    return closestColor;
}

// Find the two closest colorPalette colors for blending (brute-force, used when PALETTE_USE_LUT=0)
void findTwoClosestPaletteColors(vec3 color, out vec3 color1, out vec3 color2, out float weight) {
    float dist1 = 999999.0;
    float dist2 = 999999.0;
    color1 = color;
    color2 = color;
    
    for (int i = 0; i < PALETTE_SIZE; i++) {
        float u = (float(i) + 0.5) / PALETTE_SIZE;
        vec3 colorPaletteColor = texture2D(colortex5, vec2(u, 0.5)).rgb;
        
        vec3 diff = color - colorPaletteColor;
		float dist = diff.r * diff.r * 0.299 +
					 diff.g * diff.g * 0.587 +
					 diff.b * diff.b * 0.114;
        
        if (dist < dist1) {
            dist2 = dist1;
            color2 = color1;
            dist1 = dist;
            color1 = colorPaletteColor;
        } else if (dist < dist2) {
            dist2 = dist;
            color2 = colorPaletteColor;
        }
    }
    
    float totalDist = dist1 + dist2;
    if (totalDist > 0.0001) {
        weight = dist2 / totalDist;
    } else {
        weight = 1.0;
    }
}

// LUT lookup - single texture fetch replaces the entire palette search loop.
// The LUT is a horizontal-strip 2D texture: (LUT_SIZE*LUT_SIZE) wide x LUT_SIZE tall.
// R maps to X within each slice, G maps to Y (flipped), B maps to slice index.
vec3 samplePaletteLUT(vec3 color) {
    float N = float(PALETTE_LUT_SIZE);
    // Scale input to cell-center coordinates (matches how the LUT was generated)
    vec3 c = clamp(color, 0.0, 1.0);
    float r_coord = c.r * (N - 1.0) / N + 0.5 / N;
    float g_coord = (1.0 - c.g) * (N - 1.0) / N + 0.5 / N; // g is flipped in image
    float b_slice = c.b * (N - 1.0);
    float b_idx   = floor(b_slice + 0.5);                    // nearest slice
    // X position: slice offset + r position within slice
    float x = (b_idx + r_coord) / N;
    float y = g_coord;
    return texture2D(colortex6, vec2(x, y)).rgb;
}

// Blue noise dithering - noise to break up banding
vec3 blueNoiseDither(vec3 color, vec2 coord) {
    if (PALETTE_DITHER_STRENGTH <= 0.0) {
        return color;
    }
    
    float timeStep = floor(frameTimeCounter * PAPER_TEXTURE_SPEED);
    vec2 noiseCoord = coord + vec2(timeStep * 13.7, timeStep * 7.3);
    float noise = texture2D(noisetex, noiseCoord / noiseTextureResolution).r;
    float dither = (noise - 0.5) * (2.0 / 255.0) * PALETTE_DITHER_STRENGTH;
    
    return color + vec3(dither);
}

// ============================================================
// KUWAHARA FILTER - Painterly effect
//
// Classic 4-quadrant Kuwahara
// ============================================================
vec3 applyKuwahara(sampler2D tex, vec2 uv, vec2 texelSize) {
    // Scale radius so KUWAHARA_RADIUS is the brush size at the 1440p reference.
    // The loop iterates up to KUWAHARA_RADIUS (compile-time constant); at higher
    // resolutions the scaled r grows but is still clamped by the loop bound,
    // so set KUWAHARA_RADIUS to the maximum you ever want to use.
    float resScale = viewHeight / 1440.0;
    float r = float(KUWAHARA_RADIUS) * resScale;

    // sub-pixel jitter from blue-noise texture.
    // Two independent noise channels (R and G) give uncorrelated
    // X and Y offsets.  Scale keeps the jitter well under 1 texel
    // so it cannot move a sample across a block boundary.
    vec2 noiseUV   = uv * vec2(viewWidth, viewHeight) / float(noiseTextureResolution);
    float noiseX   = texture2D(noisetex, noiseUV).r;
    float noiseY   = texture2D(noisetex, noiseUV + vec2(0.5, 0.5)).r; // offset to decorrelate
    vec2  jitter   = (vec2(noiseX, noiseY) - 0.5) * 0.75; // range ±0.375 texels
    vec2  jitteredUV = uv + jitter * texelSize;

    vec3  mean[4];
    vec3  mean2[4];
    float cnt[4];

    for (int i = 0; i < 4; i++) {
        mean[i]  = vec3(0.0);
        mean2[i] = vec3(0.0);
        cnt[i]   = 0.0;
    }

    for (int dy = -KUWAHARA_RADIUS; dy <= KUWAHARA_RADIUS; dy++) {
        for (int dx = -KUWAHARA_RADIUS; dx <= KUWAHARA_RADIUS; dx++) {
            float fx = float(dx);
            float fy = float(dy);

            // Circular kernel — skip square corners.
            if (fx * fx + fy * fy > r * r + 0.5) continue;

            vec2 offset = vec2(fx, fy) * texelSize;
            vec3 s  = texture2D(tex, jitteredUV + offset).rgb;
            vec3 s2 = s * s;

            // Standard quadrant membership (shared axes count for both)
            if (dx <= 0 && dy <= 0) { mean[0] += s; mean2[0] += s2; cnt[0] += 1.0; }
            if (dx >= 0 && dy <= 0) { mean[1] += s; mean2[1] += s2; cnt[1] += 1.0; }
            if (dx <= 0 && dy >= 0) { mean[2] += s; mean2[2] += s2; cnt[2] += 1.0; }
            if (dx >= 0 && dy >= 0) { mean[3] += s; mean2[3] += s2; cnt[3] += 1.0; }
        }
    }

    // Hard-select the lowest-variance quadrant (original logic, unchanged)
    vec3  result      = vec3(0.0);
    float minVariance = 1e10;

    for (int i = 0; i < 4; i++) {
        vec3 m       = mean[i]  / cnt[i];
        vec3 m2      = mean2[i] / cnt[i];
        vec3 variance = abs(m2 - m * m);
        float v      = dot(variance, vec3(0.299, 0.587, 0.114));

        if (v < minVariance) {
            minVariance = v;
            result = m;
        }
    }

    return result;
}

// =============================================================================
// PURKINJE SHIFT - scotopic blue-green desaturation at low light levels
// Based on snippit from Emi_V7 in shaderlabs discord which was based on
// Jasmin Patry's implementation (Advances in Real-Time Rendering 2021)
// https://advances.realtimerendering.com/s2021/jpatry_advances2021/index.html#/167
// =============================================================================
vec3 applyPurkinjeShift(vec3 c, float strength) {
    // Rod+cone response matrix (LMS + rod channel in w).
    // mat4x3 = 4 columns, 3 rows (GLSL column-major).
    // vec3 * mat4x3 dots each column with the vec, giving vec4.
	c = max(c, vec3(0.0));
	
    const mat4x3 M = mat4x3(
        7.69684945, 18.4248204,  2.06809497,
        2.43113687, 18.6979422,  3.01246326,
        0.28911757,  1.40183293, 13.7922962,
        0.46638595, 15.5643680, 10.0599647
    );

    // Scale input so rod activation kicks in at dim-display levels,
    // not at scene-white.
    vec3 cScaled = c * 4.0;
    vec4 q = cScaled * M;

    vec3 m = vec3(0.63721, 0.39242, 1.6064); // peak cone sensitivities
    vec3 k = vec3(0.2,     0.2,     0.29);   // rod input strength per channel

    // Gain compression: inversesqrt gives cone desaturation as rods take over.
    vec3 g = inversesqrt(1.0 + (0.33 / m) * (q.rgb + k * q.w));

    // The original Patry formulation computes:
    //   dC = inv(trans(mat3(M))) * inv(A) * (K/S) * m0 * diag(k) * diag(1/m) * g * q.w
    // All matrices except g are constants, so we pre-bake the entire chain into
    // one mat3 to avoid inverse() calls at runtime. inverse() on a const mat3
    // in GLSL is evaluated at compile time and is undefined behaviour on many
    // drivers -- the result is often mat3(0), which silently kills the effect.
    //
    // Pre-computed: (K/S) * inv(trans(mat3(M))) * inv(A) * m0 * diag(k) * diag(1/m)
    // (Patry params: K=45, S=10, k3=0.6, r=0.139, p=0.6189)
    const mat3 CHAIN = mat3(
         0.24548786, -0.03449561,  0.01599435,  // col 0
        -0.42143578,  0.14355386,  0.21041495,  // col 1
        -0.13117901, -0.03134829,  0.30043936   // col 2
    );

    vec3 dC = CHAIN * (g * q.w);

    // 0.05 is Patry's base scale; multiply by strength for user control.
    return c + dC * (0.05 * strength);
}

// Compute how far into night we are (0=day, 1=full night), mirroring the
// godrays night-fade logic so all night effects transition together.
float nightBlend() {
    return smoothstep(PURKINJE_THRESHOLD, PURKINJE_THRESHOLD + 0.08, sunAngle)
         - smoothstep(0.96, 1.0, sunAngle);
}

// Apply colorPalette quantization with smooth blending
vec3 applyPaletteQuantization(vec3 color) {
    #if PALETTE_ENABLED == 0
    return color;
    #endif
    
    float originalLum = 0.0;
    #if PALETTE_PRESERVE_LUMINANCE == 1
        originalLum = dot(color, vec3(0.299, 0.587, 0.114));
    #endif
    
    vec3 quantizedColor;

    #if PALETTE_USE_LUT == 1
        // Fast path: single texture fetch into the pre-baked LUT.
        // Smoothness and two-nearest blending are baked into the LUT at generation time.
        quantizedColor = samplePaletteLUT(color);
    #else
        // Brute-force path: loop over all palette colors every pixel.
        if (PALETTE_SMOOTHNESS > 0.0) {
            vec3 color1, color2;
            float weight;
            findTwoClosestPaletteColors(color, color1, color2, weight);
            float blendFactor = smoothstep(0.0, PALETTE_SMOOTHNESS, weight);
            quantizedColor = mix(color2, color1, blendFactor);
        } else {
            float dist;
            quantizedColor = findClosestPaletteColor(color, dist);
        }
    #endif
    
    #if PALETTE_PRESERVE_LUMINANCE == 1
        float quantizedLum = dot(quantizedColor, vec3(0.299, 0.587, 0.114));
        if (quantizedLum > 0.001) {
            quantizedColor *= originalLum / quantizedLum;
            quantizedColor = clamp(quantizedColor, vec3(0.0), vec3(1.0));
        }
    #endif
    
    return mix(color, quantizedColor, PALETTE_STRENGTH);
}

// =============================================================================
// VIBRANCE FUNCTION Based on snippit from belmu (shaderlabs discord)
// =============================================================================
void applyVibrance(inout vec3 color, float intensity) {
    if (abs(intensity) < 0.001) return; // Skip if no effect
    
    float mn = minOf3(color);
    float mx = maxOf3(color);
    float sat = (1.0 - clamp01(mx - mn)) * clamp01(1.0 - mx) * luminance(color) * 5.0;
    vec3 lightness = vec3((mn + mx) * 0.5);
    
    // Positive vibrance
    color = mix(color, mix(lightness, color, intensity), sat);
    // Negative vibrance
    color = mix(color, lightness, (1.0 - lightness) * (1.0 - intensity) * 0.5 * abs(intensity));
}

// =============================================================================
// SATURATION Based on snippit from belmu (shaderlabs discord)
// =============================================================================
void applySaturation(inout vec3 color, float intensity) {
    if (abs(intensity - 1.0) < 0.001) return; // Skip if no change
    color = mix(vec3(luminance(color)), color, intensity);
}

// =============================================================================
// NATURAL CONTRAST FUNCTION based on snippit from RedstoneCake (shaderlabs discord)
// =============================================================================
void applyContrastChannel(inout float channel, float a, float b) {
    if (channel < 0.5) {
        channel = b * pow(channel, a);
    } else {
        channel = 1.0 - b * pow(abs(channel - 1.0), a);
    }
}

void applyContrast(inout vec3 color, float amount) {
    if (abs(amount - 1.0) < 0.001) return; // Skip if no change
    
    float b = exp2(amount - 1.0);
    applyContrastChannel(color.r, amount, b);
    applyContrastChannel(color.g, amount, b);
    applyContrastChannel(color.b, amount, b);
}

// =============================================================================
// BRIGHTNESS FUNCTION based on snippit form Builderb0y (shaderlabs discord)
// =============================================================================
void applyBrightness(inout vec3 color, float factor) {
    if (abs(factor) < 0.001) return; // Skip if no change
    
    float coefficient = exp(factor) - 1.0;
    color = (coefficient * color + color) / (coefficient * color + 1.0);
    color = clamp(color, vec3(0.0), vec3(1.0));
}

// =============================================================================
// LIFT GAMMA GAIN FUNCTION Based on snippit from belmu (shaderlabs discord)
// =============================================================================
void applyLiftGammaGain(inout vec3 color, float lift, float gamma, float gain) {
    if (abs(lift) < 0.001 && abs(gamma - 1.0) < 0.001 && abs(gain - 1.0) < 0.001) return;
    
    vec3 lerpV = clamp(pow(color, vec3(gamma)), vec3(0.0), vec3(1.0));
    color = gain * lerpV + lift * (1.0 - lerpV);
    color = clamp(color, vec3(0.0), vec3(1.0));
}

// =============================================================================
// COLOR TEMPERATURE FUNCTION
// Shifts the white balance of the image: positive = warm (orange), negative = cool (blue)
// Uses a blackbody-inspired RGB shift along the warm/cool axis.
// =============================================================================
void applyColorTemperature(inout vec3 color, float temp) {
    if (abs(temp) < 0.001) return; // Skip if no change

    // Warm shift: boost red/green, reduce blue. Cool shift: boost blue, reduce red/green.
    vec3 warmShift = vec3(0.1, 0.05, -0.15);  // per-channel delta at max warm
    vec3 coolShift = vec3(-0.1, -0.03, 0.2);   // per-channel delta at max cool

    vec3 shift = (temp > 0.0) ? warmShift * temp : coolShift * (-temp);
    color = clamp(color + shift, vec3(0.0), vec3(1.0));
}

// MASTER COLOR GRADING FUNCTION
vec3 applyColorGrading(vec3 color) {
    #if COLOR_GRADING_ENABLED == 0
    return color;
    #endif
    
    vec3 gradedColor = color;
    
    // Pre-clamp the input to prevent issues with out-of-range values
    gradedColor = clamp(gradedColor, vec3(0.0), vec3(1.0));
    
    // Apply color adjustments in proper order
    applyLiftGammaGain(gradedColor, LIFT_AMOUNT, GAMMA_AMOUNT, GAIN_AMOUNT);
    applyContrast(gradedColor, CONTRAST_AMOUNT);
    applyBrightness(gradedColor, BRIGHTNESS_FACTOR);
    applySaturation(gradedColor, SATURATION_AMOUNT);
    applyVibrance(gradedColor, VIBRANCE_AMOUNT);
    applyColorTemperature(gradedColor, COLOR_TEMP_AMOUNT);
    
    return gradedColor;
}

float softMinClamp(float value, float minVal, float softness) {
    float diff = value - minVal;
    if (diff < 0.0) {
        // Use exponential curve to approach but never quite reach minimum
        return minVal + diff * exp(diff / softness);
    }
    return value;
}

vec3 applySoftBrightnessLimits(vec3 color, vec3 paperBase) {
    vec3 minColor = paperBase * PAPER_MIN_BRIGHTNESS;
    vec3 maxColor = paperBase;
    
    // Apply soft minimum and hard maximum
    color.r = softMinClamp(color.r, minColor.r, PAPER_BRIGHTNESS_SOFTNESS);
    color.g = softMinClamp(color.g, minColor.g, PAPER_BRIGHTNESS_SOFTNESS);
    color.b = softMinClamp(color.b, minColor.b, PAPER_BRIGHTNESS_SOFTNESS);
    
    // Keep hard maximum clamp
    return min(color, maxColor);
}

// Pre-adjust colors before clamping
vec3 prepareDarkRegionsForGrain(vec3 color, vec3 paperBase) {
    float brightness = dot(color, vec3(0.299, 0.587, 0.114));
    vec3 minColor = paperBase * PAPER_MIN_BRIGHTNESS;
    
    if (brightness < DARKNESS_THRESHOLD * 1.5) {
        // In dark regions, lift the color slightly to give room for grain darkening
        float liftAmount = smoothstep(DARKNESS_THRESHOLD * 1.5, 0.0, brightness) * 0.15;
        color = mix(color, color + liftAmount, 0.5);
    }
    
    return color;
}

// Calculate white vignette factor
float calculateWhiteVignette(vec2 texcoord) {
    #if WHITE_VIGNETTE_ENABLED == 0
    return 0.0;
    #endif
    
    // Convert to centered coordinates (-1 to 1)
    vec2 centered = (texcoord - 0.5) * 2.0;
    
    // Calculate distance from center
    float distance = length(centered);
    
    // Create vignette falloff
    float vignette = smoothstep(VIGNETTE_RADIUS - (VIGNETTE_SOFTNESS-0.1), VIGNETTE_RADIUS + (VIGNETTE_SOFTNESS-0.1), distance);
    
    return vignette * VIGNETTE_STRENGTH-0.1;
}

// Generate radial speed lines effect
float generateRadialSpeedLines(vec2 uv, float speed, float dynamicInnerRadius) {
    #if SPEED_LINES_ENABLED == 0
    return 0.0;
    #endif
    
    #if SPEED_LINES_STATIC == 0
        // Dynamic mode: check speed threshold
        float speedFade = smoothstep(SPEED_THRESHOLD - SPEED_FADE_IN, SPEED_THRESHOLD + SPEED_FADE_IN * 3.0, speed);
		speedFade = speedFade * speedFade; // Square it for even more gradual ramp
        
        if (speedFade <= 0.0) {
            return 0.0;
        }
    #else
        // Static mode: always show at full intensity
        float speedFade = 1.0;
        // Use the base SPEED_LINE_INNER_RADIUS instead of dynamic one
        dynamicInnerRadius = SPEED_LINE_INNER_RADIUS;
    #endif
    
    vec2 centered = (uv - 0.5) * 2.0;
    float radius = length(centered);
    float angle = atan(centered.y, centered.x);
    float normalizedAngle = (angle + 3.14159265) / (2.0 * 3.14159265);
    
    // Time-stepping system synchronized with paper effects
    float timeStep = floor(frameTimeCounter * SPEED_LINE_ANIMATION_SPEED);
    float animSeed = hash(vec2(timeStep, 42.0)) * 1000.0;
    
    // Calculate which line segment we're in
    float lineSegment = normalizedAngle * float(SPEED_LINE_COUNT);
    float lineIndex = floor(lineSegment);
    float linePosition = fract(lineSegment);
    
    // Random inner radius per line - but now relative to dynamic inner radius
    float innerRadiusSeed = hash(vec2(lineIndex + animSeed * 0.02, timeStep * 0.08));
    float randomInnerRadius = dynamicInnerRadius + (innerRadiusSeed - 0.5) * SPEED_LINE_INNER_RADIUS_VARIATION * dynamicInnerRadius;
    randomInnerRadius = max(randomInnerRadius, 0.02);
    
    // Length variation
    float lengthSeed = hash(vec2(lineIndex * 1.3 + animSeed * 0.02, timeStep * 0.05));
    float lineLength = mix(0.6, 1.0, lengthSeed);
    float adjustedOuterRadius = randomInnerRadius + (SPEED_LINE_OUTER_RADIUS - randomInnerRadius) * lineLength;
    
    // Check if we're in the valid radius range for this specific line
    if (radius < randomInnerRadius || radius > adjustedOuterRadius) {
        return 0.0;
    }
    
    
    // Create line pattern
    float distanceFromLineCenter = abs(linePosition - 0.5) * 2.0;
    
    // Random thickness per line
    float lineThicknessSeed = hash(vec2(lineIndex + animSeed * 0.01, timeStep * 0.1));
    float randomThickness = mix(1.0, SPEED_LINE_THICKNESS_VARIATION, lineThicknessSeed);
    
    // Calculate dynamic thickness
    float radiusProgress = (radius - randomInnerRadius) / (adjustedOuterRadius - randomInnerRadius);
    float animeThickness = pow(radiusProgress, 0.5);
    float dynamicThickness = SPEED_LINE_BASE_THICKNESS * randomThickness * animeThickness;
    
    if (radiusProgress < 0.1) {
        dynamicThickness *= radiusProgress * 10.0;
    }
    
    float lineIntensity = smoothstep(dynamicThickness, dynamicThickness * 0.5, distanceFromLineCenter);
    
    // Movement and visibility
    float lineMovement = hash(vec2(lineIndex + animSeed, timeStep)) - 0.5;
    float movementOffset = lineMovement * 0.3;
    float adjustedPosition = fract(linePosition + movementOffset);
    float adjustedDistance = abs(adjustedPosition - 0.5) * 2.0;
    lineIntensity = smoothstep(dynamicThickness, dynamicThickness * 0.5, adjustedDistance);
    
    float visibilitySeed = hash(vec2(lineIndex * 0.7 + animSeed * 0.03, timeStep * 0.15));
    float lineVisibility = smoothstep(0.3, 0.7, visibilitySeed);
    lineIntensity *= lineVisibility;
    
    // Organic noise
    vec2 noiseUV = centered * SPEED_LINE_NOISE_SCALE + vec2(animSeed * 0.1);
    float noiseValue = noise(noiseUV + vec2(timeStep * 0.1));
    float noiseMask = 1.0 + (noiseValue - 0.5) * SPEED_LINE_NOISE_STRENGTH;
    lineIntensity *= noiseMask;
    
    // Radial fading
    float radialFade = pow(radiusProgress, SPEED_LINE_FADE_POWER);
    lineIntensity *= radialFade;
    
    // Random intensity
    float intensityVariation = hash(vec2(lineIndex * 0.3 + animSeed * 0.05, 0.0));
    lineIntensity *= mix(0.4, 1.0, intensityVariation);
    
    // Apply speed-based fade
    lineIntensity *= speedFade;
    
    return clamp(lineIntensity * SPEED_LINE_INTENSITY, 0.0, 1.0);
}

// Apply speed lines to the final color
vec3 applySpeedLines(vec3 baseColor, vec2 uv, float speed, float dynamicInnerRadius) {
    float speedLineAmount = generateRadialSpeedLines(uv, speed, dynamicInnerRadius);
    
    if (speedLineAmount <= 0.0) {
        return baseColor;
    }
    
    #if SPEED_LINE_WHITE == 1
        vec3 whiteContribution = vec3(speedLineAmount);
        return baseColor + whiteContribution;
    #else
        vec3 blackContribution = vec3(speedLineAmount);
        return max(baseColor - blackContribution, vec3(0.0));
    #endif
}

// ============================================================
// Volumetric Godray Function
// Uses screen-space radial light scattering (crepuscular rays)
// ============================================================
#if GODRAYS_ENABLED == 1
vec3 computeGodrays(vec2 uv, vec3 lightPos, float nightAmt) {
    vec4 sunClip = gbufferProjection * vec4(lightPos, 1.0);
    if (sunClip.w <= 0.0) return vec3(0.0);

    vec2 sunNDC  = sunClip.xy / sunClip.w;
    vec2 sunUV   = sunNDC * 0.5 + 0.5;

    // Direction from pixel toward sun on screen
    vec2 delta   = uv - sunUV;
    delta       *= (1.0 / float(GODRAY_SAMPLES)) * GODRAY_DENSITY;

    float illumination = 0.0;
    float decay        = 1.0;
    vec2  sampleUV     = uv;

    for (int i = 0; i < GODRAY_SAMPLES; i++) {
        sampleUV -= delta;

        // Clamp to valid texture range
        vec2 clampedUV = clamp(sampleUV, vec2(0.001), vec2(0.999));

        // Sample depth — sky pixels (depth == 1.0) let light through
        float depth = texture2D(depthtex0, clampedUV).r;

		float occluded = step(depth, 0.99999); // 1.0 = occluded, 0.0 = sky
	
        // Accumulate light from sky samples only
        float contribution = (1.0 - occluded) * GODRAY_WEIGHT * decay;

        illumination += contribution;
        decay        *= GODRAY_DECAY;
    }

    // Normalize
    illumination /= float(GODRAY_SAMPLES);

    // Fade out when sun is off-screen (smooth edge)
    float edgeFade = 1.0 - smoothstep(0.3, 1.2, length(sunUV - 0.5) * 2.0);

    // Apply rain fade
    float rainFade = 1.0;
    rainFade = 1.0 - rainStrength;

    float strength = illumination * GODRAY_INTENSITY * edgeFade * rainFade;

    // Fade godrays at night using sunAngle (0=sunrise, 0.25=noon, 0.5=sunset, 0.75=midnight).
    // Smoothstep over a short window around sunset (0.5) and sunrise (1.0/0.0)
    // so the transition isn't instant. During night, scale intensity down to
    // GODRAY_NIGHT_INTENSITY so moon godrays are a subtle glow, not full brightness.
    float nightAmount = smoothstep(0.46, 0.54, sunAngle) - smoothstep(0.96, 1.0, sunAngle);
	float intensityScale = mix(1.0, GODRAY_NIGHT_INTENSITY / max(GODRAY_INTENSITY, 0.001), nightAmt);
    strength *= intensityScale;

    vec3 sunColor  = vec3(1.0 + GODRAY_WARMTH, 1.0 + GODRAY_WARMTH * 0.5, 1.0 - GODRAY_WARMTH * 0.2);
    vec3 moonColor = vec3(1.0 - GODRAY_MOON_WARMTH * 0.2, 1.0 - GODRAY_MOON_WARMTH * 0.1, 1.0 + GODRAY_MOON_WARMTH);
    vec3 rayColor  = mix(sunColor, moonColor, nightAmt);

    return rayColor * strength;
}
#endif

void main() {
    vec4 colorData = texture2D(colortex0, texcoord);
    vec3 color = colorData.rgb;
    float rawOutline = colorData.a;
    
    // PAINTERLY MODE: apply Kuwahara filter before any other effects
    #if KUWAHARA_ENABLED == 1
        vec2 texelSize = vec2(1.0 / viewWidth, 1.0 / viewHeight);
        color = applyKuwahara(colortex0, texcoord, texelSize);
        rawOutline = colorData.a; // keep outline data from original
    #endif
    
	// Apply palette quantization to original color BEFORE paper effects
    #if (DRAWING_STYLE == 2 || DRAWING_STYLE == 4) && PAPER_STYLE == 1
		color = blueNoiseDither(color, texcoord * vec2(viewWidth, viewHeight));
        color = applyPaletteQuantization(color);
        #if PURKINJE_ENABLED == 1
            color = applyPurkinjeShift(color, PURKINJE_STRENGTH * nightBlend());
        #endif
    #endif
	
    vec4 edgeData = texture2D(colortex1, texcoord);
    vec2 edgeDirection = edgeData.rg;
    float hatchingOutline = edgeData.b;
	float cameraSpeed = edgeData.a;  // Get speed from alpha channelx
    
    // Get draw-on progress
    float textureProgress = getDrawOnProgress();
    
    #if BLOOM_ENABLED == 1
        vec3 bloomColor = texture2D(colortex2, texcoord).rgb;
    #else
        vec3 bloomColor = vec3(0.0);
    #endif
    
    #if PAPER_STYLE == 1
        // Start with paper base color
        vec3 paperBase = vec3(
            PAPER_WHITENESS * (1.0 + PAPER_WARMTH * 0.1),
            PAPER_WHITENESS * (1.0 + PAPER_WARMTH * 0.05),
            PAPER_WHITENESS * (1.0 - PAPER_WARMTH * 0.1)
        );
        
        // Initialize with pure paper
        vec3 finalColor = paperBase;
        

		// Only add content based on draw-on progress
		if (textureProgress > 0.0 && DISABLE_TEXTURES == 0) {
			// Apply existing color processing
			vec3 processedColor = color;
			
			#if ADVANCED_GRAIN_SYSTEM == 1
				vec2 grainUV = texcoord * vec2(viewWidth, viewHeight) / (512.0 * (viewHeight / 1440.0));
				
				// Pre-adjust dark regions to give room for grain
				processedColor = prepareDarkRegionsForGrain(processedColor, paperBase);
				
				// Apply advanced grain system
				processedColor = applyPencilGrainInteraction(processedColor, grainUV);
			#endif
			
			float brightness = dot(processedColor, vec3(0.299, 0.587, 0.114));
			float darknessFactor = smoothstep(DARKNESS_THRESHOLD - DARKNESS_SMOOTHNESS, DARKNESS_THRESHOLD, brightness);
			darknessFactor = darknessFactor * darknessFactor;
			
			vec3 darkColor = processedColor * (1.0 + vec3(PAPER_WARMTH * 0.1, PAPER_WARMTH * 0.05, -PAPER_WARMTH * 0.1));
			darkColor *= 0.3;
			
			vec3 coloredPaper = processedColor * paperBase;
			vec3 grayscalePaper = paperBase * brightness;
			vec3 paperColor = mix(grayscalePaper, coloredPaper, 1.0 - GRAYSCALE_STRENGTH);
			
			vec3 textureColor = mix(darkColor, paperColor, darknessFactor);
			
			// Calculate vignette effect
			float vignetteAmount = calculateWhiteVignette(texcoord);
			
			// Apply strong vignette to texture content
			vec3 vignetteColor = mix(textureColor, paperBase, vignetteAmount);
			
			float brushMask;
			if (textureProgress >= 1.0) {
				// Effect is complete, show everything normally
				brushMask = 1.0;
			} else {
				brushMask = getEnhancedBrushMask(texcoord, textureProgress);
			}

			// Blend texture based on brush mask
			finalColor = mix(paperBase, vignetteColor, brushMask);
			
			// Clamp to paper brightness
			finalColor = clamp(finalColor, 
				  paperBase * PAPER_MIN_BRIGHTNESS*0.9, 
				  paperBase * PAPER_MAX_BRIGHTNESS);
		}
	
        
        // Add bloom with texture progress
        #if BLOOM_ENABLED == 1
            vec3 processedBloom = bloomColor;
            if (GRAYSCALE_STRENGTH > 0.0) {
                float bloomBrightness = dot(bloomColor, vec3(0.299, 0.587, 0.114));
                vec3 grayscaleBloom = vec3(bloomBrightness);
                processedBloom = mix(bloomColor, grayscaleBloom, GRAYSCALE_STRENGTH);
            }
            
            vec3 paperMaxBright = paperBase * PAPER_MAX_BRIGHTNESS;
            vec3 headroom = max(paperMaxBright - finalColor, vec3(0.0));
            float avgHeadroom = (headroom.r + headroom.g + headroom.b) / 3.0;
            float maxHeadroom = (paperMaxBright.r + paperMaxBright.g + paperMaxBright.b) / 3.0;
            
            float headroomFactor = avgHeadroom / max(maxHeadroom, 0.001);
            float adaptiveBloomIntensity = BLOOM_INTENSITY * mix(0.2, 1.0, headroomFactor) * textureProgress;
            
            vec3 bloomContribution = processedBloom * adaptiveBloomIntensity;
            finalColor += bloomContribution;
            finalColor = min(finalColor, paperMaxBright);
        #endif
        
		vec2 paperUV = texcoord * vec2(viewWidth, viewHeight) / (512.0 * (viewHeight / 1440.0));
		float paperNoise = paperTexture(paperUV);
		float brightness = dot(finalColor, vec3(0.299, 0.587, 0.114));
		float darknessFactor = smoothstep(DARKNESS_THRESHOLD - DARKNESS_SMOOTHNESS, DARKNESS_THRESHOLD, brightness);
		float textureStrength = mix(0.1, 1.0, darknessFactor);
		finalColor += paperNoise * textureStrength;
		
		// Render regular pencil lines with standard blending
		vec2 decodedEdgeDir = edgeDirection * 2.0 - 1.0;
		#if ENABLE_SOFT_PENCIL_LINE == 1
			float pencilLine = renderPencilLineWithDirectionalEdges(rawOutline, texcoord, edgeDirection);
		#else
			float pencilLine = renderPencilLine(rawOutline, texcoord);
		#endif
		float finalOutline = pencilLine * OUTLINE_STRENGTH;
		

		// Apply regular outlines with standard blending
		float sceneBrightness = dot(finalColor.rgb, vec3(0.299, 0.587, 0.114));
		float adjustedOutline = finalOutline * smoothstep(0.1, 0.6, sceneBrightness);
		
		
		// Pencil color - adaptive to scene or fixed
		#if DRAWING_STYLE == 2 && COLOR_ADAPTIVE_PENCIL == 1
			// Make pencil adopt the underlying color
			vec3 underlyingColor = finalColor;
			
			// Calculate base brightness for scaling effects
			float baseBrightness = dot(underlyingColor, vec3(0.299, 0.587, 0.114));
			
			// Scale effects based on brightness: darker areas get more effect
			// inverseScale: 1.0 for black, 0.0 for white
			float inverseScale = 1.0 - baseBrightness;
			float biasedScale = mix(1.0, inverseScale, PENCIL_BRIGHTNESS_BIAS);
			
			// Convert to HSV to manipulate brightness and saturation
			vec3 pencilHSV = rgb2hsv(underlyingColor);
			
			// Boost saturation more on darker colors
			float satBoost = 1.0 + (PENCIL_SATURATION_BOOST - 1.0) * biasedScale;
			pencilHSV.y = min(pencilHSV.y * satBoost, 1.0);
			
			// Darken more on darker surfaces
			float darkenAmount = PENCIL_DARKEN_AMOUNT * biasedScale;
			pencilHSV.z = max(pencilHSV.z - darkenAmount, 0.0);
			
			// Convert back to RGB
			vec3 pencilTone = hsv2rgb(pencilHSV);
			
			// Use subtractive blending instead of multiplicative
			vec3 darkeningAmount = (underlyingColor - pencilTone) * adjustedOutline;
			finalColor = finalColor - darkeningAmount;
			finalColor = max(finalColor, vec3(0.0));
		#else
			// Original fixed pencil color with multiplicative blending
			vec3 pencilTone = vec3(PENCIL_R, PENCIL_G, PENCIL_B);
			finalColor *= mix(vec3(1.0), pencilTone, adjustedOutline);
		#endif
				
        
        // Add hatching based on progress
        if (hatchingOutline > 0.0) {
            float hatchingLine = renderPencilLine(hatchingOutline, texcoord);
            float finalHatching = hatchingLine * OUTLINE_STRENGTH;
            
            #if COLOR_GRID_ENABLED == 1
				// Get the original color before any processing for grid color generation
				vec3 originalSceneColor = texture2D(colortex0, texcoord).rgb;
				
				// Generate oversaturated grid color based on original image
				vec3 gridColor = generateGridColor(originalSceneColor, texcoord);
				
				#if ADAPTIVE_COLOR_STRENGTH == 1
				// Make color strength adaptive to original image properties
				float originalBrightness = dot(originalSceneColor, vec3(0.299, 0.587, 0.114));
				float originalSaturation = rgb2hsv(originalSceneColor).y;
				float adaptiveStrength = GRID_COLOR_INTENSITY * (0.5 + originalSaturation * 0.5) * (0.7 + originalBrightness * 0.3);
				#else
				float adaptiveStrength = GRID_COLOR_INTENSITY;
				#endif
				
				// Apply colored hatching lines
				vec3 coloredHatching = mix(vec3(1.0), gridColor, adaptiveStrength);
				finalColor *= mix(vec3(1.0), coloredHatching, finalHatching);
				
				// Color bleeding effect - subtle color influence in surrounding areas
				#if COLOR_BLEEDING > 0.0
					if (finalHatching > 0.1) {
						// Sample neighboring pixels to create bleeding effect
						vec2 pixelSize = vec2(1.0 / viewWidth, 1.0 / viewHeight);
						float bleedRadius = 3.0;
						vec3 bleedColor = vec3(0.0);
						float bleedWeight = 0.0;
						
						for (float x = -bleedRadius; x <= bleedRadius; x += 1.0) {
							for (float y = -bleedRadius; y <= bleedRadius; y += 1.0) {
								if (x == 0.0 && y == 0.0) continue;
								
								vec2 sampleUV = texcoord + vec2(x, y) * pixelSize;
								float sampleHatching = texture2D(colortex1, sampleUV).b;
								
								if (sampleHatching > 0.1) {
									float dist = sqrt(x*x + y*y);
									float weight = 1.0 / (1.0 + dist);
									bleedColor += gridColor * weight * sampleHatching;
									bleedWeight += weight;
								}
							}
						}
						
						if (bleedWeight > 0.0) {
							bleedColor /= bleedWeight;
							// Apply subtle bleeding
							finalColor = mix(finalColor, finalColor * (1.0 + bleedColor * COLOR_BLEEDING * 0.2), 0.3);
						}
					}
				#endif
			#else
				#if DRAWING_STYLE == 2 && COLOR_ADAPTIVE_PENCIL == 1
					// Adaptive hatching color
					vec3 underlyingColor = finalColor;
					
					float baseBrightness = dot(underlyingColor, vec3(0.299, 0.587, 0.114));
					float inverseScale = 1.0 - baseBrightness;
					float biasedScale = mix(1.0, inverseScale, PENCIL_BRIGHTNESS_BIAS);
					
					vec3 hatchHSV = rgb2hsv(underlyingColor);
					
					float satBoost = 1.0 + (PENCIL_SATURATION_BOOST - 1.0) * biasedScale;
					hatchHSV.y = min(hatchHSV.y * satBoost, 1.0);
					
					float darkenAmount = PENCIL_DARKEN_AMOUNT * biasedScale;
					hatchHSV.z = max(hatchHSV.z - darkenAmount, 0.0);
					
					vec3 pencilTone = hsv2rgb(hatchHSV);
					
					vec3 darkeningAmount = (underlyingColor - pencilTone) * finalHatching;
					finalColor = finalColor - darkeningAmount;
				#else
					vec3 pencilTone = vec3(PENCIL_R, PENCIL_G, PENCIL_B);
					finalColor = finalColor - finalHatching * (1.0 - pencilTone);
				#endif
				finalColor = max(finalColor, vec3(0.0));
			#endif
        }
		
		// Option 1: Hard Clamp (matches existing PAPER_MAX_BRIGHTNESS approach)
		//finalColor = clamp(finalColor, paperBase * PAPER_MIN_BRIGHTNESS, paperBase * PAPER_MAX_BRIGHTNESS);
		
		// OR Option 2: Soft minimum with hard maximum
		finalColor = applySoftBrightnessLimits(finalColor, paperBase);
		
		//Darkness paper grain effect
		if (brightness < DARKNESS_THRESHOLD * 2.0) { // Affect areas up to 20% brightness
			float darkGrain = paperTexture(paperUV * 1.5); // Different scale/seed
			float darkStrength = smoothstep(DARKNESS_THRESHOLD * 2.0, 0.0, brightness);
			
			// Apply darkening grain to dark areas
			finalColor += darkGrain * darkStrength * -0.3; // Negative = darkening
			finalColor = max(finalColor, vec3(0.0)); // Prevent negative colors
		}

		finalColor = applySpeedLines(finalColor, texcoord, cameraSpeed, vDynamicInnerRadius);
		
		// Apply volumetric godrays
		#if GODRAYS_ENABLED == 1
			float nightAmount = smoothstep(0.46, 0.54, sunAngle) - smoothstep(0.96, 1.0, sunAngle);
			// Primary light (sun or moon, whichever is dominant)
			finalColor += computeGodrays(texcoord, shadowLightPosition, nightAmount);
			#if GODRAY_DUAL_TWILIGHT == 1
				float twilightStrength = smoothstep(0.40, 0.50, sunAngle) * (1.0 - smoothstep(0.50, 0.60, sunAngle))
									   + smoothstep(0.90, 1.00, sunAngle) * (1.0 - smoothstep(0.00, 0.10, sunAngle));
				finalColor += computeGodrays(texcoord, -shadowLightPosition, 1.0 - nightAmount) * twilightStrength;
			#endif
		#endif
		
		// Apply color grading to final color before output
		#if DRAWING_STYLE == 2 || DRAWING_STYLE == 4
			//finalColor = applyPaletteQuantization(finalColor);
		#endif
		#if PURKINJE_ENABLED == 1 && (DRAWING_STYLE == 2 || DRAWING_STYLE == 4)
			#if !((DRAWING_STYLE == 2 || DRAWING_STYLE == 4) && PAPER_STYLE == 1)
				finalColor = applyPurkinjeShift(finalColor, PURKINJE_STRENGTH * nightBlend());
			#endif
		#endif
		finalColor = applyColorGrading(finalColor);
	
		
        
        gl_FragData[0] = vec4(finalColor, 1.0);
    #else
        // Non-paper mode - still apply progress
        vec3 finalColor = mix(vec3(1.0), color, textureProgress);
        
        #if BLOOM_ENABLED == 1
            finalColor += bloomColor * BLOOM_INTENSITY * textureProgress;
        #endif
		
		// Apply volumetric godrays
		#if GODRAYS_ENABLED == 1
			float nightAmount = smoothstep(0.46, 0.54, sunAngle) - smoothstep(0.96, 1.0, sunAngle);
			// Primary light (sun or moon, whichever is dominant)
			finalColor += computeGodrays(texcoord, shadowLightPosition, nightAmount);
			#if GODRAY_DUAL_TWILIGHT == 1
				float twilightStrength = smoothstep(0.40, 0.50, sunAngle) * (1.0 - smoothstep(0.50, 0.60, sunAngle))
									   + smoothstep(0.90, 1.00, sunAngle) * (1.0 - smoothstep(0.00, 0.10, sunAngle));
				finalColor += computeGodrays(texcoord, -shadowLightPosition, 1.0 - nightAmount) * twilightStrength;
			#endif
		#endif
		
		// Apply palette quantization in non-paper painterly mode
		#if DRAWING_STYLE == 4
			finalColor = blueNoiseDither(finalColor, texcoord * vec2(viewWidth, viewHeight));
			finalColor = applyPaletteQuantization(finalColor);
		#endif
		#if PURKINJE_ENABLED == 1 && (DRAWING_STYLE == 2 || DRAWING_STYLE == 4)
			finalColor = applyPurkinjeShift(finalColor, PURKINJE_STRENGTH * nightBlend());
		#endif
        
        finalColor = applyColorGrading(finalColor);
        
        gl_FragData[0] = vec4(finalColor, 1.0);
    #endif
}