# Horizon:zero Dawn Cloud System

## 64 posts in this topic

Another interesting part of the system is the weather simulation.
I found that a good way to handle cloud formation/dissipation and movement is using Cellular Automata as Dobashi tried:

http://evasion.imag.fr/~Antoine.Bouthors/research/dea/sig00_cloud.pdf

A very brief summary of the paper is this:

https://graphics.stanford.edu/courses/cs348b-competition/cs348b-05/clouds/index.html

A working implementaton is here:

https://software.intel.com/en-us/articles/dynamic-volumetric-cloud-rendering-for-games-on-multi-core-platforms

All the simulation is a 3D grid, and it can be used to generate the weather map.

Accumulating cloudiness on the y-axis could create the cloudtype parameter, and if over a certain threshold the coverage too.

Probably another pass could see spots areas with taller clouds (maybe doing a downscaled version of the simulation map) to check precipitation values.

Just a train of thoughts!

1

##### Share on other sites

@SourceDude22 have you finish it?

0

##### Share on other sites

Posted (edited)

Yaaaaaaaaaaayyyy!* Looking forward to it

*Excited Kermit voice

Related, does anyone know how Reset does their variable distance volume marching for their precipitation effect/large scale god rays? EG:

Near camera volume marching is fine, you set your z-slices and go. Clouds are fine if they're on a defined plane/sphere. But they also seem to use such to represent rain, a very cool effect.

I ask because the most dramatic scattering, or god rays, often comes between clouds/distant mountains. And somehow that seems to be accomplished here. A logarithmic march? If everything (including the clouds) was in some sort of shadowmap like depth buffer you could get a summed area table, then do a gradient domain like march. EG skip space till sunlight, etc. Any other ideas?

Edited by FreneticPonE
1

##### Share on other sites

Been working on this for a long time so I figured I’d share what I have, but I can’t find the right numbers to make the clouds look, well, good.

The highlights are overblown and the shadows are way too deep. The powder term is also making a mess here.

Here is the picture from the GPU pro book, for comparison.

Some more angles:

Silver lining effect, seems to reach too far into the interiors of the clouds, even with the absorption turned way up. Meanwhile the holdouts are too dark.

Aerial view, the powder term causes some very strange shadow casting. They actually make the clouds behind them lighter instead of darker. Clouds also look too dark and generally unrealistic.

This is the only view that looks okay, but the highlights and shadows still look bad.

Here is my source code (GLSL) if it helps. I’m brute forcing the lightmarching with 16 samples to debug the lighting model.

#version 330 core

{
vec3  position;     // [ 0 ..< 12]
float z;            // [12 ..< 16]
vec3  direction;    // [16 ..< 28]
float twice_size;   // [28 ..< 32]
float half_h;       // [32 ..< 36]
float half_k;       // [36 ..< 40]
vec2  shift;        // [40 ..< 48]
mat3  world;        // [48 ..< 96] size = 96
} camera;

uniform sampler3D low_freq_noise;

in Planet
{
vec3  center;
vec2  cloud_deck;
vec3  sun;
} planet;

out vec4 color;

vec2 sphere_intersect(const vec3 ray, const vec3 camera_to_center, const float r)
{
float midpoint = dot(camera_to_center, ray);
float D = r*r - dot(camera_to_center, camera_to_center) + midpoint*midpoint;
if (D <= 0)
{
return vec2(0, 0);
}
else
{
float half_depth = sqrt(D);
return vec2(max(0, midpoint - half_depth), max(0, midpoint + half_depth));
}
}

vec2 box_intersection(const vec3 p1, const vec3 p2, const vec3 start, const vec3 ray)
{
vec3 ray_inverse = 1 / ray;
vec3 v1 = (p1 - start) * ray_inverse;
vec3 v2 = (p2 - start) * ray_inverse;
vec3 n  = min(v1, v2);
vec3 f  = max(v1, v2);
vec2 slice = vec2(max(n.x, max(n.y, n.z)), min(f.x, min(f.y, f.z)));

if (slice.x < slice.y)
{
return max(slice, 0);
}
else
{
return vec2(0, 0);
}
}

float height_fraction(const vec3 position)
{
return clamp(0.5 * (position.z + 1f), 0, 1);
}

{
return max(smoothstep(0.00, 0.07, h) - smoothstep(0.07, 0.11, h), 0); // stratus, could be better
}

{
//return max(smoothstep(0.00, 0.22, h) - smoothstep(0.4, 0.62, h), 0); // cumulus
return smoothstep(0.3, 0.35, h) - smoothstep(0.425, 0.7, h); // cumulus
}

{
return smoothstep(0, 0.1, h) - smoothstep(0.7, 1, h); // cumulonimbus
}

float erode(const float x, const float e)
{
return max(1 - (1 - x) / e, 0);
}

