官术网_书友最值得收藏!

Implementing smoke within the Reach profile

If implemented in a naive fashion, rendering smoke could place a significant burden on the hardware of a device running a game under the Reach profile.

In this recipe, you'll learn a method for improving the distribution of work and data between the CPU and GPU, to hopefully get as close as possible to the unachievable goal of hardware instancing in an environment that doesn't allow for custom shaders.

In the following illustration, you can see this recipe in use within a stylized city scene:

Implementing smoke within the Reach profile

Getting ready

An image of a smoke "particle" is required for this special effect, but don't feel pressured to spend too much time or effort creating anything too elaborate. Although it forms the heart of the display, the technique's repetition and distortion of even the most basic smoke pattern can result in some quite intricate and realistic results.

How to do it...

To create smoke within the Reach Profile:

  1. Start by creating a new smoke particle class:
    class ReachSmokeParticle
    {
    
  2. Insert instance variables for the position and age of the particle:
    public Vector3 Position;
    public float PositionDelta;
    public float Scale;
    public float Rotation;
    public float RotationDelta;
    public float Age;
    public float AgeDelta;
    public bool Visible;
    public Texture2D Texture;
    public Vector2 TextureOrigin;
    
  3. Add an Update() method to calculate a particle's details:
    public void Update(
    GameTime gameTime,
    Vector3 wind,
    Vector3 spawnPoint,
    float spawnRadius,
    Random random)
    {
    var timeScale = (float)gameTime.ElapsedGameTime.TotalSeconds;
    Position += ((Vector3.Up * PositionDelta) + wind) *
    timeScale;
    Rotation += RotationDelta * timeScale;
    Age += AgeDelta * timeScale;
    if (Age > 1)
    {
    var offset = ((.5f - (float)random.NextDouble()) *
    (Vector3.Right * spawnRadius)) +
    ((.5f - (float)random.NextDouble()) *
    (Vector3.Forward * spawnRadius));
    Position = spawnPoint + offset;
    Age = 0;
    Visible = true;
    }
    }
    
  4. Continue by adding a Draw() method:
    public void Draw(
    SpriteBatch spriteBatch,
    Viewport viewport,
    Matrix view,
    Matrix projection,
    float projectedScale)
    {
    if (!Visible)
    {
    return;
    }
    var projectedPosition = viewport.Project(
    Position,
    projection,
    view,
    Matrix.Identity);
    var screenPosition = new Vector2(
    projectedPosition.X, projectedPosition.Y);
    var tint = Color.FromNonPremultiplied(
    255, 255, 255, 255 - (int)(255f * Age));
    var displayScale = Scale * projectedScale;
    spriteBatch.Draw(
    Texture, screenPosition,
    null, tint, Rotation, TextureOrigin,
    displayScale,
    SpriteEffects.None, projectedPosition.Z);
    }
    
  5. Now, add the class that will be emitting the freshly defined particles:
    class ReachSmoke
    {
    
  6. Insert some instance variables to the new class to hold the details of the particles:
    SpriteBatch spriteBatch;
    Texture2D smoke;
    Vector2 halfSmokeSize;
    List<ReachSmokeParticle> particles;
    Vector3 spawnPoint = Vector3.Zero;
    float spawnRadius = 0.2f;
    Random random = new Random();
    Vector3 wind = Vector3.Right * 0.3f;
    
  7. Add a constructor to create instances of all the particles:
    public ReachSmoke(
    GraphicsDevice graphicsDevice,
    ContentManager content)
    {
    spriteBatch = new SpriteBatch(graphicsDevice);
    smoke = content.Load<Texture2D>("smoke/smoke");
    halfSmokeSize = new Vector2(
    smoke.Width / 2, smoke.Height / 2);
    var particleCount = 300;
    particles = new List<ReachSmokeParticle>();
    for (var index = 0; index < particleCount; index++)
    {
    var particle = new ReachSmokeParticle()
    {
    Texture = smoke,
    TextureOrigin = halfSmokeSize,
    Position = spawnPoint,
    PositionDelta =
    (0.8f * (float)random.NextDouble()) + .2f,
    Scale =
    (0.8f * (float)random.NextDouble()) + .2f,
    Rotation = (float)random.NextDouble(),
    RotationDelta = 0.5f - (float)random.NextDouble(),
    Age = (float)random.NextDouble(),
    AgeDelta =
    (0.8f * (float)random.NextDouble()) + .2f,
    Visible = false
    };
    particles.Add(particle);
    }
    }
    
  8. Add an Update() method to update all the particles to their latest positions:
    public void Update(GameTime gameTime)
    {
    foreach (var particle in particles)
    {
    particle.Update(
    gameTime,
    wind,
    spawnPoint, spawnRadius,
    random);
    }
    }
    
  9. Finish the class with the addition of the Draw() method to render the particles onto the screen:
    public void Draw(
    Matrix view, Matrix projection, Matrix world,
    Viewport viewport)
    {
    var scaleTestPositionOne = viewport.Project(
    Vector3.Zero,
    projection, view, Matrix.Identity);
    if (scaleTestPositionOne.Z < 0)
    {
    return;
    }
    var scaleTestPositionTwo = viewport.Project(
    Vector3.Up + Vector3.Right,
    projection, view, Matrix.Identity);
    var projectedScale = Vector3.Distance(
    scaleTestPositionOne, scaleTestPositionTwo) /
    (smoke.Height * 2);
    if (projectedScale > 5f)
    {
    return;
    }
    spriteBatch.Begin(
    SpriteSortMode.Deferred,
    BlendState.AlphaBlend, null,
    DepthStencilState.DepthRead, null);
    foreach (var particle in particles)
    {
    particle.Draw(spriteBatch,
    viewport, view, projection, projectedScale);
    }
    spriteBatch.End();
    }
    

How it works...

Inspecting the constructor of the ReachSmoke class, we can see the creation of the smoke particles.

A diverse selection of random sizes, speeds, and states throughout the particles lessens the chance of players being able to spot any obvious signs of repetition, despite the use of only one texture across all of the particles.

In order to lessen the chance of unwanted pressure on the garbage collector, we create a set number of particles at the beginning and recycle them as required.

Within the Update() method of the ReachSmokeParticle class, the code to move and rotate each particle can be seen along the recycling process that re-spawns a particle once it has reached the end of its life.

Eagle-eyed readers may notice that a given particle's visibility isn't enabled until it has re-spawned at least once. This delay in visibility is done to give the particles the best chance of appearing in a reasonably even manner, and avoid one large, unrealistic clump at the beginning.

To enjoy the best chance of maximum performance, the Draw() methods of both the ReachSmoke and ReachSmokeParticle classes are designed with the idea of harnessing the optimizations present in the SpriteBatch class.

With this in mind, the transformation from 3D world space into 2D screen space is performed on the CPU via .NET code. This allows the GPU to receive and display the particles in one unified update, without the need to recalculate or reload.

Handling the 3D calculations ourselves presents two issues:

  • The first is that of perspective and the need to scale, which is dependent on how far away the viewer is from the smoke.

    A measurement of the distance between screen positions of two known points in 3D space is made at the start of the Draw() method of ReachSmoke, to help approximate the scaling of perspective.

  • The second problem is of depth, where the SpriteBatch class is more commonly called upon to draw images over the top of a 3D scene rather than within it.

    Thankfully, the Draw() method of the SpriteBatch class has an override that allows us to specify the depth of a given texture, and use this to ensure that particles appear correctly in front of, and behind other elements in a 3D scene.

There's more...

With some tweaking of the numbers and the texture, a variety of effects from flame, to steam, and even bubbles, can be achieved with a similar approach.

One addition that can really improve the realism of particle-based effects such as smoke, is animating the texture of each particle as it moves through the scene.

Examples of this can be found in games such as Naughty Dog's Uncharted 3, where they utilized an additional texture as a "movement map", where the color of each pixel in the movement map dictated the direction and intensity of the translation/distortion applied to the corresponding pixel in the texture map.

主站蜘蛛池模板: 玉溪市| 义乌市| 秭归县| 泸溪县| 泽州县| 新野县| 荔波县| 彩票| 奉新县| 岳池县| 新平| 大庆市| 松阳县| 汝城县| 潜江市| 合作市| 盘山县| 罗源县| 台前县| 南开区| 中超| 女性| 泽库县| 司法| 渭源县| 温州市| 芜湖县| 都昌县| 新丰县| 丰都县| 保山市| 石门县| 合水县| 乐业县| 曲水县| 博罗县| 门头沟区| 广汉市| 富锦市| 浦北县| 道真|