Skip to content

Flow


WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

ts
import { AtmosphericComponent, CameraUtil, ClusterLightingBuffer, Color, ComputeGPUBuffer, ComputeShader, Engine3D, HoverCameraController, Material, MeshRenderer, Object3D, PassType, PlaneGeometry, RendererPassState, RenderShaderPass, Scene3D, Shader, ShaderLib, Texture, Time, Vector3, Vector4, View3D, webGPUContext } from '@orillusion/core';

class Demo_Flow {
    async run() {
        await Engine3D.init({});

        let scene = new Scene3D();
        let sky = scene.addComponent(AtmosphericComponent);
        await this.initScene(scene);

        let camera = CameraUtil.createCamera3DObject(scene);

        camera.perspective(60, webGPUContext.aspect, 0.01, 10000.0);
        let ctl = camera.object3D.addComponent(HoverCameraController);
        ctl.setCamera(0, 0, 4, new Vector3(0, 1, 0));
        ctl.maxDistance = 1000;

        let view = new View3D();
        view.scene = scene;
        view.camera = camera;

        Engine3D.startRenderView(view);
    }

    async initScene(scene: Scene3D) {
        let obj = new Object3D();
        obj.addComponent(FlowSimulator);
        scene.addChild(obj);
    }
}

type FlowSimulatorConfig = {
    GROUP_SIZE: number;
    NUM: number;
    SPAWN_RADIUS: number;
    BASE_LIFETIME: number;
    MAX_ADDITIONAL_LIFETIME: number;
    PRESIMULATION_DELTA_TIME: number;
    INITIAL_SPEED: number;
    INITIAL_TURBULENCE: number;
    NOISE_OCTAVES: number;
    directionX: number;
    directionY: number;
    directionZ: number;
};

class FlowSimulator extends MeshRenderer {
    protected mConfig: FlowSimulatorConfig;
    protected mGlobalArgs: ComputeGPUBuffer;
    protected mFlowComputePipeline: FlowSimulatorPipeline;

    constructor() {
        super();
        this.mConfig = {
            GROUP_SIZE: 128,
            NUM: 60000,
            SPAWN_RADIUS: 0.1,
            BASE_LIFETIME: 10,
            MAX_ADDITIONAL_LIFETIME: 5,
            PRESIMULATION_DELTA_TIME: 0.1,
            INITIAL_SPEED: 2,
            INITIAL_TURBULENCE: 0.3,
            NOISE_OCTAVES: 3,
            directionX: 0.0, //X -1~1
            directionY: 0.2, //Y -1~1
            directionZ: 0.0 //Z -1~1
        };
    }

    public init() {
        super.init();
        this.alwaysRender = true;
        this.geometry = new PlaneGeometry(0.01, 0.01, 1.0, 1.0, Vector3.Z_AXIS);
        this.material = new FlowSimulatorMaterial();
        let device = webGPUContext.device;

        var globalArgsData = new Float32Array(4);
        this.mGlobalArgs = new ComputeGPUBuffer(globalArgsData.byteLength);
        globalArgsData[0] = this.transform.worldMatrix.index;
        this.mGlobalArgs.setFloat32Array('', globalArgsData);
        this.mGlobalArgs.apply();

        this.mFlowComputePipeline = new FlowSimulatorPipeline(this.mConfig);

        this.instanceCount = this.mConfig.NUM;
    }

    public stop() {}

    public onCompute(view: View3D, command?: GPUCommandEncoder) {
        this.mFlowComputePipeline.compute(command);
    }

    public nodeUpdate(view: View3D, passType: PassType, renderPassState: RendererPassState, clusterLightingBuffer: ClusterLightingBuffer) {
        let material = this.materials[0];
        let passes = material.getPass(passType);
        if (passes) {
            for (let i = 0; i < passes.length; i++) {
                var subs = passes[i];
                subs.setStorageBuffer(`particlePosition`, this.mFlowComputePipeline.positionBuffer);
                subs.setStorageBuffer(`particleGlobalData`, this.mGlobalArgs);
            }
        }
        super.nodeUpdate(view, passType, renderPassState, clusterLightingBuffer);
    }
}

class FlowSimulatorBuffer {
    protected mPositionBuffer: ComputeGPUBuffer;
    protected mNewPositionBuffer: ComputeGPUBuffer;
    protected mSpawnBuffer: ComputeGPUBuffer;
    protected mInputData: Float32Array;
    protected mInputBuffer: ComputeGPUBuffer;
    protected mConfig: FlowSimulatorConfig;

