Contents:
For reasoning behind these extensions,
see also my paper Shadow maps and projective texturing in X3D
(accepted for Web3D 2010 conference). PDF linked here has some absolutely minor
corrections (for projection* fields and fixed URLs)
compared to the conference version.
The slides
from the presentation are also available.
Specification below comes from this paper (section 4). Text below has some additional notes, mostly specific to our engine and implementation.
Note that the paper, and so portions of the text below, are Copyright 2010 by ACM, Inc. See the link for details, in general non-commercial use is fine, but commercial use usually requires asking ACM for permission. This is a necessary exception from my usual rules of publishing everything on GNU GPL.
|
|
|
|
|
|
|
|
One of the shadows algorithms implemented in our engine is shadow maps.
Shadow maps work completely orthogonal to shadow
volumes (see
shadow volumes docs), which means that you can freely mix
both shadow approaches within a single scene.
Shadow maps, described here, are usually more adviced: they are simpler to use
(in the simplest case, just add "shadows TRUE" to your light source,
and it just works with an abritrary 3D scene),
and have a better implementation (shadow maps from multiple light sources
cooperate perfectly thanks to the shaders pipeline).
Most important TODO about shadow maps: PointLight
sources do not cast shadow maps yet. (Easy to do, please report if you need it.)
Our VRML/X3D demo models contain many demos using shadow maps.
Download them and open with view3dscene files insde shadow_maps subdirectory.
See in particular the nice model inside shadow_maps/sunny_street/,
that was used for some screenshots visible on this page.
In the very simplest case, to make the light source just cast shadows
on everything, set the shadows field of the light source
to TRUE.
*Light {
... all normal *Light fields ...
SFBool [] shadows FALSE
}
This is equivalent to adding this light source to every shape's
receiveShadows field. Read on to know more details.
This is the simplest extension to enable shadows.
TODO: In the future, this field (shadows on light) and
receiveShadows field (see below) should be suitable for
other shadows implementations too
We plan to use it for shadow volumes in the future too
(removing old shadowVolumesMain extensions and such),
and maybe ray-tracer too. shadowCaster (see below) already works
for all our shadows implementations.
If you use X3D shader nodes, like ComposedShader and related nodes, be aware that your custom shaders
may be ignored. Browsers have to use internal shaders to produce nice
shading for shadow receivers. Use instead
our compositing shaders extensions for X3D, like Effect and related nodes to write shader code that can cooperate
with other effects (like shadow maps, and much more).
Or (less adviced)
use the lower-level nodes described below to activate shadow maps more manuallly.
Appearance {
... all normal Appearance fields ...
MFNode [] receiveShadows [] # [X3DLightNode] list
}
Each light present in the receiveShadows list will cast shadows on
the given shape. That is, contribution of the light source
will be scaled down if the light is occluded at a given fragment.
The whole light contribution is affected, including the ambient term.
We do not make any additional changes to the X3D lighting model.
The resulting fragment color is the sum of all the visible lights (visible
because they are not occluded, or because they don't cast shadows on this shape),
modified by the material emissive color and fog, following the X3D specification.
The following extensions make it possible to precisely setup and control shadow maps. Their use requires a basic knowledge of the shadow map approach, and they are necessarily closely tied to the shadow map workflow. On the other hand, they allow the author to define custom shaders for the scene and control every important detail of the shadow mapping process.
These lower-level extensions give a complete and flexible system to
control the shadow maps, making the Appearance.receiveShadows
and X3DLightNode.shadows features only a shortcuts
for the usual setup.
We make a shadow map texture by the GeneratedShadowMap node,
and project it on the shadow receiver by
ProjectedTextureCoordinate.
An example X3D code (in classic encoding) for a shadow map setup:
DEF MySpot SpotLight {
location 0 0 10
direction 0 0 -1
projectionNear 1
projectionFar 20
}
Shape {
appearance Appearance {
material Material { }
texture GeneratedShadowMap { light USE MySpot update "ALWAYS" }
}
geometry IndexedFaceSet {
texCoord ProjectedTextureCoordinate {
projector USE MySpot
}
# ... other IndexedFaceSet fields
}
}
Note that the shadow texture will be applied in a very trivial way,
just to generate intensity values (0 - in shadow, 1 - not in shadow).
If you want to have some nice shading, you should use GLSL shader
to sample the depth texture (like shadow2DProj(shadowMap, gl_TexCoord[0]).r)
and do there any shading you want. Using shaders is generally
the way to make shadow mapping both beautiful and in one pass (read: fast),
and it's the way of the future anyway. You can start from a trivial
fragment shader in our examples:
shadow_map.fs.
Note that view3dscene's menu items View -> Shadow Maps -> ... do not affect the lower-level shadow maps. Essentially, when using the lower-level nodes, you directly control the shaders (and everything else) yourself.
Remember: If you don't want to write your own GLSL shader,
and you need nice shadows, then these lower-level extensions are not for you.
Instead, you could use easy receiveShadows:
DEF MySpot SpotLight {
location 0 0 10
direction 0 0 -1
}
Shape {
appearance Appearance {
material Material { }
receiveShadows MySpot
}
geometry IndexedFaceSet { .... }
}
Using the receiveShadows approach is simpler,
also the browser will use nice internal GLSL shaders automatically.
The motivation behind the extensions in this section is that we want to use light sources as cameras. This means that lights need additional parameters to specify projection details.
To every X3D light node (DirectionalLight, SpotLight,
PointLight) we add new fields:
*Light {
... all normal *Light fields ...
SFFloat [in,out] projectionNear 0 # must be >= 0
SFFloat [in,out] projectionFar 0 # must be > projectionNear, or = 0
SFVec3f [in,out] up 0 0 0
SFNode [] defaultShadowMap NULL # [GeneratedShadowMap]
}
The fields projectionNear and projectionFar specify the near
and far values for the projection used when rendering to the shadow map texture.
These are distances from the light position, along the light direction.
You should always try to make projectionNear as large as possible
and projectionFar as small as possible,
this will make depth precision better (keeping projectionNear large
is more important for this). At the same time, your projection range
must include all your shadow casters.
The field up is the "up" vector of the light camera when capturing
the shadow map. This is used only with non-point lights
(DirectionalLight and SpotLight).
Although we know the direction of the light source,
but for shadow mapping we also need to know the "up" vector to have camera
parameters fully determined.
You usually don't need to provide the "up" vector value in the file.
We intelligently guess (or fix your provided value) to be always Ok.
The "up" value is processed like this:
These properties are specified at the light node, because both shadow map generation and texture coordinate calculation must know them, and use the same values (otherwise results would not be of much use).
The field defaultShadowMap is meaningful only when some
shape uses the receiveShadows feature. This will be described
in the later section.
DirectionalLight gets additional fields to specify orthogonal
projection rectangle (projection XY sizes) and location for
the light camera. Although directional light is conceptually at infinity
and doesn't have a location, but for making a texture projection
we actually need to define the light's location.
DirectionalLight {
... all normal *Light fields ...
SFVec4f [in,out] projectionRectangle 0 0 0 0 #
# left, bottom, right, top (order like for OrthoViewpoint.fieldOfView).
# Must be left < right and bottom < top, or all zero
SFVec3f [in,out] projectionLocation 0 0 0 # affected by node's transformation
}
When projectionNear, projectionFar, up,
projectionRectangle have (default) zero values, then some sensible
values are automatically calculated for them by the browser.
projectionLocation will also be automaticaly adjusted,
if and only if projectionRectangle is zero.
This will work perfectly for shadow receivers marked by the
receiveShadows field.
This feature was not "invented" at the time of submitting the
PDF paper to the Web3D 2010 conference,
so it's not documented there.
|
|
|
|
SpotLight gets additional field to explicitly specify a perspective projection angle.
SpotLight {
... all normal *Light fields ...
SFFloat [in,out] projectionAngle 0
}
Leaving projectionAngle at the default zero value is equivalent
to setting projectionAngle to 2 * cutOffAngle.
This is usually exactly what is needed.
Note that the projectionAngle is
the vertical and horizontal field of view for the square texture,
while cutOffAngle is the angle of the half of the cone
(that's the reasoning for *2 multiplier).
Using 2 * cutOffAngle as projectionAngle
makes the perceived light cone fit nicely inside the projected
texture rectangle. It also means that some texture space is essentially
wasted — we cannot perfectly fit a rectangular texture into a circle shape.
Images on the right show how a light cone fits within the projected texture.
Now that we can treat lights as cameras, we want to render shadow maps from the light sources. The rendered image is stored as a texture, represented by a new node:
GeneratedShadowMap : X3DTextureNode {
SFNode [in,out] metadata NULL # [X3DMetadataObject]
SFString [in,out] update "NONE" # ["NONE"|"NEXT_FRAME_ONLY"|"ALWAYS"]
SFInt32 [] size 128
SFNode [] light NULL # any light node
SFFloat [in,out] scale 4.0
SFFloat [in,out] bias 4.0
SFString [] compareMode "COMPARE_R_LEQUAL" # ["COMPARE_R_LEQUAL" | "COMPARE_R_GEQUAL" | "NONE"]
}
|
|
|
|
The update field determines how often the shadow map should be
regenerated. It is analogous to the update field in the standard
GeneratedCubeMapTexture node.
"NONE" means that the texture is not generated.
It is the default value (because it's the most conservative,
so it's the safest value).
"ALWAYS" means that the shadow map must be always accurate.
Generally, it needs to be generated every time shadow caster's geometry
noticeably changes.
The simplest implementation may just render the shadow map at every frame.
"NEXT_FRAME_ONLY" says to update the shadow map
at the next frame, and afterwards change the value back to "NONE".
This gives the author an explicit control over when the texture is
regenerated, for example by sending "NEXT_FRAME_ONLY"
values by a Script node.
The field size gives the size of the (square) shadow map texture
in pixels.
The field light specifies the light node from which to generate the map.
Ideally, implementation should support all three X3D light source types.
NULL will prevent the texture from generating.
It's usually comfortable to "USE" here some existing light node,
instead of defining a new one.
TODO: for now, we do not handle shadow maps from PointLight
nodes.
Note that the light node instanced inside the GeneratedShadowMap.light
or ProjectedTextureCoordinate.projector fields isn't
considered a normal light, that is it doesn't shine anywhere.
It should be defined elsewhere in the scene to actually
act like a normal light. Moreover, it should not be
instanced many times (outside of GeneratedShadowMap.light
and ProjectedTextureCoordinate.projector), as then it's
unspecified from which view we will generate the shadow map.
|
|
|
|
|
|
|
|
Fields scale and bias are used
to offset the scene rendered to the shadow map.
This avoids the precision problems inherent in the shadow maps comparison.
In short, increase them if you see
a strange noise appearing on the shadow casters (but don't increase them too much,
or the shadows will move back).
You may increase the bias a little more
carelessly (it is multiplied by a constant implementation-dependent offset,
that is usually something very small).
Increasing the scale has to be done a little more carefully
(it's effect depends on the polygon slope).
Images on the right show the effects of various
scale and bias values.
You can adjust the bias, scale
and size interactively in
view3dscene.
Using the Edit->Lights Editor feature, you can configure
the defaultShadowMap parameters for a given light,
and immediately see the results.
For an OpenGL implementation
that offsets the geometry rendered into the shadow map,
scale and bias are an obvious parameters (in this order)
for the glPolygonOffset call.
Other implementations are free to ignore these parameters, or derive
from them values for their offset methods.
Field compareMode allows to additionally do depth comparison
on the texture. For texture coordinate (s, t, r, q),
compare mode allows to compare r/q with texture(s/q, t/q).
Typically combined with the projective texture mapping, this is the moment when we
actually decide which screen pixel is in the shadow and which is not.
Default value COMPARE_R_LEQUAL is the most useful
value for standard shadow mapping, it generates 1 (true) when
r/q <= texture(s/q, t/q), and 0 (false) otherwise. Recall from
the shadow maps algorithm that, theoretically, assuming infinite shadow map
resolution and such, r/q should never be smaller than the texture value
(it can only be equal or larger).
When the compareMode is set to NONE,
the comparison is not done, and depth texture values are returned directly.
This is very useful to visualize shadow maps, for debug and demonstration
purposes — you can view the texture as a normal grayscale (luminance) texture.
In particular, problems with tweaking the projectionNear and
projectionFar values become easily solvable when you can actually
see how the texture contents look.
For OpenGL implementations, the most natural format for a shadow map texture
is the GL_DEPTH_COMPONENT (see ARB_depth_texture).
This makes it ideal for typical shadow map operations.
For GLSL shader, this is best used with sampler2DShadow
(for spot and directional lights) and
samplerCubeShadow (for point lights).
Unless the compareMode is NONE, in which case
you should treat them like a normal grayscale textures
and use the sampler2D or the samplerCube types.
Variance Shadow Maps notes:
If you turn on Variance Shadow Maps (e.g. by view3dscene menu View -> Shadow Maps -> Variance Shadow Maps), then
the generated textures are a little different.
If you used the simple "receiveShadows" field, everything is taken
care of for you. But if you use lower-level nodes and write your own
shaders, you must understand the differences:
for VSM, shadow maps are treated always as sampler2D, with the first
two components being E(depth) and E(depth^2).
See the paper about Variance Shadow Maps,
and see example GLSL shader code to handle them.
We propose a new ProjectedTextureCoordinate node:
ProjectedTextureCoordinate : X3DTextureCoordinateNode {
SFNode [in,out] projector NULL # [SpotLight, DirectionalLight, X3DViewpointNode]
}
This node generates texture coordinates, much like the standard
TextureCoordinateGenerator node.
More precisely, a texture coordinate (s, t, r, q) will be generated for a fragment
that corresponds to the shadow map pixel on the position (s/q, t/q),
with r/q being the depth (distance from the light source or the viewpoint,
expressed in the same way as depth buffer values are stored in the shadow map).
In other words, the generated texture coordinates will contain the actual
3D geometry positions, but expressed in the projector's frustum coordinate system.
This cooperates closely with the GeneratedShadowMap.compareMode = COMPARE_R_LEQUAL behavior,
see the previous subsection.
This can be used in all situations when the light or the viewpoint act like
a projector for a 2D texture. For shadow maps, projector should be
a light source.
When a perspective Viewpoint is used as the projector,
we need an additional rule. That's because the viewpoint doesn't explicitly
determine the horizontal and vertical angles of view, so it doesn't precisely
define a projection. We resolve it as follows: when the viewpoint
that is not currently bound is used as a projector,
we use Viewpoint.fieldOfView for both the horizontal and vertical
view angles. When the currently bound viewpoint is used,
we follow the standard Viewpoint specification for calculating
view angles based on the Viewpoint.fieldOfView and the window sizes.
(TODO: our current implementation doesn't treat currently bound
viewpoint this way.)
We feel that this is the most useful behavior for scene authors.
When the geometry uses a user-specified vertex shader, the implementation
should calculate correct texture coordinates on the CPU.
This way shader authors still benefit from the projective texturing extension.
If the shader author wants to implement projective texturing inside the shader,
he is of course free to do so, there's no point in using
ProjectedTextureCoordinate at all then.
Note that this is not suitable for point lights. Point lights do not have a direction, and their shadow maps can no longer be single 2D textures. Instead, they must use six 2D maps. For point lights, it's expected that the shader code will have to do the appropriate texture coordinate calculation: a direction to the point light (to sample the shadow map cube) and a distance to it (to compare with the depth read from the texture).
Deprecated: In older engine versions, instead of this node
you had to use TextureCoordinateGenerator.mode = "PROJECTION"
and TextureCoordinateGenerator.projectedLight. This is still
handled (for compatibility), but should not be used in new models.
Placing a light on the receiveShadows list is equivalent to
adding the appropriate GeneratedShadowMap to the shape's textures,
and adding the appropriate ProjectedTextureCoordinate to the geometry
texCoord field. Also, receiveShadows makes
the right shading (for example by shaders) automatically used.
In fact, the receiveShadows feature may be
implemented by a simple transformation of the X3D node graph.
Since the receiveShadows and defaultShadowMap
fields are not exposed (they do not have accompanying
input and output events) it's enough to perform such transformation
once after loading the scene.
Note that the texture nodes of the shadow receivers
may have to be internally changed to multi-texture nodes during this operation.
An author may also optionally specify
a GeneratedShadowMap node inside the light's
defaultShadowMap field. See the lights extensions section
for defaultShadowMap declaration. Note that when
GeneratedShadowMap
is placed in a X3DLightNode.defaultShadowMap field,
then the GeneratedShadowMap.light value is ignored (we always
use the light containing defaultShadowMap declaration then).
Leaving the defaultShadowMap as NULL means that an
implicit shadow map with default browser settings should be generated
for this light. This must behave like update was set to
ALWAYS.
In effect, to enable the shadows the author must merely
specify which shapes receive the shadows (and from which lights)
by the Appearance.receiveShadows field. This way the author
doesn't have to deal with lower-level tasks:
GeneratedShadowMap nodes.ProjectedTextureCoordinate nodes.Appearance.shadowCaster)By default, every Shape in the scene casts a shadow.
This is the most common setup for shadows.
However it's sometimes useful to explicitly
disable shadow casting (blocking of the light) for some tricky shapes.
For example, this is usually desired for shapes that visualize
the light source position.
For this purpose we extend the Appearance node:
Appearance {
... all Appearance fields ...
SFBool [in,out] shadowCaster TRUE
}
Note that if you disable shadow casting on your shadow receivers
(that is, you make all the objects only casting or only receiving the shadows,
but not both) then you avoid some offset problems with shadow maps. The bias
and scale parameters of the GeneratedShadowMap
become less crucial then.
This is honoured by all our shadow implementations: shadow volumes, shadow maps (that is, both methods for dynamic shadows in OpenGL) and also by our ray-tracers.
Note that no shadow algorithm can deal with transparency by alpha-blending. So these shapes are not treated as shadow casters, by any shadow algorithm right now.
Copyright Michalis Kamburelis. Some images copyright Cat-astrophe Games and Paweł Wojciechowicz. You can redistribute this on terms of the GNU General Public License.
We use cookies. Like every other frickin' website on the Internet. Blink twice if you understand.