unity3d虚拟视觉效果创建

xman 的头像

 Introduction,介绍

In this tutorial we'll write some C# scripts to display increasingly complex graphs. You'll learn to

create graphs, from a single line to animated volumes;

control a particle system;

write various mathematical functions;

change behavior while in play mode;

use the Start and GetComponent methods;

write loops, both single and nested;

use arrays, enumerations, and delegates.

You're assumed to know your way around Unity's editor and know the basics of creating C# scripts. If you've completed the Clock tutorial you're good to go.

Preparations

We start by opening a new project without any packages. We'll be creating our graphs inside a unit cube, placed between (0, 0, 0) and (1, 1, 1). Let's set up the editor so we get a good view of this area. The 4 Split is a handy predefined view configuration so let's select that. It's in Window / Layout / 4 Spit or in the dropdown list at the top right of the screen. Set the view mode of all of them to Textured. Also flip the perspective view around by clicking on the box at the center of the compass.

Now create a particle system via GameObject / Create Other / Particle System and set its position to (0, 0, 0). This gives us a reference point for calibrating our views. Now zoom and pan the views so they're focused on the unit cube.

Finally, select the Main Camera and make it match the perpective view via GameObject / Align With View (we did the opposite in the Clock tutorial). If that doesn't work right, make sure the correct view is active by clicking in it and then try again.

The particle system is currently producing animated particles at random, which we don't need. Disable the Ellipsoid Particle Emitter by unchecking the Emit field. This causes the preview particles to die out. Also remove the Particle Animator component, because we don't need it. This leaves us with an inert particle system that we can use to visualize graph data.

Creating the first graph

The first graph we'll create will be a simple line graph where the value of Y depends on the value of X. We'll use the positions of the particles to visualize this.

Rename the particle system object to Graph 1, create a C# script named Grapher1 as a minimal GameObject class, and drag it to our object so it gets added as a component.

using UnityEngine;

public class Grapher1 : MonoBehaviour {

}

To get to the particles, we need access to the particle emitter component first. We could do this by adding a public ParticleEmitter variable and then dragging the component to the corresponding field, like we did in the Clock tutorial. However, there's a different way that doesn't require manual configuration.

We use the GetComponent method to retrieve our particle emitter component. We do this inside the special Start method, which is called once before updates start happening. This allows us to keep out variable private, so it doesn't show up in the inspector view.

Not EllipsoidParticleEmitter?How does GetComponent work?When is Start called exactly?

using UnityEngine;

public class Grapher1 : MonoBehaviour {

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

}

}

Using the emitter, we can produce an array of Particle structs which will serve as the points of our graph. To get them, we first need to instruct the emitter to generate a certain number of particles, then fetch those particles and store them for later use.

How much particles should we create? The more particles, the higher the sample resolution of our graph. Let's make it customizable, with a default resolution of 10.

How do arrays work?

using UnityEngine;

public class Grapher1 : MonoBehaviour {

public int resolution = 10;

private Particle[] points;

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

emitter.Emit(resolution);

points = emitter.particles;

}

}

Now that we have the points it's time to position them along the X axis. The first point should be placed at 0 and the last should be placed at 1. All other points should be placed in between. So the distance – or X increment – between two points is 1 / (resolution - 1).

Besides the position, we can also use color to provide the same information. We'll make the red color component of the points equals to their position along the X axis.

We'll use a for loop to iterate over all points and set both their position and color, which are struct values of type Vector3 and Color.

How does a for loop work?What does i++ do?What does new do?

using UnityEngine;

public class Grapher1 : MonoBehaviour {

public int resolution = 10;

private Particle[] points;

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

emitter.Emit(resolution);

points = emitter.particles;

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);

}

}

}

So far, it doesn't work. When playing, we now get some random white particles instead of what we want. This is because the particle array that we got out of the emitter is a copy. Changing this copy doesn't affect the particles in the particle system at all. We need to assign the particle array back to the emitter. That will copy our data back into the particle system, replacing its old particles. We will do this in the Update method.

using UnityEngine;

public class Grapher1 : MonoBehaviour {

public int resolution = 10;

private Particle[] points;

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

emitter.Emit(resolution);

points = emitter.particles;

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);

}

}

void Update () {

emitter.particles = points;

}

}

That's it! We now get a nice black to red line of points along the X axis. How many points are shown depends on what we put in the resolution field before entering play mode.

Right now, resolution is only taken into account when the graph is initialized. Updating its value while in play mode doesn't do anything. Let's change that.

