Добавление визуальных эффектов
Brief Overview of Particle SystemsНемного про системы частиц#
Visual effects play an important role in enhancing the realism of the images generated by 3D applications, be it a simulator or a game. To simulate various phenomena such as fire, smoke, explosions, electric sparks, fountains, jet engine plumes, wake waves, magic and many others, particle systems are extensively used in 3D graphics. These effects are crucial in creating believable images that captivate the audience.Важный вклад в реалистичность изображения, получаемого на выходе 3D приложения, будь то симулятор или игра , вносят всевозможные визуальные эффекты. Для моделирования огня, дыма, взрывов, электрических искр, фонтанов, шлейфов от реактивных двигателей, кильватерного следа, магии и многих других эффектов, без которых сложно себе представить правдоподобную картинку,, в трехмерной графике используются системы частиц.
Particles can be material points, elementary sprites, triangles, small polygons, or even geometric shapes. Typically, the system releases particles at random points within a predetermined volume, such as a sphere, cylinder, or cone, or from a single point in various directions. The system determines the lifetime of a particle, and destroys it when that time runs out.Частицы – это материальные точки, элементарные спрайты, треугольники, маленькие полигоны или даже геометрические фигуры. В общем случае система испускает частицы в случайных точках в пределах заранее определенного объема (например, сферы, цилиндра или конуса), либо из одной точки в разных направлениях. Система определяет время жизни частицы, и уничтожает ее, когда это время заканчивается.
Each particle in the system dynamically changes its coordinates based on a specific algorithm, such as the law of universal gravitation. Additionally, particles are not static and may alter both their position and shape over time.Каждая частица в системе динамически меняет свои координаты в соответствии с определенным алгоритмом (например, по закону всемирного тяготения). Еще одна отличительная особенность – частицы не статичны – они могут со временем менять не только свое положение, но и форму.
A particle system in UNIGINE consists of three main entities:Система частиц в UNIGINE включает три составляющие:
- Emitter – the source that emits particles according to the values set in the Emitter parameters.Эмиттер – источник, испускающий частицы в соответствии со значениями, установленными в параметрах группы Emitter.
- Particles themselves, which are emitted according to the predefined behavior after emission.Сами частицы, которые после испускания ведут себя согласно заранее определенному поведению.
- Additional physical effects applied to the particles that affect their behavior.Дополнительные физические эффекты, применяемые к частицам и влияющие на их поведение.
To create the desired effects in our game, we will need a flash and smoke effect for when a shot is fired, as well as a bullet impact effect that leaves a trace on the hit surface. Both effects will use the particle system, and the latter effect will also include a decal projection to simulate the impact. We'll start with creating a simplified version of the flash and smoke effect manually before using the ready-made assets.Для нашей игры нам понадобится два эффекта: вспышка и дым для выстрела, и попадание пули в объект оставлением следа в месте попадания. В обоих будут использоваться системы частиц, второй эффект будет дополнительно проецировать декаль, чтобы смоделировать след на поверхности. Сначала посмотрим, как можно создать упрощенный вариант первого эффекта вручную, а потом можно использоваться готовые ассеты.
Creating a Simple Effect TemplateСобираем простой шаблон эффекта#
The shot effect will have two components — flash and smoke, and we'll use a separate particle system for each. Let's start with the flash:Эффект выстрела будет включать две составляющих: вспышка и дым, для каждой из них будем использовать отдельную систему частиц. Начнем со вспышки:
- Let's add a new particle system to the scene (Create -> Particle System -> Particles) and call it flash.Добавим на сцену новую систему частиц (Create -> Particle System -> Particles) и назовем ее flash.
- The created system emits particles continuously, but for the flash we need to do it once, so set the Spawn Rate to 0. Now we need to turn off the node in the World Hierarchy and turn it on to see what the flash will look like.Созданная система испускает частицы непрерывно, а для вспышки нам нужно сделать это один раз, для этого Spawn Rate установим в 0. Теперь, чтобы посмотреть, как будет выглядеть вспышка, нужно будет выключить ноду в World Hierarchy и снова включить.
- We don't want our single particle imitating the flash to move anywhere after it appears, we only need it increase in size. The particle behavior after emission is controlled by the Behavior After Emission group parameters. There we will set Velocity to 0 so that the particle does not move anywhere, and the Increase In Radius value — to 1.5.Нам нужно, чтобы наша единственная частица, моделирующая вспышку, после появления не двигалась куда-то, а просто увеличивалась в размере. Поведение частиц после испускания управляется параметрами группы Behavior After Emission. Здесь мы установим Velocity равным 0, чтобы частица никуда не перемещалась, а вот для Increase In Radius установим значение 1.5.
- Let's check what the effect looks like — turn off and on the flash node. It's already better, but it's too slow for a flash, so we need to speed up the effect. To do this, reduce Life Time to 0.1.Проверим эффект (выключите и включите ноду flash), уже лучше, но для вспышки слишком медленно, надо ускорить эффект. Для этого уменьшим Life Time до 0.1.
- Now you only need to replace the material — in the Surface Material group assign the data/particles/materials/gun_flash_0_mat.mat material.Теперь осталось заменить материал – в группе параметров Surface Material назначьте материал data/particles/materials/gun_flash_0_mat.mat.
The flash is ready, now let's add some smoke. It's done as follows:Со вспышкой закончили, теперь добавим немного дыма, сделав следующее:
- Clone the first particle system and name this copy smoke.Склонируем первую систему частиц и назовем копию smoke.
- Assign the data/particles/materials/gun_smoke_gray_mat.mat material to it.Назначим ей материал data/particles/materials/gun_smoke_gray_mat.mat.
-
To synchronize several particle systems, we need to combine them into a hierarchy (the Duration value of the parent particle system must be big enough to enclose Duration and Delay intervals of all child particle systems). So, to make smoke automatically appear when flash is activated, add smoke as a child node for flash.To synchronize several particle systems, we need to combine them into a hierarchy (the Duration value of the parent particle system must be big enough to enclose Duration and Delay intervals of all child particle systems). So, to make smoke automatically appear when flash is activated, add smoke as a child node for flash.
To synchronize several particle systems, we need to combine them into a hierarchy (the Duration value of the parent particle system must be big enough to enclose Duration and Delay intervals of all child particle systems). So, to make smoke automatically appear when flash is activated, add smoke as a child node for flash.Чтобы синхронизировать несколько систем частиц нужно объединить их в иерархию (при этом значение параметра Duration у родительской системы частиц должно покрывать все интервалы Duration и Delay дочерних систем частиц). Итак, чтобы при активации вспышки автоматически появлялся и дым – добавьте smoke в качестве дочерней ноды для flash.
- In our case everything is simple, each of the systems generates one particle, but we need to make the smoke appear with a slight delay and last a little longer. For this, we'll set the Delay parameter in the Emission group equal to 0.1, and Life Time — to 0.2. For Increase In Radius, set the value to 0.3.В нашем случае все просто, каждая из систем генерирует по одной частице, но нужно, чтобы дым появлялся с небольшой задержкой и длился чуть дольше, для этого установим параметр Delay в группе Emission равным 0.1, а Life Time – равным 0.2. Для Increase In Radius установим значение 0.3.
- The last step is to drag the flash node from World Hierarchy to Asset Browser and rename the resulting asset to gun_fire.node. Now it is ready to be used for the fire effect (the node itself can be removed from the scene).Последний шаг – перетащите ноду flash из World Hierarchy в Asset Browser и переименуйте получившийся ассет в gun_fire.node. Теперь он готов к использованию для эффекта выстрела (ноду эффекта из сцены можно удалить).
Implementing the Muzzle Flash and Hit EffectРеализация эффекта вспышки выстрела и попадания пули#
Visual effects for shooting can be implemented in a separate component. You can get information about the hit point and spawn a NodeReference representing the hit effect at this point oriented along the hit normal. For the muzzle flash, you can attach a NodeDummy to the muzzle of the pistol, and spawn a muzzle flash NodeReference at this position.Визуальные эффекты для стрельбы могут быть реализованы в отдельном компоненте. Вы можете получить информацию о точке попадания и отобразить в этой точке NodeReference, имитирующий эффект попадания, с ориентацией вдоль нормали. Для вспышки выстрела можно прикрепить NodeDummy к дулу пистолета и создать NodeReference вспышки выстрела в этой точке.
In the component code below, the OnHit() and OnShoot() methods implement this logic.В приведенном ниже коде компонента методы OnHit() и OnShoot() реализуют эту логику.
-
Create the VFXController.cs component and copy the code below.Create the VFXController.cs component and copy the code below.
Create the VFXController.cs component and copy the code below.VFXController.csVFXController.csVFXController.cs
using System; using System.Collections; using System.Collections.Generic; using Unigine; #region Math Variables #if UNIGINE_DOUBLE using Vec3 = Unigine.dvec3; using Mat4 = Unigine.dmat4; #else using Vec3 = Unigine.vec3; using Mat4 = Unigine.mat4; #endif #endregion [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- identifier is generated automatically for a new component public class VFXController : Component { // NodeReference for hit and muzzle flash visualization [ParameterFile(Filter = ".node")] public string hitPrefab = null; [ParameterFile(Filter = ".node")] public string muzzleFlashPrefab = null; public void OnShoot(Mat4 transform) { // if the flash effect NodeReference is not specified, we do nothing if (string.IsNullOrEmpty(muzzleFlashPrefab)) return; // load the NodeReference for muzzle flash visualization Node muzzleFlashVFX = World.LoadNode(muzzleFlashPrefab); // set the muzzle flash node transformation to the pistol muzzle coordinates muzzleFlashVFX.WorldTransform = transform; } public void OnHit(Vec3 hitPoint, vec3 hitNormal, Unigine.Object hitObject) { // if no hit node is specified, do nothing if (string.IsNullOrEmpty(hitPrefab)) return; // load the node for hit visualization from the file Node hitVFX = World.LoadNode(hitPrefab); // place the loaded node in the hit point and set its direction according to the hit normal vector hitVFX.Parent = hitObject; hitVFX.WorldPosition = hitPoint; hitVFX.SetWorldDirection(hitNormal, vec3.UP, MathLib.AXIS.Y); } }
Create the VFXController.cs component and copy the code below.Создайте компонент VFXController.cs и скопируйте приведенный ниже код.
VFXController.csVFXController.cs
using System; using System.Collections; using System.Collections.Generic; using Unigine; #region Math Variables #if UNIGINE_DOUBLE using Vec3 = Unigine.dvec3; using Mat4 = Unigine.dmat4; #else using Vec3 = Unigine.vec3; using Mat4 = Unigine.mat4; #endif #endregion [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента public class VFXController : Component { // NodeReference для эффектов вспышки выстрела и попадания [ParameterFile(Filter = ".node")] public string hitPrefab = null; [ParameterFile(Filter = ".node")] public string muzzleFlashPrefab = null; public void OnShoot(Mat4 transform) { // если не задан NodeReference эффекта вспышки, ничего не делаем if (string.IsNullOrEmpty(muzzleFlashPrefab)) return; // загружаем NodeReference эффекта выстрела Node muzzleFlashVFX = World.LoadNode(muzzleFlashPrefab); // устанавливаем положение вспышки на указанные координаты дула пистолета muzzleFlashVFX.WorldTransform = transform; } public void OnHit(Vec3 hitPoint, vec3 hitNormal, Unigine.Object hitObject) { // если нода эффекта попадания не указана ничего не делаем if (string.IsNullOrEmpty(hitPrefab)) return; // загружаем ноду эффекта попадания из файла Node hitVFX = World.LoadNode(hitPrefab); // устанавливаем загруженную ноду в указанную точку попадания и разворачиваем ее в направлении вектора нормали hitVFX.Parent = hitObject; hitVFX.WorldPosition = hitPoint; hitVFX.SetWorldDirection(hitNormal, vec3.UP, MathLib.AXIS.Y); } }
-
Modify the WeaponController.cs component in order to use logic of VFXController.cs.Modify the WeaponController.cs component in order to use logic of VFXController.cs.
Modify the WeaponController.cs component in order to use logic of VFXController.cs.WeaponController.csWeaponController.csWeaponController.cs
using System; using System.Collections; using System.Collections.Generic; using Unigine; #region Math Variables #if UNIGINE_DOUBLE using Vec3 = Unigine.dvec3; #else using Vec3 = Unigine.vec3; #endif #endregion [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- identifier is generated automatically for a new component public class WeaponController : Component { public PlayerDummy shootingCamera = null; public ShootInput shootInput = null; public NodeDummy weaponMuzzle = null; ${#HL}$ public VFXController vfx = null; ${HL#}$ public int damage = 1; // Intersection mask to sort out which objects can be hit by a bullet [ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)] public int mask = ~0; public void Shoot() { ${#HL}$ // visualize the shoot effect if (weaponMuzzle) vfx.OnShoot(weaponMuzzle.WorldTransform); ${HL#}$ // set the line starting point (p0) in the camera position // and end point (p1) in the point 100 units away in the camera view direction Vec3 p0 = shootingCamera.WorldPosition; Vec3 p1 = shootingCamera.WorldPosition + shootingCamera.GetWorldDirection() * 100; // create an object to store the intersection normal WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); // get the first object intersected by the (p0,p1) line Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo); // if the intersection is found if (hitObject) { // render the intersection normal to the surface in the hit point using Visualizer Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f); ${#HL}$ // visualize the hit effect in the intersection point vfx.OnHit(hitInfo.Point, hitInfo.Normal, hitObject); ${HL#}$ } } private void Update() { // handle user input: check if the fire button is pressed if (shootInput.IsShooting()) Shoot(); } }
Modify the WeaponController.cs component in order to use logic of VFXController.cs.Измените компонент WeaponController.cs, чтобы использовать логику VFXController.cs.
WeaponController.csWeaponController.cs
using System; using System.Collections; using System.Collections.Generic; using Unigine; #region Math Variables #if UNIGINE_DOUBLE using Vec3 = Unigine.dvec3; #else using Vec3 = Unigine.vec3; #endif #endregion [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента public class WeaponController : Component { public PlayerDummy shootingCamera = null; public ShootInput shootInput = null; ${#HL}$ public NodeDummy weaponMuzzle = null; public VFXController vfx = null; ${HL#}$ public int damage = 1; // маска Intersection чтобы определить, в какие объекты могут попадать пули [ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)] public int mask = ~0; public void Shoot() { ${#HL}$ // визуализируем эффект выстрела if (weaponMuzzle) vfx.OnShoot(weaponMuzzle.WorldTransform); ${HL#}$ // задаем начало отрезка (p0) в позиции камеры и конец (p1) - в точке удаленной на 100 единиц // в направлении взгляда камеры Vec3 p0 = shootingCamera.WorldPosition; Vec3 p1 = shootingCamera.WorldPosition + shootingCamera.GetWorldDirection() * 100; // создаем объект для хранения intersection-нормали WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); // ищем первый объект, который пересекает отрезок (p0, p1) Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo); // if the intersection is found if (hitObject) { // отрисовывает нормаль к поверхности в точке попадания при помощи Visualizer Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f); ${#HL}$ // визуализируем эффект попадания в точке пересечения vfx.OnHit(hitInfo.Point, hitInfo.Normal, hitObject); ${HL#}$ } } private void Update() { // обработка пользовательского ввода: проверяем нажата ли клавиша “огонь” if (shootInput.IsShooting()) Shoot(); } }
- Add the VFXController.cs component to the player Dummy Node.Добавьте компонент VFXController.cs к Dummy Node player.
-
Now add a point where the shoot effect will be visualized. Create a NodeDummy, call it muzzle, make it a child of the pistol Skinned Mesh, and place it near the end of the weapon muzzle.Now add a point where the shoot effect will be visualized. Create a NodeDummy, call it muzzle, make it a child of the pistol Skinned Mesh, and place it near the end of the weapon muzzle.
Now add a point where the shoot effect will be visualized. Create a NodeDummy, call it muzzle, make it a child of the pistol Skinned Mesh, and place it near the end of the weapon muzzle.Now add a point where the shoot effect will be visualized. Create a NodeDummy, call it muzzle, make it a child of the pistol Skinned Mesh, and place it near the end of the weapon muzzle.Теперь добавим точку, в которой будет визуализироваться эффект выстрела. Создайте NodeDummy, назовите его muzzle, сделайте его дочерним элементом pistol (Skinned Mesh) и поместите его рядом с дулом оружия.
- Select the player Dummy Node, assign the muzzle node to the Weapon Muzzle field in the WeaponController section.Выберите Dummy Node player, назначьте ноду muzzle полю Weapon Muzzle в разделе WeaponController.
Assign the player Dummy Node to the Vfx field in the WeaponController section.Assign the player Dummy Node to the Vfx field in the WeaponController section.
Assign the player Dummy Node to the Vfx field in the WeaponController section.Assign the player Dummy Node to the Vfx field in the WeaponController section.Назначьте Dummy Node player полю Vfx в разделе WeaponController.
- Add the data/fps/bullet/bullet_hit.node asset to the Hit Prefab field of the VFXController section.Добавьте ассет data/fps/bullet/bullet_hit.node в поле Hit Prefab раздела VFXController.
-
Add the gun_fire.node asset that we prepared earlier to the Muzzle Flash Prefab field.Add the gun_fire.node asset that we prepared earlier to the Muzzle Flash Prefab field.
Add the gun_fire.node asset that we prepared earlier to the Muzzle Flash Prefab field.Add the gun_fire.node asset that we prepared earlier to the Muzzle Flash Prefab field.Добавьте ассет gun_fire.node, который мы подготовили ранее, в поле Muzzle Flash Prefab.
- Now you can press Play and test the shooting visual effects.Теперь вы можете нажать Play и протестировать визуальные эффекты при стрельбе.
VFX LifetimeВремя отображения эффектов#
With each shot, a new node is generated to indicate where the bullet hit. You can fire quite a lot of shots during the game. And it could be times more if we had a machine gun! We need to be mindful of performance and avoid unnecessary resource usage. Therefore, we should regularly delete nodes that are no longer critical for gameplay, such as the trace effect on walls after some time.При каждом выстреле у нас создается новая нода отмечающая место попадания пули. За время игры можно настрелять порядочно, а если у нас был бы пулемет? Чтобы не расходовать вычислительные ресурсы впустую и сохранять производительность на приемлемом уровне, надо привыкать удалять лишнее. По сути, через некоторое время ноды эффекта следов на стенах можно удалить, если это не критично для геймплея.
To control the duration of visual effects, you can add the Lifetime.cs component that will allow you to define a time interval for the node during which it will live and after which it will be deleted. Here's the code for this component:Для управления продолжительностью жизни визуальных эффектов, мы можем добавить компонент Lifetime.cs, который позволит определить для ноды интервал времени, в течение которого он будет отображаться, а также удалит ноду по истечении этого времени. Вот его код:
LifeTime.csLifeTime.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class Lifetime : Component
{
[ShowInEditor][Parameter(Tooltip = Object's lifetime (in seconds))]
private float lifeTime = 1.0f;
private float startTime = 0.0f;
void Init()
{
// запоминаем время создания объекта
startTime = Game.Time;
}
void Update()
{
// ждем пока не истечет время жизни ноды, затем удаляем ее
if (Game.Time - startTime > lifeTime)
node.DeleteLater();
}
}
This component is already added to the bullet_hit.node and gun_fire.node NodeReferences:Этот компонент уже добавлен в шаблоны (NodeReferences) bullet_hit.node и gun_fire.node:
For the bullet_hit.node, the Life Time parameter is set to 1 second.For the bullet_hit.node, the Life Time parameter is set to 1 second.
For the bullet_hit.node, the Life Time parameter is set to 1 second.For the bullet_hit.node, the Life Time parameter is set to 1 second.Для bullet_hit.node параметр Life Time устанавливается равным 1 секунде.
For the gun_fire.node, the Life Time parameter is set to 5 seconds.For the gun_fire.node, the Life Time parameter is set to 5 seconds.
For the gun_fire.node, the Life Time parameter is set to 5 seconds.For the gun_fire.node, the Life Time parameter is set to 5 seconds.Для gun_fire.node параметр Life Time устанавливается равным 5 секундам.