11 ottobre 2012

XNA - Advanced Zima 2D Shadows - MANUAL

XNA - Advanced Zima 2D SHADOW - MANUAL

>>Download the source code here <<















Table of contents
  1. Preliminary notes 
  2. Components Details
  3. Shadows generation process
  4. Using the shadow system
  5. Performance tuning and hints
  6. Customization options
  7. Changes from Catalim Zima's version

------------------------------------------------------------------------------------------------

1. PRELIMINARY NOTES

File Version

ATTENTION: This manual is written upon the latest version of the system:
  • Assembly: 2.0.0.3
  • File name: shadows2d_reach_recode_part2_c.zip
Some parameters and objects name have been changed since the first release, so I recommend you to download the latest version here.


Catalim Zima credits

It is very important to me to underline that this work is made upon the great sample by
Catalim Zima.

His idea behind this fast and pixel perfect shadow system is simply genial.

I warmly recommend you to read this post by Catalim Zima that explains how it works.

Although the understanding of the core mechanics is not necessary to use this version, I would recommend at least a brief read.

Bugs

I didn't test this solution on XBOX, neither tried it with different resolutions. I think some problems may come outIf you find one, just tell me here.

------------------------------------------------------------------------------------------------

2. COMPONENTS DETAILS






The entire shadowing system is formed by the following components:
  • LightSource
  • ShadowCasterMap
  • ShadowMapResolver
  • LightsFX
  • QuadRenderComponent (old Ziggyware staff, still good)
  • ShadowMapReductionStep
  • HLSL effects

LightSource




This class do nothing by itself, it contains all the information needed to define your lights. LightSources are processed by the ShadowMapResolvers.

Members:
  • Int - Radius - Determines the size of your light.
  • Vector2 - Size - Just a "shortcut" to have a Radius * 2.
  • Vector2 - Position - Relative to the screen. Vector2(0, 0) is the upper left corner.
  • Color - Color - Stores the color of the light.
  • RenderTarget2D - PrintedLight - It's the texture that contains the printed light after being processed by a ShadowMapResolver.
  • Void - Draw() -  Method to help printing the light with the correct size and position.
  • Enum - Quality - Read below.
Quality - Enum LightAreaQuality. It determines the real size of the PrintedLight texture:
  • VeryLow ->  PrintedLight will be 0.1 the Size of the light.
  • Low  ->  PrintedLight will be 0.25 the Size of the light.
  • Middle  ->  PrintedLight will be 0.5 the Size of the light.
  • High  ->  PrintedLight will be 0.75 the Size of the light.
  • VeryHigh  ->  PrintedLight will be the same Size of the light.

The real size of PrintedLight (expressed in pixel) is: Radius * 2 * Quality

If you create a light with Radius = 256 and Quality = Low, you'll have PrintedLight sized 64x64 ( Radius * 2 * Quality ).
That means the visual quality of your light will the one of a texture sized 64x64 stretched to 512x512.


ShadowCasterMap


It creates a shadow casters map.
A shadow caster map is a Black and White map of the elements that will stop the light.
(white = light passes, black = stop)

ShadowCastersMaps always cover the entire screen.

Members:
  • RenderTarget2D  - Map - Texture that contains the black and white map.
  • Void - StartGeneratingShadowCasterMap() - This method start the creation of the Map.
  • Void - AddShadowCaster() - This method adds a sprite to the Map.
  • Void - EndGeneratingShadowCasterMap()  - This method stops the creation of the Map.
  • Enum - PrecisionSettings - See below
PrecisionSettings determines the effective size of the texture containing the shadow caster map (Map):
  • VeryLow ->  Map will be 0.1 the size of the screen.
  • Low  ->   Map will be 0.25 the size of the screen.
  • Middle  ->   Map will be 0.5 the size of the screen (suggested setting).
  • High  ->   Map will be 0.75 the size of the screen.
  • VeryHigh  ->   Map will be the same size of the screen.

ShadowMapResolver




This class is the heart of the system.
The ShadowMapResolver generates a light map by combining a LightSources and a ShadowCasterMap. Then it stores the map in LightSource.PrintedLight.

The process of creating the light map is quite complex and it's almost identical to the Catalim Zima's original one, so if you want to dig in it just read Zima's post here. At the end of this manual I'll list the main changes from Zima's version.