A simple way to detect a change of resolution is by storing it twice and then constantly checking whether both values are still the same. If at some point they're different, we need to rebuild the graph. We'll create a private variable currentResolution for this purpose.

using UnityEngine;

public class Grapher1 : MonoBehaviour {

public int resolution = 10;

private int currentResolution;

private Particle[] points;

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

currentResolution = resolution;

emitter.Emit(resolution);

points = emitter.particles;

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);

}

}

void Update () {

if(currentResolution != resolution){

// currently do nothing

}

emitter.particles = points;

}

}

Because rebuilding is almost the same as the first initialization, let's move that code into a new private method which we name CreatePoints. That way we can reuse the code. The only thing we need to add clearing any previous particles before emitting new ones.

using UnityEngine;

public class Grapher1 : MonoBehaviour {

public int resolution = 10;

private int currentResolution;

private Particle[] points;

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

CreatePoints();

}

private void CreatePoints () {

currentResolution = resolution;

emitter.ClearParticles();

emitter.Emit(resolution);

points = emitter.particles;

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);

}

}

void Update () {

if(currentResolution != resolution){

CreatePoints();

}

emitter.particles = points;

}

}

Now the graph is recreated as soon as we change the value of resolution. However, you'll notice that the console will spit out errors whenever resolution is equal to 1, even while typing. This is because the caluculation of increment will result in a division by zero. One way to prevent this is to make sure that resolution is always at least 2.

using UnityEngine;

public class Grapher1 : MonoBehaviour {

public int resolution = 10;

private int currentResolution;

private Particle[] points;

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

CreatePoints();

}

private void CreatePoints () {

if(resolution < 2){

resolution = 2;

}

currentResolution = resolution;

emitter.ClearParticles();

emitter.Emit(resolution);

points = emitter.particles;

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);

}

}

void Update () {

if(currentResolution != resolution){

CreatePoints();

}

emitter.particles = points;

}

}

Now it's time to set the Y position of the points. Let's start simple and make Y equal to X. In other words, we're visualizing the mathematical equation y = x or the function f(x) = x. To do this, we need to loop over all points, get their position, use the X value to compute the Y value, then set their new position. Once again we use a for loop, which we'll execute each update. (The code snippet below only contains the Update method, everything else stays the same.)Why not directly set position.y?

void Update () {

if(currentResolution != resolution){

CreatePoints();

}

for(int i = 0; i < resolution; i++){

Vector3 p = points[i].position;

p.y = p.x;

points[i].position = p;

}

emitter.particles = points;

}

The next step is to make the point's green color component the same as its Y position. As red plus green becomes yellow, this will cause the line to go from black to yellow.

void Update () {

if(currentResolution != resolution){

CreatePoints();

}

for(int i = 0; i < resolution; i++){

Vector3 p = points[i].position;

p.y = p.x;

points[i].position = p;

Color c = points[i].color;

c.g = p.y;

points[i].color = c;

}

emitter.particles = points;

}

Showing multiple graphs

Just one graph is a little boring. It would be nice if we had multiple graphs to show. All that's needed is different ways to compute p.y, the rest of the code can stay the same. Let's make this explicit by extracting the code that computes p.y and put it in its own method, which we'll call Linear. All this method does is mimic the mathematical function f(x) = x. We're making this method static because it doesn't require an object to function. All it needs is an input value.What does return do?Does it need to be static?

void Update () {

if(currentResolution != resolution){

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;

}

emitter.particles = points;

}

private static float Linear (float x) {

return x;

}

It's easy to add other mathematical functions by creating more methods and calling them instead of Linear. Let's add three new method. The first is Exponential, which calculates f(x) = x2. The second is Parabola, which calculates f(x) = (2x - 1)2. The third is Sine, which calculates f(x) = (sin(2πx) + 1) / 2.What's Mathf?

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);

}

Having to change the code each time we want to switch between these three options isn't very handy. Let's create an enumeration type which contains en entry for each function we want to show. We call it FunctionOption, but because we define it inside our class it's officially known as Grapher1.FunctionOption.

Add a public variable named function of the new type. This gives us a nice field in the inspector for selecting functions.

What's an enum?

using UnityEngine;

public class Grapher1 : MonoBehaviour {

public enum FunctionOption {

Linear,

Exponential,

Parabola,

Sine

}

public FunctionOption function;

public int resolution = 10;

private int currentResolution;

private Particle[] points;

private ParticleEmitter emitter;

// methods not shown in this example

}

Selecting a function in the inspector is nice, but it doesn't do anything yet. Each update, we need to decide which method to call based on the value of function. There are various ways to do this and we'll use an array of delegates.