    constructor(config: FlowSimulatorConfig) {
        this.mConfig = config;
        this.initGPUBuffer(config);
    }

    public updateInputData() {
        const { INITIAL_TURBULENCE, NOISE_OCTAVES, directionX, directionY, directionZ } = this.mConfig;

        this.mInputBuffer.setFloat('persistence', INITIAL_TURBULENCE);
        this.mInputBuffer.setFloat('OCTAVES', NOISE_OCTAVES);
        this.mInputBuffer.setFloat('directionX', directionX);
        this.mInputBuffer.setFloat('directionY', directionY);
        this.mInputBuffer.setFloat('directionZ', directionZ);
        this.mInputBuffer.apply();
    }

    public initGPUBuffer(config: FlowSimulatorConfig) {
        const { NUM, SPAWN_RADIUS, BASE_LIFETIME, MAX_ADDITIONAL_LIFETIME } = config;

        const position = new Float32Array(4 * NUM);
        for (let i = 0; i < NUM; ++i) {
            position[i * 4 + 0] = SPAWN_RADIUS * Math.pow(Math.random(), 1 / 3) * Math.sqrt(1.0 - Math.pow(Math.random() * 2.0 - 1.0, 2)) * Math.cos(Math.random() * 2.0 * Math.PI); // x
            position[i * 4 + 1] = SPAWN_RADIUS * Math.pow(Math.random(), 1 / 3) * Math.sqrt(1.0 - Math.pow(Math.random() * 2.0 - 1.0, 2)) * Math.sin(Math.random() * 2.0 * Math.PI); // y
            position[i * 4 + 2] = SPAWN_RADIUS * Math.pow(Math.random(), 1 / 3) * (Math.random() * 2.0 - 1.0); // z
            position[i * 4 + 3] = BASE_LIFETIME * Math.random(); // w
        }
        this.mPositionBuffer = new ComputeGPUBuffer(position.length);
        this.mPositionBuffer.setFloat32Array('', position);
        this.mPositionBuffer.apply();

        this.mNewPositionBuffer = new ComputeGPUBuffer(position.length);
        this.mNewPositionBuffer.setFloat32Array('', position);
        this.mNewPositionBuffer.apply();

        const spawn = new Float32Array(4 * NUM);
        for (let i = 0; i < NUM; ++i) {
            spawn[i * 4 + 0] = SPAWN_RADIUS * Math.pow(Math.random(), 1 / 3) * Math.sqrt(1.0 - Math.pow(Math.random() * 2.0 - 1.0, 2)) * Math.cos(Math.random() * 2.0 * Math.PI); // x
            spawn[i * 4 + 1] = SPAWN_RADIUS * Math.pow(Math.random(), 1 / 3) * Math.sqrt(1.0 - Math.pow(Math.random() * 2.0 - 1.0, 2)) * Math.sin(Math.random() * 2.0 * Math.PI); // y
            spawn[i * 4 + 2] = SPAWN_RADIUS * Math.pow(Math.random(), 1 / 3) * (Math.random() * 2.0 - 1.0); // z
            spawn[i * 4 + 3] = BASE_LIFETIME + Math.random() * MAX_ADDITIONAL_LIFETIME;
        }
        this.mSpawnBuffer = new ComputeGPUBuffer(spawn.length);
        this.mSpawnBuffer.setFloat32Array('', spawn);
        this.mSpawnBuffer.apply();

        const { PRESIMULATION_DELTA_TIME, INITIAL_TURBULENCE, NOISE_OCTAVES, directionX, directionY, directionZ } = config;
        this.mInputBuffer = new ComputeGPUBuffer(8);
        this.mInputBuffer.setFloat('count', NUM);
        this.mInputBuffer.setFloat('time', PRESIMULATION_DELTA_TIME);
        this.mInputBuffer.setFloat('deltatime', PRESIMULATION_DELTA_TIME);
        this.mInputBuffer.setFloat('persistence', INITIAL_TURBULENCE);
        this.mInputBuffer.setFloat('OCTAVES', NOISE_OCTAVES);
        this.mInputBuffer.setFloat('directionX', directionX);
        this.mInputBuffer.setFloat('directionY', directionY);
        this.mInputBuffer.setFloat('directionZ', directionZ);
        this.mInputBuffer.apply();
    }
}

