44
$\begingroup$

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?

$\endgroup$
3
  • $\begingroup$ Older versions of Mathematica had the routine Shadow[] in the package Graphics`Graphics3D`. You might want to look into it. $\endgroup$ Commented Jan 29, 2012 at 12:01
  • $\begingroup$ Similar question: stackoverflow.com/q/6955692/615464 $\endgroup$ Commented Jan 29, 2012 at 21:32
  • $\begingroup$ @SjoerdC.deVries good find. I didn't check the old site... $\endgroup$
    – cormullion
    Commented Jan 29, 2012 at 21:35

4 Answers 4

26
$\begingroup$

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, [email protected], 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

$\endgroup$
1
  • $\begingroup$ Excellent - just what I needed! Thanks. $\endgroup$
    – cormullion
    Commented Jan 30, 2012 at 10:11
46
$\begingroup$

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

$\endgroup$
4
  • 2
    $\begingroup$ 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}; $\endgroup$
    – Michael E2
    Commented Jan 11, 2014 at 19:59
  • 1
    $\begingroup$ @MichaelE2, many thanks for the improved initialisation code - I just couldn't figure out how to get it right. $\endgroup$ Commented Jan 12, 2014 at 11:14
  • $\begingroup$ The cow-on-a-hill is very impressive. $\endgroup$ Commented Jan 12, 2014 at 13:06
  • 1
    $\begingroup$ Great, this is the way to go for smooth looking shadows on arbitrary polygons (I didn't see this until now). $\endgroup$
    – Jens
    Commented Feb 7, 2015 at 4:21
25
$\begingroup$

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.

$\endgroup$
4
  • $\begingroup$ Very good answer! $\endgroup$
    – magma
    Commented Jan 29, 2012 at 15:48
  • $\begingroup$ I'll just note that this is more or less what Shadow[] in older versions of Mathematica does. $\endgroup$ Commented Jan 31, 2012 at 1:29
  • $\begingroup$ Wish I could up-vote twice! @J.M. what is Shadow[]? Couldn't find it in the documentation. $\endgroup$
    – Dror
    Commented Feb 15, 2013 at 16:06
  • $\begingroup$ @Dror, It's one of the routines in the old Graphics`Graphics` package in earlier versions of Mathematica... $\endgroup$ Commented Mar 23, 2013 at 12:29
21
$\begingroup$

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

$\endgroup$
1
  • $\begingroup$ That's an impressive piece of code. I'm going to work through it later today. Thanks! $\endgroup$
    – cormullion
    Commented Jan 31, 2012 at 11:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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