This project is read-only.

Particle accuracy at high positions.

May 27, 2010 at 11:59 PM

Hey

For particle emitter positions larger than (0,0,1000) (we're doing a racing game with infinite track going along the z-direction (roughly, z >> x,y)) we're getting strange behaviour probably related to the limited accuracy of float.

The following code generates a sphere around the center point that quickly expands from that point. (sort of an explosion)

<particle name="telesphere" blend="alpha" texture="spark.png" gpu_buffer_position="true">
    <logic>
        <once>
        <set target="size" arg0=".02"/>
          <!-- This will generate a sphere! -->
          <!-- Save the positions! -->
          <set target="local1" arg0="position.x"/>
          <set target="local2" arg0="position.y"/>
          <set target="local3" arg0="position.z"/>
         
          <!-- Radius -->
          <set target="velocity.x" arg0="0.02"/>
          <set target="velocity.y" arg0="0.02"/>
          <set target="velocity.z" arg0="0.02"/>
         
          <set target="position.x" arg0=".5"/>
          <set target="position.y" arg0=".5"/>
          <set target="position.z" arg0=".5"/>
         
          <!-- Theta -->
          <rand target="user0" arg0="0" arg1="3.141"/>
          <sin target="local0" arg0="user0"/>
          <mul target="velocity.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="velocity.y" arg0="local0"/>
          <cos target="local0" arg0="user0"/>
          <mul target="velocity.z" arg0="local0"/>

          <sin target="local0" arg0="user0"/>
          <mul target="position.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="position.y" arg0="local0"/>
          <cos target="local0" arg0="user0"/>
          <mul target="position.z" arg0="local0"/>
         
          <!-- Phi -->
          <rand target="user0" arg0="0" arg1="6.282"/>
          <cos target="local0" arg0="user0"/>
          <mul target="velocity.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="velocity.y" arg0="local0"/>

          <cos target="local0" arg0="user0"/>
          <mul target="position.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="position.y" arg0="local0"/>

          <!-- Add saved positions to the calculated local ones -->
          <add target="position.x" arg0="local1"/>
          <add target="position.y" arg0="local2"/>
          <add target="position.z" arg0="local3"/>

          <rand target="red" arg0=".5" arg1="1"/>
          <rand target="green" arg0=".5" arg1="1"/>
      </once>
     <frame>
        <mul target="alpha" arg0=".9"/>
        <mul target="velocity.x" arg0="1.2"/>
        <mul target="velocity.y" arg0="1.2"/>
        <mul target="velocity.z" arg0="1.2"/>
      </frame>
    </logic>
  </particle>

But at large distances the points are no longer randomly distributed and form a nice sphere but are layed out into planes.

Is there a way to circumvent that problem?

Thanks for your help

Best

Rafael

<particle name="telesphere" blend="alpha" texture="spark.png" gpu_buffer_position="true">
    <logic>
        <once>
        <set target="size" arg0=".02"/>
          <!-- This will generate a sphere! -->
          <!-- Save the positions! -->
          <set target="local1" arg0="position.x"/>
          <set target="local2" arg0="position.y"/>
          <set target="local3" arg0="position.z"/>
          
          <!-- Radius -->
          <set target="velocity.x" arg0="0.02"/>
          <set target="velocity.y" arg0="0.02"/>
          <set target="velocity.z" arg0="0.02"/>
          
          <set target="position.x" arg0=".5"/>
          <set target="position.y" arg0=".5"/>
          <set target="position.z" arg0=".5"/>
          
          <!-- Theta -->
          <rand target="user0" arg0="0" arg1="3.141"/>
          <sin target="local0" arg0="user0"/>
          <mul target="velocity.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="velocity.y" arg0="local0"/>
          <cos target="local0" arg0="user0"/>
          <mul target="velocity.z" arg0="local0"/>

          <sin target="local0" arg0="user0"/>
          <mul target="position.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="position.y" arg0="local0"/>
          <cos target="local0" arg0="user0"/>
          <mul target="position.z" arg0="local0"/>
          
          <!-- Phi -->
          <rand target="user0" arg0="0" arg1="6.282"/>
          <cos target="local0" arg0="user0"/>
          <mul target="velocity.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="velocity.y" arg0="local0"/>

          <cos target="local0" arg0="user0"/>
          <mul target="position.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="position.y" arg0="local0"/>

          <!-- Add saved positions to the calculated local ones -->
          <add target="position.x" arg0="local1"/>
          <add target="position.y" arg0="local2"/>
          <add target="position.z" arg0="local3"/>

          <rand target="red" arg0=".5" arg1="1"/>
          <rand target="green" arg0=".5" arg1="1"/>
      </once>
     <frame>
        <mul target="alpha" arg0=".9"/>
        <mul target="velocity.x" arg0="1.2"/>
        <mul target="velocity.y" arg0="1.2"/>
        <mul target="velocity.z" arg0="1.2"/>
      </frame>
    </logic>
  </particle>

Th

May 28, 2010 at 12:26 AM
Edited May 28, 2010 at 12:27 AM

Hi.

What system are you running this particle system on? xbox/pc? And if the PC, what video card do you have? (can you tell me what happens if you try forcing the system to run on the CPU with ParticleSystem.ForceUseCpuParticleSystem = true; - PC only).

That is very strange. The 'gpu_buffer_position' property is designed specifically to deal with this situation.
When the particle system is run on the GPU (which is the only option on the 360) then particle data is stored in 16bit floating point - which obviously has some precision limitations.

However, when you set 'gpu_buffer_position' to true, the position set during 'Once' is stored in user1/user2/user3. The real position value is then set to zero. So any velocity effect applied is a starting point of zero (when rendering, the position+user value will be used to get the vertex position). A distance of 1,000 should be well within the range where you will start seeing precision issues on the GPU.

I'm really finding this confusing. You should not be seeing these types of issues if that flag is set to true. A particle started at 0,0,0 or 0,0,10000 should be calculating in exactly the same way... Just the user3 should be different

 

 

May 28, 2010 at 12:44 AM

Thanks for your quick answer!

I see the issue on XBOX and the PC. If I turn gpu_buffer_position off, the whole effect also extends onto the velocities. (Whole group of dots move together)

If I force the CPU Particle System, everything works as expected and I see a nice sphere expanding.

Somehow it looks like the starting positions are not exact enough (because the sphere starts with a radius of 0.5 the starting positions are are in the z-range 999.5 -> 1000.5 which might already be affected by accuracy (?). The expansion afterwards works normally (as expected by turning the gpu_buffer_position flag on). It's just the initial positions that are not working.

I'll try saving the z-offset in user0 and then add it in the loop with an if_equals 0 - but if there's a nicer solution I'd be glad to know.

 

 

May 28, 2010 at 12:55 AM
Edited May 28, 2010 at 12:57 AM

Ahh ok. That makes sense.

I thought you were seeing the particles expand in an odd way.
I'm not too surprised that you are seeing starting position accuracy issues then. FP16 does lose accuracy surprisingly quickly, as it has roughly 11 bits of precision (so around 2048 distinct values within an exponent).

The fix should be fairly simple, but it's assuming that there aren't particles constantly spawning.
What I would suggest is each frame check the number of particles that are active in the ParticleSystem (you can call GetParticleCount for each particle type).
When there are no particles currently being drawn in the given frame, then store the position of the camera somewhere.

Then, when you create your particles, subtract the stored position of the camera (so the position you are spawning the particles at will be the difference to the last stored camera position). When drawing the particle system, simply set the world matrix to that stored position.

This way, as you are firing particles, the stored position won't change, but as soon as all the particles are all finished, it'll get set to the camera position.
Think of it as the particle system following the camera around, but stopping moving when it's actually being used.

Does that make sense?

May 28, 2010 at 1:05 AM
Edited May 28, 2010 at 2:15 AM

Yes, that sounds like a great idea. Indeed as all the particles connected to the player are playercentric anyway I can just keep the system at player position and set the current player velocity in the global values to get a feeling of the particles being static compared to the world.

I also just tried my idea of keeping the z-offset in user0 and applying it on the first loop iteration. It also works well. But as the x-values are also increasing over time i have to think of a trick to pass more than just one value over into the loop.

Edit: Here the tricked code to transport the small values over into the loop part. Interestingly it does work in z-direction, but not in x-direction - i see stripes of particles instead of a sphere for high values (>2000,0,2000). I'll give your solution a try then.

Edit2: See below.

May 28, 2010 at 2:15 AM

Your idea worked like a charm. I'm using both tricks combined now and get a perfect sphere. Thank you a lot.

Here's the code for the wrapper class in case anyone else has a similar problem once:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xen.Ex.Graphics.Display;
using Microsoft.Xna.Framework;
using Xen.Ex.Graphics;
using Xen;
using Xen.Camera;

namespace lun.Particles
{
    class LunParticleDrawer3D:IUpdate,IDraw
    {
        private Matrix world = Matrix.Identity;
        Xen.Ex.Graphics.Display.VelocityBillboardParticles3D particles;
        public ParticleSystem ps;
        public Camera3D cam;
        public LunParticleDrawer3D(ParticleSystem system, bool useRotationValueToScaleVelocityEffect, float velocityExtentionScale, Camera3D camera)
        {
            particles = new VelocityBillboardParticles3D(system, useRotationValueToScaleVelocityEffect, velocityExtentionScale);
            this.ps = system;
            this.cam = camera;
        }

        public UpdateFrequency Update(UpdateState state)
        {
            int sum = 0;
            for (int i = 0; i < ps.ParticleTypeCount; i++) sum += ps.GetParticleCount(i);
            if (sum <= 0)
            {
                world = Matrix.CreateTranslation(cam.Position);
            }
            return UpdateFrequency.PartialUpdate10hz;
        }

        public void Draw(DrawState state)
        {
            ps.GlobalValues[0] = world.Translation.X;
            ps.GlobalValues[1] = world.Translation.Y;
            ps.GlobalValues[2] = world.Translation.Z;
            state.PushWorldMatrix(ref world);
            particles.Draw(state);
            state.PopWorldMatrix();
        }

        public bool CullTest(ICuller culler)
        {
            return particles.CullTest(culler);
        }
    }
}

And the final particle:

<particle name="telesphere" blend="alpha" texture="spark.png" gpu_buffer_position="true">
    <logic>
        <once>
        <set target="size" arg0=".02"/>
          <!-- This will generate a sphere! -->
          <!-- Save the positions! -->
          <set target="local1" arg0="position.x"/>
          <set target="local2" arg0="position.y"/>
          <set target="local3" arg0="position.z"/>
          <!-- Radius -->
          <set target="velocity.x" arg0=".02"/>
          <set target="velocity.y" arg0=".02"/>
          <set target="velocity.z" arg0=".02"/>
          
          <set target="position.x" arg0=".5"/>
          <set target="position.y" arg0=".5"/>
          <set target="position.z" arg0=".5"/>
          
          <!-- Theta -->
          <rand target="user0" arg0="0" arg1="3.141"/>
          <sin target="local0" arg0="user0"/>
          <mul target="velocity.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="velocity.y" arg0="local0"/>
          <cos target="local0" arg0="user0"/>
          <mul target="velocity.z" arg0="local0"/>

          <sin target="local0" arg0="user0"/>
          <mul target="position.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="position.y" arg0="local0"/>
          <cos target="local0" arg0="user0"/>
          <mul target="position.z" arg0="local0"/>
          
          <!-- Phi -->
          <rand target="user0" arg0="0" arg1="6.282"/>
          <cos target="local0" arg0="user0"/>
          <mul target="velocity.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="velocity.y" arg0="local0"/>

          <cos target="local0" arg0="user0"/>
          <mul target="position.x" arg0="local0"/>
          <sin target="local0" arg0="user0"/>
          <mul target="position.y" arg0="local0"/>

          <!-- Add saved positions to the calculated local ones -->
          <set target="red" arg0="position.x"/>
          <set target="green" arg0="position.y"/>
          <set target="blue" arg0="position.z"/>
          <set target="user0" arg0="1"/>
          <add target="position.x" arg0="local1"/>
          <add target="position.y" arg0="local2"/>
          <set target="position.z" arg0="local3"/>
          <sub target="position.x" arg0="global0"/>
          <sub target="position.y" arg0="global1"/>
          <sub target="position.z" arg0="global2"/>
        </once>
      <frame>
        <if_notequal arg0="user0" arg1="0">
          <set target="position.x" arg0="red"/>
          <set target="position.y" arg0="green"/>
          <set target="position.z" arg0="blue"/>
          <rand target="red" arg0=".5" arg1="1"/>
          <rand target="green" arg0=".5" arg1="1"/>
          <set target="blue" arg0="1"/>
          <set target="user0" arg0="0"/>
        </if_notequal>
        <mul target="alpha" arg0=".9"/>
        <mul target="velocity.x" arg0="1.2"/>
        <mul target="velocity.y" arg0="1.2"/>
        <mul target="velocity.z" arg0="1.2"/>
      </frame>
    </logic>
  </particle>

May 28, 2010 at 10:31 AM

Excellent :-)

One thing I'll point out (which doesn't actually have any effect here). The cull test you are performing would be invalid for most other classes (because it doesn't know about the world matrix changes you are going to make when drawing (some classes let you pass in a matrix when calling CullTest).
However with the particle system this doesn't actually matter, because it only does a cull test if you have a culling proxy set on the system (it's practically impossible to determine the size and area of the particle system at runtime). So CullTest() in this case should always be returning true.