Using Flow Maps with Particle System

Starting from Babylon.js v8.6, you can provide a flow map to control how your particles behave. All particle systems support that feature.

Here is a complete example to try:

Definition

A flow map is a screen-aligned image that is used to control the direction and intensity of forces applied to particles based on their position on the screen, not in world space.

This means the flow behaves consistently relative to the viewport, regardless of camera movement or rotation. Because we are speaking of direction in derived from a screen aligned image, we will present directions in relation to the screen such as screen-left or screen-up.

The flow map is a standard texture where each pixel encodes a 3D direction vector and strength using a Color4 format (assuming the camera is pointing towards Z):

Red (R): X-component of the flow direction (0 β†’ screen-left, 1 β†’ screen-right, 0.5 β†’ no movement)
Green (G): Y-component of the flow direction (0 β†’ screen-down, 1 β†’ screen-up, 0.5 β†’ no movement)
Blue (B): Z-component of the flow direction (0 β†’ toward the screen, 1 β†’ away from the screen, 0.5 β†’ no movement)
Alpha (A): flow strength (0 β†’ no effect)

Example

When used with a regular Particle system, the flow map will be stored as a UInt8 array:

    const flowMapUrl = "https://assets.babylonjs.com/textures/particleMotion_flowmap.png";
    particleSystem.flowMap = await BABYLON.FlowMap.FromUrlAsync(flowMapUrl);

You can control the overall impact of the flow map with particleSystem.flowMapStrength.

When used with GPU particle system, the flow map is a regular texture:

const flowMapUrl = "https://assets.babylonjs.com/textures/particleMotion_flowmap.png";
particleSystem.flowMap = new BABYLON.Texture(flowMapUrl);

Example using a GPU particle system:

12 Likes

Cool!!

What about to introduce an IFlowable interface and a processFlowable public method so we can apply the underlying math not just to particles?

export interface IFlowable {
    direction: Vector3;
    position: Vector3;
}
/**
 * Class used to represent a particle flow map.
 * #5DM02T#7
 * GPUParts: #5DM02T#12 (webgl2)
 * GPUParts: #5DM02T#13 (webgpu)
 */
export class FlowMap {
    /**
     * Create a new flow map.
     * @param width defines the width of the flow map
     * @param height defines the height of the flow map
     * @param data defines the data of the flow map
     */
    constructor(
        public readonly width: number,
        public readonly height: number,
        public readonly data: Uint8ClampedArray
    ) {}

    public processFlowable(flowable: IFlowable, scaledUpdateSpeed: number, strength = 1, scene = Engine.LastCreatedScene) {
        // Convert world pos to screen pos
        if (!scene) {
            return;
        }

        Vector3.TransformCoordinatesToRef(flowable.position, scene.getTransformMatrix(), ScreenPos);

        const u = ScreenPos.x * 0.5 + 0.5;
        const v = 1.0 - (ScreenPos.y * 0.5 + 0.5);

        const x = Math.floor(u * this.width);
        const y = Math.floor(v * this.height);

        // Clamp
        if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
            return;
        }

        const index = (y * this.width + x) * 4;
        const r = this.data[index];
        const g = this.data[index + 1];
        const b = this.data[index + 2];
        const a = this.data[index + 3];

        const fx = (r / 255.0) * 2.0 - 1.0;
        const fy = (g / 255.0) * 2.0 - 1.0;
        const fz = (b / 255.0) * 2.0 - 1.0;
        const localStrength = a / 255.0;

        FlowVector.set(fx, fy, fz);
        FlowVector.scaleToRef(scaledUpdateSpeed * strength * localStrength, ScaledFlowVector);

        flowable.direction.addInPlace(ScaledFlowVector); // Update particle velocity
    }

    /** @internal */
    public _processParticle(particle: Particle, system: ThinParticleSystem, strength = 1) {
        const scene = system.getScene()!;
        this.processFlowable(particle, system._tempScaledUpdateSpeed, strength, scene);
    }

#5DM02T#21

Moving meshes using the flowmap:

3 Likes

We can make it even more customizable by passing the tranformation matrix into the processFlowable function:

   public processFlowable(flowable: IFlowable, strength = 1, matrix = Engine.LastCreatedScene?.getTransformMatrix()) {
        if (!matrix) {
            return;
        }

        // Convert world pos to screen pos
        Vector3.TransformCoordinatesToRef(flowable.position, matrix, ScreenPos);
        ...
}

    public _processParticle(particle: Particle, system: ThinParticleSystem, strength = 1) {
        this.processFlowable(particle, system._tempScaledUpdateSpeed * strength, system.getScene()?.getTransformMatrix());
    }

Aaand even more customizable with flowMapSamplePosOrTransformationMatrix:

    public processFlowable(flowable: IFlowable, strength = 1, flowMapSamplePosOrTransformationMatrix?: IVector3Like | Matrix) {
        if (!flowMapSamplePosOrTransformationMatrix) {
            return;
        }

        // Convert world pos to screen pos
        if (flowMapSamplePosOrTransformationMatrix instanceof Matrix) {
            Vector3.TransformCoordinatesToRef(flowable.position, flowMapSamplePosOrTransformationMatrix, ScreenPos);
        } else {
            ScreenPos.x = flowMapSamplePosOrTransformationMatrix.x;
            ScreenPos.y = flowMapSamplePosOrTransformationMatrix.y;
            ScreenPos.z = flowMapSamplePosOrTransformationMatrix.z;
        }
       ...

    public _processParticle(particle: Particle, system: ThinParticleSystem, strength = 1, matrix?: Matrix) {
        // we can get rid of the system: ThinParticleSystem parameter and calculate the strength in the particleSystem.flowMap setter
        this.processFlowable(particle, system._tempScaledUpdateSpeed * strength, matrix);
    }

particleSystem.ts:

        ...
        if (value) {
            const matrix = this.getScene()?.getTransformMatrix(); // reuse this
            this._flowMapUpdate = {
                process: (particle: Particle) => {
                    this._flowMap!._processParticle(particle, this, this.flowMapStrength, matrix);
                },
                previousItem: null,
                nextItem: null,
            };
            _ConnectAfter(this._flowMapUpdate, this._directionProcessing!);
        }
       ...

#5DM02T#24

@Deltakosh IFlowable added by RolandCsibrei Β· Pull Request #16570 Β· BabylonJS/Babylon.js Β· GitHub - Draft PR. If you don’t consider this as a good idea, just drop the PR :wink:

2 Likes

DO IT!! This is good!

1 Like

Ready for review buddy!

A few issues with the PR but besides that all good! I’ll merge as soon as it is green

I already fixed them :wink:

1 Like

I love that mate :smiley:

1 Like