Lab 3 Shadow Giuseppe Maggiore
Adding Shadows to the Scene
First we need to declare a very useful helper object that will allow us to draw textures to the screen without creating a quad vertex buffer // A sprite batch is an XNA helper for drawing textures on the screen; it // is very useful to visualize intermediate computations and implementing // simple viewports. SpriteBatch sprite_batch;
The sprite batch is initialized by passing it the graphics device // We initialize the sprite batch sprite_batch = new SpriteBatch(GraphicsDevice);
We need a place where we can store the rays we will cast from the light; since all the rays are parallel to the light direction, we just store their length in a texture // We need a render target, that is a texture that can be used as a destination for // a rendering operation. This way we can draw the scene from the POV of the light // and store the depth of each direction starting from the light. RenderTarget2D shadow_map; // A render target needs a depth-stencil buffer of the same size. Since our render // target has a different size than the screen, we cannot use the default depth // buffer. DepthStencilBuffer shadow_depth;
We can create the shadow map and its associated depth buffer by calling their constructors and specifying the desired size and format // We create a 1024x1024 render target and depth buffer. The two must have the same bit // depth: single (a float32) is 4 bytes, and Depth24Stencil8 is 4 bytes too int shadow_size = 1024; shadow_map = new RenderTarget2D(GraphicsDevice, shadow_size, shadow_size, 1, SurfaceFormat.Single); shadow_depth = new DepthStencilBuffer(GraphicsDevice, shadow_size, shadow_size, DepthFormat.Depth24Stencil8);
When creating the shadow map, we first save a reference to the previous depth buffer, and then we set our depth buffer and the shadow map as a render target: all subsequent rendering operations will write the shadow map pixels // We store the previous depth buffer var depth = GraphicsDevice.DepthStencilBuffer; // We set our render target and our depth buffer GraphicsDevice.DepthStencilBuffer = shadow_depth; GraphicsDevice.SetRenderTarget(0, shadow_map);
We create view and projection matrices for drawing the scene from the point of view of the light the view matrix is relatively straightforward to compute: Matrix.CreateLookAt will suffice the projection matrix should be treated with more care: it should fit the scene as tightly as possible; if the projection covers too much (zmin that is too small or zmax too big) then huge precision errors will occur
To draw the scene, we start by restoring the original depth buffer and render target (otherwise nothing will appear on the screen since rendering will fill the shadow map) // We restore the initial depth buffer and the default render target. GraphicsDevice.DepthStencilBuffer = depth; GraphicsDevice.SetRenderTarget(0, null);
We can also draw the shadow map itself to see what is going in it // We can draw the shadow map on top of the screen, for debugging // purposes. sprite_batch.begin(spriteblendmode.alphablend, SpriteSortMode.Immediate, SaveStateMode.SaveState); sprite_batch.draw(shadow_map.gettexture(), new Rectangle(200, 0, 200, 200), Color.White); sprite_batch.end();
The shaders parameters are various matrices (scene seen from the camera and from the light) // Regular world, view and projection matrices float4x4 World; float4x4 CameraView; float4x4 CameraProjection; // View and projection matrices that define the // scene seen from the POV of the light. float4x4 LightView; float4x4 LightProjection; float4 DiffuseColor;
Also, the shadow map as a texture is needed for accessing the depth data // Texture that stores the minimum z-distances // of various pixels from the light. texture ShadowMapTexture; // Sample to read values from ShadowMapTexture sampler shadow_map_sampler = sampler_state { Texture = <ShadowMapTexture>; MinFilter = LINEAR; MagFilter = LINEAR; MipFilter = LINEAR; AddressU = Clamp; AddressV = Clamp; };
We have two vertex shaders: one for outputting the depth of each pixel from the camera, and another for drawing with shadows; the two shaders outputs are: // Input vertex is just a position, so we don't need // to declare it as a struct. Output vertex for shadow // mapping contains the transformed position and the // original world position (which the pixel shader will // transform with the light view and projection). struct VS_OUTPUT { float4 Position : POSITION0; float4 vpos : TEXCOORD0; }; // Output vertex for shadow map construction: it // contains the transformed position and the depth of // the pixel. struct VS_SHADOW_OUTPUT { float4 Position : POSITION; float Depth : TEXCOORD0; };
A very useful function is the one which transforms a vertex by the light viewpoint: // Helper function that transforms a position by the // light viewpoint. float4 GetPositionFromLight(float4 position) { float4x4 wvp = mul(mul(world, LightView), LightProjection); return mul(position, wvp); }
One vertex shader simply outputs the position and the depth
The corresponding pixel shader simply outputs the depth: // When outputting the depth from the light, the // pixel shader returns a fake color which will // be ignored apart from the first component, // which will be stored in the single float // of our render target (the shadow map). float4 RenderShadowMapPS( VS_SHADOW_OUTPUT In ) : COLOR0 { return float4(in.depth.x,0,0,1); }
The vertex shader that outputs the position for computing the shadow is quite simple: // Vertex shader that outputs the transformed position // and the original position. VS_OUTPUT RenderShadowsVS(float4 position : POSITION0) { VS_OUTPUT Output; float4x4 wvp = mul(mul(world, CameraView), CameraProjection); Output.Position = mul(position, wvp); Output.vPos = position; return Output; }
The pixel shader that computes the shadow begins by transforming the original position by the light viewpoint
Then we read from the shadow map the depth of the shadow along the ray that goes from this vertex to the light
Now we compute our distance from the light and compare it with the shadow depth
Remember: two shaders require two techniques: technique ShadowRender { pass P0 { VertexShader = compile vs_2_0 RenderShadowsVS(); PixelShader= compile ps_2_0 RenderShadowsPS(); } } technique ShadowMapRender { pass P0 { VertexShader = compile vs_2_0 RenderShadowMapVS(); PixelShader= compile ps_2_0 RenderShadowMapPS(); } }
THAT S IT: GOOD LUCK!