We first define a delegate type for methods that have a single float as both input and output, which corresponds to our function methods. We call it FunctionDelegate. Then we add a static array named functionDelegates and fill it with delegates to our methods, in the same order that we named them in our enumeration.

What's a delegate?Why list the method names twice?

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;

public int resolution = 10;

private int currentResolution;

private Particle[] points;

private ParticleEmitter emitter;

// methods not shown in this example

}

Now we can select the desired delegate from the array based on our function variable, by casting it to an integer. We store this delegate in a temporary variable and use it to calculate the value of Y.

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;

}

emitter.particles = points;

}

Finally, we can change which function is graphed while in play mode!

Although we need to to recreate the graph each time we select another function, the rest of the time nothing really changes. So it's not required to compute the points each update. However, things change if we introduce time into the functions. As an example, let's change the Sine method so it calculates f(x) = (sin(2πx + Δ) + 1) / 2, where Δ is equal to the play time. This results in a slowly animating sine wave. Here's the entire script.

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;

public int resolution = 10;

private int currentResolution;

private Particle[] points;

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

CreatePoints();

}

private void CreatePoints () {

if(resolution < 2){

resolution = 2;

}

currentResolution = resolution;

emitter.ClearParticles();

emitter.Emit(resolution);

points = emitter.particles;

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);

}

}

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;

}

emitter.particles = points;

}

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);

}

}

Adding an extra dimension

Up to this points we're only using the X axis for input, and in one case time as well. Now we will make a new graph object that uses the Z axis too, thus producing a grid instead of a line.

Create a new Unity object just like Graph 1 along with a new grapher script, calling them Graph 2 and Grapher2 instead. You can speed this up by duplicating them and then making the necessary changes. Disable Graph 1 by toggling the checkbox in front of its name field, because we're not using it anymore. Copy the code from Grapher1 to Grapher2, only changing the class name to Grapher3. We'll modify the rest of the code in a moment.

To change the line into a square grid, we change the CreatePoints method of Grapher2. We need to create a lot more points and use a nested for loop to initialize them. We now set the Z position and the blue color component too.

private void CreatePoints () {

if(resolution < 2){

resolution = 2;

}

currentResolution = resolution;

emitter.ClearParticles();

emitter.Emit(resolution * resolution);

points = emitter.particles;

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);

}

}

}

Now we have a nice flat grid! But shouldn't it show the Linear function? It does, but currently only for the first row of points along the Z axis. If you select a different function, only these points will change while the rest remain as they are. This is because in the Update method only loops over resolution points, while it should loop over all of them. We'll use the Length property that every array has to fix this.

void Update () {

if(currentResolution != resolution){

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;

}

emitter.particles = points;

}

Now we can see our functions again, extended along to Z axis. Increasing the resolution results in a nice smooth surface. However, you'll notice that if you set the resolution higher than 127, part of the graph will disappear. This is because there is a limit to how many particles a Particle Renderer will display. So it's a good idea to limit the resolution to 127, which translates to 16129 points.

private void CreatePoints () {

if(resolution < 2){

resolution = 2;

}

else if(resolution > 127){

resolution = 127;

}

currentResolution = resolution;

emitter.ClearParticles();

emitter.Emit(resolution * resolution);

points = emitter.particles;

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);

}

}

}

There's something else going on that's pretty weird. Try rotating the perspective view while displaying the parabola. From some angles, the graph is drawn wrong. This is because the particles are drawn in the order that we've created them, they don't take view direction into account. You can fix this by setting the Stretch Particles field of the Particle Renderer component to Sorted Billboard. While this makes sure that the graph is show correctly from all view angles, it results in a massive performance hit as well. It's best to stick to the Billboard option and simply look at the graph from only a few angles.

Let's update our function code so we can take advantage of the new dimension. First change the input paramaters of FunctionDelegate to a vector and a float instead of just a single float. While we could specify the X and Z position separately, we'll simply give it the entire position vector. We'll also include the current time, instead of having to look it up inside the functions themselves.

private delegate float FunctionDelegate (Vector3 p, float t);

Now we need to update the function methods accordingly and change how the delegate is called.

void Update () {

if(currentResolution != resolution){

CreatePoints();

}

FunctionDelegate f = functionDelegates[(int)function];

for(int i = 0; i < points.Length; i++){

Vector3 p = points[i].position;

p.y = f(p, Time.timeSinceLevelLoad);

points[i].position = p;

Color c = points[i].color;

c.g = p.y;

points[i].color = c;

}

emitter.particles = points;

}

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);

}

