Render roads directly as part of the tile texture in shaders/simple.frag

This commit is contained in:
2026-11-09 21:11:06 +01:00
parent d266715418
commit a8841f59d9
6 changed files with 184 additions and 50 deletions
+12 -3
View File
@@ -12,7 +12,10 @@ types:
color: "#c49a62"
border_color: "#6b4b23"
width: 3.5
world_width: 0.11
world_width: 0.068
border_width: 0.022
texture_scale: 2.4
opacity: 0.90
cost: 5
stone:
id: "stone"
@@ -21,7 +24,10 @@ types:
color: "#9aa1a8"
border_color: "#464b50"
width: 4.0
world_width: 0.13
world_width: 0.078
border_width: 0.020
texture_scale: 2.8
opacity: 0.94
cost: 15
rail:
id: "rail"
@@ -30,5 +36,8 @@ types:
color: "#3a3c40"
border_color: "#181a1c"
width: 4.5
world_width: 0.15
world_width: 0.086
border_width: 0.024
texture_scale: 3.0
opacity: 0.96
cost: 30
+1 -15
View File
@@ -3079,25 +3079,11 @@ void QSceneRenderer::paintGL()
}
}
// 2. Render 3D Roads (above terrain elevation, writes depth so units and cities render on top)
// 2. Render 3D Units & Cities (Cylinders & Bastions; roads are rendered directly in the tile shader texture)
float fSunAngle = 45.0f;
QVector3D vSunPos(-sin(fSunAngle * PI / 180.0f), -cos(fSunAngle * PI / 180.0f), 0.6f);
vSunPos.normalize();
m_unitRenderer.renderRoads(
GameManager::instance().getTileRoads(),
GameManager::instance().roadConfigs(),
m_point,
m_numPoints05,
Projection,
vMatrix,
vSunPos,
cityViewRadius,
cityView ? cityView->tileX : -1,
cityView ? cityView->tileY : -1
);
// 3. Render 3D Units & Cities (Cylinders & Bastions)
Unit *selected = GameManager::instance().getSelectedUnit();
m_unitRenderer.renderUnits(
GameManager::instance().getUnits(),
+16 -4
View File
@@ -17,7 +17,10 @@ RoadConfigManager::RoadConfigManager()
dirt.colorStr = "#c49a62";
dirt.borderColorStr = "#6b4b23";
dirt.width = 3.5f;
dirt.worldWidth = 0.11f;
dirt.worldWidth = 0.068f;
dirt.borderWidth = 0.022f;
dirt.textureScale = 2.4f;
dirt.opacity = 0.90f;
dirt.cost = 5;
m_types["dirt"] = dirt;
@@ -28,7 +31,10 @@ RoadConfigManager::RoadConfigManager()
stone.colorStr = "#9aa1a8";
stone.borderColorStr = "#464b50";
stone.width = 4.0f;
stone.worldWidth = 0.13f;
stone.worldWidth = 0.078f;
stone.borderWidth = 0.020f;
stone.textureScale = 2.8f;
stone.opacity = 0.94f;
stone.cost = 15;
m_types["stone"] = stone;
@@ -39,7 +45,10 @@ RoadConfigManager::RoadConfigManager()
rail.colorStr = "#3a3c40";
rail.borderColorStr = "#181a1c";
rail.width = 4.5f;
rail.worldWidth = 0.15f;
rail.worldWidth = 0.086f;
rail.borderWidth = 0.024f;
rail.textureScale = 3.0f;
rail.opacity = 0.96f;
rail.cost = 30;
m_types["rail"] = rail;
}
@@ -75,7 +84,10 @@ bool RoadConfigManager::loadConfigFile(const QString &filePath)
rCfg.colorStr = it.value().hasChild("color") ? it.value()["color"].asString("#c49a62") : "#c49a62";
rCfg.borderColorStr = it.value().hasChild("border_color") ? it.value()["border_color"].asString("#6b4b23") : "#6b4b23";
rCfg.width = it.value().hasChild("width") ? it.value()["width"].asFloat(3.5f) : 3.5f;
rCfg.worldWidth = it.value().hasChild("world_width") ? it.value()["world_width"].asFloat(0.11f) : 0.11f;
rCfg.worldWidth = it.value().hasChild("world_width") ? it.value()["world_width"].asFloat(0.068f) : 0.068f;
rCfg.borderWidth = it.value().hasChild("border_width") ? it.value()["border_width"].asFloat(0.022f) : 0.022f;
rCfg.textureScale = it.value().hasChild("texture_scale") ? it.value()["texture_scale"].asFloat(2.4f) : 2.4f;
rCfg.opacity = it.value().hasChild("opacity") ? it.value()["opacity"].asFloat(0.90f) : 0.90f;
rCfg.cost = it.value().hasChild("cost") ? it.value()["cost"].asInt(5) : 5;
m_types[rCfg.id] = rCfg;
}
+4 -1
View File
@@ -30,7 +30,10 @@ struct RoadTypeConfig {
QString colorStr = "#c49a62";
QString borderColorStr = "#6b4b23";
float width = 3.5f;
float worldWidth = 0.11f;
float worldWidth = 0.068f;
float borderWidth = 0.022f;
float textureScale = 2.4f;
float opacity = 0.90f;
int cost = 5;
QColor getColor() const { return QColor(colorStr); }
+108 -12
View File
@@ -17,6 +17,16 @@ 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;
@@ -79,6 +89,19 @@ const vec2 EDGE_MIDPOINTS[6] = vec2[6](
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;
@@ -92,18 +115,7 @@ float getRiverDistance(vec2 p, int mask) {
}
if (count == 2) {
vec2 p0 = EDGE_MIDPOINTS[edges[0]];
vec2 p2 = EDGE_MIDPOINTS[edges[1]];
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));
return bezierCurveDist(p, EDGE_MIDPOINTS[edges[0]], EDGE_MIDPOINTS[edges[1]]);
} else {
float minDist = 999.0;
for (int i = 0; i < count; ++i) {
@@ -114,6 +126,35 @@ float getRiverDistance(vec2 p, int mask) {
}
}
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);
@@ -276,6 +317,61 @@ void main() {
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);
+43 -15
View File
@@ -127,31 +127,31 @@ void Tile::init(QSceneRenderer* s, int x, int y)
buildHeights();
if (Tile::m_image_grass == NULL)
Tile::m_image_grass = new QImage("/home/bart/project/tiletest/images/grass.jpg");
Tile::m_image_grass = new QImage("/home/bart/git/Ascent-of-Man/images/grass.jpg");
if (Tile::m_image_border == NULL)
Tile::m_image_border = new QImage("/home/bart/project/tiletest/images/border.png");
Tile::m_image_border = new QImage("/home/bart/git/Ascent-of-Man/images/border.png");
if (Tile::m_image_rock == NULL)
Tile::m_image_rock = new QImage("/home/bart/project/tiletest/images/Texturelabs_Stone_136XL_seamless.jpg");
Tile::m_image_rock = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Stone_136XL_seamless.jpg");
if (Tile::m_image_sand == NULL)
Tile::m_image_sand = new QImage("/home/bart/project/tiletest/images/Texturelabs_Soil_126XL_seamless.jpg");
Tile::m_image_sand = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Soil_126XL_seamless.jpg");
if (Tile::m_image_water == NULL)
Tile::m_image_water = new QImage("/home/bart/project/tiletest/images/Texturelabs_Water_152XL_seamless.jpg");
Tile::m_image_water = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Water_152XL_seamless.jpg");
if (Tile::m_image_snow == NULL)
Tile::m_image_snow = new QImage("/home/bart/project/tiletest/images/Texturelabs_Water_155XL_seamless.jpg");
Tile::m_image_snow = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Water_155XL_seamless.jpg");
if (Tile::m_image_typemap == NULL)
Tile::m_image_typemap = new QImage("/home/bart/project/tiletest/images/typemap.png");
Tile::m_image_typemap = new QImage("/home/bart/git/Ascent-of-Man/images/typemap.png");
if (Tile::m_image_sahara == NULL)
Tile::m_image_sahara = new QImage("/home/bart/project/tiletest/images/Texturelabs_Soil_133L_seamless.jpg");
Tile::m_image_sahara = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Soil_133L_seamless.jpg");
if (Tile::m_image_tundra == NULL)
Tile::m_image_tundra = new QImage("/home/bart/project/tiletest/images/Texturelabs_Soil_135XL_seamless.jpg");
Tile::m_image_tundra = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Soil_135XL_seamless.jpg");
if (Tile::m_image_redsand == NULL)
Tile::m_image_redsand = new QImage("/home/bart/project/tiletest/images/Texturelabs_RedSand_seamless.jpg");
Tile::m_image_redsand = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_RedSand_seamless.jpg");
if (Tile::m_image_redrock == NULL)
Tile::m_image_redrock = new QImage("/home/bart/project/tiletest/images/Texturelabs_RedRock_seamless.jpg");
Tile::m_image_redrock = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_RedRock_seamless.jpg");
if (Tile::m_image_forest == NULL)
Tile::m_image_forest = new QImage("/home/bart/project/tiletest/images/Texturelabs_Forest_seamless.jpg");
Tile::m_image_forest = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Forest_seamless.jpg");
if (Tile::m_image_farm == NULL)
Tile::m_image_farm = new QImage("/home/bart/project/tiletest/images/farm.jpeg");
Tile::m_image_farm = new QImage("/home/bart/git/Ascent-of-Man/images/farm.jpeg");
if (Tile::m_texture_grass == NULL) {
m_texture_grass = new QOpenGLTexture(*m_image_grass);
@@ -247,8 +247,8 @@ void Tile::init(QSceneRenderer* s, int x, int y)
if (first){
m_program = new QOpenGLShaderProgram();
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/bart/project/tiletest/shaders/simple.vert");
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/bart/project/tiletest/shaders/simple.frag");
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/bart/git/Ascent-of-Man/shaders/simple.vert");
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/bart/git/Ascent-of-Man/shaders/simple.frag");
m_program->link();
}
m_program->bind();
@@ -361,6 +361,34 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
m_program->setUniformValue("farmTextureScale", fScale);
m_program->setUniformValue("farmMappingMode", mappingMode);
const TileRoad *road = GameManager::instance().getRoadAt(m_x, m_y);
if (road != nullptr) {
const RoadTypeConfig *rCfg = GameManager::instance().roadConfigs().getRoadType(road->roadTypeId);
int rType = 0;
if (road->roadTypeId == "stone") rType = 1;
else if (road->roadTypeId == "rail") rType = 2;
float rRad = rCfg ? (rCfg->worldWidth * 0.5f) : 0.034f;
float rBorder = rCfg ? rCfg->borderWidth : 0.022f;
float rScale = rCfg ? rCfg->textureScale : 2.4f;
float rOpacity = rCfg ? rCfg->opacity : 0.90f;
QColor col = rCfg ? rCfg->getColor() : QColor("#c49a62");
QColor bCol = rCfg ? rCfg->getBorderColor() : QColor("#6b4b23");
m_program->setUniformValue("hasRoad", 1);
m_program->setUniformValue("roadMask", (int)road->connectionMask);
m_program->setUniformValue("roadType", rType);
m_program->setUniformValue("roadRadius", rRad);
m_program->setUniformValue("roadBorderWidth", rBorder);
m_program->setUniformValue("roadTextureScale", rScale);
m_program->setUniformValue("roadOpacity", rOpacity);
m_program->setUniformValue("roadColor", QVector3D(col.redF(), col.greenF(), col.blueF()));
m_program->setUniformValue("roadBorderColor", QVector3D(bCol.redF(), bCol.greenF(), bCol.blueF()));
} else {
m_program->setUniformValue("hasRoad", 0);
m_program->setUniformValue("roadMask", 0);
}
m_program->setUniformValue("time", time);
m_program->setUniformValue("riverMask", (int)m_riverMask);
m_object.bind();