Unity-可编辑的星星特效
阅前须知
本文前置知识:CustomData与CustomList
绘制星星
创建一个名为Star的普通脚本,定义以下字段,然后将这个脚本挂在一个空对象上
//网格
private Mesh mesh;
//顶点
private Vector3[] vertices;
//颜色
private Color[] colors;
//三角形(顶点索引)
private int[] triangles;
顶点网格
思路
首先,我们要绘制顶点网格。以vertices[0] 为中心点。其余所有的顶点围绕中心点绘制。
按照定义顺序绘制三角形,例如:
第一个三角形的索引顶点为:0-1-2
第二个三角形的索引顶点为:0-2-3
第三个三角形的索引顶点为:0-3-4
请注意,我们还没有定义控制点,也就是距离中心点一定距离的初始定义点Points 。
所有后续的顶点会按照初始定义的点按照均分角度依次旋转。其次每一个顶点都应该拥有他的颜色。
这里我们使用上篇文章使用的ColorPoint ,现在我们继续定义
//中心点
public ColorPoint center;
//顶点
public ColorPoint[] points;
//迭代次数
public int frequency = 1;
实现
首先初始化网格,注意所有代码我们暂时写在Start里
void Start()
{
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Star Mesh";
}
下限判断
//迭代次数至少为1
if (frequency < 1)
{
frequency = 1;
}
//顶点数字初始化
if (points == null)
{
points = new ColorPoint[0];
}
确定顶点总数
顶点总数为迭代次数与控制点长度的乘积,你可能还不太明白,但是这没有关系,读下去你会明白的。
int numberOfPoints = frequency * points.Length;
初始化顶点数组,颜色数组,索引数组
vertices = new Vector3[numberOfPoints + 1];
colors = new Color[numberOfPoints + 1];
triangles = new int[numberOfPoints * 3];
注意,这里顶点与颜色数组都进行了加1,这是因为中心点,而我们计算的顶点总数是不包含中心点的。
顶点总数就等于三角形总数,因此索引数组是三倍的顶点总数。
请注意,顶点总数有他的下限,也就是3个顶点,如果仅有两个顶点时无法绘制三角形的,因为一个顶点在最上面一个在最下面,他们与中心点构成了直线而不是三角形。
代码如下:
if (numberOfPoints >= 3)
{
//中心点
vertices[0] = center.position;
colors[0] = center.color;
//均分角度
float angle = -360f / numberOfPoints;
//迭代次数
for (int repetitions = 0, v = 1, t = 1; repetitions < frequency; repetitions++)
{
//每次迭代,遍历所有控制点
for (int p = 0; p < points.Length; p += 1, v += 1, t += 3)
{
//顶点旋转
vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[p].position;
colors[v] = points[p].color;
//索引赋值
triangles[t] = v;
triangles[t + 1] = v + 1;
}
}
//最后一个三角形循环到第一个顶点
triangles[triangles.Length - 1] = 1;
}
最后将各数组赋予给网格
mesh.vertices = vertices;
mesh.colors = colors;
mesh.triangles = triangles;
让我们看看效果: 呈现紫色,是因为还没有赋予材质,没有着色器绘制。
创建一个材质并将材质赋予对象,然后创建一个Shader并使用下面的Shader代码
Shader "MyShader/Star" {
SubShader {
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
Lighting Off
ZWrite Off
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct data {
float4 vertex : POSITION;
fixed4 color: COLOR;
};
data vert (data v) {
v.vertex = UnityObjectToClipPos(v.vertex);
return v;
}
fixed4 frag(data f) : COLOR {
return f.color;
}
ENDCG
}
}
}
现在,让我们来看看效果:
数据参考:
自定义编辑器
[CustomEditor(typeof(Star)), CanEditMultipleObjects]
public class StarInspector : Editor
{
public override void OnInspectorGUI()
{
SerializedProperty
points = serializedObject.FindProperty("points"),
frequency = serializedObject.FindProperty("frequency");
serializedObject.Update();
EditorGUILayout.PropertyField(serializedObject.FindProperty("center"));
EditorList.Show(points,EditorListOption.Buttons | EditorListOption.ListLabel);
EditorGUILayout.IntSlider(frequency, 1, 20);
int totalPoints = frequency.intValue * points.arraySize;
if (totalPoints < 3)
{
EditorGUILayout.HelpBox("At least three points are needed.", MessageType.Warning);
}
else
{
EditorGUILayout.HelpBox(totalPoints + " points in total.", MessageType.Info);
}
serializedObject.ApplyModifiedProperties();
}
}
如果你不明白上述代码,我建议你先阅读CustomList
编辑器模式
到目前为止,我们要查看效果必须要运行才可以查看,并且不可更改,这十分麻烦。接下来,我们将介绍如何编写编辑器模式
我们需要做的第一件事是告诉Unity,我们的组件应该在编辑模式下处于活动状态。我们通过添加ExecuteInEditMode 类属性来表明这一点。从现在开始,只要编辑器中出现星号,就会调用Start方法。 因为我们在开始时创建了一个网格,所以它将在编辑模式下创建。当我们将它分配给一个MeshFilter 时,它将持久化并保存在场景中。我们不希望这种情况发生,因为我们是动态生成网格的。我们可以通过设置适当的HideFlags来阻止Unity保存网格。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode,RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour
{
public int numberOfPoints = 10;
private Mesh mesh;
private Vector3[] vertices;
private Color[] colors;
private int[] triangles;
public ColorPoint center;
public ColorPoint[] points;
public int frequency = 1;
void Start()
{
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Star Mesh";
mesh.hideFlags = HideFlags.HideAndDontSave;
if (frequency < 1)
{
frequency = 1;
}
if (points == null)
{
points = new ColorPoint[0];
}
int numberOfPoints = frequency * points.Length;
vertices = new Vector3[numberOfPoints + 1];
colors = new Color[numberOfPoints + 1];
triangles = new int[numberOfPoints * 3];
if (numberOfPoints >= 3)
{
vertices[0] = center.position;
colors[0] = center.color;
float angle = -360f / numberOfPoints;
for (int repetitions = 0, v = 1, t = 1; repetitions < frequency; repetitions++)
{
for (int p = 0; p < points.Length; p += 1, v += 1, t += 3)
{
vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[p].position;
colors[v] = points[p].color;
triangles[t] = v;
triangles[t + 1] = v + 1;
}
}
triangles[triangles.Length - 1] = 1;
}
mesh.vertices = vertices;
mesh.colors = colors;
mesh.triangles = triangles;
}
}
编辑网格更新
我们先将网格更新封装为一个方法,并添加reset 重置也要调用网格更新
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode,RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour
{
public int numberOfPoints = 10;
private Mesh mesh;
private Vector3[] vertices;
private Color[] colors;
private int[] triangles;
public ColorPoint center;
public ColorPoint[] points;
public int frequency = 1;
void Start()
{
UpdateMesh();
}
void Reset()
{
UpdateMesh();
}
private void OnEnable()
{
UpdateMesh();
}
public void UpdateMesh()
{
if (mesh == null)
{
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Star Mesh";
mesh.hideFlags = HideFlags.HideAndDontSave;
}
if (frequency < 1)
{
frequency = 1;
}
if (points == null)
{
points = new ColorPoint[0];
}
int numberOfPoints = frequency * points.Length;
if (vertices == null || vertices.Length != numberOfPoints + 1)
{
vertices = new Vector3[numberOfPoints + 1];
colors = new Color[numberOfPoints + 1];
triangles = new int[numberOfPoints * 3];
mesh.Clear();
}
if (numberOfPoints >= 3)
{
vertices[0] = center.position;
colors[0] = center.color;
float angle = -360f / numberOfPoints;
for (int repetitions = 0, v = 1, t = 1; repetitions < frequency; repetitions++)
{
for (int p = 0; p < points.Length; p += 1, v += 1, t += 3)
{
vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[p].position;
colors[v] = points[p].color;
triangles[t] = v;
triangles[t + 1] = v + 1;
}
}
triangles[triangles.Length - 1] = 1;
}
mesh.vertices = vertices;
mesh.colors = colors;
mesh.triangles = triangles;
}
}
编辑器属性改变即更新网格,其中撤销也要更新网格
if (serializedObject.ApplyModifiedProperties() ||
Event.current.commandName == "UndoRedoPerformed")
{
foreach (Star s in targets)
{
s.UpdateMesh();
}
}
预制体不更新网格
if (PrefabUtility.GetPrefabType(s) != PrefabType.Prefab) {
s.UpdateMesh();
}
禁用多对象编辑
if (!serializedObject.isEditingMultipleObjects)
{
int totalPoints = frequency.intValue * points.arraySize;
if (totalPoints < 3)
{
EditorGUILayout.HelpBox("At least three points are needed.", MessageType.Warning);
}
else
{
EditorGUILayout.HelpBox(totalPoints + " points in total.", MessageType.Info);
}
}
场景视图编辑器
//句柄在所有轴上的单元增量
private static Vector3 pointSnap = Vector3.one * 0.1f;
void OnSceneGUI()
{
//获得目标对象
Star star = target as Star;
Transform starTransform = star.transform;
float angle = -360f / (star.frequency * star.points.Length);
for (int i = 0; i < star.points.Length; i++)
{
//偏移量
Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i);
//原偏移量
Vector3 oldPoint = starTransform.TransformPoint(rotation * star.points[i].position),
//创造一个句柄,传入位置和旋转
newPoint = Handles.FreeMoveHandle(
oldPoint, Quaternion.identity, 0.02f, pointSnap, Handles.DotHandleCap);
//判断位置是否相同
if (oldPoint != newPoint)
{
Undo.RecordObject(star, "Move");
//这里要注意,所有网格的位置都是局部空间,因此,而通过句柄返回的newPoint为世界空间位置
//因此,变化到局部空间后的位置再叠加偏移量,然后将偏移量叠加回去,因为对象数据需要的是没有偏移量的点,这里要叠加逆旋转去消除偏移量
star.points[i].position = Quaternion.Inverse(rotation) *
starTransform.InverseTransformPoint(newPoint);
star.UpdateMesh();
}
}
}
最终效果:
|