We're ready to include Z in our mathematical functions! For example, change the Parabola function to f(x,z) = 1 - (2x - 1)2 * (2z - 1)2. We can also go wild with the Sine function, layering multiple sines to get a complex oscillating effect.

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);

}

Let's finish Grapher2 by adding a Ripple function, which is a single sine wave emanating from the center of the grid. Here's the entire script.

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;

public int resolution = 10;

private int currentResolution;

private Particle[] points;

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

CreatePoints();

}

private void CreatePoints () {

if(resolution < 2){

resolution = 2;

}

else if(resolution > 127){

resolution = 127;

}

currentResolution = resolution;

emitter.ClearParticles();

emitter.Emit(resolution * resolution);

points = emitter.particles;

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);

}

}

}

void Update () {

if(currentResolution != resolution){

CreatePoints();

}

FunctionDelegate f = functionDelegates[(int)function];

for(int i = 0; i < points.Length; i++){

Vector3 p = points[i].position;

p.y = f(p, Time.timeSinceLevelLoad);

points[i].position = p;

Color c = points[i].color;

c.g = p.y;

points[i].color = c;

}

emitter.particles = points;

}

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){

float squareRadius = (p.x - 0.5f) * (p.x - 0.5f) + (p.z - 0.5f) * (p.z - 0.5f);

return 0.5f + Mathf.Sin(15 * Mathf.PI * squareRadius - 2f * t) / (2f + 100f * squareRadius);

}

}

Full-blown 3D

It's time to add the third dimension! This will turn our graph from a grid into a cube, which we can use for volumetric representations. In other words, we'll create a tiny voxel system.

Duplicate Graph 2 and Grapher2 and change them into Graph 3 and Grapher3, just like we did for the second graph. Don't forget to disable Graph 2.

We'll make a few changes to Grapher3. First, we'll limit resolution to 25, which translates to 15235 points. We also need to initialize the Y position of the points and the green color component.

private void CreatePoints () {

if(resolution < 2){

resolution = 2;

}

else if(resolution > 25){

resolution = 25;

}

currentResolution = resolution;

emitter.ClearParticles();

emitter.Emit(resolution * resolution * resolution);

points = emitter.particles;

float increment = 1f / (resolution - 1);

int i = 0;

for(int x = 0; x < resolution; x++){

for(int z = 0; z < resolution; z++){

for(int y = 0; y < resolution; y++){

Vector3 p = new Vector3(x, y, z) * increment;

points[i].position = p;

points[i++].color = new Color(p.x, p.y, p.z);

}

}

}

}

Right now Graph 3 looks the same as Graph 2, except that it seems a little brighter. This is because we still set the Y positions in the Update method. So all points with the same X and Z position will collapse to the same Y position. We must no longer set the Y position, but the color's alpha component instead. That way our functions will define the volume's density.

void Update () {

if(currentResolution != resolution){

CreatePoints();

}

FunctionDelegate f = functionDelegates[(int)function];

for(int i = 0; i < points.Length; i++){

Color c = points[i].color;

c.a = f(points[i].position, Time.timeSinceLevelLoad);

points[i].color = c;

}

emitter.particles = points;

}

Now our graph looks like a mostly solid cube. The functions aren't very visible, because they do not vary along the Y axis. Only the two animated functions, Sine and Ripple, produce somewhat interesting results.

Let's change Linear into f(x,y,z) = 1 - x - y - z. That way it starts solid at (0, 0, 0) and fades to transparent along a straight line. We can do a similar thing with Exponential as well. Even better, let's animate them a bit so it's more interesting to look at.

private static float Linear (Vector3 p, float t) {

return 1f - p.x - p.y - p.z + 0.5f * Mathf.Sin(t);

}

private static float Exponential (Vector3 p, float t) {

return 1f - p.x * p.x - p.y * p.y - p.z * p.z + 0.5f * Mathf.Sin(t);

}

Next, we update Parabola so it will produce a cylinder, once again with a little pulsating animation. We also add the third dimension to Ripple, turning it into a sphere-spawning animation.

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 + 0.5f * Mathf.Sin(t);

}

private static float Ripple (Vector3 p, float t){

float squareRadius =

(p.x - 0.5f) * (p.x - 0.5f) +

(p.y - 0.5f) * (p.y - 0.5f) +

(p.z - 0.5f) * (p.z - 0.5f);

return Mathf.Sin(4 * Mathf.PI * squareRadius - 2f * t);

}

