Last modified on 27 September 2014, at 19:40

Cg Programming/Unity/Diffuse Reflection of Skylight

A spherical building illuminated by an overcast sky from above and a green water basin from below. Note the different colors of the illumination of the building.

This tutorial covers hemisphere lighting.

It is based on diffuse per-vertex lighting as described in Section “Diffuse Reflection”. If you haven't read that tutorial yet, you should read it first.

Hemisphere lighting basically computes diffuse illumination with a huge light source that covers a whole hemisphere around the scene, for example the sky. It often includes the illumination with another hemisphere from below using a different color since the calculation is almost for free. In the photo to the left, the spherical building is illuminated by a overcast sky. However, there is also illumination by the green water basin around it, which results in a noticeable greenish illumination of the lower half of the building.

Diffuse reflection can be calculated with the surface normal vector N and the direction to the light source L.

Hemisphere LightingEdit

If we assume that each point (in direction L) of a hemisphere around a point on the surface acts as a light source, then we should integrate the diffuse illumination (given by max(0, L·N) as discussed in Section “Diffuse Reflection”) from all the points on the hemisphere by an integral. Let's call the normalized direction of the rotation axis of the hemisphere U (for “up”). If the surface normal N points in the direction of U, we have full illumination with a color specified by the user. If there is an angle γ between them (i.e. cos(γ) = U·N), only a spherical wedge (see the Wikipedia article) of the hemisphere illuminates the surface point. The fraction w of this illumination in comparison to the full illumination is:

w = \frac{1}{2}(1 + \cos(\gamma))   = \frac{1}{2}(1 + \mathbf{U}\cdot\mathbf{N})

Thus, we can compute the incoming light as w times the user-specified color of the full illumination by the hemisphere. The hemisphere in the opposite direction will illuminate the surface point with 1-w times another color (which might be black if we don't need it). The next section explains how to derive this equation for w.

Derivation of the EquationEdit

For anyone interested (and because I didn't find it on the web) here is a derivation of the equation for w. We integrate the illumination over the hemisphere at distance 1 in a spherical coordinate system attached to the surface point with the direction of N in the direction of the y axis. If N and U point in the same direction, the integral is (apart from a constant color specified by the user):

\int_0^\pi \mathrm{d} \phi \int_0^\pi \mathrm{d}\theta \sin(\theta)\,\mathbf{L} \cdot \mathbf{N} =   \int_0^\pi \mathrm{d} \phi \int_0^\pi \mathrm{d}\theta \sin(\theta) \left(
\begin{array}{c} x \\ y \\ z \end{array}
\right)\cdot \mathbf{N}

The term sin(θ) is the Jacobian determinant for our integration on the surface of a sphere of radius 1, (x, y, z) is (cos(φ)sin(θ), sin(φ)sin(θ), cos(θ)), and N = (0,1,0). Thus, the integral becomes:

\int_0^\pi \mathrm{d} \phi \int_0^\pi \mathrm{d}\theta (\sin(\theta))^2 \sin(\phi) = \pi

The constant π will be included in the user-defined color of the maximum illumination. If there is an angle γ with cos(γ) = U·N between N and U, then the integration is only over a spherical wedge (from γ to π):

w = \frac{1}{\pi} \int_\gamma^\pi \mathrm{d} \phi \int_0^\pi \mathrm{d}\theta (\sin(\theta))^2 \sin(\phi)   = \frac{1}{2}(1 + \cos(\gamma))   = \frac{1}{2}(1 + \mathbf{U}\cdot\mathbf{N})

Shader CodeEdit

The implementation is based on the code from Section “Diffuse Reflection”. In a more elaborated implementation, the contributions of other light sources would also be included, for example using the Phong reflection model as discussed in Section “Specular Highlights”. In that case, hemisphere lighting would be included in the same way as ambient lighting.

Here, however, the only illumination is due to hemisphere lighting. The equation for w is:

w = \frac{1}{2}(1 + \mathbf{U}\cdot\mathbf{N})

We implement it in world space, i.e. we have to transform the surface normal vector N to world space (see Section “Shading in World Space”), while U is specified in world space by the user. We normalize the vectors and compute w before using w and 1-w to compute the illumination based on the user-specified colors. Actually, it is pretty straightforward.

Shader "Cg per-vertex hemisphere lighting" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
      _UpperHemisphereColor ("Upper Hemisphere Color", Color) 
         = (1,1,1,1) 
      _LowerHemisphereColor ("Lower Hemisphere Color", Color) 
         = (1,1,1,1) 
      _UpVector ("Up Vector", Vector) = (0,1,0,0) 
   }
   SubShader {
      Pass {      
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
 
         // shader properties specified by users
         uniform float4 _Color; 
         uniform float4 _UpperHemisphereColor;
         uniform float4 _LowerHemisphereColor;
         uniform float4 _UpVector;
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : COLOR;
               // the hemisphere lighting computed in the vertex shader
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = _Object2World;
            float4x4 modelMatrixInverse = _World2Object; 
               // multiplication with unity_Scale.w is unnecessary 
               // because we normalize transformed vectors
 
            float3 normalDirection = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            float3 upDirection = normalize(_UpVector);
 
            float w = 0.5 * (1.0 + dot(upDirection, normalDirection));
            output.col = (w * _UpperHemisphereColor 
               + (1.0 - w) * _LowerHemisphereColor) * _Color;
 
            output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }
 
         ENDCG
      }
   } 
}

SummaryEdit

Congratulations, you have finished another tutorial! We have seen:

  • What hemisphere lighting is.
  • What the equation for hemisphere lighting is.
  • How to implement hemisphere lighting.

Further ReadingEdit

If you still want to know more

  • about lighting with the diffuse reflection, you should read Section “Diffuse Reflection”.
  • about hemisphere lighting, you could read Section 12.1 of the book “OpenGL Shading Language” (3rd edition) by Randi Rost et al., published 2009 by Addison-Wesley.


< Cg Programming/Unity

Unless stated otherwise, all example source code on this page is granted to the public domain.