float cloud_density(const vec3 position, const float detail)
{
vec4 low_freq_noise_sample = textureLod(low_freq_noise, 0.5 * (position + 1), detail);
float low_freq_fbm = low_freq_noise_sample.g * 0.725 +
low_freq_noise_sample.b * 0.3  +
low_freq_noise_sample.a * 0.15;

base_cloud = erode(base_cloud, low_freq_fbm);
base_cloud = clamp(6 * (base_cloud - 0.390625 - 0.1) + 0.5, 0, 1);

return base_cloud;
}

float exponential_integral(const float z)
{
return 0.5772156649015328606065 + log( 1e-4 + abs(z) ) + z * (1.0 + z * (0.25 + z * ( (1.0/18.0) + z * ( (1.0/96.0) + z * (1.0/600.0) ) ) ) ); // For x!=0
}

vec3 ambient_color(float h, float extinction_coeff)
{
// For now make ambient color white (less instructions)
float ambient_term = -extinction_coeff * (1.0 - h);
vec3 isotropic_scattering_top = vec3(0.8, 0.9, 1.0) * max(0.0, exp(ambient_term) - ambient_term * exponential_integral(ambient_term));

ambient_term = -extinction_coeff * h;
vec3 isotropic_scattering_bottom = vec3(0.6, 0.8, 1.0) * max(0.0, exp(ambient_term) - ambient_term * exponential_integral(ambient_term));

// Modulate the ambient term by the altitude
isotropic_scattering_top *= h * 0.5;

return 7*(isotropic_scattering_top + isotropic_scattering_bottom);
}

float henyey_greenstein(const float phase, const float g)
{
float g2 = g * g;
return 1f / (3.14159 * 4f) * (1f - g2) / pow(1f + g2 - 2f * g * phase, 1.5f);
}

float lightmarch(vec3 start, float absorption, vec3 light)
{
vec2 slice = box_intersection(vec3(-1, -1, -0.4), vec3(1, 1, 0.4), start, -light);

float ds = (slice.y - slice.x) / 16f;
float s  = slice.x + 0.5f * ds;

//float T         = 1f;
float sigma_ds  = absorption * ds;
float optical_depth = 0;
for (uint i = 0u; i < 16f; ++i)
{
vec3 position = start - s * light;
optical_depth += sigma_ds * cloud_density(position, 0);

/* early exit
if (T < 1e-8)
{
break;
}
*/
s += ds;
}

return optical_depth;
}

vec4 raymarch(vec3 ray, float absorption, vec2 slice)
{
float ds = (slice.y - slice.x) / 128f;
float s  = slice.x + 0.5f * ds;

vec4  color     = vec4(0);
float T         = 1f;
float sigma_ds  = -3*absorption * ds;
float phase     = -dot(planet.sun, ray);

for (uint i = 0u; i < 128f; ++i)
{
vec3 position = camera.position + s * ray;
float density = cloud_density(position, 0);
float Ti = exp(sigma_ds * density);
T *= Ti;

if (T < 1e-8)
{
break;
}

float lightmarch_depth = lightmarch(position, absorption, planet.sun);
float powder_factor = mix(2f*(1 - exp(-2f*lightmarch_depth)), 1f, 0.5f * (phase + 1));
float phase_factor = mix(henyey_greenstein(phase, 0.06f), henyey_greenstein(phase, 0.9f), 0.05f);
vec3 light_color = powder_factor * exp(-lightmarch_depth) * 75f*vec3(5f, 4f, 3f) * mix(1f/(4 * 3.14159f), phase_factor, exp(-lightmarch_depth)) +
ambient_color(height_fraction(position), sigma_ds * density);

color.rgb += T * light_color * vec3(1.0f) * density * ds;
color.a   += (1.0f - Ti) * (1.0f - color.a);
s += ds;
}

return color;
}

void main()
{
vec3 ray   = camera.world *
normalize(vec3(camera.twice_size * ((gl_FragCoord.x - camera.shift.x) - camera.half_h),
camera.twice_size * ((gl_FragCoord.y - camera.shift.y) - camera.half_k),
-camera.z));

vec2 slice = box_intersection(vec3(-1, -1, -0.4), vec3(1, 1, 0.4), camera.position, ray);
vec4 cloud_color;
if (slice.x == slice.y)
{
cloud_color = vec4(0, 0, 0, 0);
}
else
{
cloud_color = raymarch(ray, 7, slice);
}

vec2 horizon = sphere_intersect(ray, planet.center - camera.position, planet.cloud_deck.y + 1000000);
float optical_depth = clamp(0.00000025 * (horizon.y - horizon.x), 0, 1);
vec3 sky = vec3(mix(optical_depth*optical_depth*optical_depth*optical_depth, 1, 0.1),
mix(pow(optical_depth, 1.7), 1, 0.12),
mix(optical_depth, 1, 0.1));

//float fade = 1.0 / (1 + 0.0000001*slice.x*slice.x);
sky = cloud_color.rgb + sky * (1 - cloud_color.a);
color = vec4(sky, 1);
}