class FlowSimulatorMaterial extends Material {
    doubleSided: any;
    constructor() {
        super();
        ShaderLib.register('FlowRenderShader', FlowRenderShader);
        let shader = new Shader();
        let pass = new RenderShaderPass('FlowRenderShader', 'FlowRenderShader');
        pass.setShaderEntry(`VertMain`, `FragMain`);

        shader.addRenderPass(pass);
        shader.setUniformVector4(`transformUV1`, new Vector4(0, 0, 1, 1));
        shader.setUniformVector4(`transformUV2`, new Vector4(0, 0, 1, 1));
        shader.setUniformColor(`baseColor`, new Color());
        shader.setUniformFloat(`alphaCutoff`, 0.5);
        shader.setUniformFloat(`shadowBias`, 0.00035);

        let shaderState = pass.shaderState;
        shaderState.acceptShadow = false;
        shaderState.receiveEnv = false;
        shaderState.acceptGI = false;
        shaderState.useLight = false;

        // default value
        this.baseMap = Engine3D.res.whiteTexture;
        this.shader = shader;

        // this.transparent = true ;
    }

    public set baseMap(value: Texture) {
        // this.onChange = true;
    }

    public set envMap(texture: Texture) {
        //not need env texture
    }

    public set shadowMap(texture: Texture) {
        //not need shadowMap texture
    }

    debug() {}
}

class FlowSimulatorPipeline extends FlowSimulatorBuffer {
    protected mSimulationComputeShader: ComputeShader;
    protected mDataCopyComputeShader: ComputeShader;
    protected mFirstFrame: boolean = false;

    constructor(config: FlowSimulatorConfig) {
        super(config);
        this.mConfig = config;
        this.initPipeline();
    }

    public get positionBuffer(): ComputeGPUBuffer {
        return this.mPositionBuffer;
    }

    public compute(command: GPUCommandEncoder) {
        const { BASE_LIFETIME, PRESIMULATION_DELTA_TIME, INITIAL_SPEED } = this.mConfig;

        if (this.mFirstFrame) {
            this.mInputBuffer.setFloat('time', PRESIMULATION_DELTA_TIME);
            this.mInputBuffer.setFloat('deltatime', PRESIMULATION_DELTA_TIME);
        } else {
            this.mInputBuffer.setFloat('time', Time.time / 1000.0);
            this.mInputBuffer.setFloat('deltatime', INITIAL_SPEED * (Time.delta / 1000.0));
        }
        this.updateInputData();

        let computePass = command.beginComputePass();

        for (var i = 0; i < (this.mFirstFrame ? BASE_LIFETIME / PRESIMULATION_DELTA_TIME : 1); ++i) {
            this.mSimulationComputeShader.compute(computePass);
            this.mDataCopyComputeShader.compute(computePass);
        }

        computePass.end();

        this.mFirstFrame = false;
    }

    protected initPipeline() {
        const { NUM, GROUP_SIZE } = this.mConfig;

        this.mSimulationComputeShader = new ComputeShader(simulation.cs);
        this.mSimulationComputeShader.setStorageBuffer(`input`, this.mInputBuffer);
        this.mSimulationComputeShader.setStorageBuffer(`position`, this.mPositionBuffer);
        this.mSimulationComputeShader.setStorageBuffer(`spawn`, this.mSpawnBuffer);
        this.mSimulationComputeShader.setStorageBuffer(`newposition`, this.mNewPositionBuffer);
        this.mSimulationComputeShader.workerSizeX = Math.ceil(NUM / GROUP_SIZE);

        this.mDataCopyComputeShader = new ComputeShader(copy.cs);
        this.mDataCopyComputeShader.setStorageBuffer(`input`, this.mInputBuffer);
        this.mDataCopyComputeShader.setStorageBuffer(`position`, this.mPositionBuffer);
        this.mDataCopyComputeShader.setStorageBuffer(`newposition`, this.mNewPositionBuffer);
        this.mDataCopyComputeShader.workerSizeX = Math.ceil(NUM / GROUP_SIZE);
    }
}

