New Sky Texture

The goal of this project is to implement a new Sky Texture in Blender.
The actual Hosek-Wilkie model is old and lacks a lot of features, Blender needs an improved sky model.
And the model is in Blender 2.90 !!


  • Standalone Python sky generator
  • Implement sky model in Blender Cycles
  • Precomputation
  • Interpolation between pixels
  • Sun disk limb darkening
  • GPU support
  • OSL support
  • Code refactor
  • Code review


Sky Generator (Python)


Will the position of the Sun in the sky texture match the orientation of a sun lamp?

1 Like

This is a possibility yes. For now i concentrate on adding some other physical phenomena to the sky itself.


The main problem with Blender’s current implementation (or rather lack thereof) of the Sky model is complete lack of connection between sun and sky. There’s just some random sky UI with some direction trackball.

The proper model needs following:

  • Procedural sky texture which is solely driven by a sun lamp orientation.
  • Sun lamp intensity and color being completely overtaken and driven by the sky model, instead of relying on user to input some random values.
  • Correct default values, acting as multipliers of real intensity. So setting sky and sun intensity to 1.0 would always mean the exactly correct relation between sun and sky light intensity at any given day time.
  • Correct sun size parameter acting as multiplier, with value 1.0 being the sun disc size as observer from earth.
  • A visible sun disc on the sky with the correct HDR intensity.

Right now, Blender’s implementation is completely unusable, but the proper implementation requires mainly handing the UI side of the things correctly, e.g. minimizing the room for user error.

  1. the sky should not be driven only by the sun lamp, but also from height from sea level (and my model already does this) and turbidity control, and maybe an albedo input as well
  2. i agree with you, this could be a possibility for sure, but first i have to finish the model
  3. yes it will have correct physical values
  4. the sun size doesn’t change much during the year (it stays at 0.5 something) so this will be a fixed value that shouldn’t be changed at all
  5. yes sun disk is on the plan


for #1 I just meant direction. Right now it makes no sense in Blender that user is expected to manually match sky direction to the sun lamp direction.

for #4 it makes often sense to have that parameter available for many reasons, from simulating other planets with closer/more distant sun, to artistic purpose, such as user simply wanting to make shadows a bit softer, as if the sun was partially covered by clouds. This would affect the size of sun disk as well, but that often doesn’t matter much if sun disk is out of the frame.

1 Like

No, if you have to simultate other planets you shouldn’t rely on a sky model.
The sun size is what it is and won’t change, so if someone needs to add clouds he/she does it by adding object clouds or whatever.


You do realize that would be brutally inefficient in terms of rendering performance, right? It doesn’t seem like you are very familiar with production scenarios. Ultimately, if your sky model is going to have hardcoded, unchangeable sun size and therefore shadow softness, it’s simply destined to fail.

Here’s an example of advanced sky model UE4 has introduced recently:

I am not saying yours should be anywhere near as complex, but hardcoding even a sun size is a bit far fetched. You are free to do whatever you want of course, but then don’t expect people actually wanting to use it :slight_smile:


Added support for Ozone Absorption!
sun altitude -2 degrees, twilight.

Now with Ozone Absorption


Amazing, thank you! Would this be possible to use in Eevee as well at some point?


The first goal is to make it work as a standalone, then implement in Blender Cycles and then in Eevee ultimately


Progress has been done so far :smiley: the first implementation in Blender Cycles of the model is done!
There is still a lot of work to be done, for now the model is taking roughly 10 seconds per sample using an 8th gen i7 on notebook, so a lot of optimization is required since as of now the program is making a lot of calculations per ray.
After the optimization step we will finish the UI of the node.
Here is the branch:


Added Height control.
To infinity… and beyond!!!


Incase anyone wants to play, win64 test build available here

Had to fix some minor bugs, and while i managed to get the GPU kernels to build, they didn’t work, so they are not included.

@nacioss small diff with changes:

diff --git a/intern/cycles/kernel/svm/svm_sky_nishita.h b/intern/cycles/kernel/svm/svm_sky_nishita.h
index fb5196c23d7..439f2a2b996 100644
--- a/intern/cycles/kernel/svm/svm_sky_nishita.h
+++ b/intern/cycles/kernel/svm/svm_sky_nishita.h
@@ -11,18 +11,18 @@ ccl_constant float No = 2.504f*powf(10,25);             // number density of air
 ccl_constant float D = 149.6f*powf(10,9);               // average distance Earth-Sun (m)
 ccl_constant int Rs = 695500000;                        // radius of Sun (m)
