From eaa847ac277750448c0fd00169395570e4a36a58 Mon Sep 17 00:00:00 2001 From: brupje Date: Mon, 9 Nov 2026 21:34:29 +0100 Subject: [PATCH] Improve road curve smoothness (C1 sin^2 curve) and crisp road surface/curb contrast --- config/roads.yaml | 38 +++++++-------- roadconfig.h | 12 ++--- shaders/simple.frag | 115 ++++++++++++++++++++------------------------ 3 files changed, 77 insertions(+), 88 deletions(-) diff --git a/config/roads.yaml b/config/roads.yaml index 0005480..41582b1 100644 --- a/config/roads.yaml +++ b/config/roads.yaml @@ -1,7 +1,7 @@ # Road System Configuration default_road_type: "dirt" build_duration_ticks: 2 -meander_amplitude: 0.065 +meander_amplitude: 0.025 meander_subdivisions: 6 types: @@ -9,35 +9,35 @@ types: id: "dirt" name: "Dirt Road" speed_multiplier: 1.30 # +30% movement speed - color: "#c49a62" - border_color: "#6b4b23" + color: "#d8b480" + border_color: "#52381c" width: 3.5 - world_width: 0.068 - border_width: 0.022 - texture_scale: 2.4 - opacity: 0.90 + world_width: 0.056 + border_width: 0.012 + texture_scale: 4.5 + opacity: 0.96 cost: 5 stone: id: "stone" name: "Stone Road" speed_multiplier: 1.60 # +60% movement speed - color: "#9aa1a8" - border_color: "#464b50" + color: "#c2beb6" + border_color: "#46484c" width: 4.0 - world_width: 0.078 - border_width: 0.020 - texture_scale: 2.8 - opacity: 0.94 + world_width: 0.064 + border_width: 0.012 + texture_scale: 5.5 + opacity: 0.98 cost: 15 rail: id: "rail" name: "Railroad" speed_multiplier: 2.50 # +150% movement speed - color: "#3a3c40" - border_color: "#181a1c" + color: "#4a4e54" + border_color: "#22252a" width: 4.5 - world_width: 0.086 - border_width: 0.024 - texture_scale: 3.0 - opacity: 0.96 + world_width: 0.070 + border_width: 0.014 + texture_scale: 6.0 + opacity: 0.98 cost: 30 diff --git a/roadconfig.h b/roadconfig.h index 97d732f..f90f487 100644 --- a/roadconfig.h +++ b/roadconfig.h @@ -27,13 +27,13 @@ struct RoadTypeConfig { QString id = "dirt"; QString name = "Dirt Road"; float speedMultiplier = 1.30f; - QString colorStr = "#c49a62"; - QString borderColorStr = "#6b4b23"; + QString colorStr = "#d8b480"; + QString borderColorStr = "#52381c"; float width = 3.5f; - float worldWidth = 0.068f; - float borderWidth = 0.022f; - float textureScale = 2.4f; - float opacity = 0.90f; + float worldWidth = 0.056f; + float borderWidth = 0.012f; + float textureScale = 4.5f; + float opacity = 0.96f; int cost = 5; QColor getColor() const { return QColor(colorStr); } diff --git a/shaders/simple.frag b/shaders/simple.frag index 553aab6..317459d 100644 --- a/shaders/simple.frag +++ b/shaders/simple.frag @@ -103,23 +103,22 @@ float bezierCurveDist(vec2 p, vec2 p0, vec2 p2) { return min(min(d0, d1), min(d2, d3)); } -float meanderingBezierDist(vec2 p, vec2 p0, vec2 p2, float amp, float s1, float s2) { +float meanderingBezierDist(vec2 p, vec2 p0, vec2 p2, float amp, float s1) { vec2 chord = p2 - p0; float cLen = length(chord); vec2 perp = (cLen > 1e-4) ? vec2(-chord.y / cLen, chord.x / cLen) : vec2(0.0, 1.0); - // 6-segment meandering curve from p0 (t=0) to p2 (t=1). - // At t=0 and t=1, sin(pi*t) == 0 and sin(2*pi*t) == 0, so endpoints remain locked at p0 and p2! + // sin^2(pi*t) has BOTH zero value AND zero derivative at t=0 and t=1, + // guaranteeing C1-continuous, kink-free transitions across hex borders! const float PI_VAL = 3.14159265; vec2 prevPt = p0; float minDist = 999.0; - for (int i = 1; i <= 6; ++i) { - float t = float(i) / 6.0; + for (int i = 1; i <= 8; ++i) { + float t = float(i) / 8.0; float omt = 1.0 - t; - // Base quadratic Bezier through (0, 0) vec2 basePt = (omt * omt) * p0 + (t * t) * p2; - // Asymmetric S-curve + bend wave perpendicular to the road chord - float wave = amp * (0.65 * s1 * sin(PI_VAL * t) + 0.85 * s2 * sin(2.0 * PI_VAL * t)); + float st = sin(PI_VAL * t); + float wave = amp * s1 * (st * st); vec2 curPt = basePt + perp * wave; minDist = min(minDist, distToSegment(p, prevPt, curPt)); prevPt = curPt; @@ -127,18 +126,18 @@ float meanderingBezierDist(vec2 p, vec2 p0, vec2 p2, float amp, float s1, float return minDist; } -float meanderingBranchDist(vec2 p, vec2 pe, vec2 centerHub, float amp, float s) { - vec2 dir = centerHub - pe; - float dLen = length(dir); - vec2 perp = (dLen > 1e-4) ? vec2(-dir.y / dLen, dir.x / dLen) : vec2(0.0, 1.0); +float meanderingBranchDist(vec2 p, vec2 pe, float amp, float s1) { + float dLen = length(pe); + vec2 perp = (dLen > 1e-4) ? vec2(pe.y / dLen, -pe.x / dLen) : vec2(0.0, 1.0); const float PI_VAL = 3.14159265; vec2 prevPt = pe; float minDist = 999.0; - for (int i = 1; i <= 4; ++i) { - float t = float(i) / 4.0; - vec2 basePt = mix(pe, centerHub, t); - float wave = amp * 0.65 * s * sin(PI_VAL * t); + for (int i = 1; i <= 6; ++i) { + float t = float(i) / 6.0; + vec2 basePt = pe * (1.0 - t); + float st = sin(PI_VAL * t); + float wave = amp * 0.65 * s1 * (st * st); vec2 curPt = basePt + perp * wave; minDist = min(minDist, distToSegment(p, prevPt, curPt)); prevPt = curPt; @@ -171,11 +170,8 @@ float getRiverDistance(vec2 p, int mask) { } float getRoadDistance(vec2 p, int mask, vec2 tileCenterWorld, float amp) { - // Deterministic per-tile random signs so each tile meanders uniquely float h1 = fract(sin(dot(tileCenterWorld, vec2(12.9898, 78.233))) * 43758.5453); - float h2 = fract(sin(dot(tileCenterWorld, vec2(39.3468, 11.135))) * 24634.6345); float s1 = (h1 > 0.5) ? 1.0 : -1.0; - float s2 = (h2 > 0.5) ? 1.0 : -1.0; int edges[6]; int count = 0; @@ -187,21 +183,17 @@ float getRoadDistance(vec2 p, int mask, vec2 tileCenterWorld, float amp) { } if (count == 0) { - // Unconnected road on a single tile: short winding country track at tile center - return meanderingBezierDist(p, vec2(-0.16, -0.06 * s1), vec2(0.16, 0.06 * s1), amp * 0.6, s1, s2); + return meanderingBezierDist(p, vec2(-0.18, 0.0), vec2(0.18, 0.0), amp * 0.7, s1); } else if (count == 1) { - vec2 centerHub = vec2(s1, s2) * (amp * 0.25); - return meanderingBranchDist(p, EDGE_MIDPOINTS[edges[0]], centerHub, amp, s1); + return meanderingBranchDist(p, EDGE_MIDPOINTS[edges[0]], amp, s1); } else if (count == 2) { - return meanderingBezierDist(p, EDGE_MIDPOINTS[edges[0]], EDGE_MIDPOINTS[edges[1]], amp, s1, s2); + return meanderingBezierDist(p, EDGE_MIDPOINTS[edges[0]], EDGE_MIDPOINTS[edges[1]], amp, s1); } else { - vec2 centerHub = vec2(s1, s2) * (amp * 0.22); float minDist = 999.0; for (int i = 0; i < count; ++i) { - float branchSign = ((edges[i] % 2) == 0) ? s1 : -s2; - minDist = min(minDist, meanderingBranchDist(p, EDGE_MIDPOINTS[edges[i]], centerHub, amp * 0.75, branchSign)); + minDist = min(minDist, meanderingBranchDist(p, EDGE_MIDPOINTS[edges[i]], amp * 0.5, s1)); for (int j = i + 1; j < count; ++j) { - minDist = min(minDist, meanderingBezierDist(p, EDGE_MIDPOINTS[edges[i]], EDGE_MIDPOINTS[edges[j]], amp * 0.65, s1, s2)); + minDist = min(minDist, bezierCurveDist(p, EDGE_MIDPOINTS[edges[i]], EDGE_MIDPOINTS[edges[j]])); } } return minDist; @@ -375,59 +367,56 @@ void main() { vec2 tileCenterWorld = vWorldPos.xy - fragVert.xy; float amp = max(0.0, roadMeander); - // Additional subtle world-space domain warp inside the tile (fades to 0 at hex border so edges stay locked) - float borderFade = 1.0 - smoothstep(0.24, 0.42, length(fragVert.xy)); - vec2 microWarp = vec2( - sin(vWorldPos.y * 13.0 + vWorldPos.x * 5.0), - cos(vWorldPos.x * 13.0 - vWorldPos.y * 5.0) - ) * (amp * 0.22 * borderFade); - - float rawRoadDist = getRoadDistance(fragVert.xy + microWarp, roadMask, tileCenterWorld, amp); - float rScale = max(0.5, roadTextureScale); + float rawRoadDist = getRoadDistance(fragVert.xy, roadMask, tileCenterWorld, amp); + float rScale = max(1.0, 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; + vec3 soilTex = texture(texSand, uvRoad).rgb; + vec3 stoneTex = texture(texRock, uvRoad * 1.25).rgb; + float pebbleRelief = mix(soilTex.r, stoneTex.r, 0.5) - 0.5; - // 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); + // Very subtle micro-edge grain (0.0018) so the road has crisp, clean borders + float dRoad = max(0.0, rawRoadDist + pebbleRelief * 0.0018); 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); + // Crisp road deck and distinct dark casing/curb + float roadCoreFac = 1.0 - smoothstep(rCore * 0.80, rCore, dRoad); + float roadTotalFac = 1.0 - smoothstep(rOuter * 0.82, 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); + // Stone / Cobblestone road: bright paved stone with crisp dark curb + vec3 cobbleSurface = roadColor * (0.88 + 0.35 * pebbleRelief); + vec3 stoneCurb = roadBorderColor * (0.85 + 0.25 * pebbleRelief); + roadSurf = mix(stoneCurb, cobbleSurface, 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)); + // Railroad: dark gravel ballast bed with steel twin rails + vec3 ballast = roadBorderColor * (0.85 + 0.35 * pebbleRelief); + float railLine = smoothstep(rCore * 0.38, rCore * 0.48, rawRoadDist) + * (1.0 - smoothstep(rCore * 0.56, rCore * 0.68, rawRoadDist)); + vec3 steelRail = vec3(0.75, 0.78, 0.82); + vec3 innerBed = mix(roadColor * (0.85 + 0.30 * pebbleRelief), steelRail, railLine); + roadSurf = mix(ballast, innerBed, roadCoreFac); } 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); + // Dirt / Country road: warm sandy-ochre packed earth with two subtle cart wheel tracks & dark curb + float wheelRut = smoothstep(rCore * 0.30, rCore * 0.48, rawRoadDist) + * (1.0 - smoothstep(rCore * 0.55, rCore * 0.72, rawRoadDist)); + vec3 brightDeck = roadColor * (0.92 + 0.32 * pebbleRelief); + vec3 darkCurb = roadBorderColor * (0.85 + 0.25 * pebbleRelief); + vec3 trackedDeck = mix(brightDeck, darkCurb, wheelRut * 0.35); + roadSurf = mix(darkCurb, trackedDeck, roadCoreFac); } - // If crossing a river, render a wooden/stone bridge deck and suppress river water under the bridge + // If crossing a river, render a crisp 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); + ? vec3(0.72, 0.70, 0.66) * (0.90 + 0.25 * pebbleRelief) + : vec3(0.68, 0.48, 0.28) * (0.90 + 0.25 * pebbleRelief); roadSurf = mix(roadSurf, bridgeCol, riverWaterFac); - waterFac *= (1.0 - roadTotalFac * 0.95); + waterFac *= (1.0 - roadTotalFac * 0.98); } float blendAlpha = roadTotalFac * clamp(roadOpacity, 0.0, 1.0) * (1.0 - oceanWaterFac);