let FlowRenderShader = /* wgsl */ `
    #include "Common_vert"
    #include "Common_frag"
    #include "UnLit_frag"
    #include "UnLitMaterialUniform_frag"
    #include "MathShader"

    struct Particle_global {
        instance_index : f32,
        particles_Radius : f32,
        time : f32,
        timeDelta : f32,
    };

    @group(1) @binding(0)
    var baseMapSampler: sampler;

    @group(1) @binding(1)
    var baseMap: texture_2d<f32>;

    @group(3) @binding(0)
    var<storage, read> particlePosition : array<vec4<f32>>;

    @group(3) @binding(1)
    var<storage, read> particleGlobalData: Particle_global;

    fn calcBillboard( pos : vec3<f32>, worldMatrix:mat4x4<f32> ) -> mat4x4<f32>{
        var dir:vec3<f32> = normalize(globalUniform.cameraWorldMatrix[3].xyz - pos.xyz) ;
        let mat3 = mat3x3<f32>(
           worldMatrix[0].xyz,
           worldMatrix[1].xyz,
           worldMatrix[2].xyz
        );
        var v3Look:vec3<f32> = normalize( dir * mat3 )  ;
        var v3Right:vec3<f32> = normalize( cross( vec3<f32>( 0.0 , 1.0 , 0.0 ) * mat3 , v3Look ));
        var v3Up:vec3<f32> = cross( v3Look , v3Right );
        var matLookAt : mat4x4<f32> = mat4x4<f32>(
           vec4<f32>( v3Right.xyz , 0.0 ),
           vec4<f32>( v3Up.xyz , 0.0 ),
           vec4<f32>( v3Look.xyz , 0.0 ),
           vec4<f32>( 0.0,0.0,0.0 , 1.0 )
        );
        return matLookAt ;
    }

    fn vert(vertex:VertexAttributes) -> VertexOutput {
        var particlePos = particlePosition[vertex.index];
        var worldMatrix = models.matrix[u32(particleGlobalData.instance_index)];

        var wPosition = vertex.position.xyz;

        // wPosition *= particleGlobalData.particles_Radius;

        var v_mat4:mat4x4<f32> = calcBillboard(particlePos.xyz, worldMatrix);
        wPosition = ( v_mat4 * vec4(wPosition,1.0) ).xyz;

        wPosition.x += particlePos.x;
        wPosition.y += particlePos.y;
        wPosition.z += particlePos.z;

        ORI_VertexOut.varying_UV0 = vertex.uv;
    
        var worldPos = (worldMatrix * vec4<f32>(wPosition.xyz, 1.0));
        var viewPosition = ((globalUniform.viewMat) * worldPos);

        ORI_VertexOut.varying_WPos = worldPos;
        ORI_VertexOut.varying_WPos.w = f32(particleGlobalData.instance_index);

        var clipPosition = globalUniform.projMat * viewPosition ;

        //ORI_VertexOut.varying_ViewPos = clipPosition.xyz;

        ORI_VertexOut.member = clipPosition;
            
        //ORI_VertexOut.fragCoord = normalize(vertex.position.xy) + vec2<f32>(0.5, 0.5);
        // ORI_VertexOut.color = particleColor[vertex.index];
        ORI_VertexOut.varying_Color = vec4<f32>(1.0, 0.0, 0.0, 1.0);
        return ORI_VertexOut;
    }

    fn frag() {
        let color = ORI_VertexVarying.vColor;
        ORI_ShadingInput.BaseColor = color;
        UnLit();
    }`;

class copy {
    public static cs: string = /* wgsl */ `
            struct InputArgs {
                count: f32,
                time: f32,
                deltatime: f32,
                persistence: f32,
                OCTAVES: f32,
                directionX: f32,
                directionY: f32,
                directionZ: f32,
            };
    
            @group(0) @binding(0) var<storage, read> input: InputArgs;
            @group(0) @binding(1) var<storage, read_write> position: array<vec4<f32>>;
            @group(0) @binding(2) var<storage, read_write> newposition: array<vec4<f32>>;
            
            const size = u32(128);
            @compute @workgroup_size(size)
            fn CsMain(
                @builtin(global_invocation_id) GlobalInvocationID : vec3<u32>,
                @builtin(num_workgroups) GroupSize: vec3<u32>
            ) {
                var index = GlobalInvocationID.x;
                if(index >= u32(input.count)){
                    return;
                }

                position[index] = newposition[index];
            }
        `;
}

