Demo.prototype.addEffectStarfield = function (start, end, particle, options = {}) {
  const recalcThreshold = 0.1;

  const amountOfParticles = particle.count;
  const texture = particle.texture;
  const areaSizeX = options.areaSizeX ?? 450;
  const areaSizeY = options.areaSizeY ?? 450;
  const areaSizeZ = options.areaSizeZ ?? 450;
  const particleSize = options.particleSize ?? 2.5;
  const parentId = options.parentId ?? undefined;
  const x = options.x ?? 0;
  const y = options.y ?? 0;
  const z = options.z ?? 0;
  const speed = options.speed ?? 1.0;
  const directionFlip = options.directionFlip ?? 0.0;
  const enabled = options.enabled ?? null;
  const enabledOnStart = options.enabledOnStart ?? true;
  const blending = options.blending ?? "AdditiveBlending";
  let stars = new Array(amountOfParticles);
  const sizeX = areaSizeX;
  const sizeY = areaSizeY;
  const sizeZ = areaSizeZ;
  const colorR = options.colorR ?? 1.0;
  const colorG = options.colorG ?? 1.0;
  const colorB = options.colorB ?? 1.0;
  Utils.setSeed(options.seed ?? 666);
  for (let i = 0; i < stars.length; i++) {
    let z1 = Utils.random();

    let neonColor = undefined;
    if (options.randomNeon) {
      neonColor = {
        r: Math.random(),
        g: 0.0,
        b: Math.random()
      };
    }
    stars[i] = {
      "x0": Utils.random() * sizeX * 2 - sizeX + x,
      "y0": Utils.random() * sizeY * 2 - sizeY + y,
      "x1": Utils.random() * sizeX * 2 - sizeX + x,
      "y1": Utils.random() * sizeY * 2 - sizeY + y,
      "z1": Utils.random() * sizeZ * 2 - sizeZ + z,
      "z2": z,
      "enabled": enabledOnStart ? 1 : 0,
      "snapPrev": false,
      ...(neonColor ? { neonColor } : {})
    };
  }

  const durationTime = end - start;
  this.loader.addAnimation({
    "start": start, "duration": durationTime,
    "image": texture,
    textureProperties: [{}, { minFilter: 'NearestMipmapNearestFilter', magFilter: 'LinearFilter' }],
    "parent": parentId,
    "position": [{
      "x": 0,
      "y": 0,
      "z": 0,
    }],
    color: [{
      r: colorR,
      g: colorG,
      b: colorB
    }],
    "angle": [{ "degreesY": 180 }],
    "perspective": "3d",
    "billboard": true,
    "additive": true,
    "material": {
      "blending": blending,
      "transparent": true,
      "depthWrite": false,

    },
    "scale": [{ "uniform3d": 1 }],
    "instancer": {
      "count": stars.length,
      "runInstanceFunction": (properties) => {

        const i = properties.index;
        const count = properties.count;
        const time = properties.time;
        let object = properties.object;
        let color = properties.color;

        let scale = particleSize;




        const range = sizeZ * 2;
        const percent = -(speed * .15 * getSceneTimeFromStart()) % 1.0;
        //const percent = -(.15 * getSceneTimeFromStart()) % 1.0;
        stars[i].z2 = (percent * range + stars[i].z1);
        if (stars[i].z2 < -sizeZ) {
          stars[i].z2 += range;
          if (!stars[i].snapPrev) {
            if (enabled && Sync.get(enabled) > .1)
              stars[i].enabled = 1.0;
            else if (!enabled)
              stars[i].enabled = 1.0;
            else
              stars[i].enabled = 0.0;
          }
          stars[i].snapPrev = true;
        }
        else
          stars[i].snapPrev = false;

        scale *= stars[i].enabled;

        object.scale.x = scale;
        object.scale.y = scale;
        object.scale.z = scale;

        object.position.z = stars[i].z2;
        object.position.x = stars[i].x1;
        object.position.y = stars[i].y1;


      }
    }

  });
}

