Данная статья является переводом следующих статей:
Decals in Unity3D, Part 1
Decals in Unity3D, Part 2
Часть 1.
В первой части мы используем самый простой способ создания декали: создание префаба в точке контакта. Первым делом мы должны создать составные части нашего префаба: примитив plane и материал. Затем нам нужна текстура с альфа-каналом для того чтобы использовать шейдеры семейства Transparent в Unity. Наконец мы должны применить текстуру с альфа-каналом к созданному материалу. Вот наш plane с использованием шейдера Transparent/Diffuse и нанесенной на него текстурой:
Теперь мы просто создаем новый префаб в окне Project и перетаскиваем созданный plane на него.
Создаем новый C# скрипт в окне Project и называем его как хотим. В этом примере имя файла Decal01.cs, поэтому имя класса должно быть Decal01:
using UnityEngine;
using System.Collections;
public class Decal01 : MonoBehaviour
{
// Use this for initialization
void Start (){}
// Update is called once per frame
void Update (){}
}
Мы должны постоянно проверять, есть ли столкновение с другим объектом прежде чем создать декаль, так что давайте добавим raycasting на наш код в Update ():
void Update ()
{
RaycastHit hit;
if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
{
}
}
Теперь нам нужно создать какое то событие при котором будут создаваться наши декали, это событие будет нажатие левой кнопки мыши:
void Update ()
{
RaycastHit hit;
if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
{
if (Input.GetMouseButtonDown(0))
{
}
}
}
Создаем GameObject переменную которая будет содержать наш префаб с декалью.
using UnityEngine;
using System.Collections;
public class Decal01 : MonoBehaviour
{
public GameObject decalPrefab;
// Use this for initialization
void Start (){}
// Update is called once per frame
void Update ()
{
RaycastHit hit;
if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
{
if (Input.GetMouseButtonDown(0))
{
}
}
}
Наконец, мы должны написать код для создания префаба в точке контакта с правильной ориентацией в пространстве. Мы берем все необходимые сведения из RaycastHit (точку контакта и ориентацию точки в пространстве):
void Update ()
{
RaycastHit hit;
if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
{
if (Input.GetMouseButtonDown(0))
{
Instantiate(decalPrefab, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
}
}
}
Теперь, если вы запустите игру, эта простая система будет работать, но raycast будет определять столкновения и с вновь созданными декалями. Чтобы избежать этого, есть простой способ: просто удалить компонент Mesh Collider префаба-декали. Это Вы должны сделать в любом случае, чтобы избежать проблем в будущем. Но, скажем, у вас есть сложный уровень с большим количеством объектов, вы должны удалить коллайдеры от всех тех объектов, на которых не должны отображаться декали, и это является не хорошим решением. Вместо этого, мы можем осуществить проверку тегов, так мы сможем определить должны ли отображаться декали на объекте.
Зайдите в редактор тегов и добавьте новый тег, назовем его "DecalOn". Теперь давайте вернемся к коду и добавим простое условие создания декали:
void Update ()
{
RaycastHit hit;
if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
{
if (hit.collider.tag == "DecalOn" && Input.GetMouseButtonDown(0))
{
Instantiate(decalPrefab, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
}
}
}
Итак, теперь у нас есть три условия, которые должны быть выполнены для создания префаба-декали:
1) Луч должен пересекать объект на котором должна появиться декаль;
2) Объект должен иметь тег "DecalOn";
3) Игрок должен нажать левую кнопку мыши.
Когда все эти три условия выполняться префаб-декаль будет создан на объекте, как раз в точке контакта и с правильной ориентацией в пространстве.
Часть 2.
В этой части мы рассмотрим несколько проблем, возникающих при использовании метода с предыдущей части. Вы могли заметить одну большую проблему: на большой стене она работает отлично, но что делать если место контакта в углу объекта? Декаль будет выходить за пределы объекта:
Итак, как мы можем избежать этой проблемы? Ответ достаточно прост, но он может не удовлетворить Вас. Чтобы избежать выход декали за пределы объекта нужно выполнить проверку на уровне вершин декали: если его вершины находятся за пределами, то мы перемещаем их обратно на границу за которую они вышли. На самом деле, это довольно простая задача, но она содержит много ограничений. Прежде всего, мы не можем использовать ее в другом месте, нежели в объектах типа plane и cube. Правда, стены и колонны в играх в основном состоят из кубов различной формы и размеров, но это все равно является ограничением. Еще большее ограничение в том что объекты должны быть выравнены по осям. К тому же декали не будут работать на вращающихся объектах.
Я решил поделиться этим методом несмотря на ограничения описанные выше, потому что это может быть полезно в учебных целей, и может пригодиться для выполнения некоторых других задач. Давайте посмотрим финальную версию скрипта, а затем будем вдаваться в подробности:
using UnityEngine;
using System.Collections;
public class Decal01 : MonoBehaviour
{
public GameObject decalPrefab;
void Start ()
{
}
void Update ()
{
RaycastHit hit;
if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
{
if (hit.collider.tag == "DecalOn" && Input.GetMouseButtonDown(0))
DecalGen(hit.point, hit.normal, hit.collider);
}
}
GameObject DecalGen(Vector3 p, Vector3 n, Collider c)
{
GameObject decalInst;
decalInst = (GameObject)Instantiate(decalPrefab, p, Quaternion.FromToRotation(Vector3.up, n));
MeshFilter mf = decalInst.GetComponent(typeof(MeshFilter)) as MeshFilter;
Mesh m = mf.mesh;
Vector3[] verts = m.vertices;
for (int i = 0; i < verts.Length; i++)
{
verts[i] = decalInst.transform.TransformPoint(verts[i]);
if (verts[i].x > c.bounds.max.x)
{
verts[i].x = c.bounds.max.x;
}
if (verts[i].x < c.bounds.min.x)
{
verts[i].x = c.bounds.min.x;
}
if (verts[i].y > c.bounds.max.y)
{
verts[i].y = c.bounds.max.y;
}
if (verts[i].y < c.bounds.min.y)
{
verts[i].y = c.bounds.min.y;
}
if (verts[i].z > c.bounds.max.z)
{
verts[i].z = c.bounds.max.z;
}
if (verts[i].z < c.bounds.min.z)
{
verts[i].z = c.bounds.min.z;
}
decal mesh.verts[i] = decalInst.transform.InverseTransformPoint(verts[i]);
m.vertices = verts;
}
return decalInst;
}
}
Мы можем заметить что скрипт сильно изменился по сравнению с первой частью (он стал гораздо длиннее и сложнее): весь код создания был перенесен в новый метод, названный DecalGen. Теперь метод Update () проще, и он просто вызывает метод DecalGen когда выполняются три условия с первой части. Все функции создания декали теперь находятся в методе DecalGen. Давайте посмотрим что он делает.
Прежде всего, мы создаем новую переменную с именем "decalInst", которая будет содержать наш префаб-декаль. Она будет обработана и возвращена в конце метода.
Затем мы создаем префаб-декаль таким же способом как в первой части. Далее нам нужно взять некоторую из переменной "decalInst" информацию, для того чтобы сделать все необходимые проверки. Мы должны получить от "decalInst" MeshFilter префаба-декали, а затем его Mesh, так мы получим доступ ко всем его вершинам. Мы также создаем новый Vector3 массив для хранения вершин сетки префаба-декали и для операций на копии.
Далее идет цикл, в котором мы проходим через все вершины сетки, преобразовываем их в локальных координатах, а также проверяем отдельно каждую Vector3 компоненту (х, у и z) на выход за пределы объекта. Если ни один из них не выходит за пределы объекта, то возвращаем их на свои оси.
Затем мы преобразуем массив вершин Verts обратно в глобальное пространство, и передаем их на вершины сетки префаба-декали (мы работали с копией). Наконец, мы возвращаем наш префаб-декаль.
Если сейчас попробовать запустить игру, вы заметите, что декали никогда не будут выходить за пределы объекта. Довольно мило, да?