PS to the person having problems with making the clouds taper, you have to feed the altitude gradient into a remap function, not just multiply it into the cloud density. That makes the clouds shrink as you go up rather than just get lighter.

1

##### Share on other sites

Posted (edited)

Andrew, looking forward to your SIGGRAPH 2017 Talk.

ps. the gallery pic can't show correctly now, is it because of gamedev revision? have chance to recover it?

Edited by ChenA
0

##### Share on other sites

Yeah, I think something with the site updates may have messed up the gallery link. You can find them by going to my profile and clicking the Albums tab, or use this link:

0

##### Share on other sites

Posted (edited)

My results look a bit like yours, taylorswift:

If I drop the absorption rate to get rid of the dark areas then the clouds get blown out. But I'm not modeling scattering yet, so I think this makes sense. The only light is that which comes directly from the sun but isn't absorbed on the way. I'll see what happens when I add ambient lighting and the henyey greenstein term but right now I'm trying to make sure what I already have is correct-ish. But it looks like you already have both. What happens if you remove them?

I'm not very happy with my cloud shapes either. I don't get very clean edges even at really high sample counts. It ends up looking pretty fuzzy. Your first image looks really good in that respect. Maybe I need some kind of exponential ramp on the density.

I also don't like how the clouds appear near the horizon but in that case I might just need some fog.

Really looking forward to Andrew's talk in a couple weeks.

Edited by wild_pointer
0

##### Share on other sites
2 minutes ago, wild_pointer said:

My results look a bit like yours, taylorswift:

If I drop the absorption rate to get rid of the dark areas then the clouds get blown out. But I'm not modeling scattering yet, so I think this makes sense. The only light is that which comes directly from the sun but isn't absorbed on the way. I'll see what happens when I add ambient lighting and the henyey greenstein term but right now I'm trying to make sure what I already have is correct-ish. But it looks like you already have both. What happens if you remove them?

I'm not very happy with my cloud shapes either. I don't get very clean edges even at really high sample counts. It ends up looking pretty fuzzy. Your first image looks really good in that respect. Maybe I need some kind of exponential ramp on the density.

I also don't like how the clouds appear near the horizon but in that case I might just need some fog.

Really looking forward to Andrew's talk in a couple weeks.

Removing henyey greenstein takes away the bright fringes but brightens the clouds when viewed from the sun since it’s the source of the anisotropic shading in the clouds. Ambient lighting doesn’t do much except brighten the whole scene.

0

##### Share on other sites

Experimenting some with space-based cloud rendering gives some pretty good results:

Needless to say, this doesn’t run very fast (~30ms fullscreen) but I was surprised how far I could get with nothing but procedural noises.

1

##### Share on other sites
On 7/15/2017 at 6:14 AM, taylorswift said:

Removing henyey greenstein takes away the bright fringes but brightens the clouds when viewed from the sun since it’s the source of the anisotropic shading in the clouds. Ambient lighting doesn’t do much except brighten the whole scene.

We use a triple henyey-greenstein phase function. I'll add an implementation to our slides. They should go online in 2.5 weeks or so. The talk is 2 weeks from yesterday! I better get back to making the slides. 😀

On 7/17/2017 at 3:46 AM, taylorswift said:

Experimenting some with space-based cloud rendering gives some pretty good results:

Needless to say, this doesn’t run very fast (~30ms fullscreen) but I was surprised how far I could get with nothing but procedural noises.

Awesome. With enough noise sources and instructions you should be able to model everything. That is our goal in the long run at least. As processing power increases, the amount you can purchase with 2ms increases as well.

2

##### Share on other sites
On 7/18/2017 at 6:36 AM, VONSCHNEIDZ said:

We use a triple henyey-greenstein phase function. I'll add an implementation to our slides. They should go online in 2.5 weeks or so. The talk is 2 weeks from yesterday! I better get back to making the slides. 😀

Awesome. With enough noise sources and instructions you should be able to model everything. That is our goal in the long run at least. As processing power increases, the amount you can purchase with 2ms increases as well.

Will the talk be recorded? would be awesome for non attendees !

And example implementation would be fantastic, I've played through Horizon twice and loved the clouds, amazing!.

0

##### Share on other sites
On 7/18/2017 at 1:36 AM, VONSCHNEIDZ said:

We use a triple henyey-greenstein phase function. I'll add an implementation to our slides. They should go online in 2.5 weeks or so. The talk is 2 weeks from yesterday! I better get back to making the slides. 😀

Awesome. With enough noise sources and instructions you should be able to model everything. That is our goal in the long run at least. As processing power increases, the amount you can purchase with 2ms increases as well.

I’m still struggling with haze and cirrus clouds as when viewing from space, the lack of the thin wispy clouds is very obvious. Unfortunately, I have no idea how to produce good procedural cirrus clouds.

0

## Create an account

Register a new account