-ccl_constant int Hr = 8000;                             // Rayleigh scale height (m)
-ccl_constant int Hm = 1200;                             // Mie scale height (m)
-ccl_constant float mie_coeff = 21.0f*powf(10,-6)*1.11f; // Mie scattering coefficient
-ccl_constant float G = 0.76f;                           // aerosols anisotropy
-ccl_constant int Re = 6360000;                          // radius of Earth (m)
-ccl_constant int Ra = 6420000;                          // radius of atmosphere (m)
+ccl_static_constant int Hr = 8000;                      // Rayleigh scale height (m)
+ccl_static_constant int Hm = 1200;                             // Mie scale height (m)
+ccl_static_constant float mie_coeff = 21.0f * 1e-06 * 1.11f;  // Mie scattering coefficient
+ccl_static_constant float G = 0.76f;                           // aerosols anisotropy
+ccl_static_constant int Re = 6360000;                   // radius of Earth (m)
+ccl_static_constant int Ra = 6420000;                          // radius of atmosphere (m)
 // sRGB illuminant
-ccl_constant float D65[3][3] = {{3.2404542f, -1.5371385f, -0.4985314f},
+ccl_static_constant float D65[3][3] = {{3.2404542f, -1.5371385f, -0.4985314f},
                                 {-0.9692660f, 1.8760108f, 0.0415560f},
                                 {0.0556434f, -0.2040259f, 1.0572252f}};
 // wavelengths from 380 to 780nm in 5nm steps
-ccl_constant float lam[81] = {0.000000380f,
+ccl_static_constant float lam[81] = {0.000000380f,
@@ -104,7 +104,8 @@ ccl_constant float lam[81] = {0.000000380f,
 // irradiance on top of atmosphere
-ccl_constant float irradiance[81] = {1.45756829855592995315f,
+ccl_static_constant float irradiance[81] = {
+    1.45756829855592995315f,
@@ -186,7 +187,8 @@ ccl_constant float irradiance[81] = {1.45756829855592995315f,
 // Rayleigh scattering coefficient
-ccl_constant float rayleigh_coeff[81] = {0.00005424820087636473f,
+ccl_static_constant float rayleigh_coeff[81] = {
+    0.00005424820087636473f,
@@ -268,7 +270,8 @@ ccl_constant float rayleigh_coeff[81] = {0.00005424820087636473f,
 // Ozone absorption coefficient
-ccl_constant float ozone_coeff[81] = {0.01514919300429610517f,
+ccl_static_constant float ozone_coeff[81] = {
+    0.01514919300429610517f,
@@ -350,7 +353,7 @@ ccl_constant float ozone_coeff[81] = {0.01514919300429610517f,
 // CIE XYZ color matching functions
-ccl_constant float cmf_xyz[81][3] = {{0.0014f, 0.0000f, 0.0065f},
+ccl_static_constant float cmf_xyz[81][3] = {{0.0014f, 0.0000f, 0.0065f},
                                   {0.0022f, 0.0001f, 0.0105f},
                                   {0.0042f, 0.0001f, 0.0201f},
                                   {0.0076f, 0.0002f, 0.0362f},
@@ -469,7 +472,7 @@ ccl_device float phase_mie(float mu)
 ccl_device float distance_points(float3 P1, float3 P2)
-  return sqrtf(powf((P2[0]-P1[0]),2)+powf((P2[1]-P1[1]),2)+powf((P2[2]-P1[2]),2));
+  return sqrtf(powf((P2.x-P1.x),2)+powf((P2.y-P1.y),2)+powf((P2.z-P1.z),2));
 ccl_device float3 normalize(float lat, float lon)
@@ -484,24 +487,24 @@ ccl_device float3 normalize_sun(float lat, float lon)
 ccl_device float angle_vectors(float3 rot1, float3 rot2)
-  return rot1[0]*rot2[0]+rot1[1]*rot2[1]+rot1[2]*rot2[2];
+  return rot1.x*rot2.x+rot1.y*rot2.y+rot1.z*rot2.z;
 ccl_device float3 atmosphere_intersection(float3 pos, float3 dir)
-  float a = powf(dir[0],2)+powf(dir[1],2)+powf(dir[2],2);
-  float b = -2.0f*(dir[0]*(-pos[0])+dir[1]*(-pos[1])+dir[2]*(-pos[2]));
-  float c = powf(-pos[0],2)+powf(-pos[1],2)+powf(-pos[2],2)-powf(Ra,2);
+  float a = powf(dir.x,2)+powf(dir.y,2)+powf(dir.z,2);
+  float b = -2.0f*(dir.x*(-pos.x)+dir.y*(-pos.y)+dir.z*(-pos.z));
+  float c = powf(-pos.x,2)+powf(-pos.y,2)+powf(-pos.z,2)-powf(Ra,2);
   float t = (-b+sqrtf(powf(b,2)-4.0f*a*c))/(2.0f*a);
-  return make_float3(pos[0]+dir[0]*t, pos[1]+dir[1]*t, pos[2]+dir[2]*t);
+  return make_float3(pos.x+dir.x*t, pos.y+dir.y*t, pos.z+dir.z*t);
 ccl_device bool surface_intersection(float3 pos, float3 dir)
-  if (dir[2]>=0)
+  if (dir.z>=0)
     return false;
-  float t = (-pos[0]*dir[0]-pos[1]*dir[1]-pos[2]*dir[2])/(powf(dir[0],2)+powf(dir[1],2)+powf(dir[2],2));
-  float D = powf(-pos[0],2)-2.0f*-pos[0]*dir[0]*t+powf(dir[0]*t,2)+powf(-pos[1],2)-2.0f*(-pos[1])*dir[1]*t+powf(dir[1]*t,2)+powf(-pos[2],2)-2.0f*(-pos[2])*dir[2]*t+powf(dir[2]*t,2);
+  float t = (-pos.z*dir.z-pos.y*dir.y-pos.z*dir.z)/(powf(dir.z,2)+powf(dir.y,2)+powf(dir.z,2));
+  float D = powf(-pos.z,2)-2.0f*-pos.z*dir.z*t+powf(dir.z*t,2)+powf(-pos.y,2)-2.0f*(-pos.y)*dir.y*t+powf(dir.y*t,2)+powf(-pos.z,2)-2.0f*(-pos.z)*dir.z*t+powf(dir.z*t,2);
   if (D<=powf(Re,2))
     return true;
@@ -511,6 +514,7 @@ ccl_device bool surface_intersection(float3 pos, float3 dir)
 ccl_device float3 spec_to_xyz(float *spectrum)
   float x, y, z;
+  x = y = z = 0.0f;
   for(int i=0; i<80; i++)
     x += cmf_xyz[i][0]*spectrum[i];
@@ -525,15 +529,14 @@ ccl_device float3 spec_to_xyz(float *spectrum)
 ccl_device float3 xyz_to_srgb(float3 xyz)
-  float R = D65[0][0]*xyz[0] + D65[0][1]*xyz[1] + D65[0][2]*xyz[2];
-  float G = D65[1][0]*xyz[0] + D65[1][1]*xyz[1] + D65[1][2]*xyz[2];
-  float B = D65[2][0]*xyz[0] + D65[2][1]*xyz[1] + D65[2][2]*xyz[2];
+  float R = D65[0][0]*xyz.x + D65[0][1]*xyz.y + D65[0][2]*xyz.z;
+  float G = D65[1][0]*xyz.x + D65[1][1]*xyz.y + D65[1][2]*xyz.z;
+  float B = D65[2][0]*xyz.x + D65[2][1]*xyz.y + D65[2][2]*xyz.z;
   float exposure = 100000.0f;
   return make_float3(R*exposure, G*exposure, B*exposure);
-ccl_constant float3 earth_center = make_float3(0, 0, 0);
 ccl_constant int samples = 32;
 ccl_constant int samples_light = 16;
@@ -558,6 +561,7 @@ ccl_device void single_scattering(float3 cam_dir, float3 sun_dir, float *spectru
   float optical_depthO = 0;
   float phaseR = phase_rayleigh(mu);
   float phaseM = phase_mie(mu);
+  float3 earth_center = make_float3(0, 0, 0);
   // for each point along AB
   for(int i=0; i<samples; i++)
@@ -634,4 +638,4 @@ ccl_device void single_scattering(float3 cam_dir, float3 sun_dir, float *spectru
\ No newline at end of file

Added Air and Dust density controls


Do you like this UI?
Or do you still prefer the Normal Sphere controller?


Those two sliders give much more control imo, idk if node gurus might prefer the sphere but I would go 100% with this.


I never liked blender sphere controller, but I think you keep to default ui guidelines so new node won’t look out of place. Plus it’s might be potential issue later in review process.

1 Like

That looks great! Is there anyway to expose the Sun Elevation, Sun Rotation, Sun Height, Air Density, and Dust Density items by input sockets such that one could drive some of those values by, for example, the frame number?


While I could imagine some pretty alien stuff by driving them procedurally, if you were to just drive them based on the frame number you’d probably off easier by just keyframing as would do currently.