#version 400 uniform sampler2D texGrass; // Unit 0 uniform sampler2D texBorder; // Unit 1 uniform sampler2D texRock; // Unit 2 (Stone cliffs) uniform sampler2D texSand; // Unit 3 (Soil beach) uniform sampler2D texWater; // Unit 4 (Dynamic ocean) uniform sampler2D texSnow; // Unit 5 (Texturelabs_Water_155XL snow/ice) uniform sampler2D texTypeMap; // Unit 6 (Biome type map) uniform sampler2D texSahara; // Unit 7 (Texturelabs_Soil_133L desert sand) uniform sampler2D texTundra; // Unit 8 (Texturelabs_Soil_135XL tundra soil) uniform sampler2D texRedSand; // Unit 9 (Texturelabs_RedSand prairie sand) uniform sampler2D texRedRock; // Unit 10 (Texturelabs_RedRock canyon cliff) uniform sampler2D texForest; // Unit 11 (Texturelabs_Forest_seamless woodland floor) uniform sampler2D texFarm; // Unit 12 (images/farm.jpeg) uniform int isFarm; uniform float farmTextureScale; uniform int farmMappingMode; uniform int hasRoad; uniform int roadMask; uniform int roadType; uniform float roadRadius; uniform float roadBorderWidth; uniform float roadTextureScale; uniform float roadOpacity; uniform vec3 roadColor; uniform vec3 roadBorderColor; uniform float time; uniform int riverMask; smooth in vec3 vNormal; smooth in vec3 vWorldPos; in vec3 fragVert; in float z_pos; uniform vec3 cameraPosition; uniform mat4 modelMatrix; in vec2 tilePos; in vec2 fragTexCoord; out vec4 finalColor; struct SimpleDirectionalLight { vec3 vColor; vec3 vDirection; float fAmbientIntensity; }; uniform SimpleDirectionalLight sunLight; vec4 GetDirectionalLightColor(SimpleDirectionalLight dirLight, vec3 norm) { float fDiffuseIntensity = max(dot(norm, -dirLight.vDirection), 0.0); float fMult = dirLight.fAmbientIntensity + fDiffuseIntensity * (1.0 - dirLight.fAmbientIntensity * 0.5); return vec4(dirLight.vColor * fMult, 1.0); } vec4 GetSpecularColor(vec3 worldPos, vec3 eyePos, SimpleDirectionalLight dLight, vec3 norm, float shininess, float intensity) { vec3 vReflectedVector = normalize(reflect(dLight.vDirection, norm)); vec3 vVertexToEyeVector = normalize(eyePos - worldPos); float fSpecularFactor = max(dot(vVertexToEyeVector, vReflectedVector), 0.0); if (fSpecularFactor > 0.0) { fSpecularFactor = pow(fSpecularFactor, shininess); return vec4(dLight.vColor, 1.0) * intensity * fSpecularFactor; } return vec4(0.0); } float distToSegment(vec2 p, vec2 a, vec2 b) { vec2 ba = b - a; vec2 pa = p - a; float l2 = dot(ba, ba); if (l2 <= 1e-6) return length(pa); float h = clamp(dot(pa, ba) / l2, 0.0, 1.0); return length(pa - ba * h); } const vec2 EDGE_MIDPOINTS[6] = vec2[6]( vec2(-0.375, 0.21650635), vec2( 0.0, 0.43301270), vec2( 0.375, 0.21650635), vec2( 0.375, -0.21650635), vec2( 0.0, -0.43301270), vec2(-0.375, -0.21650635) ); float bezierCurveDist(vec2 p, vec2 p0, vec2 p2) { vec2 s0 = p0; vec2 s1 = 0.5625 * p0 + 0.0625 * p2; vec2 s2 = 0.2500 * p0 + 0.2500 * p2; vec2 s3 = 0.0625 * p0 + 0.5625 * p2; vec2 s4 = p2; float d0 = distToSegment(p, s0, s1); float d1 = distToSegment(p, s1, s2); float d2 = distToSegment(p, s2, s3); float d3 = distToSegment(p, s3, s4); return min(min(d0, d1), min(d2, d3)); } float getRiverDistance(vec2 p, int mask) { if (mask == 0) return 999.0; int edges[6]; int count = 0; for (int e = 0; e < 6; ++e) { if ((mask & (1 << e)) != 0) { edges[count] = e; count++; } } if (count == 2) { return bezierCurveDist(p, EDGE_MIDPOINTS[edges[0]], EDGE_MIDPOINTS[edges[1]]); } else { float minDist = 999.0; for (int i = 0; i < count; ++i) { float d = distToSegment(p, EDGE_MIDPOINTS[edges[i]], vec2(0.0)); minDist = min(minDist, d); } return minDist; } } float getRoadDistance(vec2 p, int mask) { int edges[6]; int count = 0; for (int e = 0; e < 6; ++e) { if ((mask & (1 << e)) != 0) { edges[count] = e; count++; } } if (count == 0) { // Unconnected road on a single tile: short natural country track segment at tile center return distToSegment(p, vec2(-0.14, -0.05), vec2(0.14, 0.05)); } else if (count == 1) { return distToSegment(p, EDGE_MIDPOINTS[edges[0]], vec2(0.0)); } else if (count == 2) { return bezierCurveDist(p, EDGE_MIDPOINTS[edges[0]], EDGE_MIDPOINTS[edges[1]]); } else { float minDist = 999.0; for (int i = 0; i < count; ++i) { minDist = min(minDist, distToSegment(p, EDGE_MIDPOINTS[edges[i]], vec2(0.0))); for (int j = i + 1; j < count; ++j) { minDist = min(minDist, bezierCurveDist(p, EDGE_MIDPOINTS[edges[i]], EDGE_MIDPOINTS[edges[j]])); } } return minDist; } } void main() { vec3 norm = normalize(vNormal); // 1. Biome Typemap Sampling // World extent: [0..225.0 km, 0..108.22 km] // Note: QImage row 0 is North (worldY = 108.22), which QOpenGLTexture maps to v = 0.0. // Invert Y so North samples row 0 and South samples row 1499. vec2 uvType = vec2(clamp(vWorldPos.x / 225.0, 0.0, 1.0), 1.0 - clamp(vWorldPos.y / 108.22, 0.0, 1.0)); vec4 typePixel = texture(texTypeMap, uvType); // Compute smooth biome weights from typemap colors: // 1. Prairie: red dominant (r - g) float prairieWeight = clamp((typePixel.r - typePixel.g - 0.20) / 0.25, 0.0, 1.0); // 2. Sahara: yellow (r & g high, b low) float saharaWeight = clamp((min(typePixel.r, typePixel.g) - 0.45) / 0.25, 0.0, 1.0) * (1.0 - prairieWeight); // 3. Forest: elevated blue with low red (b > 0.15, r < 0.25) float forestWeight = clamp((typePixel.b - 0.15) / 0.20, 0.0, 1.0) * clamp((0.25 - typePixel.r) / 0.15, 0.0, 1.0) * (1.0 - prairieWeight) * (1.0 - saharaWeight); // 4. Tundra: muted slate/olive (low saturation, moderate/low green) float sat = max(typePixel.r, max(typePixel.g, typePixel.b)) - min(typePixel.r, min(typePixel.g, typePixel.b)); float tundraWeight = clamp((0.25 - sat) / 0.15, 0.0, 1.0) * clamp((0.55 - typePixel.g) / 0.15, 0.0, 1.0) * (1.0 - prairieWeight) * (1.0 - saharaWeight) * (1.0 - forestWeight); // 5. Grassland: green dominant open plains float grasslandWeight = clamp(1.0 - (prairieWeight + saharaWeight + forestWeight + tundraWeight), 0.0, 1.0); // 2. Polar Latitude Calculation (World Y in [0..108.25 km]) float normY = clamp(vWorldPos.y / 108.25, 0.0, 1.0); float distFromCenter = abs(normY - 0.5) * 2.0; // 0.0 at middle, 1.0 at North/South poles float poleFactor = smoothstep(0.40, 0.85, distFromCenter); // Snowline in kilometers: 2.5 (2500m) in temperate zone, dropping to 0.0 (sea level) at poles! float snowLine = mix(2.5, 0.0, poleFactor); // 3. Water vs Land detection (z_pos = height in kilometers, 0.0 = sea level) float oceanWaterFac = 1.0 - smoothstep(0.005, 0.025, z_pos); // River water detection float riverDist = getRiverDistance(fragVert.xy, riverMask); float riverWaterRadius = 0.024; float riverBankRadius = 0.052; float riverWaterFac = 1.0 - smoothstep(riverWaterRadius * 0.7, riverWaterRadius, riverDist); float riverBankFac = (1.0 - smoothstep(riverWaterRadius, riverBankRadius, riverDist)) * (1.0 - riverWaterFac); // Combined water factor (ocean or river) float waterFac = max(oceanWaterFac, riverWaterFac); // 4. Dynamic animated ocean water vec2 uvW1 = vWorldPos.xy * 0.30 + vec2(time * 0.035, time * 0.020); vec2 uvW2 = vWorldPos.xy * 0.38 + vec2(-time * 0.025, time * 0.030); vec2 waveDistort1 = vec2(sin(uvW1.y * 14.0 + time * 2.2), cos(uvW1.x * 14.0 + time * 1.8)) * 0.025; vec2 waveDistort2 = vec2(cos(uvW2.y * 18.0 - time * 1.5), sin(uvW2.x * 18.0 - time * 2.5)) * 0.020; vec4 wSample1 = texture(texWater, uvW1 + waveDistort1); vec4 wSample2 = texture(texWater, uvW2 + waveDistort2); vec4 waterColor = mix(wSample1, wSample2, 0.5); waterColor.rgb = mix(waterColor.rgb, vec3(0.05, 0.30, 0.50), 0.30); vec3 waterNorm = normalize(vec3( (waveDistort1.x + waveDistort2.x) * 2.5, (waveDistort1.y + waveDistort2.y) * 2.5, 1.0 )); // 5. Triplanar mapping coordinates for Land, Cliffs & Snow vec3 blendWeights = pow(abs(norm), vec3(4.0)); blendWeights /= (blendWeights.x + blendWeights.y + blendWeights.z); vec2 uvX = vWorldPos.yz * 0.35; vec2 uvY = vWorldPos.xz * 0.35; vec2 uvZ = vWorldPos.xy * 0.35; // Rock textures: // Gray rock (Texturelabs_Stone_136XL_seamless) vec4 rockGray = texture(texRock, uvX) * blendWeights.x + texture(texRock, uvY) * blendWeights.y + texture(texRock, uvZ) * blendWeights.z; // Red rock canyon cliff (Texturelabs_RedRock_seamless) vec4 rockRed = texture(texRedRock, uvX) * blendWeights.x + texture(texRedRock, uvY) * blendWeights.y + texture(texRedRock, uvZ) * blendWeights.z; // Cliff rock: red sandstone in Prairie canyon, gray alpine elsewhere vec4 cliffRockColor = mix(rockGray, rockRed, prairieWeight); // Snow / Ice texture (Texturelabs_Water_155XL_seamless) vec4 snowColor = texture(texSnow, uvX) * blendWeights.x + texture(texSnow, uvY) * blendWeights.y + texture(texSnow, uvZ) * blendWeights.z; // Ground textures: vec4 grassColor = texture(texGrass, uvZ); vec4 forestColor = texture(texForest, uvZ); vec4 beachColor = texture(texSand, uvZ); vec4 saharaColor = texture(texSahara, uvZ); vec4 tundraColor = texture(texTundra, uvZ); vec4 redSandColor = texture(texRedSand, uvZ); // Composite ground texture across all biomes: vec4 biomeGround = grassColor * grasslandWeight + forestColor * forestWeight + saharaColor * saharaWeight + tundraColor * tundraWeight + redSandColor * prairieWeight; // Farm improvement texture applied when a farm is present if (isFarm == 1) { float scale = max(0.01, farmTextureScale); vec2 farmUV; if (farmMappingMode == 1) { // Hex-centered mapping with aspect ratio correction vec2 centered = vec2(tilePos.x - 0.5, (tilePos.y - 0.4328799) * 1.154734411); farmUV = centered * scale + vec2(0.5, 0.5); } else { // World-space coordinates: perfectly seamless and continuous across adjacent hex tiles farmUV = vWorldPos.xy * scale; } vec4 farmColor = texture(texFarm, farmUV); biomeGround = farmColor; } // Shoreline beach: where gentle slopes meet water (sea level up to ~110m, or along rivers) // Steep slopes (norm.z < 0.55) remain rocky cliffs; gentle slopes (norm.z > 0.80) get a sandy beach float gentleSlope = smoothstep(0.55, 0.85, norm.z); float coastBeach = (1.0 - smoothstep(0.015, 0.110, z_pos)) * smoothstep(0.003, 0.015, z_pos) * gentleSlope; float riverBeach = riverBankFac; float beachFac = max(coastBeach, riverBeach); // Suppress beach only at the extreme polar ice sheet edges float polarIceCover = smoothstep(0.82, 0.96, distFromCenter); beachFac *= (1.0 - polarIceCover); // Apply sandy beach across all land types gently meeting water biomeGround = mix(biomeGround, beachColor, beachFac * 0.90); // Polar pack ice on cold polar waters float polarSeaIce = smoothstep(0.72, 0.94, distFromCenter); waterColor = mix(waterColor, snowColor, polarSeaIce * 0.95); // 6. Slope & Elevation Blending Rules // norm.z: 1.0 = flat horizontal ground, < 0.55 = steep slope / vertical cliff float flatness = clamp((norm.z - 0.55) / 0.25, 0.0, 1.0); // Mountain rock elevation factor: begins at 1000m (1.0 km), ramps up towards 2200m (2.2 km) float rockLine = mix(1.0, 0.0, poleFactor); float mountainAltitude = smoothstep(rockLine, min(snowLine, rockLine + 1.2), z_pos); // Steep slopes are always rock; flat ground becomes rock as altitude rises above 1000m float rockWeight = max(1.0 - flatness, mountainAltitude); // Snow rule: starts above 2500m in temperate zone, dropping to sea level at poles float snowHeight = smoothstep(snowLine - 0.20, snowLine + 0.30, z_pos); float snowSlope = clamp((norm.z - 0.28) / 0.25, 0.0, 1.0); float snowWeight = snowHeight * snowSlope; // At polar caps, ice covers flat ground at sea level float polarLandIce = smoothstep(0.72, 0.92, distFromCenter); snowWeight = max(snowWeight, polarLandIce * snowSlope); // Composite ground: biome ground -> mountain rock above 1000m & on cliffs -> snow above 2500m vec4 nonSnowLand = mix(biomeGround, cliffRockColor, rockWeight); vec4 landColor = mix(nonSnowLand, snowColor, snowWeight); // 6b. Road Texture Integration (blended directly into the tile surface texture) if (hasRoad == 1 && oceanWaterFac < 0.95) { float rawRoadDist = getRoadDistance(fragVert.xy, roadMask); float rScale = max(0.5, roadTextureScale); vec2 uvRoad = vWorldPos.xy * rScale; vec3 soilTex = mix(texture(texSand, uvRoad).rgb, texture(texTundra, uvRoad * 1.45).rgb, 0.45); vec3 stoneTex = texture(texRock, uvRoad * 1.35).rgb; // Subtle organic edge roughness from soil texture grain so road borders blend naturally into terrain float edgeGrain = (soilTex.r - 0.5) * 0.008; float dRoad = max(0.0, rawRoadDist + edgeGrain); float rCore = max(0.014, roadRadius); float rOuter = rCore + max(0.006, roadBorderWidth); float roadCoreFac = 1.0 - smoothstep(rCore * 0.50, rCore, dRoad); float roadTotalFac = 1.0 - smoothstep(rCore * 0.65, rOuter, dRoad); if (roadTotalFac > 0.001) { vec3 roadSurf; if (roadType == 1) { // Stone / Paved road: rich stone texture modulated by configured colors vec3 stoneCenter = mix(stoneTex, roadColor, 0.55) * (0.75 + 0.50 * stoneTex.g); vec3 stoneEdge = mix(stoneTex, roadBorderColor, 0.65) * 0.85; roadSurf = mix(stoneEdge, stoneCenter, roadCoreFac); } else if (roadType == 2) { // Railroad: gravel ballast bed with dark twin rail tracks vec3 ballast = mix(stoneTex, roadBorderColor, 0.60); float railLine = smoothstep(rCore * 0.35, rCore * 0.50, rawRoadDist) * (1.0 - smoothstep(rCore * 0.55, rCore * 0.72, rawRoadDist)); roadSurf = mix(ballast, roadColor * (0.8 + 0.4 * stoneTex.r), max(roadCoreFac * 0.6, railLine)); } else { // Dirt road: natural packed earth & wheel ruts blended with soil texture float rutPattern = smoothstep(rCore * 0.25, rCore * 0.65, rawRoadDist); vec3 packedEarth = mix(soilTex, roadColor, 0.52) * (0.78 + 0.45 * soilTex.r); vec3 wornShoulder = mix(soilTex, roadBorderColor, 0.60) * (0.72 + 0.40 * soilTex.g); vec3 innerTrack = mix(packedEarth, wornShoulder, rutPattern * 0.45); roadSurf = mix(wornShoulder, innerTrack, roadCoreFac); } // If crossing a river, render a wooden/stone bridge deck and suppress river water under the bridge if (riverWaterFac > 0.01) { vec3 bridgeCol = (roadType >= 1) ? mix(stoneTex, vec3(0.58, 0.56, 0.53), 0.65) : mix(soilTex, vec3(0.55, 0.38, 0.20), 0.70); roadSurf = mix(roadSurf, bridgeCol, riverWaterFac); waterFac *= (1.0 - roadTotalFac * 0.95); } float blendAlpha = roadTotalFac * clamp(roadOpacity, 0.0, 1.0) * (1.0 - oceanWaterFac); landColor.rgb = mix(landColor.rgb, roadSurf, blendAlpha); } } // 7. Combine Land and Sea vec4 surfaceColor = mix(landColor, waterColor, waterFac); // 8. Hexagon grid border outline vec4 bcolor = texture(texBorder, vec2(tilePos.x, tilePos.y * 1.154734411)); float borderAlpha = bcolor.a * (1.0 - waterFac * 0.7); // 9. Lighting & Specular glint vec3 activeNorm = mix(norm, waterNorm, waterFac); vec4 lightDiffuse = GetDirectionalLightColor(sunLight, activeNorm); // Specular: crisp sparkle on snow and water float specShininess = mix(mix(16.0, 32.0, snowWeight), 48.0, waterFac); float specIntensity = mix(mix(0.12, 0.55, snowWeight), 0.85, waterFac); vec4 lightSpecular = GetSpecularColor(vWorldPos, cameraPosition, sunLight, activeNorm, specShininess, specIntensity); finalColor = (surfaceColor + bcolor * borderAlpha) * lightDiffuse + lightSpecular; }