Members:
  • LightsFX - LightsFX - This class contains all the HLSL effects used by the ShadowMapResolver. 
  • Void - ResolveShadows() - This method does the magic. It takes a LightSource and a ShadowCasterMap and print the light map in LightSource.PrintedLight.

LightFX




This class is a container of effects used by the ShadowMapResolvers (read above).
It is also used to print the lights over the scene with 2xMultiplicative blend.


Members:
  • Effect - ResolveShadowsEffect - This effect is used by ShadowMapResolver to resolve the shadows and produce the light map.
  • Effect -  ReductionEffect - This effect is used by the ShadowMapResolvers to digest the ShadowCasterMaps.
  • Effect -  LightBlender2x - This effect is used to print lights and shadows over a texture.
  • void PrintLightsOverTexture() - This method make easier to print lights over a texture.

Other components

QuadRenderComponent 
An old component taken from Ziggyware (the solution carries a modified version that doesn't inherit from  DrawableGameComponent).
This component is good to print stuff without messing up with the SpriteBatch.

ShadowMapReductionStep
This small class is used to create the "reduction plan" of the ShadowMapResolver. More info in the ChangeLog at the end of this document.
To make it simple: this class help storing the steps to reduce a ShadowCasterMap to a 2 pixels wide texture.

HLSL effects
This system use these .fx files:
  • resolveShadowsEffect.fx - This effect contains all the techniques used to resolve the lights.
  • reductionEffect.fx - This effect contains the techniques needed to reduce the shadow map to a 2 pixel narrow version.
  • 2xMultiBlend.fx - This effect does 2xMultiplicative blend.

------------------------------------------------------------------------------------------------

3. SHADOWS GENERATION PROCESS

Now it's time to see how this system generates the lights.


The whole process goes through the following steps:
  1. Update the ShadowCasterMap.
  2. Process the lights.
  3. Print the lights together.
  4. Print the whole scene.
  5. Print the lights over the scene.
  6. Print the sprites of the shadow caster back in the scene.

1) Update the ShadowCasterMap

The first step is update the ShadowCasterMap, that will be used to determines which parts of the screen will cast the shadows.


The process is quite simple:
  1. Call StartGeneratingCasterMap() -> It starts the creation of the Map.
  2. Call AddShadowCaster() for each texture you want to use as shadow caster -> It prints the texture in full black on the shadow caster map
  3. Call EndGeneratingCasterMap() -> It stops the creation of the Map.
Once finished we have the ShadowCasterMap ready to be used.


In the solution this step is:



2) Process Lights

This step must be done for each light you want to print.


The picture below describe what happens:
  • LightFX is passed to the ShadowResolver only when it is instanced. The LightFX contains the set of HLSL effects that the ShadowResolver will use to make the magic.
  • The method ShadowMapResolver.ResolveShadows() takes a ShadowCasterMap and a LightSource.
  • The ShadowMapResolver casts shadows and lights on the portion of the ShadowCasterMap covered by the LightSource area.
  • The light map is then printed in LightSource.PrintedLight.


In the solution this step is:


3) Print Lights together

In this step all the lights must be dumped into a single texture.


In the solution this step is:



4) Print the whole scene

The scene to be lit must be printed into a single texture:
  1. Create a RenderTarget2D that will store the scene.
  2. Before drawing the scene call the method graphicsDevice.SetRenderTarget(yourRenderTargetToStoreTheScne).
  3. Draw the scene as normal.
Do not forget to reset the render target by calling graphicsDevice.SetRenderTarget(null) or you'll keep drawing things on it and nothing will be displayed on screen.

In the solution this step is:



5) Print the lights over the scene

This step is quite easy: the lights are printed over the scene previously drawn by calling the method LightFX.PrintLightsOverTexture().
 


In the solution this step is:



6) Print the sprites of the shadow casters back in the scene

The shadow casters have been previously printed on the ShadowCasterMap, now it's time to print them back  in the scene.


In the solution this step is:



4. USING THE SHADOW SYSTEM

LightSources through ShadowMapResolvers

This is a very important aspect of the shadow system, probably the most important one: the relation between LightSources and ShadowMapResolvers.

