Добавление фантастических световых эффектов в XNA-игры

Грэг Дункан

В этом XNA-проекте демонстрируется, как автор добавляет фантастические эффекты освещения в игры. И конечно, вы получите немного кода для игр. clip_image002

XNA Light Pre-Pass: каскадные теневые карты

В этом сообщении я расскажу о собственной реализации каскадных теневых карт – общей методике, применяемой для управления направленными светотенями. Описания этой техники можно найти здесь и здесь. Как и прежде – полный исходный код примера можно найти здесь. Его использование производится на собственный страх и риск!!

Основная идея состоит в разбиении конуса видимости на более мелкие фрагменты (обычно на меньшие конусы), и генерации карты теней для каждого из них. Таким образом можно получить лучшее распределение наших любимых текселей (пикселей текстуры) теневой карты в каждой области: элемент, более близкий к камере меньше тех, которые располагаются дальше, поэтому ближе к камере получается лучшее разрешение теней. Недостатком метода является то, что в этом случае приходится выполнять больше вызовов перерисовки/изменения состояния, так как приходится рендерить больше одной теневой карты. Можно создавать атлас теневых карт для оптимизации этих вещей, просто надо сгенерировать большую теневую карту, вмещающую все меньшие теневые карты и компенсировать выборки текселов на пиксельном шейдере.

clip_image004

clip_image006

Вот снимок решения:

clip_image008

Цикл перерисовки XNA, где совершается основная магия:

 protected override void Draw(GameTime gameTime)
 {
     GraphicsDevice.BlendState = BlendState.Opaque;
     GraphicsDevice.DepthStencilState = DepthStencilState.Default;
     //clear our visible lights from previous frame
     _visibleLights.Clear();
     //perform a basic frustum test on our lights
     //we could do the same for the meshes, but its enough for this tutorial
     foreach (Light light in _lights)
     {
         if (light.LightType == Light.Type.Directional)
         {
             _visibleLights.Add(light);
         }
         else if (light.LightType == Light.Type.Spot)
         {
             if (_camera.Frustum.Intersects(light.Frustum))
                 _visibleLights.Add(light);
         }
         else if (light.LightType == Light.Type.Point)
         {
             if (_camera.Frustum.Intersects(light.BoundingSphere))
                 _visibleLights.Add(light);
         }
     }
     RenderTarget2D output = _renderer.RenderScene(_camera, _visibleLights, _meshes);
     GraphicsDevice.SetRenderTarget(null);
     GraphicsDevice.BlendState = BlendState.Opaque;
     GraphicsDevice.DepthStencilState = DepthStencilState.Default;
     //output our final composition into the backbuffer. We could output it into a
     //post-process fx, or to an object diffuse texture, or or or...well, its up to you =)
     //Just remember that we are using a floating point buffer, so we should use Point Clamp here
     _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.PointClamp, DepthStencilState.Default, RasterizerState.CullCounterClockwise);
     _spriteBatch.Draw(output, new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height), Color.White);
     if (_renderGBuffer)
     {
         int smallWidth = GraphicsDevice.Viewport.Width / 3;
         int smallHeigth = GraphicsDevice.Viewport.Height / 3;
         //draw the intermediate steps into screen
         _spriteBatch.Draw(_renderer.NormalBuffer, new Rectangle(0, 0, smallWidth, smallHeigth),
         Color.White);
         _spriteBatch.Draw(_renderer.DepthBuffer, new Rectangle(smallWidth, 0, smallWidth, smallHeigth),
         Color.White);
         _spriteBatch.Draw(_renderer.LightBuffer, new Rectangle(smallWidth * 2, 0, smallWidth, smallHeigth),
         Color.White);
         /*if (_renderer.GetShadowMap(0) != null)
         _spriteBatch.Draw(_renderer.GetShadowMap(0), new Rectangle(0, smallHeigth, smallWidth, smallHeigth),
         Color.White);
         if (_renderer.GetShadowMap(1) != null)
         _spriteBatch.Draw(_renderer.GetShadowMap(1), new Rectangle(smallWidth, smallHeigth, smallWidth, smallHeigth),
         Color.White);*/
     }
     _spriteBatch.End();
     //legacy, left here just in case
     if (_renderScreenSpaceLightQuads)
     {
         _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullCounterClockwise);
         Rectangle rect = new Rectangle();
         foreach (Light light in _visibleLights)
         {
             //redundant, but its ok for now
             Vector2 topLeftCorner, bottomRightCorner, size;
             //compute a screen-space quad that fits the light's bounding sphere
             _camera.ProjectBoundingSphereOnScreen(light.BoundingSphere, out topLeftCorner, out size);
             rect.X = (int)((topLeftCorner.X * 0.5f + 0.5f) * GraphicsDevice.Viewport.Width);
             rect.Y = (int)((topLeftCorner.Y * 0.5f + 0.5f) * GraphicsDevice.Viewport.Height);
             rect.Width = (int)((size.X * 0.5f) * GraphicsDevice.Viewport.Width);
             rect.Height = (int)((size.Y * 0.5f) * GraphicsDevice.Viewport.Height);
             Color color = light.Color;
             color.A = 150;
             _spriteBatch.Draw(_lightDebugTexture, rect, color);
         }
         _spriteBatch.End();
     }
     bool drawText = true;
     if (drawText)
     {
         _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp,
         DepthStencilState.Default, RasterizerState.CullCounterClockwise);
         _spriteBatch.DrawString(_spriteFont, "Press O / P to visualize the GBuffer", new Vector2(0, 0),
         Color.White);
         _spriteBatch.DrawString(_spriteFont, "Hold space to stop the lights", new Vector2(0, 20), Color.White);
         _spriteBatch.DrawString(_spriteFont,
         String.Format("Update:{0:00.0}, Draw:{1:00.0}, GPU:{2:00.0}", updateMs, drawMs,
         gpuMs), new Vector2(0, 40), Color.White);
         RenderStatistics renderStatistics = _renderer.RenderStatistics;
         _spriteBatch.DrawString(_spriteFont, "Visible lights: " + renderStatistics.VisibleLights,
         new Vector2(0, 60), Color.White);
         _spriteBatch.DrawString(_spriteFont, "Draw calls: " + renderStatistics.DrawCalls, new Vector2(0, 80),
         Color.White);
         _spriteBatch.DrawString(_spriteFont, "Shadow Lights: " + renderStatistics.ShadowCasterLights,
         new Vector2(0, 100), Color.White);
         _spriteBatch.DrawString(_spriteFont, "Shadow Meshes: " + renderStatistics.ShadowCasterMeshes,
         new Vector2(0, 120), Color.White);
         _spriteBatch.End();
     }
     base.Draw(gameTime);
     swDraw.Stop();
     updateMs = swUpdate.Elapsed.TotalMilliseconds;
     drawMs = swDraw.Elapsed.TotalMilliseconds;
     swGPU.Reset();
     swGPU.Start();
 }

Если вы ищете как добавить классные эффекты освещения в XNA-игры, то на этот проект стоит обратить внимание. Также пока вы здесь, убедитесь, что посмотрели и другие аналогичные проекты. Вот несколько последних: