Sphere Shading with CSS


This article describes the use of CSS style rules to simulate the shading of a sphere in sunlight. The CSS border radius, gradient fill and box-shadow rules can to simulate the 3D shading of a sphere reasonably well for arbitrary positioning of the observer, the sphere and the sun.

Fig 1 shows some photographs of a ball in sunlight, clearly showing the shadows and highlights that to be simulated with CSS properties.

Figure 1. Photographs of a ball in direct sunlight.

Fig 2 shows the HTML/CSS simulation of the ball similar to that in Fig 1. The sliders allow the angle between the sun and observer to be varied along with the azimuthal angle of the sun.

Figure 2. Simulation of sphere in sunlight, sliders vary sun elevation and azimuth.

The details of the CSS style rules to create the simulation are described below. How to calculate the position of the shadows and highlights for an arbitrary geometry of observer sphere and sun are also shown below.

The sphere appears to the observer as a circle, shaded with three distinct features:

  1. Bright and dark hemispheres. Assuming that the light source is sun, the illuminating rays are essentially all parallel. This means that all points on the hemisphere facing the sun will be directly illuminated and will scatter some light toward the observer. The unilluminated hemisphere will receive no direct light to scatter and so appear dark.

    The dark and light hemispheres are delineated by a great circle, called the terminator, the normal to which will be in the direction of the sun. The observer sees the terminator as half an ellipse.

  2. Ambient light scattering. The center of the sphere will appear brighter, grading darker toward the limb. This is due to scattering of ambient light which increases in intensity as the normal to the surface approaches the direction of the observer.
  3. Specular reflection bright spot. There will be a bright spot where the observer sees the reflected image of the sun. This is specular reflection, the incident and reflected rays are at equal angles to the surface normal at this point. The bright spot lies in the plane defined by the observer, the sphere's center and the sun.
  4. Azimuthal rotation. The bright spot and the minor axis of the terminator ellipse, as seen by the observer, lie in a straight line. This line will have some azimuthal rotation relative to the X axis depending upon the direction to the sun.

Making the basic shapes with CSS

To create a 2D simulation of the shaded sphere we create a set of three DIV elements, representing the background circle, the dark-bright shadow shape and an overlay with semi-transparent shading. The background DIV and its child DIV elements will be created and styled with a JavaScript function so the parameters determined by a given geometry may be passed as variables.

Making a circle with CSS

To create the background circle of radius R, first create a square DIV element with width and height 2*R and set the border-radius for all 4 corners to R. The "overflow: hidden" rule is added to crop any child element box-shadows that may extend outside the circle. The background-color is set to the given red, green and blue values.

Dark and bright hemispheres

The 2D projection of the dark hemisphere is created by a DIV element with the border-radius values for the left side set to R, forming a semi-circle, then right side is half an ellipse, created by setting the border-radius-right values' Y components to R, but their X components are set to the half width of the terminator ellipse minor axis. This minor axis length is determined by the angle between the observer and the sun. The method for calculating the ellipse half width is shown below.

If the angle between the observer and the sun is less than 90° the dark side will appear crescent shaped and the terminator ellipse X radius values would be negative, since CSS border-radii can't be set to a negative value, the larger hemisphere with its convex outline must be used to set the elliptical boundary. If the bright hemisphere is the larger, then the background circle is set to the darker shadow color and the bright hemisphere is drawn over it. If the darker hemisphere is bigger then the background circle is set to the bright color and the dark hemisphere is drawn over it. Here is the source code for these basic shapes.