Before to describe that, let's a have look to the light map creation pipeline:


  1. A LightSource and a ShadowCasterMap are passed to the ShadowMapResolver.
  2. The LightSource carries the texture (LightSource.PrintedLight) that will store the light map created by the ShadowMapResolver.
  3. The ShadowMapResolver process the light map using its internal RenderTargets.
  4. The result is stored in LightSource.PrintedLight, leaving the ShadowMapResolver free to process other LightSources.

Both the classes need a radius to be defined:
  • The radius of the LightSource sets the size of the texture (LightSource.PrintedLight) that will store the light map processed by the ShadowMapResolver.
  • The radius of ShadowMapResolver sets the size of the RenderTargets used to process the lights.
The difference between those radius is a key factor to consider when creating your set of LightSources and ShadowMapResolvers.

If you create a LightSource with higher radius than the ShadowMapResolver that will process it, you'll have a loss of quality because the ShadowMapResolver processes the  LightSource with its RenderTargets that have a lower resolution than the final LightSource.PrintedLight texture.

If you create a LightSource with lower radius than the ShadowMapResolver that will process it, you'll have a waste of computational power because the ShadowMapResolver processes the LightSource  with its RenderTargets that have an higher resolution than the final LightSource texture.

Now, you may think that the best solution is to pair LightSources and ShadowMapResolvers with same radius. 
Well... No. :)
The reason is in the section "Performance tuning and hints".


ShadowMapResolver - PostEffects

A cool thinks about ShadowMapResolver is the possibility to render lights with many different render options. When you call the method ResolveShadows() you have to decide what PostEffect to use:

PostEffecte are:
  • None
  • Only_BlurLow
  • Only_BlurMid
  • Only_BlurHigh
  • LinearAttenuation
  • LinearAttenuation_BlurLow
  • LinearAttenuation_BlurMid
  • LinearAttenuation_BlurHigh
  • CurveAttenuation
  • CurveAttenuation_BlurLow
  • CurveAttenuation_BlurMid
  • CurveAttenuation_BlurHigh

PostEffect.None

No effects are applied, the light map is square shaped.

This is the fastest PostEffect and it's a good option of you want to treat the shadow with additional effect made by you.
















PostEffect.Only_BlurLow

A soft blur is applied after the "PostEffetct.None" effect.


PostEffect.Only_BlurMid

A moderate blur is applied after the "PostEffetct.None" effect.


PostEffect.Only_BlurHigh

An heavy blur is applied after the "PostEffetct.None" effect.

PostEffect.LinearAttenuation (and LinearAttenuation_BlurLow, LinearAttenuation_BlurMid, LinearAttenuation_BlurHigh variants)

This effect applies a linear attenuation. The light becomes linearly darker as it get far from the center.

This effect create soft and natural looking lights, making it the best choice for environment lightings.

Three levels of blur are available.


















PostEffect.CurveAttenuation (and CurveAttenuation_BlurLow, CurveAttenuation_BlurMid, CurveAttenuation_BlurHigh variants)

This effect applies a curve attenuation. The light is very bright in the first half of the radius. Compared to the linear attenuation, this effect is brighter but less natural looking.

This effect is useful when creating lights with important gameplay meanings, like player's light.

Three levels of blur are available.

















Omni lights

This is the natural way this shadow system works. Omni lights spread all around in any direction.


Spot lights

Spot lights have direction and radius. Although this system has not natural support for this kind of lights, it is quite easy to workaround this limit.


To make a spot light you need an additional step in HLSL, just "cut" the light map after being processed by a ShadowResolver (step 2 in the previous section of the manual) with the shape you want.

My suggestion is to create an image in grayscale of the shape of the flashlight and then use it to cut the light map (you know, using it as a mask).


Fog of war

You can use this code to limit player's sight. Just make a white LightSource as big as player's sight limit. Then process is with PostEffect.None or PostEffect.CircleCut.


Ta da! You have a light map that actually is a "what you see" map.

5. PERFORMANCE  TUNING AND HINTS

This system provides many options to best tune your lights according to your needs. Every component involved in the shadow generation process have settings to help you in in this process.
Let's check them out:

