Take the 2-minute tour ×
Mathematica Stack Exchange is a question and answer site for users of Mathematica. It's 100% free, no registration required.

What's the best way to make a drop shadow for a 3D object?

 image = Graphics3D[Sphere[], Boxed -> False]

sphere without drop shadow

I can get a blurry black outline of this:

imageShadow = 
 Blur[RegionBinarize[ColorNegate[image], (* bottom left corner --> *) {{1, 1}}, 
   0.1], 20]

which could act as a good shadow:

a shadow

But combining them together is a bit harder... Any suggestions?

share|improve this question
    
Older versions of Mathematica had the routine Shadow[] in the package Graphics`Graphics3D`. You might want to look into it. –  J. M. Jan 29 '12 at 12:01
    
Similar question: stackoverflow.com/q/6955692/615464 –  Sjoerd C. de Vries Jan 29 '12 at 21:32
    
@SjoerdC.deVries good find. I didn't check the old site... –  cormullion Jan 29 '12 at 21:35

4 Answers 4

up vote 14 down vote accepted

This produces a 2D shadow. If you meant a 3D shadow (on the x-y plane), see code below.

image = Rasterize[Graphics3D[Sphere[], Boxed -> False]];
shadow = Blur[RegionBinarize[ColorNegate[image], {{1, 1}}, 0.1], 20];

image = SetAlphaChannel[image, ColorNegate@Binarize[image, {1, 1}]];

Show[{shadow, image}]

Mathematica graphics

The position of the shadow has to be fine tuned manually.

I also managed to construct it in 3D (rotatable), though I cannot make the bottom polygon transparent.

shadow = Blur[
   RegionBinarize[Graphics[Circle[], ImagePadding -> 60], {{1, 1}}, 
    0.1], 40];
shadow = SetAlphaChannel[shadow, ColorNegate@shadow];

Graphics3D[{
  Sphere[],
  EdgeForm@None, Opacity@.7, Texture@shadow, 
  Polygon[{{-1, -1, -2}, {1, -1, -2}, {1, 1, -2}, {-1, 
     1, -2}, {-1, -1, -2}}, 
   VertexTextureCoordinates -> {{0, 0}, {1, 0}, {1, 1}, {0, 1}}]
  }, Boxed -> False]

Mathematica graphics

share|improve this answer
    
Excellent - just what I needed! Thanks. –  cormullion Jan 30 '12 at 10:11

Suppose you have a flat surface with normal n going through point p0 and a directional light with direction dir, then the shadow of a point p onto this surface can be calculated according to

proj[p0_, n_, dir_][p_] := p - (p - p0).Normalize[n]/dir.Normalize[n] dir

Suppose you have a shape created with a ParametricPlot3D, for example

pt[r_, ph_, th_] := {r Cos[ph] Sin[th], r Sin[ph] Sin[th], r Cos[th]}
rf[ph_, th_] := 3/2 + 2 Cos[2 th] Sin[ph]^2

shape = ParametricPlot3D[pt[rf[ph, th], ph, th], {ph, 0, 2 Pi}, {th, 0, Pi}, 
  Mesh -> False]

Then the shadow of this shape could be calculated according to

shdw = With[{p0 = {0, 0, -4}, n = {0, 0, 1}, dir = {1, 0, -1}},
   ParametricPlot3D[proj[p0, n, dir][pt[rf[ph, th], ph, th]], 
    {ph, 0, 2 Pi}, {th, 0, Pi}, Mesh -> False, PlotStyle -> Black]];

Show[shape, shdw, PlotRange -> All]

Mathematica graphics


To get blurry edges on the shadow you could do something like this

