#version 120 const bool advancedProcedurals = #{{level:extreme}}; const bool parallax = #{{level:extreme}}; const bool scattering = #{{level:extreme}}; const bool pbrLight = #{{level:high}}; const bool simpleProcedurals = #{{level:high}}; const bool advancedAmbience = #{{level:high}}; const bool normalMapping = #{{level:medium}}; const bool simpleAmbience = #{{level:medium}}; const bool selfIllumination = #{{level:medium}}; const int nLightCount = 2; const float pi = 3.14159265358; const float tau = 6.28318530717; // constants for emissive lights and starlight intensity const float emissiveIntensity = 2.5; const float lightIntensity = 1.0; uniform sampler2D biomes, cities, differenceNoise, lookup, cityGlow, surfaceData; uniform samplerCube skybox; uniform float lightRadius[nLightCount]; uniform vec4 ownerColor; uniform float population; vec4 playerPlateProfile = vec4(vec3((1.0 - ownerColor.rgb) * 0.2 + 0.8), 0.75); // complimentary triad color harmony, should in theory always generate an appealing theme. vec3 colorLightsPrimary = ownerColor.rgb * 0.45 + 0.55; vec3 colorLightsSecondary = vec3(ownerColor.brg) * 0.45 + 0.55; vec3 colorLightsTertiary = vec3(ownerColor.gbr) * 0.85 + 0.15; const float splatSharpness = 2.0; ////Amount of population considered 'half full' const vec3 basePopulation = vec3(1.0, 0.25, 0.25); varying vec2 uv, uv2, uv3, uvB[5]; varying vec4 uvNoise, pos; varying vec3 light[nLightCount]; varying vec3 lightColor[nLightCount]; varying float dist[nLightCount]; varying vec3 normal, npos, vertCol; varying float pulse; /* black biome is base biome, its biome color picks are also what controls the ocean color picks red biome is secondary biome, that will splat on top of base green biome is third biome, that will splat on top, the poles are hardcoded to be green biome, so keep it the coldest biome pick wise. ocean and cracks a bonus biomes, so with both there can be 5 i total. Blue > 0.5 is ocean, blue < 0.5 is cracks. Ocean is using the combined height for detail, cracks is using ao to spawn in cracks. alpha is city location and density, it rules over all others but will be build under water. */ //vec2(0.0, 0.0) vulcanic //vec2(0.25, 0.0) crystal //vec2(0.5, 0.0) mountains //vec2(0.75, 0.0) cracked //vec2(0.0, 0.5) ice //vec2(0.25, 0.5) barren //vec2(0.5, 0.5) terran //vec2(0.75, 0.5) desert // x = 0-1 color wheel picker, y is intensity uniform vec2 cracksColorIntensity; uniform vec4[3] biomesPicks; // parallax scale, bias and steps const vec2 scaleBias = vec2(0.005, 0.0025); // smoothstep without the edges float smoothCurve(float x){ return x * x * (3.0 - 2.0 * x); } vec3 toLinear(vec3 x) { return pow(x, vec3(2.2)); } vec3 toGamma(vec3 x) { return pow(x, vec3(0.45)); } vec3 square(vec3 x) { return x*x; } float square(float x) { return x*x; } vec2 square(vec2 x) { return x*x; } float pow5(float x) { float y = x*x; return y*y*x; } vec2 pow5(vec2 x) { vec2 y = x*x; return y*y*x; } vec3 pow5(vec3 x) { vec3 y = x*x; return y*y*x; } vec3 pow3(vec3 x) { return x*x*x; } vec2 pow3(vec2 x) { return x*x*x; } float pow3(float x) { return x*x*x; } float pow8(float x) { x = x*x; x = x*x; return x*x; } // speculer term part 1 float D_GGX(float HdotN, float Roughness) { float m = Roughness * Roughness; float m2 = m * m; float denominator = HdotN * HdotN * (m2 - 1.0) + 1.0; float D = m2 /( pi * denominator * denominator); return D; } // specular term part 2 float V_SchlickforGGX(float Roughness, float NdotV, float NdotL) { float k = Roughness * Roughness * 0.5f; float G1V = NdotV * (1.0 - k) + k; float G1L = NdotL * (1.0 - k) + k; return 0.25f / (G1V * G1L); } // fresnel for specular term vec3 Fresnel(vec3 substance, vec3 l, vec3 h) { return max(vec3(0.0), substance + (substance - 1.0) * pow(1.0 - max(0.0,(dot(l, h))), 5.0)); } // fresnel for ambient light vec3 Fresnel2(vec3 substance, float dotProduct, float roughness) { return substance + (1.0 - substance) * pow((1.0 - dotProduct), 5.0) / (6.0 - 5.0 * roughness); } // diffuse term float OrenNayerforDiffuseOnly(float roughness, float NdotL, float NdotV) { float O = 0.62 - pow(1.0-NdotL * clamp(1.0-NdotV/2.0, 0.0,1.0), pi) * 0.62; O = mix(O, NdotL, roughness); return O; } // fail-safe get normal map z component float deriveZ(vec2 n) { float z = pow(abs(1.0 - n.x * n.x - n.y * n.y), 0.5); return z; } // calculates palette for cracks vec3 paletteCracks(float c) { return 0.5 + 0.5 * cos(tau * (1.0 * c + vec3(0.0, 0.33, 0.67)) ); } // calculates light tinting, simulating atmospheric scattering. vec3 paletteAtmoTint( float t ) { t *= t; return square(min(vec3(1.0), 0.45 + 0.45*cos( tau*((t * 0.9)+vec3(0.45, 0.55, 0.65)) ) + (1.0 - t) * 0.25)); } vec3 dp1Calc(vec3 p) { return dFdx(p); } vec3 dp2Calc(vec3 p) { return dFdy(p); } vec4 duv1Calc(vec4 uv) { return dFdx(uv); } vec4 duv2Calc(vec4 uv) { return dFdy(uv); } void main() { vec4 splatMap = texture2D(surfaceData, uvB[0]) * 2.0; for (int i = 0; i < 4; i++) { splatMap.rgb += texture2D(surfaceData, uvB[i+1]).rgb; } splatMap.rgb *= 0.166666666; float poleMask = smoothCurve(1.0 - square(vertCol.b)); splatMap.rg *= poleMask; // poles are made to be ruled by green, and water/cracks masks are made to never reach the poles - where they blend particular bad due to all splatmap grids meeing in a point splatMap.gb = mix(vec2(1.0, 0.5), splatMap.gb, poleMask); vec2 cracksWaterMask = vec2((splatMap.b - 0.5) * 2.0); cracksWaterMask = max(vec2(0.0),vec2(-1.0 * cracksWaterMask.x, cracksWaterMask.y)); vec4 uvS = vec4(uv, uv2); vec3 v = normalize(npos); vec4 n = vec4(normalize(normal), 0.5); // heightmap is stored in a later vec2 NdotV = vec2(max(0.0, dot(n.xyz, v)), 0.0); vec3 r = n.xyz; vec4 albedoR = vec4(0.0); // pure color of a surface and roughness vec3 substance = vec3(0.0); // essentially an rgb specular color extracted from the albedo through metalness float metalness = 0.0; // dielectric or metallic surface float cavity = 0.5; // hard multiplier float ao = 1.0; // detail occluder for lights // results vec3 color = vec3(0.0); vec3 lights = vec3(0.0); vec3 citylights = vec3(0.0); // because we have two uvs, for poles and equator, to avoid polar distortions and hide tiling, we need to run most things twice mat3 TBNA = mat3(0.0); mat3 TBNB = mat3(0.0); // build splatting basis vec3 detailSampA = vec3(texture2D(biomes, uvS.xy + biomesPicks[0].xy).a, texture2D(biomes, uvS.xy + biomesPicks[1].xy).a, texture2D(biomes, uvS.xy + biomesPicks[2].xy).a); vec3 detailSampB = vec3(texture2D(biomes, uvS.zw + biomesPicks[0].xy).a, texture2D(biomes, uvS.zw + biomesPicks[1].xy).a, texture2D(biomes, uvS.zw + biomesPicks[2].xy).a); vec3 splatMaskMixedA = vec3(detailSampA.r + detailSampA.g, detailSampA.g + detailSampA.b, detailSampA.b); // last one comes later vec3 splatMaskMixedB = vec3(detailSampB.r + detailSampB.g, detailSampB.g + detailSampB.b, detailSampB.b); // last one comes later splatMaskMixedA = min(vec3(1.0), (splatMaskMixedA.rgb + splatMap.rgb) * splatMap.rgb); splatMaskMixedB = min(vec3(1.0), (splatMaskMixedB.rgb + splatMap.rgb) * splatMap.rgb); // optional splatMaskMixedA = pow(splatMaskMixedA.rgb, vec3(splatSharpness)); splatMaskMixedB = pow(splatMaskMixedB.rgb, vec3(splatSharpness)); // create combined detail texture and calculate weighted splat mapping for the two uv sets detailSampA.r = mix(mix(detailSampA.r, detailSampA.g, splatMaskMixedA.r), detailSampA.b, splatMaskMixedA.g) - cracksWaterMask.y; detailSampB.r = mix(mix(detailSampB.r, detailSampB.g, splatMaskMixedB.r), detailSampB.b, splatMaskMixedB.g) - cracksWaterMask.y; if (normalMapping){ vec3 dp1 = dp1Calc(-v); vec3 dp2 = dp2Calc(-v); // derive for both uv's vec4 duv1 = duv1Calc(uvS); vec4 duv2 = duv2Calc(uvS); vec3 dp2perp = cross(dp2, normal); vec3 dp1perp = cross(normal, dp1); // create matrix A vec3 tangent = dp2perp * duv1.x + dp1perp * duv2.x; vec3 binormal = dp2perp * duv1.y + dp1perp * duv2.y; float invmax = pow(max(dot(tangent, tangent), dot(binormal, binormal)), -0.5); TBNA = mat3(tangent * invmax, binormal * invmax, normal); // create matrix B tangent = dp2perp * duv1.z + dp1perp * duv2.z; binormal = dp2perp * duv1.w + dp1perp * duv2.w; invmax = pow(max(dot(tangent, tangent), dot(binormal, binormal)), -0.5); TBNB = mat3(tangent * invmax, binormal * invmax, normal); if (parallax){ // water depth refraction float depthRefraction = mix(1.33,0.66, square(NdotV.r)); // water refraction index is 1.33, square so transparency wont cancel it out later detailSampA.r *= mix(depthRefraction, 1.0, ceil(1.0 - detailSampA.r)); detailSampB.r *= mix(depthRefraction, 1.0, ceil(1.0 - detailSampB.r)); // do parallax float fDet = dot(dp1, dp2perp); vec2 vProjVScr = (1.0/fDet) * vec2(dot(dp2perp, v), dot(dp1perp, v)); vec4 vProjVTex = (duv1 * vProjVScr.x + duv2 * vProjVScr.y); vec2 vProjVTexZ = NdotV.r * (vec2(detailSampA.r, detailSampB.r) * scaleBias.r - scaleBias.g); uvS += vProjVTex * vProjVTexZ.xxyy; } } // sample all four biomes once for each of the first two uv sets vec4 firstbiomeA = texture2D(biomes, uvS.xy + biomesPicks[0].xy);//red vec4 firstBiomeB = texture2D(biomes, uvS.zw + biomesPicks[0].xy);//red vec4 secondbiomeA = texture2D(biomes, uvS.xy + biomesPicks[1].xy);//green vec4 secondBiomeB = texture2D(biomes, uvS.zw + biomesPicks[1].xy);//green vec4 thirdbiomeA = texture2D(biomes, uvS.xy + biomesPicks[2].xy);//black vec4 thirdBiomeB = texture2D(biomes, uvS.zw + biomesPicks[2].xy);//black // mix the normals cavity and resampled heightmap vec4 dataSampA = mix(mix(firstbiomeA, secondbiomeA, splatMaskMixedA.r), thirdbiomeA, splatMaskMixedA.g); vec4 dataSampB = mix(mix(firstBiomeB, secondBiomeB, splatMaskMixedB.r), thirdBiomeB, splatMaskMixedB.g); // create combined detail splatmask to mix the first two uv sets float uvBlend = min(1.0, (dataSampA.a + dataSampB.a + vertCol.r) * vertCol.r); // mix the height for accurate combined watermask and add cavity just because we can vec4 dataSamp = vec4(mix(dataSampA.ba, dataSampB.ba, uvBlend), 0.0, 0.0); dataSamp.a = dataSamp.g - cracksWaterMask.y; dataSamp.z = max(0.0, ceil(dataSamp.a)); cracksWaterMask.y = 1.0 - cracksWaterMask.y; dataSamp.x = mix(0.5, dataSamp.x, dataSamp.z); ao = min(1.0,(dataSamp.x * 2.0)); // yeah the edges will make horrible distortions but we lerp them out and save a sample float waterLavaMix = (mix(8.0, (1.0 - ao * 0.01 + pulse * 0.01) * 2.0, dataSamp.z)); vec4 textureNoise = mix(texture2D(differenceNoise, uvNoise.xy * waterLavaMix), texture2D(differenceNoise, uvNoise.zw * waterLavaMix), vertCol.r); vec2 cracksWaterNorms = vec2(mix(vec2(textureNoise.r, textureNoise.g), vec2(textureNoise.g, textureNoise.b), pulse)) - 0.5;// // could maybe be replaced by a small simple 2D noise function float cracks = min(1.0, square(1.0 - ao) * cracksWaterMask.r); // cracksWaterNorms is just noise, so it can work for cracks intensity here lights = pow3(paletteCracks(fract(cracks * 0.025 + cracksColorIntensity.x)) * cracks * cracksColorIntensity.y); float underWaterBlur = pow((1.0 - dataSamp.z) * (1.0 - dataSamp.a - 1.0), 0.5) * 7.0 + 1.0; vec4 cityA = texture2D(cities, uvS.xy * vec2(4.0, 2.0), underWaterBlur); vec4 cityB = texture2D(cities, uvS.zw * vec2(4.0, 2.0), underWaterBlur); vec3 lightsA = texture2D(cityGlow, uvS.xy * vec2(4.0, 2.0), underWaterBlur).rgb; vec3 lightsB = texture2D(cityGlow, uvS.zw * vec2(4.0, 2.0), underWaterBlur).rgb; vec2 citySplat = clamp(vec2(pow5((1.0 - abs(vec2(dataSampA.a, dataSampB.a) - 0.5) * 2.0)) * (abs(vec2(cityA.a, cityB.a) - 0.5) + vec2(cityA.a, cityB.a))) * splatMap.a, vec2(0.0), vec2(1.0)); if (normalMapping){ // perform TBN matrix multiplication for each of the two normal maps separately and mix, save instructions by mix in height in alpha. n = vec4(mix(dataSampA.xy, cityA.xy, citySplat.r), mix(dataSampB.xy, cityB.xy, citySplat.g)); n *= 2.0; n -=1.0; //create waterNormalMap n = mix(vec4(cracksWaterNorms, cracksWaterNorms) * 0.1, n, dataSamp.z - (1.0 - square(ao)) * cracksWaterMask.x); vec3 nA = vec3(n.xy, deriveZ(n.xy)); vec3 nB = vec3(n.zw, deriveZ(n.zw)); n = mix(vec4(normalize(TBNA * nA), dataSampA.a), vec4(normalize(TBNB * nB), dataSampB.a), uvBlend); r = normalize(reflect(-v, n.xyz)); } vec3 cityData = mix(vec3(cityA.ba, citySplat.r), vec3(cityB.ba, citySplat.g), uvBlend); // create combined lookup gradients for all four biomes, clamped to not wrap the texture. vec3 albedoGradients = mix(vec3(firstbiomeA.a, secondbiomeA.a, thirdbiomeA.a), vec3(firstBiomeB.a, secondBiomeB.a, thirdBiomeB.a), uvBlend); // and make the flat slopes have less color shifts - think silt vs cliff sides. albedoGradients = clamp((1 - albedoGradients) + (vec3(biomesPicks[0].w,biomesPicks[1].w,biomesPicks[2].w) * vertCol.g), vec3(0.00390625), vec3(0.984375)); // sample albedo and roughness from lookup table vec4 firstBiomesAlbedoR = texture2D(lookup, vec2(biomesPicks[0].z, albedoGradients.r)); //, 0.0 vec4 secondBiomesAlbedoR = texture2D(lookup, vec2(biomesPicks[1].z, albedoGradients.g)); //, 0.0 vec4 thirdBiomesAlbedoR = texture2D(lookup, vec2(biomesPicks[2].z, albedoGradients.b)); //, 0.0 // create final splatmap , save instructions by mix in cavity in alpha. vec4 albedoSplatCavity = mix(vec4(splatMaskMixedA, dataSampA.b), vec4(splatMaskMixedB, dataSampB.b), uvBlend); // mix final albedo and tweak with cavity roughness values albedoR = mix(mix(firstBiomesAlbedoR, secondBiomesAlbedoR, albedoSplatCavity.r), thirdBiomesAlbedoR, albedoSplatCavity.g); cityData.rg = cityData.rg * 0.33 + 0.5; albedoR = mix(albedoR, vec4(playerPlateProfile.rgb * cityData.g, playerPlateProfile.a) * cityData.r, cityData.b); NdotV.g = (1.0 - NdotV.r) * 0.5 + 0.5; NdotV.r = dot(n.xyz, v); NdotV = max(vec2(0.0), NdotV); // creates ocean color vec3 oceanColor = texture2D(lookup, vec2(biomesPicks[0].z, max(0.995, cracksWaterMask.y * 0.05))).rgb; // from 0.0065 to 0.0025 //creates lights citylights += mix(lightsA, lightsB, uvBlend); citylights.rg *= (1.5 - underWaterBlur * 0.1) * min(1.0, cityData.b + (1.0 - dataSamp.z) * 0.5 * splatMap.a); citylights.g *= 0.5; citylights.b *= splatMap.a * 0.5; citylights = clamp(vec3(population) - vec3(0.0, 15.0, 30.0), vec3(0.0), vec3(1.0)) * citylights * emissiveIntensity; citylights = toLinear(citylights.r * colorLightsPrimary) + toLinear(citylights.g * colorLightsSecondary) + toLinear(citylights.b * colorLightsTertiary); citylights *= min(vec3(1.0), square(oceanColor.rgb) + dataSamp.z); // // creates oceans albedoR = mix(vec4((oceanColor / (1.0 + (1.0 - NdotV.y) * 0.75)) * mix(min(1.0, dataSamp.y * dataSamp.x * pow3(cracksWaterMask.y) * 16.0 + 0.25), 1.0, NdotV.y), 0.25), albedoR, dataSamp.z); // clamp to keep in PBR safe ranges - nothing in reality either albedo nor roughness is 0 or 1 and can make the math fail albedoR = clamp(albedoR, vec4(0.05), vec4(0.975)); // create sss mask vec4 invertedAlbedo = 1.0 - albedoR; float SSSmask = mix(NdotV.r * 0.5 + 0.5, 1.0 - min(1.0, pow8((1.0 - invertedAlbedo.a * invertedAlbedo.b) * (1.0 - invertedAlbedo.r * invertedAlbedo.g))) * dataSamp.y * dataSamp.x, dataSamp.z); // creates a metalness value if biomes are above 1950 on the lookup, for metallic specular on crystals. metalness = square(n.a) * ceil(mix(mix(biomesPicks[0].z, biomesPicks[1].z, albedoSplatCavity.r), biomesPicks[2].z, albedoSplatCavity.g) - 0.9521484375) * dataSamp.z; albedoR.rgb = toLinear(albedoR.rgb); substance = (0.04 - 0.04 * metalness) + albedoR.rgb * metalness; albedoR.rgb -= substance; // actual shading starts here vec3 ambientFresnel = Fresnel2(substance, NdotV.r ,albedoR.a); if (advancedAmbience){ color += square((textureCube(skybox, r, sqrt(albedoR.a) * 8.0).rgb) + 0.024) * ambientFresnel; // ambient light color += square(textureCube(skybox, n.xyz, 8.0).rgb + 0.024) * albedoR.rgb * (1.0 - ambientFresnel); } else if (simpleAmbience){ // Ambient reflections with fix mip and n instead of refect color += square(textureCube(skybox, r, 3.0).rgb * ambientFresnel); // Ambient light - average color of skybox squared color += vec3(0.006724, 0.014884, 0.067081) * albedoR.rgb * (1.0 - ambientFresnel); } else{ // Ambient color += vec3(0.006724, 0.014884, 0.067081) * (ambientFresnel + albedoR.rgb * (1.0 - ambientFresnel)); } if (pbrLight){ for (int i = 0; i < nLightCount; i++) { float distance = length(gl_LightSource[i].position.xyz - pos.xyz); // EYE SPACE, I'm sorry:o( // rest is world space vec3 L = normalize(light[i] / distance); vec2 NdotL = max(vec2(0.0), vec2(dot(mix(n.xyz, normalize(normal), 0.5),L) * 0.8 + 0.2, dot(n.xyz,L))); float sqrLightRadius = square(lightRadius[i]); float illuminance = lightIntensity * pi * (sqrLightRadius / (max(sqrLightRadius, dot(L,L) ))); // note the square to kill hard spec in deep space! float attenuation = square(1.0 / (1.0 + (gl_LightSource[i].constantAttenuation + gl_LightSource[i].linearAttenuation * distance + gl_LightSource[i].quadraticAttenuation * square(distance)))) * illuminance * NdotL.x; citylights *= clamp(1.0 - attenuation, 0.0, 1.0); if (attenuation >0.0){ vec3 VplusL = L + v * 0.5; // *0.5 is not correct but without it grazing angle specular fails, propably somewhere something is flipped space/normal space vec3 halfVec = normalize(VplusL); float HdotN = max(0.0, dot(halfVec, n.xyz)); vec3 F = Fresnel(substance, L, halfVec); float D = max(0.0, D_GGX(HdotN, albedoR.a)); float V = max(0.0, V_SchlickforGGX((1.0 + albedoR.a) * 0.5, NdotV.r, NdotL.y)); float O = OrenNayerforDiffuseOnly(albedoR.a, NdotL.y, NdotV.r); vec3 A = vec3(1.0); vec3 SSS = vec3(0.0); if (scattering) { // atmospheric light tinting simulating scattering A = vec3(paletteAtmoTint(1.0 - max(0.0, min(0.33, dot(normal,L) + 0.15)))); // sub surface scattering model float inScatter = pow(clamp(dot(L, -v), 0.0, 1.0), 12.0) * mix(8.0, 0.1, SSSmask); float normalContribution = clamp(dot(mix(normal, n.xyz, SSSmask), halfVec) * SSSmask + 1.0 - SSSmask, 0.0, 1.0); float backScatter = dataSamp.x * normalContribution / tau; SSS = mix(backScatter, 1.0, inScatter) * square(oceanColor) * NdotL.x; } color += ((D * V * F) + ((1.0 - F) * O * albedoR.rgb) + SSS) * gl_LightSource[i].diffuse.rgb * A * attenuation; } } // hard cavity multiplier color *= dataSamp.x - metalness + 1.0; } // probably not worth it to check NdotL like pbr else{ for (int i = 0; i < nLightCount; i++) { float distance = length(gl_LightSource[i].position.xyz - pos.xyz); // EYE SPACE, I'm sorry:o( // rest is world space vec3 L = normalize(light[i] / distance); float NdotL = max(0.0, dot(n.xyz,L)); float sqrLightRadius = square(lightRadius[i]); float illuminance = lightIntensity * pi * (sqrLightRadius / (max(sqrLightRadius, dot(L,L)))); // note the square to kill hard spec in deep space! float attenuation = square(1.0 / (1.0 + (gl_LightSource[i].constantAttenuation + gl_LightSource[i].linearAttenuation * distance + gl_LightSource[i].quadraticAttenuation * square(distance)))) * illuminance * NdotL; citylights *= clamp(1.0 - attenuation, 0.0, 1.0); vec3 VplusL = L + v; vec3 halfVec = normalize(VplusL); float HdotN = max(0.0, dot(halfVec, n.xyz)); vec3 S = Fresnel2(substance, HdotN ,albedoR.a); // albedoR.rgb * (dataSamp.x * 0.75) to sorta hack albedo into a classical diffuse texture lights += max(vec3(0.001), albedoR.rgb * (dataSamp.x * 0.75) + pow(S * HdotN, vec3(albedoR.a + 5.0))) * gl_LightSource[i].diffuse.rgb * attenuation; } } gl_FragColor.rgb = toGamma(clamp(color + lights + citylights, vec3(0.0), vec3(1.0))); gl_FragColor.a = 1.0; }