Finally, we'll update Sine too. We transform it into eight blobs by multiplying the square sines of X, Y, and Z together. We only animate the Z-based sine, but we make a distinction here. The top and bottom half of the graph will move in opposite directions. What does the question mark do?

private static float Sine (Vector3 p, float t){

float x = Mathf.Sin(2 * Mathf.PI * p.x);

float y = Mathf.Sin(2 * Mathf.PI * p.y);

float z = Mathf.Sin(2 * Mathf.PI * p.z + (p.y > 0.5f ? t : -t));

return x * x * y * y * z * z;

}



A nice variant would be one where all voxels are either fully visible or fully transparent. It would result in a solid but pixelated appearance. Let's add an absolute field to toggle such behaviour, along with a threshold field that determines how solid a voxel must be before it becomes visible. Each update, if absolute is switched on, we'll loop over all points and modify their alpha component based on threshold. Here's the complete script.

using UnityEngine;

public class Grapher3 : 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 bool absolute;

public float threshold = 0.5f;

public FunctionOption function;

public int resolution = 10;

private int currentResolution;

private Particle[] points;

private ParticleEmitter emitter;

void Start () {

emitter = GetComponent<ParticleEmitter>();

CreatePoints();

}

private void CreatePoints () {

if(resolution < 2){

resolution = 2;

}

else if(resolution > 25){

resolution = 25;

}

currentResolution = resolution;

emitter.ClearParticles();

emitter.Emit(resolution * resolution * resolution);

points = emitter.particles;

float increment = 1f / (resolution - 1);

int i = 0;

for(int x = 0; x < resolution; x++){

for(int z = 0; z < resolution; z++){

for(int y = 0; y < resolution; y++){

Vector3 p = new Vector3(x, y, z) * increment;

points[i].position = p;

points[i++].color = new Color(p.x, p.y, p.z);

}

}

}

}

void Update () {

if(currentResolution != resolution){

CreatePoints();

}

FunctionDelegate f = functionDelegates[(int)function];

for(int i = 0; i < points.Length; i++){

Color c = points[i].color;

c.a = f(points[i].position, Time.timeSinceLevelLoad);

points[i].color = c;

}

if(absolute){

for(int i = 0; i < points.Length; i++){

Color c = points[i].color;

c.a = c.a >= threshold ? 1f : 0f;

points[i].color = c;

}

}

emitter.particles = points;

}

private static float Linear (Vector3 p, float t) {

return 1f - p.x - p.y - p.z + 0.5f * Mathf.Sin(t);

}

private static float Exponential (Vector3 p, float t) {

return 1f - p.x * p.x - p.y * p.y - p.z * p.z + 0.5f * Mathf.Sin(t);

}

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 + 0.5f * Mathf.Sin(t);

}

private static float Sine (Vector3 p, float t){

float x = Mathf.Sin(2 * Mathf.PI * p.x);

float y = Mathf.Sin(2 * Mathf.PI * p.y);

float z = Mathf.Sin(2 * Mathf.PI * p.z + (p.y > 0.5f ? t : -t));

return x * x * y * y * z * z;

}

private static float Ripple (Vector3 p, float t){

float squareRadius =

(p.x - 0.5f) * (p.x - 0.5f) +

(p.y - 0.5f) * (p.y - 0.5f) +

(p.z - 0.5f) * (p.z - 0.5f);

return Mathf.Sin(4 * Mathf.PI * squareRadius - 2f * t);

}

}

We can now produce nice graphs from data with up to three dimensions! It's possible to create very intricate volumetric stuff, only your imagination and mathematical knowledge is the limit.
转自:http://catlikecoding.com

评论

Member since:
17 三月 2012
Last activity:
8 周 4 天

进来学习

Member since:
9 六月 2010
Last activity:
10 周 20 小时

这个初看效果怎么类似  volume shader的效果,不过真实的 却很占系统资源,这个不知道效果如何,不过外国现在的 数字技术的确比中国强  

Member since:
9 六月 2010
Last activity:
10 周 20 小时

这个初看效果怎么类似  volume shader的效果,不过真实的 却很占系统资源,这个不知道效果如何,不过外国现在的 数字技术的确比中国强  

Member since:
6 一月 2012
Last activity:
19 周 3 天

    Whould  mind  tenche me?   全看不懂啊!

Member since:
18 八月 2011
Last activity:
19 周 5 天

 nice work!

Member since:
6 七月 2011
Last activity:
1 周 4 天

 我顶。除了介绍二字,其他全是E文。。。