function demo1(holderId, rVal, gVal, bVal, R, shadowOffset) {
  var holderNode = document.getElementById(holderId),
      sphere = document.createElement("div"),
      hemiSphere = document.createElement("div"),
      dia = 2*R,
      brightColor = "rgb("+rVal+","+gVal+","+bVal+")",
      darkColor = "rgb("+Math.round(0.7*rVal)+","+Math.round(0.7*gVal)+","+Math.round(0.7*bVal)+")",
      hw = Math.abs(shadowOffset)*R,
      sphereTxt = "",
      hemiTxt = "";

  sphereTxt += "position:absolute; width:"+dia+"px; height:"+dia+"px;";
  sphereTxt += "border-radius:"+R+"px;";
  sphereTxt += "overflow: hidden;";
  if (shadowOffset <= 0)
    sphereTxt += "background-color:"+darkColor+";";

    hemiTxt += "position:absolute; left:auto; right:0;";
    hemiTxt += "width:"+(R+hw)+"px; height:"+dia+"px;";
    hemiTxt += "border-top-left-radius:"+hw+"px "+R+"px;";
    hemiTxt += "border-bottom-left-radius:"+hw+"px "+R+"px;";
    hemiTxt += "border-top-right-radius:"+R+"px "+R+"px;";
    hemiTxt += "border-bottom-right-radius:"+R+"px "+R+"px;";
    hemiTxt += "background-color:"+brightColor+";";
    sphereTxt += "background-color:"+brightColor+";";

    hemiTxt += "position:absolute; left:0; right:auto;";
    hemiTxt += "width:"+(R+hw)+"px; height:"+dia+"px;";
    hemiTxt += "border-top-left-radius:"+R+"px "+R+"px;";
    hemiTxt += "border-bottom-left-radius:"+R+"px "+R+"px;";
    hemiTxt += "border-top-right-radius:"+hw+"px "+R+"px;";
    hemiTxt += "border-bottom-right-radius:"+hw+"px "+R+"px;";
    hemiTxt += "background-color:rgba(0,0,0,0.3);";
  sphere.style.cssText = sphereTxt;
  hemiSphere.style.cssText = hemiTxt;


Add shading

Feathering the terminator

The shadow terminator line most commonly appears feathered to some extent. This can be simulated by applying a box shadow to the hemisphere overlay. The feathering adds drop shadow to the edge of the bright hemisphere, the drop shadow will be the dark color and will be "inset" if the overlay hemisphere is bright and "outset" (the CSS default) if the overlay is dark. In this example the box shadow extends 30% of the radius and 20%offset.

if (shadowOffset <= 0)
  hemiTxt += "box-shadow:inset "+(0.2*R)+"px 0 "+(0.3*R)+"px 0px "+darkColor+";";
  hemiTxt += "box-shadow: "+(0.2*R)+"px 0 "+(0.3*R)+"px 0px "+darkColor+";";


Scattered ambient light

Scattering of ambient light from the sphere surface is seen by the observer as a darkening toward the limb of the sphere. This effect is uniform around the sphere and so is best simulated by a second overlay with its background-image set to a black radial-gradient with varying transparency.

function demo3(holderId, rVal, gVal, bVal, R, shadowOffset) {
  var ...
      overlay = document.createElement("div");
      overlayTxt = "";

  overlayTxt += "position:absolute; width:"+dia+"px; height:"+dia+"px;";
  overlayTxt += "border-radius:"+R+"px;";
  overlayTxt += "background-image:radial-gradient(circle "+R+"px at 50% 50%, \
                                                  rgba(0,0,0,0.0) 40%, \
                                                  rgba(0,0,0,0.05) 75%, \
                                                  rgba(0,0,0,0.15) 100%)";
  sphere.style.cssText = sphereTxt;
  hemiSphere.style.cssText = hemiTxt;
  overlay.style.cssText = overlayTxt;


Specular reflection bright spot CSS

The bright spot caused by specular reflection of the sun can be simulated by an additional radial gradient applied to the overlay div. The spot will be white at the center with transparency increasing with distance. The offset of the bright spot from the center of the circle can be calculated (see below) or estimated.

function demo4(holderId, rVal, gVal, bVal, R, shadowOffset, specularOffset) {
  var ...
      sx = specularOffset*R,
  if (specularOffset !== null)   // only draw a bright spot if sun above horizon
    // bright spot
    overlayTxt += ", radial-gradient(circle "+0.6*R+"px at "+(R+sx)+"px "+R+"px, \
                                                    rgba(255,255,255,0.7) 8%, \
                                                    rgba(255,255,255,0.15) 65%, \
                                                    rgba(255,255,255,0.0) 100%);";

Rotating the element to match sun azimuth

The simulation so far has assumed the observer, sphere and sun all lie in the X-Z plane with the sun to the right of the sphere. In general the plane containing the observer, the sphere center and the sun will have some rotation about the Z axis. This is easily handled with a CSS transform 'rotate' applied to the sphere element.

function demo5(holderId, rVal, gVal, bVal, R, shadowOffset, specularOffset, azimDeg) {

  if (azimDeg)
    sphere.style.transform = "rotate("+(-azimDeg)+"deg)";  // switch to CW positive

Shading parameter calculations for arbitrary position of the sun

To write the CSS rules to make a good simulation of the shading a sphere the following parameters will be needed:

  • hw: Half width of the elliptical boundary between the dark-bright hemispheres.
  • sx:Distance to the center of the specular reflection bright spot.
  • az: The Sun azimuth, i.e. the angle to X axis made by the projection onto the X-Y plane of a vector in the direction of the sun.

The 3D arrangement assumes sphere is always assumed at to be at the origin of the X-Y plane (which will lie in the plane of the screen), the observer will be positioned somewhere on the Z axis (out of the screen directly above the sphere). 3D angles are measured using the right hand rule, so angles in the X-Y plane increase anti-clockwise from the X axis.

In this analysis the 3D arrangement may be simplified into two 2D geometry calculations. The dark-bright boundary ellipse width and the offset of the bright spot may be calculated in 2D. Mapping this plane onto the X-Y plane allows the relatively simple calculation of 'hw' and 'sx'. Flipping this plane back to Z-X and then rotating about the Z axis restores all the 3D information. The angle of rotation is just the sun azimuth 'az'.

Calculation of specular reflection point

The coordinate system for the calculation assumes the sphere center is at the origin of the X-Y plane, the observer is at a distance 'h' up the Y axis and the sun is in the X-Y plane with elevation angle 'σ' to the X axis.

The point of reflection P, is determined by Snell's law which requires that the angle between the incident ray and the surface normal at P to be equal to the angle between the reflected ray and the normal. The geometry of this specular reflection is shown in Fig 3.

Figure 3. A schematic diagram of the plane through source, center of a reflecting sphere and the observer showing the geometry that determines the location of the specular reflection bright spot.

From the diagram: $$ \begin {aligned} \tau &= \phi + \sigma \\ \phi &= \delta + (\frac{\pi}{2} - \tau) \\ \end {aligned}$$ Substi&tuting for \phi $$ \begin {aligned} \sigma &= \tau - \delta - \frac{\pi}{2} + \tau \\ &= 2\tau - \frac{\pi}{2} - \delta \\ \end {aligned}$$ Since, $$ \delta = sin^{-1}(\frac{P.x}{\lvert OP \rvert}) $$ Hence, $$ \sigma = 2\tau - \frac{\pi}{2} - \frac{P.x}{\lvert OP \rvert} $$

Given the elevation of the sun and the location of the observer the point of reflection can be calculated. An analytic solution is rather difficult [ref 1] and a simple binary search has been used to determine the coordinates radial offset of P.

The JavaScript code to calculate find the radial offset of P is shown below. It assumes a unit sphere at the origin, an observer on Y axis at height h and an incoming parallel rays from S at an elevation angle sAng. The function returns X component of reflection point P that reflects a ray from S going through O.

function calcSpecularOffset(h, sAng)
  var tau,                            // tau = angle of reflection point
      tauMax = Math.PI/2,
      tauMin = Math.asin(1/h),
      sigma,                          // incident ray elevation
      sigmaMax = tauMax,
      sigmaMin = -Math.PI/2 + tauMin,
      hi, lo;                         // the limits of search arc

  function rayAngle(h, pAng) // observer Y coord, reflection point angle (rads) from X axis
    // assume a sphere at the origin radius 1
    // given observer at O and reflection point at P
    // return the angle of elevation of the incident ray
    var P = {x:Math.cos(pAng), y:Math.sin(pAng)},
        O = {x:0, y:h},
        PO = vSubtract(O, P),
        lenOP = Math.sqrt(vDot(PO, PO)),
        delta = Math.asin(P.x/lenOP),
        angle = 2*pAng - Math.PI/2 - delta;

    return angle;

  if ((sAng <= sigmaMax) && (sAng >= sigmaMin))   // target angle must lie within hi and lo
    lo = tauMin;
    hi = tauMax;
    do {
      tau = (hi + lo)/2;   // try tau in the middle of hi and lo
      sigma = rayAngle(h, tau);
      if (sAng > sigma) {
        lo = tau; }
      else {
        hi = tau; }
    } while (Math.abs(sigma-sAng) > 0.01);
  return Math.cos(tau);   // P.x as a fraction of the radius

Calculation of dark hemisphere boundary ellipse

A sphere directly ill-fated by a distant source will have dark and light hemispheres, the boundary between the two will be a circle on the surface lying in the plane perpendicular to the direction of the source and whose center is at the center of the sphere.

Figure 4. A schematic diagram of the plane through source, center of a reflecting sphere and the observer showing the geometry of the dark-bright hemisphere dividing line.

The observer sees this dividing circular line as half an ellipse. The length of the major axis will be the diameter of the sphere and the length of the minor axis will be determined by the angle between the observer and the source. The geometry to determine the length of the minor axis is shown in Fig 4.

From the diagram:

hw  = -Math.sin(σ);   //minor axis half length

1. Eberly D, "Computing a Point of Reflection on a Sphere"