public static void SimulateParticleSpawn(IPartSysExternal external, PartSysEmitter emitter, int particleIdx, float timeToSimulate) { var spec = emitter.GetSpec(); var partSpawnTime = emitter.GetParticleSpawnTime(particleIdx); var worldPosVar = emitter.GetWorldPosVar(); var particleX = worldPosVar.X; var particleY = worldPosVar.Y; var particleZ = worldPosVar.Z; var emitOffsetX = GetParticleValue(emitter, particleIdx, PartSysParamId.emit_offset_X, partSpawnTime); var emitOffsetY = GetParticleValue(emitter, particleIdx, PartSysParamId.emit_offset_Y, partSpawnTime); var emitOffsetZ = GetParticleValue(emitter, particleIdx, PartSysParamId.emit_offset_Z, partSpawnTime); if (spec.GetOffsetCoordSys() == PartSysCoordSys.Polar) { var coords = SphericalDegToCartesian(emitOffsetX, emitOffsetY, emitOffsetZ); particleX += coords.X; particleY += coords.Y; particleZ += coords.Z; } else { particleX += emitOffsetX; particleY += emitOffsetY; particleZ += emitOffsetZ; } switch (spec.GetSpace()) { case PartSysEmitterSpace.ObjectPos: case PartSysEmitterSpace.ObjectYpr: { if (spec.GetParticleSpace() != PartSysParticleSpace.SameAsEmitter) { // TODO: Figure out this formula... var scale = 1.0f - emitter.GetParticleAge(particleIdx) / timeToSimulate; var prevObjPos = emitter.GetPrevObjPos(); var objPos = emitter.GetObjPos(); var dX = prevObjPos.X + (objPos.X - prevObjPos.X) * scale; var dY = prevObjPos.Y + (objPos.Y - prevObjPos.Y) * scale; var dZ = prevObjPos.Z + (objPos.Z - prevObjPos.Z) * scale; var rotation = 0.0f; if (spec.GetSpace() == PartSysEmitterSpace.ObjectYpr) { rotation = emitter.GetPrevObjRotation() + (emitter.GetObjRotation() - emitter.GetPrevObjRotation()) * scale; } RotateAndMove(dX, dY, dZ, rotation, ref particleX, ref particleY, ref particleZ); } } break; case PartSysEmitterSpace.Bones: { var scale = 1.0f - emitter.GetParticleAge(particleIdx) / timeToSimulate; if (emitter.GetBoneState() == null) { break; } if (emitter.GetBoneState().GetRandomPos(scale, out var bonePos)) { particleX = bonePos.X; particleY = bonePos.Y; particleZ = bonePos.Z; } } break; case PartSysEmitterSpace.NodePos: case PartSysEmitterSpace.NodeYpr: { if (spec.GetParticleSpace() != PartSysParticleSpace.SameAsEmitter) { var scale = 1.0f - emitter.GetParticleAge(particleIdx) / timeToSimulate; if (spec.GetSpace() == PartSysEmitterSpace.NodeYpr) { if (!external.GetBoneWorldMatrix(emitter.GetAttachedTo(), spec.GetNodeName(), out var boneM)) { boneM = Matrix4x4.Identity; } var ppos = new Vector3(particleX, particleY, particleZ); var newpos = Vector3.Transform(ppos, boneM); particleX = newpos.X; particleY = newpos.Y; particleZ = newpos.Z; } else { var prevObjPos = emitter.GetPrevObjPos(); var objPos = emitter.GetObjPos(); var dX = prevObjPos.X + (objPos.X - prevObjPos.X) * scale; var dY = prevObjPos.Y + (objPos.Y - prevObjPos.Y) * scale; var dZ = prevObjPos.Z + (objPos.Z - prevObjPos.Z) * scale; RotateAndMove(dX, dY, dZ, 0.0f, ref particleX, ref particleY, ref particleZ); } } } break; default: break; } var state = emitter.GetParticleState(); state.SetState(ParticleStateField.PSF_X, particleIdx, particleX); state.SetState(ParticleStateField.PSF_Y, particleIdx, particleY); state.SetState(ParticleStateField.PSF_Z, particleIdx, particleZ); // Initialize particle color SetParticleParam(emitter, particleIdx, PartSysParamId.emit_init_red, partSpawnTime, ParticleStateField.PSF_RED, 255.0f); SetParticleParam(emitter, particleIdx, PartSysParamId.emit_init_green, partSpawnTime, ParticleStateField.PSF_GREEN, 255.0f); SetParticleParam(emitter, particleIdx, PartSysParamId.emit_init_blue, partSpawnTime, ParticleStateField.PSF_BLUE, 255.0f); SetParticleParam(emitter, particleIdx, PartSysParamId.emit_init_alpha, partSpawnTime, ParticleStateField.PSF_ALPHA, 255.0f); // Initialize particle velocity var partVelX = GetParticleValue(emitter, particleIdx, PartSysParamId.emit_init_vel_X, partSpawnTime); var partVelY = GetParticleValue(emitter, particleIdx, PartSysParamId.emit_init_vel_Y, partSpawnTime); var partVelZ = GetParticleValue(emitter, particleIdx, PartSysParamId.emit_init_vel_Z, partSpawnTime); if (spec.GetSpace() == PartSysEmitterSpace.ObjectYpr) { if (spec.GetParticleSpace() != PartSysParticleSpace.SameAsEmitter) { var scale = 1.0f - emitter.GetParticleAge(particleIdx) / timeToSimulate; var rotation = 0.0f; if (spec.GetSpace() == PartSysEmitterSpace.ObjectYpr) { rotation = (emitter.GetObjRotation() - emitter.GetPrevObjRotation()) * scale + emitter.GetPrevObjRotation(); } // Rotate the velocity vector according to the current object rotation in the world // TODO: Even for rotation == 0, this will flip the velocity vector Rotate2D(rotation, ref partVelX, ref partVelZ); } } else if (spec.GetSpace() == PartSysEmitterSpace.NodeYpr) { if (spec.GetParticleSpace() != PartSysParticleSpace.SameAsEmitter) { var objId = emitter.GetAttachedTo(); if (!external.GetBoneWorldMatrix(objId, spec.GetNodeName(), out var boneMatrix)) { boneMatrix = Matrix4x4.Identity; } // Construct a directional vector (not a positional one, w=0 here) for the velocity var dirVec = new Vector3(partVelX, partVelY, partVelZ); dirVec = Vector3.TransformNormal(dirVec, boneMatrix); partVelX = dirVec.X; partVelY = dirVec.Y; partVelZ = dirVec.Z; } } // Are particle coordinates defined as polar coordinates? Convert them to cartesian here if (spec.GetParticleVelocityCoordSys() == PartSysCoordSys.Polar) { var cartesianVel = SphericalDegToCartesian(partVelX, partVelY, partVelZ); partVelX = cartesianVel.X; partVelY = cartesianVel.Y; partVelZ = cartesianVel.Z; } state.SetState(ParticleStateField.PSF_VEL_X, particleIdx, partVelX); state.SetState(ParticleStateField.PSF_VEL_Y, particleIdx, partVelY); state.SetState(ParticleStateField.PSF_VEL_Z, particleIdx, partVelZ); // I don't know why it's taken at lifetime 0. // TODO: Figure out if this actually *never* changes? var posVarX = GetParticleValue(emitter, particleIdx, PartSysParamId.part_posVariation_X, 0); var posVarY = GetParticleValue(emitter, particleIdx, PartSysParamId.part_posVariation_Y, 0); var posVarZ = GetParticleValue(emitter, particleIdx, PartSysParamId.part_posVariation_Z, 0); // For a polar system, convert these positions to cartesian to apply them to the // rendering position if (spec.GetParticlePosCoordSys() == PartSysCoordSys.Polar) { state.SetState(ParticleStateField.PSF_POS_AZIMUTH, particleIdx, 0); state.SetState(ParticleStateField.PSF_POS_INCLINATION, particleIdx, 0); state.SetState(ParticleStateField.PSF_POS_RADIUS, particleIdx, 0); // Convert to cartesian and add to the actual current particle position // As usual, x, y, z here are (azimuth, inclination, radius) var cartesianPos = SphericalDegToCartesian(posVarX, posVarY, posVarZ); posVarX = cartesianPos.X; posVarY = cartesianPos.Y; posVarZ = cartesianPos.Z; } // Apply the position variation to the initial position and store it posVarX += particleX; posVarY += particleY; posVarZ += particleZ; state.SetState(ParticleStateField.PSF_POS_VAR_X, particleIdx, posVarX); state.SetState(ParticleStateField.PSF_POS_VAR_Y, particleIdx, posVarY); state.SetState(ParticleStateField.PSF_POS_VAR_Z, particleIdx, posVarZ); /* * The following code will apply particle movement after * spawning a particle retroactively. This should only happen * for high frequency particle systems that spawn multiple particles * per frame. * Also note how particle age instead of particle lifetime is used here * to access parameters of the emitter. */ var partAge = emitter.GetParticleAge(particleIdx); if (partAge != 0) { var partAgeSquared = partAge * partAge; particleX += partVelX * partAge; particleY += partVelY * partAge; particleZ += partVelZ * partAge; var param = emitter.GetParamState(PartSysParamId.part_accel_X); if (param != null) { var accelX = param.GetValue(emitter, particleIdx, partAge); particleX += accelX * partAgeSquared * 0.5f; partVelX += accelX * partAge; } param = emitter.GetParamState(PartSysParamId.part_accel_Y); if (param != null) { var accelY = param.GetValue(emitter, particleIdx, partAge); particleY += accelY * partAgeSquared * 0.5f; partVelY += accelY * partAge; } param = emitter.GetParamState(PartSysParamId.part_accel_Z); if (param != null) { var accelZ = param.GetValue(emitter, particleIdx, partAge); particleZ += accelZ * partAgeSquared * 0.5f; partVelZ += accelZ * partAge; } state.SetState(ParticleStateField.PSF_VEL_X, particleIdx, partVelX); state.SetState(ParticleStateField.PSF_VEL_Y, particleIdx, partVelY); state.SetState(ParticleStateField.PSF_VEL_Z, particleIdx, partVelZ); param = emitter.GetParamState(PartSysParamId.part_velVariation_X); if (param != null) { particleX += param.GetValue(emitter, particleIdx, partAge) * partAge; } param = emitter.GetParamState(PartSysParamId.part_velVariation_Y); if (param != null) { particleY += param.GetValue(emitter, particleIdx, partAge) * partAge; } param = emitter.GetParamState(PartSysParamId.part_velVariation_Z); if (param != null) { particleZ += param.GetValue(emitter, particleIdx, partAge) * partAge; } state.SetState(ParticleStateField.PSF_X, particleIdx, particleX); state.SetState(ParticleStateField.PSF_Y, particleIdx, particleY); state.SetState(ParticleStateField.PSF_Z, particleIdx, particleZ); } // Simulate rotation for anything other than a point particle if (spec.GetParticleType() != PartSysParticleType.Point) { var emitterAge = emitter.GetParticleSpawnTime(particleIdx); var rotation = emitter.GetParamValue(PartSysParamId.emit_yaw, particleIdx, emitterAge, 0.0f); emitter.GetParticleState().SetState(ParticleStateField.PSF_ROTATION, particleIdx, rotation); } }