Objects obscure their own shadows when viewed from the position of the light source. Consider the following 3D scene:
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:
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]
Rotate the Graphics3D
to the desired viewpoint and click the button to get this:
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]
Shadow[]
in the packageGraphics`Graphics3D`
. You might want to look into it. $\endgroup$