class simulation {
    public static cs: string = /* wgsl */ `

        struct InputArgs {
            count: f32,
            time: f32,
            deltatime: f32,
            persistence: f32,
            OCTAVES: f32,
            directionX: f32,
            directionY: f32,
            directionZ: f32,
        };

        @group(0) @binding(0) var<storage, read> input: InputArgs;
        @group(0) @binding(1) var<storage, read_write> position: array<vec4<f32>>;
        @group(0) @binding(2) var<storage, read_write> spawn: array<vec4<f32>>;
        @group(0) @binding(3) var<storage, read_write> newposition: array<vec4<f32>>;
        
        fn mod289v(x: vec4<f32>) -> vec4<f32>{
            return x - floor(x * (1.0 / 289.0)) * 289.0;
        }
        
        fn mod289(x: f32) -> f32{
            return x - floor(x * (1.0 / 289.0)) * 289.0;
        }
        
        fn permutev(x: vec4<f32>) -> vec4<f32>{
            return mod289v(((x*34.0)+1.0)*x);
        }
        
        fn permute(x: f32) -> f32{
            return mod289(((x*34.0)+1.0)*x);
        }
        
        fn taylorInvSqrtv(r: vec4<f32>) -> vec4<f32>{
            return 1.79284291400159 - 0.85373472095314 * r;
        }
        
        fn taylorInvSqrt(r: f32) -> f32{
            return 1.79284291400159 - 0.85373472095314 * r;
        }
        
        fn grad4(j: f32, ip: vec4<f32>) -> vec4<f32>{
            let ones = vec4(1.0, 1.0, 1.0, -1.0);
            var p: vec4<f32>;
            var s: vec4<f32>;
            p = vec4(floor(fract(vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0, 0.0);
            p.w = 1.5 - dot(abs(p.xyz), ones.xyz);
            s = vec4<f32>(p < vec4(0.0));
            p = vec4(p.xyz + (s.xyz*2.0 - 1.0) * s.www, p.w); 
            return p;
        }
        
        fn simplexNoiseDerivatives(v: vec4<f32>) -> vec4<f32>{
            let C = vec4( 0.138196601125011,0.276393202250021,0.414589803375032,-0.447213595499958);
            let F4 = 0.309016994374947451;
            var i  = floor(v + dot(v, vec4(F4)) );
            var x0 = v -   i + dot(i, C.xxxx);
            var i0: vec4<f32>;
            var isX = step( x0.yzw, x0.xxx );
            var isYZ = step( x0.zww, x0.yyz );
            // i0.x = isX.x + isX.y + isX.z;
            // i0.yzw = 1.0 - isX;
            i0 = vec4(isX.x + isX.y + isX.z, 1.0 - isX);
            i0.y += isYZ.x + isYZ.y;
            // i0.zw += 1.0 - isYZ.xy;
            i0 = vec4(i0.xy, i0.zw + (1.0 - isYZ.xy));
            i0.z += isYZ.z;
            i0.w += 1.0 - isYZ.z;
            var i3 = clamp( i0, vec4<f32>(0.0), vec4<f32>(1.0) );
            var i2 = clamp( i0 - 1.0, vec4<f32>(0.0), vec4<f32>(1.0) );
            var i1 = clamp( i0 - 2.0, vec4<f32>(0.0), vec4<f32>(1.0) );
            var x1 = x0 - i1 + C.xxxx;
            var x2 = x0 - i2 + C.yyyy;
            var x3 = x0 - i3 + C.zzzz;
            var x4 = x0 + C.wwww;
            i = mod289v(i); 
            var j0 = permute( permute( permute( permute(i.w) + i.z) + i.y) + i.x);
            var j1 = permutev( permutev( permutev( permutev (
                    i.w + vec4(i1.w, i2.w, i3.w, 1.0 ))
                    + i.z + vec4(i1.z, i2.z, i3.z, 1.0 ))
                    + i.y + vec4(i1.y, i2.y, i3.y, 1.0 ))
                    + i.x + vec4(i1.x, i2.x, i3.x, 1.0 ));
            var ip = vec4(1.0/294.0, 1.0/49.0, 1.0/7.0, 0.0) ;
            var p0 = grad4(j0,   ip);
            var p1 = grad4(j1.x, ip);
            var p2 = grad4(j1.y, ip);
            var p3 = grad4(j1.z, ip);
            var p4 = grad4(j1.w, ip);
            var norm = taylorInvSqrtv(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
            p0 *= norm.x;
            p1 *= norm.y;
            p2 *= norm.z;
            p3 *= norm.w;
            p4 *= taylorInvSqrt(dot(p4,p4));
            var values0 = vec3(dot(p0, x0), dot(p1, x1), dot(p2, x2));
            var values1 = vec2(dot(p3, x3), dot(p4, x4));
            var m0 = max(0.5 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), vec3<f32>(0.0));
            var m1 = max(0.5 - vec2(dot(x3,x3), dot(x4,x4)), vec2<f32>(0.0));
            var temp0 = -6.0 * m0 * m0 * values0;
            var temp1 = -6.0 * m1 * m1 * values1;
            var mmm0 = m0 * m0 * m0;
            var mmm1 = m1 * m1 * m1;
            var dx = temp0[0] * x0.x + temp0[1] * x1.x + temp0[2] * x2.x + temp1[0] * x3.x + temp1[1] * x4.x + mmm0[0] * p0.x + mmm0[1] * p1.x + mmm0[2] * p2.x + mmm1[0] * p3.x + mmm1[1] * p4.x;
            var dy = temp0[0] * x0.y + temp0[1] * x1.y + temp0[2] * x2.y + temp1[0] * x3.y + temp1[1] * x4.y + mmm0[0] * p0.y + mmm0[1] * p1.y + mmm0[2] * p2.y + mmm1[0] * p3.y + mmm1[1] * p4.y;
            var dz = temp0[0] * x0.z + temp0[1] * x1.z + temp0[2] * x2.z + temp1[0] * x3.z + temp1[1] * x4.z + mmm0[0] * p0.z + mmm0[1] * p1.z + mmm0[2] * p2.z + mmm1[0] * p3.z + mmm1[1] * p4.z;
            var dw = temp0[0] * x0.w + temp0[1] * x1.w + temp0[2] * x2.w + temp1[0] * x3.w + temp1[1] * x4.w + mmm0[0] * p0.w + mmm0[1] * p1.w + mmm0[2] * p2.w + mmm1[0] * p3.w + mmm1[1] * p4.w;
            return vec4(dx, dy, dz, dw) * 49.0;
        }
        
        const size = u32(128);
        @compute @workgroup_size(size)
        fn CsMain(
            @builtin(global_invocation_id) GlobalInvocationID : vec3<u32>,
            @builtin(num_workgroups) GroupSize: vec3<u32>
        ) {
            var index = GlobalInvocationID.x;
            if(index >= u32(input.count)){
                return;
            }
            
            let time = input.time;
            let deltatime = input.deltatime;
            let persistence = input.persistence;
            var OCTAVES = u32(input.OCTAVES);
            let directionX = input.directionX;
            let directionY = input.directionY;
            let directionZ = input.directionZ;
        
            var oldPosition = vec3<f32>(position[index][0], position[index][1], position[index][2]);
            var noisePosition = oldPosition * 1.50000000;
            var noiseTime = time * 0.00025000;
            var xNoisePotentialDerivatives = vec4(0.0);
            var yNoisePotentialDerivatives = vec4(0.0);
            var zNoisePotentialDerivatives = vec4(0.0);
            
            for (var i = u32(0); i < OCTAVES; i++) {
                var scale = (1.0 / 2.0) * pow(2.0, f32(i));
                var noiseScale = pow(persistence, f32(i));
        
                if (persistence == 0.0 && i == u32(0)) {
                    noiseScale = 1.0;
                }
                xNoisePotentialDerivatives += simplexNoiseDerivatives(vec4(noisePosition * pow(2.0, f32(i)), noiseTime)) * noiseScale * scale;
                yNoisePotentialDerivatives += simplexNoiseDerivatives(vec4((noisePosition + vec3(123.4, 129845.6, -1239.1)) * pow(2.0, f32(i)), noiseTime)) * noiseScale * scale;
                zNoisePotentialDerivatives += simplexNoiseDerivatives(vec4((noisePosition + vec3(-9519.0, 9051.0, -123.0)) * pow(2.0, f32(i)), noiseTime)) * noiseScale * scale;
            }
        
            var noiseVelocity = vec3(zNoisePotentialDerivatives[1] - yNoisePotentialDerivatives[2], 
                                    xNoisePotentialDerivatives[2] - zNoisePotentialDerivatives[0],
                                    yNoisePotentialDerivatives[0] - xNoisePotentialDerivatives[1]) * 0.07500000;
            var velocity = vec3(directionX, directionY, directionZ);
            var totalVelocity = velocity + noiseVelocity;
        
            var temp = oldPosition + totalVelocity * deltatime;
        
            var oldLifetime = position[index][3];
            var newLifetime = oldLifetime - deltatime;
            if (newLifetime < 0.0) {
                temp = vec3<f32>(spawn[index][0], spawn[index][1], spawn[index][2]);
                newLifetime = spawn[index][3] + newLifetime;
            }
            
            newposition[index][0] = temp.x;
            newposition[index][1] = temp.y;
            newposition[index][2] = temp.z;
            newposition[index][3] = newLifetime;
        }
    `;
}

new Demo_Flow().run()

Released under the MIT License