LightSource
  • Radius -Biggest the light, hardest the computation. Although it is obvious, it is less obvious that the size of the light and the computation power required are exponentially related because the number of pixels in your light is given by (radius * 2) * (radius * 2) * Quality (discussed later).
    A light with radius 64 will have 16384 pixels to compute.
    A light with radius 128 will have 65536 pixels to compute (4 times the previous light).
    A light with radius 256 will have 262144 pixels to compute  (16 times the first light!!).
    If you need big lights, you should lower the quality of the light using the next setting described.
  • Quality -This sets the real size of the texture of the light.
    If you set the quality to Middle, the texture size will be (radius * 2 * 0.5). If you set the quality to high it will be (radius * 2 * 1).
    This is a powerful settings, especially when creating big lights.
    Just consider that if you set the quality to Middle you reduce the number of pixel to compute by four times!
    A light with radius 256 and Quality set to Very High will have a resulting texture of 512x512, with 262144 pixels to compute.
    A light with radius 256 and Quality Middle will have a resulting texture of 256x256, with 65536   pixels to compute. Four times less!
    Reducing the quality don't affect the radius of the light (the light won't become smaller).
    Check the section "Component Details" for more info.
Blur
  • Applying blur to the lights is almost always good.
  • The blur will make low quality lights look great.
  • It is pointless to set the Quality to VeryHigh and then blur the light.
  • Higher the blur, lower the Quality of the light you can set.
Coupling LightSource and ShadowMapResolver 

When you instance a new ShadowMapResolver you must set the resolverRadius which determines the size of its RenderTargets used to process the LightSources.

The size of the resolverRadius is a very important aspect to consider, it will directly affect the quality of your lights and the performance of your code.
  • Small ShadowMapResolver vs HighQuality LightSource -If you process high quality LightSources with a small radius ShadowMapResolvers you'll have graphically appealing lights afflicted by precision problems and shaking effect while moving. 
  • Big ShadowMapResolver vs LowQuality LightSource -If you process low quality LightSources with big radius ShadowMapResolvers you'll have bad looking lights. The low resolution of the light will waste the high precision and details given by the big size of the ShadowMapResolver.
Recommended settings
  • Recommended settings 1: Best common purpose settings -LightSource.Quality = Middle  /  ShadowMapResolver.Radius = half the LightSource radius. This setting provides both good precision and good graphics without demanding too much computational power.
  • Recommended settings 2: Very fast lights -LightSource.Quality = Low; ShadowMapResolver.Radius = one third the LightSource radius.
  • Recommended settings 3: Many lights and few big shadow caster sprites in the scene - If in your scene shadows are cast by big sized objects, you can use ShadowMapResolvers with smaller radius (a third or a quarter the radius of your lights). 
  • Recommended settings 4: Many lights and many small shadow caster sprites in the scene -   If in your scene shadows are cast by small sized objects, you need  ShadowMapResolver with radius that is AT LEAST half the radius of your lights.
Particular settings
  • Lights in deep space -LightSource.Quality = VeryHigh / ShadowMapResolver.Radius = half the LightSource radius. No Blur. In deep space lights are very sharp.
  • Retro-style fog of war -LightSource.Quality = VeryLow / ShadowMapResolver.Radius = half the LightSource radius. No Blur.
Settings to avoid
  • Ultra quality -LightSource.Quality = VeryHigh / ShadowMapResolver.Radius = same radius of the LightSource. You don't need so much quality, unless you have very specific artistic needs.
  • Small ShadowMapResolver and HighQuality LightSource -This setting gives sharp looking shadows that animate bad while moving (shaking and stuttering).
  • Big ShadowMapResolver and LowQuality LightSource -This setting gives very precise shadows that animates smoothly while moving but printed with grainy bad looking graphics.

ShadowCasterMap
  • The quality setting determines the size of the texture that stores the map of the shadow casters:
    • Very Low: 10% the size of the screen
    • Low: 25% the size of the screen
    • Middle: 50% the size of the screen (recommended setting)
    • High: 75% the size of the screen
    • Very High: 100%the size of the screen
  • With resolutions lower than 1024x768 the quality setting will make no difference in terms of performances.
  • The Quality setting becomes important with resolutions higher than 1280x1024.
    The reason for that is the ShadowCasterMap stores the map of the shadow casters in a single texture that takes a photo of the entire screen. Low resolutions means a small texture. High resolution means a big one hardest to compute.
  • Middle quality is generally the best setting.
  • Increase the Quality if you need more precision when computing shadows.
  • Decrease the Quality if you are experience performance issues and you already tried tweaking LightSources quality and ShadowMapResolvers radius.
6. CUSTOMIZATION OPTIONS

