本文由Aoi翻译,转载请注明出处。文章来自于catlikecoding,原文作者介绍了Unity制作图表、可视化数据的方法。更多的名词解释内容,请点击末尾的“原文链接”查看。
显示多个图表
只有一个图表的话是不是有点枯燥呢?如果能展示多个图表那多好啊!我们需要做的是用不同的方式来计算p.y,代码的其他部分可以保持原样。直接抽取计算p.y的代码,放到它自己的函数里,我们就叫它Linear。这个函数所做的是模仿数学函数f(x) = x。我们把这个做成静态的,因为它不需要对象来实现功能,只需输入值就可以了。
void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
for (int i = 0; i < resolution; i++) {
Vector3 p = points[i].position;
p.y = Linear(p.x);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}
private static float Linear (float x) {
return x;
}
通过创建更多函数并调用它们代替Linear来添加其他数学函数也很简单。我们来添加三个新函数。第一个是幂数,计算f(x) = x2。第二个是抛物线,计算f(x) = (2x - 1)2。第三个是正弦,计算f(x) = (sin(2πx) + 1) / 2。
private static float Exponential (float x) {
return x * x;
}
private static float Parabola (float x){
x = 2f * x - 1f;
return x * x;
}
private static float Sine (float x){
return 0.5f + 0.5f * Mathf.Sin(2 * Mathf.PI * x);
}
四个函数的图
每次在这三个选择之间切换需要改变代码,即使在播放模式下也并不难。创建一个枚举类型,包含想要展示的每个函数的项。我们把它叫做FunctionOption,但是由于我们是在class里定义的,所以它的正是名称是Grapher1.FunctionOption。
添加新类型的公共变量命名函数。这个给我们一个很好地字段,可以在检查器中选择功能。
public enum FunctionOption {
Linear,
Exponential,
Parabola,
Sine
}
public FunctionOption function;
选择使用哪个功能
在检查器中选择功能很好,但是现在还起不到什么作用。每次更新,我们需要根据功能的值来决定调用什么函数。有多种方法可以实现这个,我们使用委托阵列。
首先为函数定义一个委托类型,有一个单精度浮点数作为输入和输出,这相当于函数方法。我们叫它FunctionDelegate。然后添加一个静态阵列,命名functionDelegates,用委托函数填写。在枚举中以相同的顺序将他们命名。
现在我们可以根据功能变量从阵列中选择想要的委托,通过把它计算成一个整数。把这个委托存储在临时变量里,并使用它来计算Y的值。
private delegate float FunctionDelegate (float x);
private static FunctionDelegate[] functionDelegates = {
Linear,
Exponential,
Parabola,
Sine
};
void Update () {
if(currentResolution != resolution){
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
for(int i = 0; i < resolution; i++){
Vector3 p = points[i].position;
p.y = f(p.x);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}
终于,我们可以在播放模式下改变函数图形了!
虽然每次选择另一个函数的时候都要重新创建图形,其他时候基本不用改变。所以不用在每次更新的时候都计算点。然而,如果给函数加上时间就不一样了。例如,改变一下正弦函数,变为f(x) = (sin(2πx + Δ) + 1) / 2,Δ等于播放时间。这将形成一个缓慢的正弦波动画。这里是完整的脚本。
using UnityEngine;
public class Grapher1 : MonoBehaviour {
public enum FunctionOption {
Linear,
Exponential,
Parabola,
Sine
}
private delegate float FunctionDelegate (float x);
private static FunctionDelegate[] functionDelegates = {
Linear,
Exponential,
Parabola,
Sine
};
public FunctionOption function;
[Range(10, 100)]
public int resolution = 10;
private int currentResolution;
private ParticleSystem.Particle[] points;
private void CreatePoints () {
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution];
float increment = 1f / (resolution - 1);
for (int i = 0; i < resolution; i++) {
float x = i * increment;
points[i].position = new Vector3(x, 0f, 0f);
points[i].color = new Color(x, 0f, 0f);
points[i].size = 0.1f;
}
}
void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
for (int i = 0; i < resolution; i++) {
Vector3 p = points[i].position;
p.y = f(p.x);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}
private static float Linear (float x) {
return x;
}
private static float Exponential (float x) {
return x * x;
}
private static float Parabola (float x){
x = 2f * x - 1f;
return x * x;
}
private static float Sine (float x){
return 0.5f + 0.5f * Mathf.Sin(2 * Mathf.PI * x + Time.timeSinceLevelLoad);
}
}
添加一个额外的维度
到目前为止,我们只用X轴输入,又一次用了时间。现在我们将创建一个使用Z轴的新图表对象,因此产出的将不再是一条线,而是网格。
确保你不是在播放模式。创建一个新的Unity对象,就像Graph 1,同时要有一个新的绘图脚本,把它们叫做Graph 2和Grapher2 。你可以复制并需要的地方做修改,这样能加快速度。勾选其名称前的复选框来禁用Graph 1,因为我们不会再用到它了。从Grapher1 中复制代码到Grapher2,先只改变Grapher2的class名,一会再修改代码的其他部分。
完成上述内容的最快方法是复制脚本并编辑class名称,然后复制对象并重命名,然后把新脚本拖拽到就的上面。
切换到一个新的图
为了把线变成一个正方形网格,我们需要改变Grapher2的CreatePoints函数。创建更多的点,用一个嵌套的for循环初始化它们。现在设置Z轴以及蓝色的分量。
private void CreatePoints () {
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution * resolution];
float increment = 1f / (resolution - 1);
int i = 0;
for (int x = 0; x < resolution; x++) {
for (int z = 0; z < resolution; z++) {
Vector3 p = new Vector3(x * increment, 0f, z * increment);
points[i].position = p;
points[i].color = new Color(p.x, 0f, p.z);
points[i++].size = 0.1f;
}
}
}
发现到这里会切到最大视图,可以在右上角切回去:
扁平的网格
现在出现了一个漂亮的扁平网格。但是它不应该显示线性函数吗?是的,但是现在只用于显示沿着Z轴的第一行点。如果你选了一个不同的函数,只有这些点会改变,其他的将保持不变。这是因为Update函数目前只遍历分辨率点,然而它应该是遍历所有点的。
void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
for (int i = 0; i < points.Length; i++) {
Vector3 p = points[i].position;
p.y = f(p.x);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}
四个函数的网格
现在我们再次看到了函数,拓展到了Z轴。然而,有些奇怪的事发生了。显示抛物线的时候尝试旋转透视图,从某些角度,图形绘制是错误的。这是因为粒子是按我们创建的顺序显示的。为了修正这一点,设置粒子系统渲染器模块的Sort Mode为“By Distance”,而不是None。这个确保从所有角度看图形都显示正确,但这也会降低性能。所以在显示大量点的时候不要使用它。幸运的是,如果我们只从正确的方向看图,我们就可以忽略排序。
Sort Mode的 None 和By Distance.
现在来更新功能代码,这样我们就能利用新坐标的优势。首先改变FunctionDelegate输入参数,改为一个vector和一个float,来代替单一的float。虽然可以分别指定X和Z的位置,我们简单地将整个位置矢量。还包含当前时间,而不必在函数里面寻找。
private delegate float FunctionDelegate (Vector3 p, float t);
现在我们需要相应地更新功能函数,改变委托的调用方法。
void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
float t = Time.timeSinceLevelLoad;
for (int i = 0; i < points.Length; i++) {
Vector3 p = points[i].position;
p.y = f(p, t);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}
private static float Linear (Vector3 p, float t) {
return p.x;
}
private static float Exponential (Vector3 p, float t) {
return p.x * p.x;
}
private static float Parabola (Vector3 p, float t){
p.x = 2f * p.x - 1f;
return p.x * p.x;
}
private static float Sine (Vector3 p, float t){
return 0.5f + 0.5f * Mathf.Sin(2 * Mathf.PI * p.x + t);
}
我们已经准备好在数学函数加入Z。例如:改变抛物线函数为f(x,z) = 1 - (2x - 1)2 × (2z - 1)2。我们还可以拓展一下正弦函数,分层多个正弦来得到一个复合的振荡效果。
private static float Parabola (Vector3 p, float t){
p.x += p.x - 1f;
p.z += p.z - 1f;
return 1f - p.x * p.x * p.z * p.z;
}
private static float Sine (Vector3 p, float t){
return 0.50f +
0.25f * Mathf.Sin(4f * Mathf.PI * p.x + 4f * t) * Mathf.Sin(2f * Mathf.PI * p.z + t) +
0.10f * Mathf.Cos(3f * Mathf.PI * p.x + 5f * t) * Mathf.Cos(5f * Mathf.PI * p.z + 3f * t) +
0.15f * Mathf.Sin(Mathf.PI * p.x + 0.6f * t);
}
更加有趣的抛物线和正弦函数
让我们以添加Ripple函数来完成Grapher2,这是从网格中心发出的单一正弦波。这里是完整的脚本。
using UnityEngine;
public class Grapher2 : MonoBehaviour {
public enum FunctionOption {
Linear,
Exponential,
Parabola,
Sine,
Ripple
}
private delegate float FunctionDelegate (Vector3 p, float t);
private static FunctionDelegate[] functionDelegates = {
Linear,
Exponential,
Parabola,
Sine,
Ripple
};
public FunctionOption function;
[Range(10, 100)]
public int resolution = 10;
private int currentResolution;
private ParticleSystem.Particle[] points;
private void CreatePoints () {
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution * resolution];
float increment = 1f / (resolution - 1);
int i = 0;
for (int x = 0; x < resolution; x++) {
for (int z = 0; z < resolution; z++) {
Vector3 p = new Vector3(x * increment, 0f, z * increment);
points[i].position = p;
points[i].color = new Color(p.x, 0f, p.z);
points[i++].size = 0.1f;
}
}
}
void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
float t = Time.timeSinceLevelLoad;
for (int i = 0; i < points.Length; i++) {
Vector3 p = points[i].position;
p.y = f(p, t);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}
private static float Linear (Vector3 p, float t) {
return p.x;
}
private static float Exponential (Vector3 p, float t) {
return p.x * p.x;
}
private static float Parabola (Vector3 p, float t){
p.x = 2f * p.x - 1f;
p.z = 2f * p.z - 1f;
return 1f - p.x * p.x * p.z * p.z;
}
private static float Sine (Vector3 p, float t){
return 0.50f +
0.25f * Mathf.Sin(4 * Mathf.PI * p.x + 4 * t) * Mathf.Sin(2 * Mathf.PI * p.z + t) +
0.10f * Mathf.Cos(3 * Mathf.PI * p.x + 5 * t) * Mathf.Cos(5 * Mathf.PI * p.z + 3 * t) +
0.15f * Mathf.Sin(Mathf.PI * p.x + 0.6f * t);
}
private static float Ripple (Vector3 p, float t){
p.x -= 0.5f;
p.z -= 0.5f;
float squareRadius = p.x * p.x + p.z * p.z;
return 0.5f + Mathf.Sin(15f * Mathf.PI * squareRadius - 2f * t) / (2f + 100f * squareRadius);
}
}
Comments NOTHING