With[{p0 = {0, 0, -4}, n = {0, 0, 1}, dir = {1/3, 1/2, -1},
    plotr = {{-8, 8}, {-8, 8}, {-5, 4}}},

  (* blurred image of shadow to be used as a texture *)
  tex = Blur[Rasterize[
    ParametricPlot[proj[p0, n, dir][pt[rf[ph, th], ph, th]][[;; 2]], 
      {ph, 0, 2 Pi}, {th, 0, Pi}, 
      Mesh -> False, 
      PlotStyle -> {Black, Opacity[1]}, 
      Axes -> False, Frame -> False,
      PlotRange -> plotr[[;; 2]],
      Background -> None], 
    Background -> None], 10];

  shdw = Graphics3D[{Texture[ImageData[tex]], EdgeForm[],
    Polygon[
      p0 + RotationTransform[{{0, 0, 1}, n}][Flatten[{#, 0}]] & /@ 
        Tuples[plotr[[;; 2]]][[{1, 2, 4, 3}]],
      VertexTextureCoordinates -> Tuples[{0, 1}, 2][[{1, 2, 4, 3}]]]}];

  Show[shdw, shape,
    Lighting -> {{"Directional", White, {0, -1, 1}}}, 
    PlotRange -> plotr,
    Axes -> False]]
  ]

Mathematica graphics

Similar to István's solutions, I'm using a blurred rasterized image of the projected shape as a texture for the surface on which the shadow is projected. To get a transparent texture I'm using ImageData[tex] as the texture rather than tex itself. To get the scaling right when applying the texture, I'm using the same PlotRange for tex as for the polygon.

share|improve this answer
    
Very good answer! –  magma Jan 29 '12 at 15:48
    
thanks - excellent job. –  cormullion Jan 30 '12 at 10:09
    
I'll just note that this is more or less what Shadow[] in older versions of Mathematica does. –  J. M. Jan 31 '12 at 1:29
    
Wish I could up-vote twice! @J.M. what is Shadow[]? Couldn't find it in the documentation. –  Dror Feb 15 '13 at 16:06
    
@Dror, It's one of the routines in the old Graphics`Graphics` package in earlier versions of Mathematica... –  J. M. Mar 23 '13 at 12:29

Objects obscure their own shadows when viewed from the position of the light source. Consider the following 3D scene:

enter image description here

If this scene is illuminated with a light source located at the camera position, the shadow of the cow will land on any part of the walls or floor which is obscured by the cow. Suppose we rasterize this scene with the lighting turned off and the walls glowing white - this provides a silhouette of the cow:

enter image description here

To use this silhouette as a shadow texture, it is necessary to map from the 3D coordinates of the wall polygons to the 2D image position. This can be done using the ViewMatrix which @Heike shows how to compute in this answer.

Here is some code which allows a 3D scene to be interactively rotated with a button which creates a shadow from the current viewpoint, outputting a copy of the Graphics3D with the shadow applied and a single light source at the appropriate position.

First the code (thanks to Michael E2 for fixing the initialisation of ViewVector):

theta[{x_, y_, z_}] := ArcTan[z, Norm[{x, y}]];
phi[{x_, y_, z_}] := If[Norm[{x, y}] > .0001, ArcTan[x, y], 0];
alpha[vert_, v1_] := ArcTan[{-Sin[phi[v1]], Cos[phi[v1]], 0}.vert, 
   Cross[v1/Norm[v1], {-Sin[phi[v1]], Cos[phi[v1]], 0}].vert];
tt[v1_, vert_, center_, r_, scale_] := TransformationMatrix[
   RotationTransform[-alpha[vert/scale, v1], {0, 0, 1}].
   RotationTransform[-theta[v1], {0, 1, 0}].
   RotationTransform[-phi[v1], {0, 0, 1}].
   ScalingTransform[r {1, 1, 1}].
   TranslationTransform[-center]];
pp[ang_] := {{1, 0, -Tan[ang], 1}, {0, 1, -Tan[ang], 1}, 
   {0, 0, -Tan[ang], 0}, {0, 0, -2 Tan[ang], 2}};
spos[{t_, p_}, {x_, y_, z_}] := {#1, #2}/#4 & @@ (p.t.{x, y, z, 1});
shadoweffect = # ~Blur~ 10 ~ImageAdd~ 0.3 &;

shadowCaster[gr_, shadowpolys_] := 
 DynamicModule[{pr, center, scale, v1, vv, theta, vert, va, vm, tex, vcf},
  pr = Charting`get3DPlotRange@Graphics3D[gr];
  scale = 1/Abs[#1 - #2] & @@@ pr;
  center = Mean /@ pr; va = 25 Degree; vert = {0., 0., 1.};
  v1 = 4.5 (Last@Transpose[pr] - center) + center; vv = {v1, center};
  Panel[Column[{
     Dynamic[Graphics3D[{gr, shadowpolys}, ViewAngle -> Dynamic[va], 
       ViewVector -> Dynamic[vv, (vv = #; center = vv[[2]]; v1 = vv[[1]] - center) &], 
       ViewVertical -> Dynamic[vert], Boxed -> False]],
     Button["Shadow",
      vm = {tt[v1, vert, center, Cot[va/2]/Norm[v1], scale], pp[va/2]};
      tex = Rasterize[Graphics3D[{gr, {Glow[White], EdgeForm[], shadowpolys}}, 
         ViewMatrix -> vm, Boxed -> False, Lighting -> None],  ImageSize -> 800]; 
      vcf = With[{m = {tt[v1, vert, center, Cot[va/2]/Norm[v1], scale], 
             pp[va/2]}}, spos[m, #] &];
      Print[Graphics3D[{gr, {Texture[shadoweffect@tex],
          shadowpolys /. Polygon[pts_, rest___] :> 
          Polygon[pts, rest, VertexTextureCoordinates -> vcf /@ pts]}}, 
        Lighting -> {{"Directional", White, vv}, {"Ambient", GrayLevel[0.1]}}, 
          Boxed -> False]], Method -> "Queued"]}]]]

It works like this. Create some 3D objects which will cast shadows:

cow = Translate[
   Scale[ExampleData[{"Geometry3D", "Cow"}][[1]], 3], {0.2, 0, 0.75}];

and some polygons which will receive the shadows:

walls = {
   Polygon[{{-1, -1, 0}, {-1, 1, 0}, {1, 1, 0}, {1, -1, 0}}],
   Polygon[{{-1, -1, 0}, {-1, -1, 1.5}, {1, -1, 1.5}, {1, -1, 0}}],
   Polygon[{{-1, -1, 0}, {-1, -1, 1.5}, {-1, 1, 1.5}, {-1, 1, 0}}]};

Then evaluate

shadowCaster[cow, walls]

enter image description here

Rotate the Graphics3D to the desired viewpoint and click the button to get this:

enter image description here

Note that there is only one shadow texture, which is used by all the shadow receiving polygons. It is therefore relatively quick even when a large number of polygons are used. e.g. here are ~3000 polygons approximating a curved surface:

surface = {EdgeForm[], Cases[Normal@
     Plot3D[Exp[-2 (x^2 + y^2)^4] - 1, {x, -1, 1}, {y, -1, 1}, 
      PlotPoints -> 40, PlotRange -> All, 
      MaxRecursion -> 0], _Polygon, -1]};

shadowCaster[cow, surface]

enter image description here

enter image description here

share|improve this answer
2  
This is awesome - ray-tracing next? I did have a problem, if all variables weren't initialized properly. Here are some suggestions: pr = Charting`get3DPlotRange@Graphics3D[gr]; scale = 1/Abs[#1 - #2] & @@@ pr; center = Mean /@ pr; va = 25 Degree; vert = {0., 0., 1.}; v1 = 4.5 (Last@Transpose[pr] - center) + center; vv = {v1, center}; –  Michael E2 Jan 11 at 19:59
1  
@MichaelE2, many thanks for the improved initialisation code - I just couldn't figure out how to get it right. –  Simon Woods Jan 12 at 11:14
    
The cow-on-a-hill is very impressive. –  bobthechemist Jan 12 at 13:06

Since a drop shadow is a projection, I thought it's useful to provide a more general solution that creates this projection for arbitrary Graphics3D objects with less manual tuning. I am skipping the blur effect because I want to focus on the projection issue (Mathematica isn't a ray tracer, so I feel it's a bit too painful to simulate shadow boundaries using blur).

Here is my code:

Options[project] = {"ObjectCenter" -> {0, 0, 0}, 
   "DarkShadow" -> True};
project[x_, direction_, normal_, OptionsPattern[]] := Module[
  {d, n, darkShadow, center},
  darkShadow = OptionValue["DarkShadow"];
  center = OptionValue["ObjectCenter"];
  d = Normalize[direction];
  n = Normalize[normal];
  x /. Graphics3D[gr_, opts___] :> Graphics3D[
     {
      If[darkShadow, Black],
      GeometricTransformation[
       If[darkShadow,
        gr /. {
          Glow[_] -> Glow[],

          r_?(MemberQ[{RGBColor, Hue, CMYKColor, GrayLevel}, 
               Head[#]] &) -> Black
          },
        gr
        ],
       Composition[
        TranslationTransform[direction + center],
        Quiet[Check[RotationTransform[{d, n}], Identity],
         {RotationMatrix::degen, RotationTransform::spln}
         ],
        ScalingTransform[10^-3, d],
        Quiet@Check[
          ScalingTransform[1./(n.d), n - (n.d) d],
          Identity
          ],
        TranslationTransform[-center]
        ]
       ]
      },
     opts
     ]
  ]

The argument x is a 3D plot or graphics object. The second variable, direction, is parallel to the light rays and its length is equal to the offset between the object and its shadow (assuming the object was centered at the origin). The third argument, normal, is the normal vector of the surface onto which the shadow is projected.

Edit

The translation by vector direction is applied after the object has effectively been moved to a plane containing the origin. This happens in the "flattening step" (the multiplication by 10^-3 in ScalingTransform). If you want the translation to start from a different position, this can be specified via the option "ObjectCenter". For an example with a more specific description, see also this answer.

End edit

To illustrate this, I'll define a sample object (displayed below with its shadow):

gg = Graphics3D[{{Opacity[.5], Cuboid[]}, {Blue, 
    Translate[Scale[Cuboid[], .2], {1, 1, 1}/2]}, , {Glow[Red], Red, 
    Translate[Scale[Sphere[], .5], -{1, 1, 1}/4]}}, Boxed -> False]

Now display it with some coordinate axis for orientation, assuming light going in the direction {0,1,1} and falling on a surface tilted into the space diagonal {1,1,1}:

Show[gg, 
     project[gg, 2.1 {0, 1, 1}, {1, 1, 1}],
     Graphics3D[{Map[{Apply[RGBColor, #], Arrow[Tube[{{0, 0, 0}, #}]]} &, 
    2 IdentityMatrix[3]]}]
]

3D object and its shadow under illumination from diagonally below

The projection is stretched if the shadow surface isn't perpendicular to the rays. Of course there is the special case where the shadow surface is at a grazing angle to the light. I decided to handle this by not stretching the shadow.

Also observe that translucent regions create less dark shadows. And the whole thing is still a 3D object, not a bitmap.

Another example:

Show[gg, 
     project[gg, -1.5 {0, 0, 1}, {0, 1, 1}], 
     Graphics3D[{Map[{Apply[RGBColor, #], Arrow[Tube[{{0, 0, 0}, #}]]} &, 
    2 IdentityMatrix[3]]}]
]

3D object and its shadow under illumination from above

share|improve this answer
    
That's an impressive piece of code. I'm going to work through it later today. Thanks! –  cormullion Jan 31 '12 at 11:19

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.