LightFX
  • This class stores the .FX file used to compute shadows.
  • The LightFX also stores the .FX file to print the lights over the scene.
  • You can create your .FX files to create new shadow computation behavior and pass them to new instances of the LightFX class.

PostEffects
To add new PostEffects:
  1. Open the .FX file "resolveShadowsEffect.fx".
  2. Copy one of the existing techniques that concur in the post effects (such as BlurVerticallyLowNoAttenuation).
  3. Edit your one technique.
  4. Add a new entry to the enum PostEffect.
  5. Open ShadowMapResolver.cs and go to the method ResolveShadows()
  6. Add a Case for your PostEffect in "switch (postEffect)"

ShadowMapResolver
  • You can create ShadowMapResolvers with custom behaviors by instancing them passing your custom LightFX.

7. CHANGES FROM CATALIM ZIMA'S VERSION


Customized BlendState

Catalim Zima version

  • A customized BlandState object is used to do multiplicative blending (to print lights over the scene).


FunHazard version

  • The multiplicative blend is achieved via HLSL. This make the system Reach Profile compatible.


LightSource


Catalim Zima version

  • LightSource are named LightArea.
  • Each light stored its copy of the shadow casters map.


FunHazard version

  • LightArea is renamed LightSource.
  • Added Color and quality settings.
  • Lights no more store a copy of the shadow casters map.


ShadowMapResolver

Catalim Zima version

  • The reduction of the shadow caster map is done halving it until 2 pixel wide. That means that reducing a shadow caster map sized 512x512 will take 9 steps.
    The reason why Zima didn't improve this aspect was to keep the code simpler and more readable.
  • ShadowMapResolver size can be 128, 256, 512 or 1024.
  • Shadows are always blurred.


FunHazard version

  • The reduction is optimized to reduce the map as much as possible in a single step, with a maximum reduction power of 16 pixels per step.
    That means that reducing a shadow caster map sized 512x512 will take 2 steps. (It may be surprising, but this improvement gave only a +5% of performance.)
  • ShadowMapResolver size can be set freely.
  • Different PostEffects can be selected to draw shadows.

ShadowCasterMap

Catalim Zima version

  • Each LightArea stores its own shadow caster map.
  • It  must be updated every frame.

FunHazard version

  • The ShadowCasterMap is a separate class, letting reuse the same map to process different lights. Or process the same light with different ShadowCasterMaps.
  • The ShadowCasterMap has a quality setting.
  • It doesn't need to be updated every frame.

