Improve road curve smoothness (C1 sin^2 curve) and crisp road surface/curb contrast
This commit is contained in:
+19
-19
@@ -1,7 +1,7 @@
|
|||||||
# Road System Configuration
|
# Road System Configuration
|
||||||
default_road_type: "dirt"
|
default_road_type: "dirt"
|
||||||
build_duration_ticks: 2
|
build_duration_ticks: 2
|
||||||
meander_amplitude: 0.065
|
meander_amplitude: 0.025
|
||||||
meander_subdivisions: 6
|
meander_subdivisions: 6
|
||||||
|
|
||||||
types:
|
types:
|
||||||
@@ -9,35 +9,35 @@ types:
|
|||||||
id: "dirt"
|
id: "dirt"
|
||||||
name: "Dirt Road"
|
name: "Dirt Road"
|
||||||
speed_multiplier: 1.30 # +30% movement speed
|
speed_multiplier: 1.30 # +30% movement speed
|
||||||
color: "#c49a62"
|
color: "#d8b480"
|
||||||
border_color: "#6b4b23"
|
border_color: "#52381c"
|
||||||
width: 3.5
|
width: 3.5
|
||||||
world_width: 0.068
|
world_width: 0.056
|
||||||
border_width: 0.022
|
border_width: 0.012
|
||||||
texture_scale: 2.4
|
texture_scale: 4.5
|
||||||
opacity: 0.90
|
opacity: 0.96
|
||||||
cost: 5
|
cost: 5
|
||||||
stone:
|
stone:
|
||||||
id: "stone"
|
id: "stone"
|
||||||
name: "Stone Road"
|
name: "Stone Road"
|
||||||
speed_multiplier: 1.60 # +60% movement speed
|
speed_multiplier: 1.60 # +60% movement speed
|
||||||
color: "#9aa1a8"
|
color: "#c2beb6"
|
||||||
border_color: "#464b50"
|
border_color: "#46484c"
|
||||||
width: 4.0
|
width: 4.0
|
||||||
world_width: 0.078
|
world_width: 0.064
|
||||||
border_width: 0.020
|
border_width: 0.012
|
||||||
texture_scale: 2.8
|
texture_scale: 5.5
|
||||||
opacity: 0.94
|
opacity: 0.98
|
||||||
cost: 15
|
cost: 15
|
||||||
rail:
|
rail:
|
||||||
id: "rail"
|
id: "rail"
|
||||||
name: "Railroad"
|
name: "Railroad"
|
||||||
speed_multiplier: 2.50 # +150% movement speed
|
speed_multiplier: 2.50 # +150% movement speed
|
||||||
color: "#3a3c40"
|
color: "#4a4e54"
|
||||||
border_color: "#181a1c"
|
border_color: "#22252a"
|
||||||
width: 4.5
|
width: 4.5
|
||||||
world_width: 0.086
|
world_width: 0.070
|
||||||
border_width: 0.024
|
border_width: 0.014
|
||||||
texture_scale: 3.0
|
texture_scale: 6.0
|
||||||
opacity: 0.96
|
opacity: 0.98
|
||||||
cost: 30
|
cost: 30
|
||||||
|
|||||||
+6
-6
@@ -27,13 +27,13 @@ struct RoadTypeConfig {
|
|||||||
QString id = "dirt";
|
QString id = "dirt";
|
||||||
QString name = "Dirt Road";
|
QString name = "Dirt Road";
|
||||||
float speedMultiplier = 1.30f;
|
float speedMultiplier = 1.30f;
|
||||||
QString colorStr = "#c49a62";
|
QString colorStr = "#d8b480";
|
||||||
QString borderColorStr = "#6b4b23";
|
QString borderColorStr = "#52381c";
|
||||||
float width = 3.5f;
|
float width = 3.5f;
|
||||||
float worldWidth = 0.068f;
|
float worldWidth = 0.056f;
|
||||||
float borderWidth = 0.022f;
|
float borderWidth = 0.012f;
|
||||||
float textureScale = 2.4f;
|
float textureScale = 4.5f;
|
||||||
float opacity = 0.90f;
|
float opacity = 0.96f;
|
||||||
int cost = 5;
|
int cost = 5;
|
||||||
|
|
||||||
QColor getColor() const { return QColor(colorStr); }
|
QColor getColor() const { return QColor(colorStr); }
|
||||||
|
|||||||
+52
-63
@@ -103,23 +103,22 @@ float bezierCurveDist(vec2 p, vec2 p0, vec2 p2) {
|
|||||||
return min(min(d0, d1), min(d2, d3));
|
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;
|
vec2 chord = p2 - p0;
|
||||||
float cLen = length(chord);
|
float cLen = length(chord);
|
||||||
vec2 perp = (cLen > 1e-4) ? vec2(-chord.y / cLen, chord.x / cLen) : vec2(0.0, 1.0);
|
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).
|
// sin^2(pi*t) has BOTH zero value AND zero derivative at t=0 and 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!
|
// guaranteeing C1-continuous, kink-free transitions across hex borders!
|
||||||
const float PI_VAL = 3.14159265;
|
const float PI_VAL = 3.14159265;
|
||||||
vec2 prevPt = p0;
|
vec2 prevPt = p0;
|
||||||
float minDist = 999.0;
|
float minDist = 999.0;
|
||||||
for (int i = 1; i <= 6; ++i) {
|
for (int i = 1; i <= 8; ++i) {
|
||||||
float t = float(i) / 6.0;
|
float t = float(i) / 8.0;
|
||||||
float omt = 1.0 - t;
|
float omt = 1.0 - t;
|
||||||
// Base quadratic Bezier through (0, 0)
|
|
||||||
vec2 basePt = (omt * omt) * p0 + (t * t) * p2;
|
vec2 basePt = (omt * omt) * p0 + (t * t) * p2;
|
||||||
// Asymmetric S-curve + bend wave perpendicular to the road chord
|
float st = sin(PI_VAL * t);
|
||||||
float wave = amp * (0.65 * s1 * sin(PI_VAL * t) + 0.85 * s2 * sin(2.0 * PI_VAL * t));
|
float wave = amp * s1 * (st * st);
|
||||||
vec2 curPt = basePt + perp * wave;
|
vec2 curPt = basePt + perp * wave;
|
||||||
minDist = min(minDist, distToSegment(p, prevPt, curPt));
|
minDist = min(minDist, distToSegment(p, prevPt, curPt));
|
||||||
prevPt = curPt;
|
prevPt = curPt;
|
||||||
@@ -127,18 +126,18 @@ float meanderingBezierDist(vec2 p, vec2 p0, vec2 p2, float amp, float s1, float
|
|||||||
return minDist;
|
return minDist;
|
||||||
}
|
}
|
||||||
|
|
||||||
float meanderingBranchDist(vec2 p, vec2 pe, vec2 centerHub, float amp, float s) {
|
float meanderingBranchDist(vec2 p, vec2 pe, float amp, float s1) {
|
||||||
vec2 dir = centerHub - pe;
|
float dLen = length(pe);
|
||||||
float dLen = length(dir);
|
vec2 perp = (dLen > 1e-4) ? vec2(pe.y / dLen, -pe.x / dLen) : vec2(0.0, 1.0);
|
||||||
vec2 perp = (dLen > 1e-4) ? vec2(-dir.y / dLen, dir.x / dLen) : vec2(0.0, 1.0);
|
|
||||||
const float PI_VAL = 3.14159265;
|
const float PI_VAL = 3.14159265;
|
||||||
|
|
||||||
vec2 prevPt = pe;
|
vec2 prevPt = pe;
|
||||||
float minDist = 999.0;
|
float minDist = 999.0;
|
||||||
for (int i = 1; i <= 4; ++i) {
|
for (int i = 1; i <= 6; ++i) {
|
||||||
float t = float(i) / 4.0;
|
float t = float(i) / 6.0;
|
||||||
vec2 basePt = mix(pe, centerHub, t);
|
vec2 basePt = pe * (1.0 - t);
|
||||||
float wave = amp * 0.65 * s * sin(PI_VAL * t);
|
float st = sin(PI_VAL * t);
|
||||||
|
float wave = amp * 0.65 * s1 * (st * st);
|
||||||
vec2 curPt = basePt + perp * wave;
|
vec2 curPt = basePt + perp * wave;
|
||||||
minDist = min(minDist, distToSegment(p, prevPt, curPt));
|
minDist = min(minDist, distToSegment(p, prevPt, curPt));
|
||||||
prevPt = curPt;
|
prevPt = curPt;
|
||||||
@@ -171,11 +170,8 @@ float getRiverDistance(vec2 p, int mask) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float getRoadDistance(vec2 p, int mask, vec2 tileCenterWorld, float amp) {
|
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 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 s1 = (h1 > 0.5) ? 1.0 : -1.0;
|
||||||
float s2 = (h2 > 0.5) ? 1.0 : -1.0;
|
|
||||||
|
|
||||||
int edges[6];
|
int edges[6];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@@ -187,21 +183,17 @@ float getRoadDistance(vec2 p, int mask, vec2 tileCenterWorld, float amp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
// Unconnected road on a single tile: short winding country track at tile center
|
return meanderingBezierDist(p, vec2(-0.18, 0.0), vec2(0.18, 0.0), amp * 0.7, s1);
|
||||||
return meanderingBezierDist(p, vec2(-0.16, -0.06 * s1), vec2(0.16, 0.06 * s1), amp * 0.6, s1, s2);
|
|
||||||
} else if (count == 1) {
|
} else if (count == 1) {
|
||||||
vec2 centerHub = vec2(s1, s2) * (amp * 0.25);
|
return meanderingBranchDist(p, EDGE_MIDPOINTS[edges[0]], amp, s1);
|
||||||
return meanderingBranchDist(p, EDGE_MIDPOINTS[edges[0]], centerHub, amp, s1);
|
|
||||||
} else if (count == 2) {
|
} 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 {
|
} else {
|
||||||
vec2 centerHub = vec2(s1, s2) * (amp * 0.22);
|
|
||||||
float minDist = 999.0;
|
float minDist = 999.0;
|
||||||
for (int i = 0; i < count; ++i) {
|
for (int i = 0; i < count; ++i) {
|
||||||
float branchSign = ((edges[i] % 2) == 0) ? s1 : -s2;
|
minDist = min(minDist, meanderingBranchDist(p, EDGE_MIDPOINTS[edges[i]], amp * 0.5, s1));
|
||||||
minDist = min(minDist, meanderingBranchDist(p, EDGE_MIDPOINTS[edges[i]], centerHub, amp * 0.75, branchSign));
|
|
||||||
for (int j = i + 1; j < count; ++j) {
|
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;
|
return minDist;
|
||||||
@@ -375,59 +367,56 @@ void main() {
|
|||||||
vec2 tileCenterWorld = vWorldPos.xy - fragVert.xy;
|
vec2 tileCenterWorld = vWorldPos.xy - fragVert.xy;
|
||||||
float amp = max(0.0, roadMeander);
|
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 rawRoadDist = getRoadDistance(fragVert.xy, roadMask, tileCenterWorld, amp);
|
||||||
float borderFade = 1.0 - smoothstep(0.24, 0.42, length(fragVert.xy));
|
float rScale = max(1.0, roadTextureScale);
|
||||||
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;
|
vec2 uvRoad = vWorldPos.xy * rScale;
|
||||||
|
|
||||||
vec3 soilTex = mix(texture(texSand, uvRoad).rgb, texture(texTundra, uvRoad * 1.45).rgb, 0.45);
|
vec3 soilTex = texture(texSand, uvRoad).rgb;
|
||||||
vec3 stoneTex = texture(texRock, uvRoad * 1.35).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
|
// Very subtle micro-edge grain (0.0018) so the road has crisp, clean borders
|
||||||
float edgeGrain = (soilTex.r - 0.5) * 0.008;
|
float dRoad = max(0.0, rawRoadDist + pebbleRelief * 0.0018);
|
||||||
float dRoad = max(0.0, rawRoadDist + edgeGrain);
|
|
||||||
|
|
||||||
float rCore = max(0.014, roadRadius);
|
float rCore = max(0.014, roadRadius);
|
||||||
float rOuter = rCore + max(0.006, roadBorderWidth);
|
float rOuter = rCore + max(0.006, roadBorderWidth);
|
||||||
|
|
||||||
float roadCoreFac = 1.0 - smoothstep(rCore * 0.50, rCore, dRoad);
|
// Crisp road deck and distinct dark casing/curb
|
||||||
float roadTotalFac = 1.0 - smoothstep(rCore * 0.65, rOuter, dRoad);
|
float roadCoreFac = 1.0 - smoothstep(rCore * 0.80, rCore, dRoad);
|
||||||
|
float roadTotalFac = 1.0 - smoothstep(rOuter * 0.82, rOuter, dRoad);
|
||||||
|
|
||||||
if (roadTotalFac > 0.001) {
|
if (roadTotalFac > 0.001) {
|
||||||
vec3 roadSurf;
|
vec3 roadSurf;
|
||||||
if (roadType == 1) {
|
if (roadType == 1) {
|
||||||
// Stone / Paved road: rich stone texture modulated by configured colors
|
// Stone / Cobblestone road: bright paved stone with crisp dark curb
|
||||||
vec3 stoneCenter = mix(stoneTex, roadColor, 0.55) * (0.75 + 0.50 * stoneTex.g);
|
vec3 cobbleSurface = roadColor * (0.88 + 0.35 * pebbleRelief);
|
||||||
vec3 stoneEdge = mix(stoneTex, roadBorderColor, 0.65) * 0.85;
|
vec3 stoneCurb = roadBorderColor * (0.85 + 0.25 * pebbleRelief);
|
||||||
roadSurf = mix(stoneEdge, stoneCenter, roadCoreFac);
|
roadSurf = mix(stoneCurb, cobbleSurface, roadCoreFac);
|
||||||
} else if (roadType == 2) {
|
} else if (roadType == 2) {
|
||||||
// Railroad: gravel ballast bed with dark twin rail tracks
|
// Railroad: dark gravel ballast bed with steel twin rails
|
||||||
vec3 ballast = mix(stoneTex, roadBorderColor, 0.60);
|
vec3 ballast = roadBorderColor * (0.85 + 0.35 * pebbleRelief);
|
||||||
float railLine = smoothstep(rCore * 0.35, rCore * 0.50, rawRoadDist)
|
float railLine = smoothstep(rCore * 0.38, rCore * 0.48, rawRoadDist)
|
||||||
* (1.0 - smoothstep(rCore * 0.55, rCore * 0.72, rawRoadDist));
|
* (1.0 - smoothstep(rCore * 0.56, rCore * 0.68, rawRoadDist));
|
||||||
roadSurf = mix(ballast, roadColor * (0.8 + 0.4 * stoneTex.r), max(roadCoreFac * 0.6, railLine));
|
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 {
|
} else {
|
||||||
// Dirt road: natural packed earth & wheel ruts blended with soil texture
|
// Dirt / Country road: warm sandy-ochre packed earth with two subtle cart wheel tracks & dark curb
|
||||||
float rutPattern = smoothstep(rCore * 0.25, rCore * 0.65, rawRoadDist);
|
float wheelRut = smoothstep(rCore * 0.30, rCore * 0.48, rawRoadDist)
|
||||||
vec3 packedEarth = mix(soilTex, roadColor, 0.52) * (0.78 + 0.45 * soilTex.r);
|
* (1.0 - smoothstep(rCore * 0.55, rCore * 0.72, rawRoadDist));
|
||||||
vec3 wornShoulder = mix(soilTex, roadBorderColor, 0.60) * (0.72 + 0.40 * soilTex.g);
|
vec3 brightDeck = roadColor * (0.92 + 0.32 * pebbleRelief);
|
||||||
vec3 innerTrack = mix(packedEarth, wornShoulder, rutPattern * 0.45);
|
vec3 darkCurb = roadBorderColor * (0.85 + 0.25 * pebbleRelief);
|
||||||
roadSurf = mix(wornShoulder, innerTrack, roadCoreFac);
|
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) {
|
if (riverWaterFac > 0.01) {
|
||||||
vec3 bridgeCol = (roadType >= 1)
|
vec3 bridgeCol = (roadType >= 1)
|
||||||
? mix(stoneTex, vec3(0.58, 0.56, 0.53), 0.65)
|
? vec3(0.72, 0.70, 0.66) * (0.90 + 0.25 * pebbleRelief)
|
||||||
: mix(soilTex, vec3(0.55, 0.38, 0.20), 0.70);
|
: vec3(0.68, 0.48, 0.28) * (0.90 + 0.25 * pebbleRelief);
|
||||||
roadSurf = mix(roadSurf, bridgeCol, riverWaterFac);
|
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);
|
float blendAlpha = roadTotalFac * clamp(roadOpacity, 0.0, 1.0) * (1.0 - oceanWaterFac);
|
||||||
|
|||||||
Reference in New Issue
Block a user