diff --git a/config/roads.yaml b/config/roads.yaml index e761b53..0005480 100644 --- a/config/roads.yaml +++ b/config/roads.yaml @@ -1,8 +1,8 @@ # Road System Configuration default_road_type: "dirt" build_duration_ticks: 2 -meander_amplitude: 0.045 -meander_subdivisions: 5 +meander_amplitude: 0.065 +meander_subdivisions: 6 types: dirt: diff --git a/shaders/simple.frag b/shaders/simple.frag index 23c65f6..553aab6 100644 --- a/shaders/simple.frag +++ b/shaders/simple.frag @@ -24,6 +24,7 @@ uniform float roadRadius; uniform float roadBorderWidth; uniform float roadTextureScale; uniform float roadOpacity; +uniform float roadMeander; uniform vec3 roadColor; uniform vec3 roadBorderColor; @@ -102,6 +103,49 @@ 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) { + 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! + 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; + 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)); + vec2 curPt = basePt + perp * wave; + minDist = min(minDist, distToSegment(p, prevPt, curPt)); + prevPt = curPt; + } + 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); + 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); + vec2 curPt = basePt + perp * wave; + minDist = min(minDist, distToSegment(p, prevPt, curPt)); + prevPt = curPt; + } + return minDist; +} + float getRiverDistance(vec2 p, int mask) { if (mask == 0) return 999.0; @@ -126,7 +170,13 @@ float getRiverDistance(vec2 p, int mask) { } } -float getRoadDistance(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; for (int e = 0; e < 6; ++e) { @@ -137,18 +187,21 @@ float getRoadDistance(vec2 p, int mask) { } 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)); + // 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); } else if (count == 1) { - return distToSegment(p, EDGE_MIDPOINTS[edges[0]], vec2(0.0)); + vec2 centerHub = vec2(s1, s2) * (amp * 0.25); + return meanderingBranchDist(p, EDGE_MIDPOINTS[edges[0]], centerHub, amp, s1); } else if (count == 2) { - return bezierCurveDist(p, EDGE_MIDPOINTS[edges[0]], EDGE_MIDPOINTS[edges[1]]); + return meanderingBezierDist(p, EDGE_MIDPOINTS[edges[0]], EDGE_MIDPOINTS[edges[1]], amp, s1, s2); } else { + vec2 centerHub = vec2(s1, s2) * (amp * 0.22); float minDist = 999.0; for (int i = 0; i < count; ++i) { - minDist = min(minDist, distToSegment(p, EDGE_MIDPOINTS[edges[i]], vec2(0.0))); + float branchSign = ((edges[i] % 2) == 0) ? s1 : -s2; + minDist = min(minDist, meanderingBranchDist(p, EDGE_MIDPOINTS[edges[i]], centerHub, amp * 0.75, branchSign)); for (int j = i + 1; j < count; ++j) { - minDist = min(minDist, bezierCurveDist(p, EDGE_MIDPOINTS[edges[i]], EDGE_MIDPOINTS[edges[j]])); + minDist = min(minDist, meanderingBezierDist(p, EDGE_MIDPOINTS[edges[i]], EDGE_MIDPOINTS[edges[j]], amp * 0.65, s1, s2)); } } return minDist; @@ -319,7 +372,17 @@ void main() { // 6b. Road Texture Integration (blended directly into the tile surface texture) if (hasRoad == 1 && oceanWaterFac < 0.95) { - float rawRoadDist = getRoadDistance(fragVert.xy, roadMask); + 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); vec2 uvRoad = vWorldPos.xy * rScale; diff --git a/tile.cpp b/tile.cpp index 47f28ac..677bee7 100644 --- a/tile.cpp +++ b/tile.cpp @@ -372,6 +372,7 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire float rBorder = rCfg ? rCfg->borderWidth : 0.022f; float rScale = rCfg ? rCfg->textureScale : 2.4f; float rOpacity = rCfg ? rCfg->opacity : 0.90f; + float rMeander = GameManager::instance().roadConfigs().meanderAmplitude(); QColor col = rCfg ? rCfg->getColor() : QColor("#c49a62"); QColor bCol = rCfg ? rCfg->getBorderColor() : QColor("#6b4b23"); @@ -382,6 +383,7 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire m_program->setUniformValue("roadBorderWidth", rBorder); m_program->setUniformValue("roadTextureScale", rScale); m_program->setUniformValue("roadOpacity", rOpacity); + m_program->setUniformValue("roadMeander", rMeander); m_program->setUniformValue("roadColor", QVector3D(col.redF(), col.greenF(), col.blueF())); m_program->setUniformValue("roadBorderColor", QVector3D(bCol.redF(), bCol.greenF(), bCol.blueF())); } else {