8 commenti:

  1. Fantastic work on this! Love it and thank you!

    Quick note: I'm using this with monotouch/monogame, and discovered that constant values in shaders don't work (as per the monogame wiki). So the following lines:

    float precision8bitH = distanceH % unit8bit;
    float precision8bitV = distanceV % unit8bit;

    need to be changed to

    float precision8bitH = distanceH % 0.00390625f;
    float precision8bitV = distanceV % 0.00390625f;

    to work on monogame. This took me waaaaaaay too long to figure out - hopefully it'll help someone else as well :)

    Cheers,
    Jeff
    jeffsim.at.wanderlinggames.com

    RispondiElimina
    Risposte
    1. I'm reading it only now sorry :)

      Thank you very much for the correction, I'm going to apply it on the mono version right now

      :)

      Elimina
  2. btw; I could be wrong, but I think you're missing a call to set the 'renderTargetSize' parameter in ExecuteTechniqueDistortAndComputeDistance. FullScreenVS is using it but it doesn't look like it's getting set (at least, in the first frame).

    RispondiElimina
    Risposte
    1. Wow Jeff, you are right :)
      I'm gonna fix it right now and publish the update
      Thank you again :)

      Elimina
  3. Does this new implementation fix the fact that if you use this shadows in a world with a camera the borders of the lights areas goes from black to transparent ? (sorry if I didn't explain it well :x)

    RispondiElimina
    Risposte
    1. Sorry for the delay of the answer ;)

      Ehm I'm not sure to have understood the problem, can you send me a screen?

      g.r [A T] funhazard.com

      thank you very much

      Elimina
  4. Hello! I want to thank you very much, this is really good! But I have a small issue: how to use this with a transform matrix (camera)? For the lights themselves, I can simply do this:
    ----
    // We print the lights in an image
    GraphicsDevice.SetRenderTarget(screenLights);
    {
    GraphicsDevice.Clear(Color.Black);
    spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, null, null, null, null, transformMatrix);
    {
    ...
    }
    spriteBatch.End();
    }
    ----

    But the shadows themselves get misplaced because the engine thinks they're actually in a screen position and creates them according to that position. For them to work, some kind of transformation has to enter in the ResolveShadows step. I've been trying to convert the LightSources properties manually, but I haven't been successful.

    P.S.: I'm generating a shadowMap based on a RenderTarget where I draw the sprites I want to cast shadows, so It's fixed to the current perspective (I know this doesn't work with shadow caster objects next to the border, but that can be fixed by increasing the RenderTarget size). I'm also using MonoGame 3.2, Windows OpenGL. If I disable the transform matrix when printing the lights and attach a light to the mouse, it works flawlessly (but I have to recreate the light every time, with a radius * zoom).

    P.P.S. This is my transform matrix:
    ----
    return Matrix.CreateTranslation(new Vector3(-Position.X, Position.Y, 0f)) *
    Matrix.CreateRotationZ(rotation) *
    Matrix.CreateScale(new Vector3(zoom, zoom, 1f)) *
    Matrix.CreateTranslation(new Vector3(Viewport.X * 0.5f, Viewport.Y * 0.5f, 0));
    ----

    RispondiElimina
    Risposte
    1. Ok, so I managed to make it work with a camera! Here's what I did:

      - Camera: The camera I'm using is this one:
      ----
      return Matrix.CreateTranslation(new Vector3(Position.X, Position.Y, 0f)) *
      Matrix.CreateRotationZ(rotation) *
      Matrix.CreateScale(new Vector3(zoom, zoom, 1f)) *
      Matrix.CreateTranslation(new Vector3(Viewport.X * 0.5f, Viewport.Y * 0.5f, 0f));
      ----

      - Position: To correct the position, I transform it with the transform matrix (ignore zoom for now):
      ----
      Vector2 tempPosition;
      tempPosition = lightSource.Position;
      shadowMapResolver.ResolveShadows(shadowCasterMap, lightSource, PostEffect.LinearAttenuation_BlurHigh,
      Vector2.Transform(LightSources[i].Position /* * world coordinates to screen coordinates, if
      lightSource.Draw was done on world coordinates and not on pixel coordinates*/, transformMatrix), zoom);
      lightSource.Position = tempPosition;
      ----
      tempPosition is used because modifying the position directly would be permanent.

      - Rotation: To correct the rotation, I made a new Draw Method on LightSource.cs:
      ----
      public void Draw(SpriteBatch spriteBatch, float rotation)
      {
      int size = (int)(this.Radius * 2f);
      spriteBatch.Draw(this.PrintedLight, new Rectangle((int)this.Position.X, (int)this.Position.Y, size, size),
      null, this.Color, -rotation, new Vector2(this.PrintedLight.Width * 0.5f, this.PrintedLight.Height * 0.5f), SpriteEffects.None, 0f);
      }
      ----

      - Scale (zoom): To correct the zoom, I modified some methods on LightSource.cs and on ShadowMapResolver.cs (hence zoom as the last argument where I told you to ignore it by then):
      LightSource.cs:
      ----
      public Vector2 RelativeZero(float zoom)
      {
      return new Vector2(Position.X - this.Radius * zoom, Position.Y - this.Radius * zoom);
      }

      public Vector2 RelativeZeroHLSL(ShadowCasterMap shadowMap, float zoom)
      {
      Vector2 sizedRelativeZero = this.RelativeZero(zoom) * shadowMap.PrecisionRatio;
      ...
      }
      ----
      ShadowMapResolver.cs:
      ----
      private void ExecuteTechniqueDistortAndComputeDistance(ShadowCasterMap shadowCasterMap, LightSource light, RenderTarget2D destination, string techniqueName, float zoom)
      {
      ...
      this.lightsFX.ResolveShadowsEffect.Parameters["lightRelativeZero"].SetValue(light.RelativeZeroHLSL(shadowCasterMap, zoom));

      Vector2 shadowCasterMapPortion = (light.Size * zoom * shadowCasterMap.PrecisionRatio) / shadowCasterMap.Size;
      ...
      }
      ----
      And of course I made ResolveShadows pass the zoom argument to ExecuteTechniqueDistortAndComputeDistance.

      And once again, thanks!

      Elimina