Demo.prototype.addEffectMeshStarfield = function (start, end, particle, options = {}) {

  const amountOfParticles = particle.count;
  const objectName = particle.object;
  const areaSizeX = options.areaSizeX ?? 450;
  const areaSizeY = options.areaSizeY ?? 450;
  const areaSizeZ = options.areaSizeZ ?? 450;
  const particleSize = options.particleSize ?? 2.5;
  const parentId = options.parentId ?? undefined;
  const x = options.x ?? 0;
  const y = options.y ?? 0;
  const z = options.z ?? 0;
  const speed = options.speed ?? 1.0;
  const colorR = options.colorR ?? 1.0;
  const colorG = options.colorG ?? 1.0;
  const colorB = options.colorB ?? 1.0;
  const enabled = options.enabled ?? null;
  const enabledOnStart = options.enabledOnStart ?? false;
  const safeZoneX = options.safeZoneX ?? 0;
  const safeZoneY = options.safeZoneY ?? 0;
  const targetCenter = options.targetCenter ?? false;
  const targetCenterRealtime = options.targetCenterRealtime ?? false;
  const randomAngle = options.randomAngle ?? false;
  let meshStars = new Array(amountOfParticles);
  const sizeX = areaSizeX;
  const sizeY = areaSizeY;
  const sizeZ = areaSizeZ;
  const halfSafeX = safeZoneX * 0.5;
  const halfSafeY = safeZoneY * 0.5;
  Utils.setSeed(options.seed ?? 666);
  for (let i = 0; i < meshStars.length; i++) {
    let angleY = 0;
    let angleX = 0;
    let angleZ = 0;

    if (randomAngle) {
      angleY = Utils.random() * Math.PI * 2;
      angleX = Utils.random() * Math.PI * 2;
      angleZ = Utils.random() * Math.PI * 2;
    }
    let px = Utils.random() * sizeX * 2 - sizeX;
    let py = Utils.random() * sizeY * 2 - sizeY;
    if (safeZoneX > 0 && safeZoneY > 0) {
      if (Math.abs(px) < halfSafeX && Math.abs(py) < halfSafeY) {

        if (Math.abs(px) / halfSafeX < Math.abs(py) / halfSafeY) {
          px = (px >= 0 ? halfSafeX : -halfSafeX) + (px >= 0 ? 1 : -1) * Utils.random() * (sizeX - halfSafeX);
        } else {
          py = (py >= 0 ? halfSafeY : -halfSafeY) + (py >= 0 ? 1 : -1) * Utils.random() * (sizeY - halfSafeY);
        }
      }
    }

    if (targetCenter) {

      const dx = px;
      const dy = py;
      const dz = Math.sqrt(px * px + py * py);
      angleY = Math.atan2(dx, dz);
      angleX = Math.sin(getSceneTimeFromStart() * 15 + i) * .314 + Math.atan2(-dy, Math.sqrt(dx * dx + dz * dz));
    } else {
      angleY = .5 * (Utils.random() * Math.PI - Math.PI * 0.5);
      angleX = .5 * (Utils.random() * Math.PI - Math.PI * 0.5);
    }

    meshStars[i] = {
      "x1": px,
      "y1": py,
      "z1": Utils.random() * sizeZ * 2 - sizeZ,
      "z2": z,
      "scaleVariance": Utils.random() * .5 + .5,
      "enabled": enabledOnStart ? 1 : 0,
      "snapPrev": false,
      "angleY": angleY,
      "angleX": angleX,
      "angleZ": angleZ
    };
  }

  const durationTime = end - start;
  this.loader.addAnimation({
    "start": start, "duration": durationTime,
    "object": {
      "name": objectName
    },
    "parent": parentId,
    "position": [{
      "x": 0,
      "y": 0,
      "z": -sizeZ,
    }],
    "angle": [{ "degreesY": 180 }],
    "perspective": "3d",
    "color": [{
      "r": 1.0,
      "g": 1.0,
      "b": 1.0
    }],
    shader: {
      vertexShaderPrefix: `
            uniform float uvyOffset;
            `,
      vertexShaderSuffix: `
                vMapUv.y += uvyOffset;
        `,
      variable: [
        { name: 'uvyOffset', value: [options.uvyOffset ?? 0] },
      ]
    },
    "scale": [{ "uniform3d": 1 }],
    "instancer": {
      "count": meshStars.length,
      "runInstanceFunction": (properties) => {

        const i = properties.index;
        const count = properties.count;
        const time = properties.time;
        let angle = properties.angle;
        let object = properties.object;
        let color = properties.color;

        const range = sizeZ * 2;

        const rawZ = meshStars[i].z1 - speed * 0.15 * getSceneTimeFromStart() * range;
        const newZ = ((rawZ + sizeZ) % range + range) % range - sizeZ;

        const didWrap = (meshStars[i].z2 < -sizeZ * 0.25 && newZ > sizeZ * 0.25);
        meshStars[i].z2 = newZ;

        if (didWrap) {
          if (enabled && Sync.get(enabled) > .1)
            meshStars[i].enabled = 1.0;
          else if (!enabled)
            meshStars[i].enabled = 1.0;
          else
            meshStars[i].enabled = 0.0;
        }

        const currentSize = typeof particleSize === 'function' ? particleSize() : particleSize;
        let scale = meshStars[i].scaleVariance * currentSize * meshStars[i].enabled;
        object.color = properties.color;
        object.scale.x = scale;
        object.scale.y = scale;
        object.scale.z = scale;

        object.position.z = meshStars[i].z2;
        object.position.x = meshStars[i].x1;
        object.position.y = meshStars[i].y1;

        if (targetCenterRealtime) {
          const px = meshStars[i].x1;
          const py = meshStars[i].y1;
          const pz = meshStars[i].z2;

          const dx = px;
          const dy = py;
          const dz = +5 - pz;
          angle.degreesY = Math.atan2(dx, dz) + Math.sin(-getSceneTimeFromStart() * 4 + i) * .164;
          angle.degreesX = Math.sin(getSceneTimeFromStart() * 5 + i) * .314 + Math.atan2(-dy, Math.sqrt(dx * dx + dz * dz));
        } else {
          angle.degreesZ = meshStars[i].angleZ;
          angle.degreesY = meshStars[i].angleY;
          angle.degreesX = meshStars[i].angleX;
        }
      }
    }
  });
}



Demo.prototype.addEffectMeshRepeater = function (start, end, particle, options = {}) {
const envMat = {
  type: 'Standard',
  envMap: {
    name: options.env,
    textureProperties: {
      mapping: 'EquirectangularReflectionMapping',
    }
  },
  roughness: 0.0,
  metalness: 1.0
};
  const count = particle.count;
  const objectName = particle.object;
  const x = options.x ?? 0;
  const y = options.y ?? 0;
  const z = options.z ?? 0;
  const particleX = options.particleX ?? null;
  const particleY = options.particleY ?? null;
  const particleZ = options.particleZ ?? null;
  const bomb = options.bomb ?? false;
  const bombExplosion = options.bombExplosion ?? {};
  const bombExplosionEnabled = bombExplosion.enabled ?? null;
  const particleAngleX = options.particleAngleX ?? null;
  const particleAngleY = options.particleAngleY ?? null;
  const particleAngleZ = options.particleAngleZ ?? null;
  const spacing = options.spacing ?? 10;
  const speed = options.speed ?? 1.0;
  const particleSize = options.particleSize ?? 1.0;
  const parentId = options.parentId ?? undefined;
  const colorR = options.colorR ?? 1.0;
  const colorG = options.colorG ?? 1.0;
  const colorB = options.colorB ?? 1.0;
  const enabled = options.enabled ?? null;
  const enabledOnStart = options.enabledOnStart ?? true;
  const angleX = options.angleX ?? 0;
  const angleY = options.angleY ?? 0;
  const angleZ = options.angleZ ?? 0;
  const hueRotateEnabled = options.hueRotateEnabled ?? 0;
  const hueRotate = options.hueRotate ?? (() => getSceneTimeFromStart());
  const totalLength = count * spacing;
  const self = this;
  const env = options.env ? true : false;
  const shader = options.shader ?? null;
  const material = options.material ?? undefined;
  const maroHack = options.maroHack ?? false;


  let items = new Array(count);
  for (let i = 0; i < count; i++) {
    const zOffset = particleZ ? (typeof particleZ === 'function' ? particleZ({ index: i }) : particleZ) : 0;
    items[i] = {
      "z1": i * spacing + zOffset,
      "z2": i * spacing + zOffset,
      "enabled": enabledOnStart ? 1 : 0,
      "bombLanded": false,
      "bombLandTime": 0,
      "bombLandWorldX": 0,
      "bombLandWorldY": 0,
      "bombLandWorldZ": 0
    };
  }

  const durationTime = end - start;
  this.loader.addAnimation({
    "start": start, "duration": durationTime,
    "object": {
      "name": objectName
    },
    "parent": parentId,
    "position": [{
      "x": x,
      "y": y,
      "z": z,
    }],
    "perspective": "3d",
    "color": [{
      "r": colorR,
      "g": colorG,
      "b": colorB
    }],
    angle: [{ "degreesZ": options.repeaterAngle ?? 0 }],
    "scale": [{ "uniform3d": options.hackScale ?? 1.0 }],
    shader: shader ? shader : {
      fragmentShaderPrefix: window.hueFSPrefix,
      fragmentShaderSuffix: window.hueFSSuffix,
      variable: [
        { name: 'enabled', value: [hueRotateEnabled] },
        { name: 'hueRotate', value: [hueRotate] },
      ]
    },
    material: env ? envMat : material,

    "instancer": {
      "count": items.length,
      "runInstanceFunction": (properties) => {

        const i = properties.index;
        const time = properties.time;
        let angle = properties.angle;
        let object = properties.object;


        const rawZ = items[i].z1 - speed * getSceneTimeFromStart();
        const newZ = ((rawZ % totalLength) + totalLength) % totalLength;


        const prevZ = items[i].z2;
        const didWrap = Math.abs(newZ - prevZ) > totalLength * 0.5;
        items[i].z2 = newZ;

        if (didWrap) {
          if (enabled && Sync.get(enabled) > .1)
            items[i].enabled = 1.0;
          else if (!enabled)
            items[i].enabled = 1.0;
          else
            items[i].enabled = 0.0;
          items[i].bombLanded = false;
        }

        if (maroHack && items[i].enabled == 1.0 && i !== 6) {
            items[i].enabled = i === 6 ? 1.0 : 0.0;
        }

        const currentSize = typeof particleSize === 'function' ? particleSize(properties) : particleSize;
        let scale = currentSize * items[i].enabled;

        object.scale.x = scale;
        object.scale.y = scale;
        object.scale.z = scale;


        const offsetX = particleX ? (typeof particleX === 'function' ? particleX(properties) : particleX) : 0;
        const offsetY = particleY ? (typeof particleY === 'function' ? particleY(properties) : particleY) : 0;

        object.position.x = offsetX;
        object.position.y = offsetY;
        object.position.z = newZ - totalLength * 0.5;

        const particleRotX = particleAngleX ? (typeof particleAngleX === 'function' ? particleAngleX(properties) : particleAngleX) : 0;
        const particleRotY = particleAngleY ? (typeof particleAngleY === 'function' ? particleAngleY(properties) : particleAngleY) : 0;
        const particleRotZ = particleAngleZ ? (typeof particleAngleZ === 'function' ? particleAngleZ(properties) : particleAngleZ) : 0;

        angle.degreesX = (typeof angleX === 'function' ? angleX() : angleX) + particleRotX;
        angle.degreesY = (typeof angleY === 'function' ? angleY() : angleY) + particleRotY;
        angle.degreesZ = (typeof angleZ === 'function' ? angleZ() : angleZ) + particleRotZ;


        if (bomb && items[i].enabled) {
          const bombTarget = bombExplosion.bombEndY ?? 0;


          if (items[i].bombLanded && offsetY > bombTarget + 2) {
            items[i].bombLanded = false;
          }

          if (!items[i].bombLanded && (!bombExplosionEnabled || Sync.get(bombExplosionEnabled) > 0.1)) {
            if (offsetY <= bombTarget + 0.5) {
              items[i].bombLanded = true;
              items[i].bombLandTime = getSceneTimeFromStart();
              items[i].bombLandWorldX = (typeof x === 'function' ? x() : x) + offsetX;
              items[i].bombLandWorldY = (typeof y === 'function' ? y() : y) + bombTarget;
              items[i].bombLandWorldZ = (typeof z === 'function' ? z() : z) + object.position.z;
            }
          }
        }

      }
    }
  });


  if (bomb) {
    const exp = bombExplosion;
    const expAmount = exp.amount ?? 15;
    const expDuration = exp.duration ?? 1.5;
    const expScale = exp.particleScale ?? 1.0;
    const expMaxDist = exp.maxDist ?? 3;
    const totalExpParticles = count * expAmount;

    function generateExpParticles(total) {
      let arr = new Array(total);
      for (let j = 0; j < total; j++) {
        let xS = Math.random() * (exp.xStartDim ?? 1) - (exp.xStartDim ?? 1) * 0.5;
        let yS = Math.random() * (exp.yStartDim ?? 1) - (exp.yStartDim ?? 1) * 0.5;
        let zS = Math.random() * (exp.zStartDim ?? 1) - (exp.zStartDim ?? 1) * 0.5;
        let xDir = (xS != 0) ? xS / Math.abs(xS) : 0;
        let yDir = (yS != 0) ? yS / Math.abs(yS) : 0;
        let zDir = (zS != 0) ? zS / Math.abs(zS) : 0;
        let xD = Math.random() * ((exp.xDim ?? 0.1) + xDir);
        let yD = Math.random() * ((exp.yDim ?? 0.1) + yDir);
        let zD = Math.random() * ((exp.zDim ?? 0.0) + zDir);
        let len = Math.sqrt(xD * xD + yD * yD + zD * zD) || 1;

        arr[j] = {
          x1: xS, y1: yS, z1: zS,
          x2: len * xDir * expMaxDist + (xD / len) * (Math.random() * expMaxDist * 2 - expMaxDist) + (exp.xOffset ?? 0) + xS,
          y2: len * yDir * expMaxDist + (yD / len) * (Math.random() * expMaxDist * 2 - expMaxDist) + (exp.yOffset ?? 0) + yS,
          z2: len * zDir * expMaxDist + (zD / len) * (Math.random() * expMaxDist * 2 - expMaxDist) + (exp.zOffset ?? 0) + zS,
          startAngle: Math.random() * 360,
          speedAngle: (Math.random() * 4 - 2) * 250,
        };
      }
      return arr;
    }

    function makeExpRenderFn(particles) {
      return (properties) => {
        const item = items[Math.floor(properties.index / expAmount)];
        const now = getSceneTimeFromStart();
        const elapsed = now - item.bombLandTime;
        const percent = elapsed / expDuration;

        if (!item.bombLanded || !item.bombLandTime
          || (bombExplosionEnabled && Sync.get(bombExplosionEnabled) <= 0.1)
          || percent < 0 || percent > 1) {
          properties.object.scale.x = 0;
          properties.object.scale.y = 0;
          properties.object.scale.z = 0;
          return;
        }

        const p = particles[properties.index];
        const mix = Math.log((percent * 7.0 + 1.0)) / 3;

        properties.object.position.x = Utils.mix(p.x1, p.x2, mix) + item.bombLandWorldX;
        properties.object.position.y = Utils.mix(p.y1, p.y2, mix) + item.bombLandWorldY;
        properties.object.position.z = Utils.mix(p.z1, p.z2, mix) + item.bombLandWorldZ - speed * elapsed;
        properties.object.scale.x = expScale;
        properties.object.scale.y = expScale;
        properties.object.scale.z = expScale;
        properties.angle.degreesZ = p.startAngle + now * p.speedAngle;

        const alpha = 1.0 - percent * percent;
        properties.color.a = alpha;
        properties.color.r = alpha;
        properties.color.g = alpha;
        properties.color.b = alpha;
      };
    }

    const explosionLayers = [
      { texture: exp.particleTexture ?? 'img/particle.png', blending: exp.blendingMode ?? "AdditiveBlending" },
      { texture: exp.particleTexture2 ?? 'img/particle.png', blending: exp.blendingMode2 ?? "SubtractiveBlending" },
    ];

    explosionLayers.forEach(function (layer) {
      self.loader.addAnimation({
        "start": start, "duration": durationTime,
        "image": layer.texture,
        "perspective": "3d",
        "billboard": true,
        "scale": [{ "uniform3d": 1.0 }],
        "material": {
          "blending": layer.blending,
          "transparent": true,
          "depthWrite": false
        },
        "instancer": {
          "count": totalExpParticles,
          "runInstanceFunction": makeExpRenderFn(generateExpParticles(totalExpParticles))
        }
      });
    });
  }
}