# Mathematical Surfaces

Sculpting with Numbers

- Create a function library.
- Use a delegate and an enumeration type.
- Display 2D functions with a grid.
- Define surfaces in 3D space.

This is the third tutorial in a series about learning the basics of working with Unity. It is a continuation of the Building a Graph tutorial, so we won't start a new project. This time we'll make it possible to display multiple and more complex functions.

This tutorial is made with Unity 2019.4.10f1.

## Function Library

After finishing the previous tutorial we have a point graph that shows an animated sine wave while in play mode. It is also possible to show other mathematical functions. You can change the code and the function will change along with it. You can even do this while the Unity editor is in play mode. Execution will be paused, the current game state saved, then the scripts are compiled again, and finally the game state is reloaded and play resumes. This is known as a hot reload. Not everything survives a hot reload, but our graph does. It will switch to animating the new function, without being aware that something changed.

While changing code during play mode can be convenient, it is not a handy way to switch back and forth between multiple functions. It would be much better if we could change the function via a configuration option of the graph.

### Library Class

We could declare multiple mathematical functions inside `Graph`

, but let's dedicate that class to displaying a function, leaving it unaware of the exact mathematical equations. This is an example of specialization and separation of concerns.

Create a new `FunctionLibrary`

C# script and put it in the *Scripts* folder, next to `Graph`

. You could either use a menu option to create a new asset or duplicate and the rename `Graph`

. In either case clear the file contents and start with using `UnityEngine`

and declaring an empty `FunctionLibrary`

class that doesn't extend anything.

using UnityEngine; public class FunctionLibrary {}

This class isn't going to be a component type. We're also not going to create an object instance of it. Instead we'll use it to provide a collection of publicly-accessible methods that represent math functions, similar to Unity's `Mathf`

.

To signify that this class is not to be used as an object template mark it as static, by writing the `static`

keyword before `class`

.

public static class FunctionLibrary {}

### Function Method

Our first function is going to be the same sine wave that `Graph`

is currently showing. We need to create a method for it. This works the same as creating an `Awake`

or `Update`

method, except that we will name it `Wave`

instead.

public static class FunctionLibrary { void Wave () {} }

By default methods are instance methods, which means that they have to be invoked on an object instance. To make them work directly at the class level we have to mark it as static, just like `FunctionLibrary`

itself.

static void Wave () {}

And to make it publicly accessible give it the `public`

access modifier as well.

public static void Wave () {}

This method is going to represent our mathematical function `f(x,t)=sin(pi(x+t))`. This means that it must produce a result, which has to be a floating-point number. So instead of `void`

the return type of the function needs to be `float`

.

public static float Wave () {}

Next, we have to add the two parameters to the method's parameter list, just like for the mathematical function. The only difference is that we have to write the type in front of each parameter, which is `float`

.

public static float Wave (float x, float t) {}

Now we can put the code that computes the sine wave inside the method, using its `x`

and `t`

parameters.

public static float Wave (float x, float t) { Mathf.Sin(Mathf.PI * (x + t)); }

The last step is to explicitly indicate what the result of the method is. As this is a `float`

method, it has to return a `float`

when it's done. We indicate this by writing `return`

followed by what the result is supposed to be, which is our mathematical computation.

public static float Wave (float x, float t) { return Mathf.Sin(Mathf.PI * (x + t)); }

It is now possible to invoke this method inside `Graph.Update`

, using `position.x`

and `time`

as arguments for its parameters. Its result can by used to set the point's Y coordinate, instead of an explicit math equation.

void Update () { float time = Time.time; for (int i = 0; i < points.Length; i++) { Transform point = points[i]; Vector3 position = point.localPosition; position.y = FunctionLibrary.Wave(position.x, time); point.localPosition = position; } }

### Implicitly using a Type

We'll be using `Mathf.PI`

, `Mathf.Sin`

, and other methods from `Mathf`

at lot in `FunctionLibrary`

. It would be nice if we could write those without having to explicitly mention the type all the time. We can make this possible by adding another `using`

statement at the top of the `FunctionLibrary`

file, with the extra `static`

keyword followed by the explicit `UnityEngine.Mathf`

type. That makes all constant and static members of the type usable without explicitly mentioning the type itself.

using UnityEngine; using static UnityEngine.Mathf;

Now we can shorten the code in `Wave`

by omitting `Mathf`

.

public static float Wave (float x, float z, float t) { return Sin(PI * (x + t)); }

### A Second Function

Let's add another function method. This time we'll make a slightly more complex function, using more than one sine wave. Begin by duplicating the `Wave`

method and renaming it to `MultiWave`

.

public static float Wave (float x, float t) { return Sin(PI * (x + t)); } public static float MultiWave (float x, float t) { return Sin(PI * (x + t)); }

We'll keep the sine function that we already have, but add something extra to it. To make that easy, assign the current result to an `y`

variable before returning it.

public static float MultiWave (float x, float t) { float y = Sin(PI * (x + t)); return y; }

The simplest way to add more complexity to a sine wave is to add another one that has double the frequency. This means that it changes twice as fast, which is done by multiplying the argument of the sine function by 2. At the same time, we'll halve the result of this function. That keeps the shape of the new sine wave the same as the old one, but at half size.

float y = Sin(PI * (x + t)); y += Sin(2f * PI * (x + t)) / 2f; return y;

This gives us the mathematical function `f(x,t)=sin(pi(x+t))+sin(2pi(x+t))/2`. As both the positive and negative extremes of the sine function are 1 and −1, the maximum and minimum values of this new function could be 1.5 and −1.5. To guarantee that we stay in the −1–1 range, we should divide the sum by 1.5.

return y / 1.5f;

Division requires a bit more work than multiplication, so it's a rule of thumb to prefer multiplication over division. However, constant expressions like `1f / 2f`

and also `2f * Mathf.PI`

are already reduced to a single number by the compiler. So we could rewrite our code to only use multiplication at runtime. We have to make sure that the constant portions are reduced first, using operation order and brackets.

y += Sin(2f * PI * (x + t)) * (1f / 2f); return y * (2f / 3f);

We can also directly write `0.5f`

instead of `1f / 2f`

, but the inverse of 1.5 cannot be written exactly in decimal notation so we'll keep using `2f / 3f`

, which the compiler reduces to a floating-point representation of it with maximum precision.

y += 0.5f * Sin(2f * PI * (x + t));

Now use this function instead of `Wave`

in `Graph.Update`

and see what it looks like.

position.y = MultiSineFunction(position.x, t);

You could say that a smaller sine wave is now following a larger sine wave. We can also make the smaller one slide along the larger, for example by halving the time for the larger wave. The result will be a function that doesn't just slide as time progresses, it changes its shape. It now takes four seconds for the pattern to repeat.

float y = Sin(PI * (x + 0.5f * t)); y += 0.5f * Sin(2f * PI * (x + t));

### Selecting Functions in the Editor

The next thing we can do is add some code that makes it possible to control which method is used by `Graph`

. We could do this with a slider, just like for the graph's resolution. As we have two functions to choose from, we'll need a serializable integer field with a range of 0–1. Name it `function`

so it's obvious what it controls.

[SerializeField, Range(10, 100)] int resolution = 10; [SerializeField, Range(0, 1)] int function = 0;

Now we can check `function`

inside the loop of `Update`

. If it is zero then the graph should display `Wave`

. To make that choice we'll use the `if`

statement followed by an expression and a code block. This works like `while`

except it doesn't loop back, so the block either gets executed or skipped. In this case the test is whether `function`

equals zero, which can be done with the `==`

equality operator.

void Update () { float time = Time.time; for (int i = 0; i < points.Length; i++) { Transform point = points[i]; Vector3 position = point.localPosition; if (function == 0) { position.y = FunctionLibrary.Wave(position.x, time); } point.localPosition = position; } }

We can follow the if-block with `else`

and another block, which gets executed if the test failed. In that case the graph should display `MultiWave`

instead.

if (function == 0) { position.y = FunctionLibrary.Wave(position.x, time); } else { position.y = FunctionLibrary.MultiWave(position.x, time); }

This makes it possible to control the function via the graph's inspector, also while we're in play mode.

### Ripple Function

Let's add a third function to our library, one that produces a ripple-like effect. We create it by making a sine wave move away from the origin, instead of always traveling in the same direction. We can do this by basing it on the distance from the center, which is the absolute of X. Start with computing only that, with the help of `Mathf.Abs`

, in a new `FunctionLibrary.Ripple`

method. Store the distance in a `d`

variable and then return it.

public static float Ripple (float x, float t) { float d = Abs(x); return d; }

To show it, increase the range of `Graph.function`

to 2 and add another block for the `Wave`

method in `Update`

. We can chain multiple conditional blocks by writing another `if`

directly after `else`

, so it becomes an if-else block which should be executed when `function`

equals 1. Then add a new `else`

block for the ripple.

[SerializeField, Range(0, 2)] … void Update () { float time = Time.time; for (int i = 0; i < points.Length; i++) { Transform point = points[i]; Vector3 position = point.localPosition; if (function == 0) { position.y = FunctionLibrary.Wave(position.x, time); } else if (function == 1) { position.y = FunctionLibrary.MultiWave(position.x, time); } else { position.y = FunctionLibrary.Ripple(position.x, time); } point.localPosition = position; } }

Back to `FunctionLibrary.Ripple`

, we use the distance as input for the sine function and make that the result. Specifically, we'll use `y=sin(4pid)` with `d=|x|` so the ripple goes up and down multiple times in the graph's domain.

public static float Ripple (float x, float t) { float d = Abs(x); float y = Sin(4f * PI * d); return y; }

The result is visually hard to interpret because Y varies too much. We can reduce that by decreasing the amplitude of the wave. But a ripple doesn't have a fixed amplitude, it decreases with distance. So let's turn our function into `y=sin(4pid)/(1+10d)`.

float y = Sin(4f * PI * d); return y / (1f + 10f * d);

The finishing touch is animating the ripple. To make it flow outward we have to subtract the time from the value we pass to the sine function. Let's use `pit` so the final function becomes `y=sin(pi(4d-t))/(1+10d)`.

float y = Sin(PI * (4f * d - t)); return y / (1f + 10f * d);

## Managing Methods

A sequence of conditional blocks works for two or three functions, but it gets unwieldy fast when trying to support more than that. It would be much more convenient if we could ask our library for a reference to a method based on some criteria and then invoke it repeatedly.

### Delegates

It is possible to get a reference to a method by using a delegate. A delegate is a special type that defines what kind of method something can reference. There isn't a standard delegate type for our mathematical function methods, but we can define it ourselves. Because it is a type we could create it in its own file, but as it's specifically for methods of our library we'll define it inside the `FunctionLibrary`

class, making it an inner or nested type.

To create the delegate type duplicate the `Wave`

function, rename it to `Function`

and replace its code block with a semicolon. This defines a method signature without an implementation. We then turn it into a delegate type by replacing the `static`

keyword with `delegate`

.

public static class FunctionLibrary { public delegate float Function (float x, float t); … }

Now we can introduce a `GetFunction`

method that returns a `Function`

given an index parameter, using the same if-else logic that we used in the loop, except that in each block we return the appropriate method instead of invoking it.

public delegate float Function (float x, float t); public static Function GetFunction (int index) { if (index == 0) { return Wave; } else if (index == 1) { return MultiWave; } else { return Ripple; } }

Next, we use this method to get a function delegate at the beginning of `Graph.Update`

, based on `function`

, and store it in a variable. Because this code isn't inside `FunctionLibrary`

we have to refer to the nested delegate type as `FunctionLibrary.Function`

.

void Update () { FunctionLibrary.Function f = FunctionLibrary.GetFunction(function); … }

Then invoke the delegate variable instead of an explicit method in the loop.

FunctionLibrary.Function f = FunctionLibrary.GetFunction(function); float time = Time.time; for (int i = 0; i < points.Length; i++) { Transform point = points[i]; Vector3 position = point.localPosition;~~//if (function == 0) {~~~~// position.y = FunctionLibrary.Wave(position.x, time);~~~~//}~~~~//else if (function == 1) {~~~~// position.y = FunctionLibrary.MultiWave(position.x, time);~~~~//}~~~~//else {~~~~// position.y = FunctionLibrary.Ripple(position.x, time);~~~~//}~~position.y = f(position.x, time); point.localPosition = position; }

### An Array of Delegates

`Graph.Update`

a lot, but we only moved the if-else code to `FunctionLibrary.GetFunction`

. We can get rid of it completely by replacing it with indexing an array. Begin by adding a static field for a `functions`

array to `FunctionLibrary`

.
public delegate float Function (float x, float t); static Function[] functions; public static Function GetFunction (int index) { … }

We're always going to put the same elements in this array, so we can explicitly define its contents as part of its declaration. This is done by assigning a comma-separated array element sequence, between curly brackets. The simplest is an empty list.

static Function[] functions = {};

This means that we immediately get an array instance, but it is empty. Change this so it will contain delegates to our method, in the same order as before.

static Function[] functions = { Wave, MultiWave, Ripple };

The `GetFunction`

method can now simply index the array to return the appropriate delegate.

public static Function GetFunction (int index) { return functions[index]; }

### Enumerations

An integer slider works, but it is not obvious that 0 represents the wave function and so on. It would be clearer if we had a dropdown list containing the function names. We can use an enumeration to achieve this.

Enumerations can be created by defining an `enum`

type. Once again we'll do this inside `FunctionLibrary`

, this time naming it `FunctionName`

. In this case the type name is followed by a list of labels within curly brackets. We can use a copy of the array element list, but without the semicolon. Note that these are simple labels, they don't reference anything, although they follow the same rules as type names. It's our responsibility to keep the two lists identical.

public delegate float Function (float x, float t); public enum FunctionName { Wave, MultiWave, Ripple } static Function[] functions = { Wave, MultiWave, Ripple };

Now replace the index parameter of `GetFunction`

with a name parameter of type `FunctionName`

. This indicates that the argument has to be a valid function name.

public static Function GetFunction (FunctionName name) { return functions[name]; }

Enumerations can be considered syntactical sugar. By default, each label of the enumeration represents an integer. The first label corresponds to 0, the second label to 1, and so on. So we can use the name to index the array. However, the compiler will complain that an enumeration cannot be implicitly cast to an integer. We have to explicitly perform this cast.

return functions[(int)name];

The last step is to change the type of the `Graph.function`

field to `FunctionLibrary.FunctionName`

and remove its `Range`

attribute.

~~//[SerializeField, Range(0, 2)]~~[SerializeField] FunctionLibrary.FunctionName function = default;

The inspector of `Graph`

now shows a dropdown list containing the function names, with spaces added between capitalized words.

## Adding Another Dimension

So far our graph only contains a single line of points. We map 1-dimensional values to other 1D values, though if you take time into account it's actually mapping 2D values to 1D values. So we're already mapping higher-dimensional input to a 1D value. Like we added time, we can add additional spatial dimensions as well.

Currently, we're using the X dimension as the spatial input for our functions. The Y dimension is used to display the output. That leaves Z as a second spatial dimension to use for input. Adding Z an an input upgrades our line to a square grid.

### 3D Colors

With Z no longer constant, change our *Point Surface* shader so it also modifies the blue albedo component, by removing the `.rg`

and `.xy`

code from the assignment.

surface.Albedo = saturate(input.worldPos * 0.5 + 0.5);

And adjust our *Point URP* shader graph so that Z is treated the same as X and Y.

### Upgrading the Functions

To support a second non-time input for our functions, add a `z`

parameter after the `x`

parameter of the `FunctionLibrary.Function`

delegate type.

public delegate float Function (float x, float z, float t);

This requires us to also add the parameter to our three function methods.

public static float Wave (float x, float z, float t) { … } public static float MultiWave (float x, float z, float t) { … } public static float Ripple (float x, float z, float t) { … }

And also add `position.z`

as an argument when invoking the function in `Graph.Update`

.

position.y = f(position.x, position.z, time);

### Creating a Grid of Points

To show the Z dimension we have to turn our line of points into a grid of points. We can do this by creating multiple lines, each offset one step along Z. We'll use the same range for Z as we use for X, so we'll create as many lines as we currently have points. This means that we have to square the amount of points. Adjust the creation of the `points`

array in `Awake`

so it's big enough to contain all the points.

points = new Transform[resolution * resolution];

As we increase the X coordinate each iteration of the loop in `Awake`

based on the resolution, simply creating more points will result in a single long line. We have to adjust the initialization loop to take the second dimension into account.

First, let's keep track of the X coordinate explicitly. Do this by declaring and incrementing an `x`

variable inside the `for`

loop, along with the `i`

iterator variable. The third section of the `for`

statement can be turned into a comma-separated lists for this purpose.

points = new Transform[resolution * resolution]; for (int i = 0, x = 0; i < points.Length; i++, x++) { … }

Each time we finish a row we have to reset `x`

back to zero. A row is finished when `x`

has become equal to `resolution`

, so we can use an `if`

block at the top of the loop to take care of this. Then use `x`

instead of `i`

to calculate the X coordinate.

for (int i = 0, x = 0; i < points.Length; i++, x++) { if (x == resolution) { x = 0; } Transform point = Instantiate(pointPrefab); position.x = (x + 0.5f) * step - 1f; … }

Next, each row has to be offset along the Z dimension. This can be done by adding a `z`

variable to the `for`

loop as well. This variable must not be incremented each iteration. Instead, it only increments when we move on to the next row, for which we already have an `if`

block. Then set the position's Z coordinate just like its X coordinate, using `z`

instead of `x`

.

for (int i = 0, x = 0, z = 0; i < points.Length; i++, x++) { if (x == resolution) { x = 0; z += 1; } Transform point = Instantiate(pointPrefab); position.x = (x + 0.5f) * step - 1f; position.z = (z + 0.5f) * step - 1f; point.localPosition = position; point.localScale = scale; point.SetParent(transform, false); points[i] = point; }

We now create a square grid of points instead of a single line. Because our functions still only use the X dimension it will look like the original points have been extruded into lines.

### Better Visuals

Because our graph is now 3D I'll be looking at it from a perspective viewpoint from now on, using the game window. To quickly choose a good camera position you can find a nice viewpoint in the scene window while in play mode, exit play mode and then make the game's camera match the viewpoint. You can do that via *GameObject / Align With View* with *Main Camera* selected and the scene window visible. I made it look roughly down on the XZ diagonal. Then I changed the Y rotation of *Directional Light* from −30 to 30, to improve the lighting for that view angle.

Besides that, we can tweak the shadow quality a bit. Shadows probably already look acceptable when using the default render pipeline, but they're configured for looking far away while we're looking at our graph up close.

You can select the visual quality level for the default render pipeline by going to the *Quality* project settings and selecting one of the preconfigured levels. The default dropdown controls which level gets used by default for standalone apps.

We can tweak performance and precision of the shadows further by going to the *Shadows* section below and reducing *Shadow Distance* to 10 and setting *Shadow Cascades* to *No Cascades*. The default settings render shadows up to four times, which is overkill for us.

The URP doesn't use these settings, instead its shadows are configured via the inspector of our *URP* asset. It already uses no cascades by default, but its shadow distance can be reduced to 10. Also, to match the standard *Ultra* quality of the default render pipeline enable *Soft Shadows* and increase *Shadow Resolution* under *Lighting* to 4096.

Finally, you might now see obvious visual tearing while in play mode. This can be prevented from happening in the game window by enabling *VSync (Game view only)* via the second dropdown menu from the left of the game window's toolbar. When enabled the presentation of new frames is synchronized with the display refresh rate. This only works reliably if no scene window is visible at the same time. VSync is configured for standalone apps via the *Other* section of the quality settings.

### Incorporating Z

The simplest way to use Z in the `Wave`

function is to use the sum of both X and Z instead of just X. That will create a diagonal wave.

public static float Wave (float x, float z, float t) { return Sin(PI * (x + z + t)); }

And the most straightforward change for `MultiSine`

is to make each wave use a separate dimension. Let's make the smaller one use Z.

public static float MultiWave (float x, float z, float t) { float y = Sin(PI * (x + 0.5f * t)); y += 0.5f * Sin(2f * PI * (z + t)); return y * (2f / 3f); }

We can also add a third wave that travels along the XZ diagonal. Let's use the same wave as `Wave`

does, except with the time slowed down to a quarter. Then scale the result by dividing by 2.5 to keep it inside the −1–1 domain.

float y = Sin(PI * (x + 0.5f * t)); y += 0.5f * Sin(2f * PI * (z + t)); y += Sin(PI * (x + z + 0.25f * t)); return y * (1f / 2.5f);

Note that the first and third waves will cancel each other out at regular intervals.

Finally, to make the ripple spread in all directions on the XZ plane we have to calculate the distance in both dimensions. We can use the Pythagorean theorem for this, with help of the `Mathf.Sqrt`

method.

public static float Ripple (float x, float z, float t) { float d = Sqrt(x * x + z * z); float y = Sin(PI * (4f * d - t)); return y / (1f + 10f * d); }

## Leaving the Grid

By using X and Z to define Y we are able to create functions that describe a large variety of surfaces, but they're always linked to the XZ plane. No two points can have the same X and Z coordinates while having a different Y coordinate. This means that the curvature of our surfaces is limited. Their slopes cannot become vertical and they cannot fold backward. To make this possible, our functions would have to not only output Y, but also X and Z.

### Three-dimensional Functions

If our functions were to output 3D positions instead of 1D values we could use them to create arbitrary surfaces. For example, the function `f(x,z)=[[x],[0],[z]]` describes the XZ plane, while the function `f(x,z)=[[x],[z],[0]]` describes the XY plane.

Because the input parameters for these functions no longer correspond to the final X and Z coordinates, it's no longer appropriate to name them `x` and `z`. Instead, they're used to create a parametric surface and are often named `u` and `v`. So we'd get functions like `f(u,v)=[[u],[sin(pi(u+v))],[v]]`.

Adjust our `Function`

delegate type to support this new approach. The only required change is replacing its `float`

return type with `Vector3`

, but let's also rename its parameters.

public delegate Vector3 Function (float u, float v, float t);

We now have to adjust our function methods accordingly. We'll simply use U and V directly for X and Z. It isn't requires to adjust the parameter names—only their types need to match the delegate—but let's do this to stay consistent. If your code editor supports it you can quickly refactor-rename parameters and other things so it gets renamed everywhere it's used in one go, via a menu or context menu option.

Begin with `Wave`

. Have it initially declare a `Vector3`

variable, then set its components, and then return it. We don't have to give the vector an initial value because we set all its fields before returning it.

public static Vector3 Wave (float u, float v, float t) { Vector3 p; p.x = u; p.y = Sin(PI * (u + v + t)); p.z = v; return p; }

Then give `MultiWave`

and `Ripple`

the same treatment.

public static Vector3 MultiWave (float u, float v, float t) { Vector3 p; p.x = u; p.y = Sin(PI * (u + 0.5f * t)); p.y += 0.5f * Sin(2f * PI * (v + t)); p.y += Sin(PI * (u + v + 0.25f * t)); p.y *= 1f / 2.5f; p.z = v; return p; } public static Vector3 Ripple (float u, float v, float t) { float d = Sqrt(u * u + v * v); Vector3 p; p.x = u; p.y = Sin(PI * (4f * d - t)); p.y /= 1f + 10f * d; p.z = v; return p; }

Because the X and Z coordinates of points are no longer constant we can also no longer rely on their initial values in `Graph.Update`

. We can solve this problem by replacing the loop in `Update`

with the same one used in `Awake`

, except that we can now directly assign the function result to the point's position.

void Update () { FunctionLibrary.Function f = FunctionLibrary.GetFunction(function); float time = Time.time; float step = 2f / resolution; for (int i = 0, x = 0, z = 0; i < points.Length; i++, x++) { if (x == resolution) { x = 0; z += 1; } float u = (x + 0.5f) * step - 1f; float v = (z + 0.5f) * step - 1f; points[i].localPosition = f(u, v, time); } }

Note that we only need to recalculate `v`

when `z`

changes. This does require us to set its initial value before the start of the loop.

float v = 0.5f * step - 1f; for (int i = 0, x = 0, z = 0; i < points.Length; i++, x++) { if (x == resolution) { x = 0; z += 1; v = (z + 0.5f) * step - 1f; } float u = (x + 0.5f) * step - 1f;~~float v = (z + 0.5f) * step - 1f;~~points[i].localPosition = f(u, v, time); }

Also note that because `Update`

now uses the resolution, changing it while in play mode will deform the graph, stretching or squashing the grid into a rectangle.

We no longer need to initialize the positions in `Awake`

, so we can make that method a lot simpler. We can suffice with setting only the scale and parent of the points.

void Awake () { float step = 2f / resolution; var scale = Vector3.one * step;~~//var position = Vector3.zero;~~points = new Transform[resolution * resolution]; for (int i = 0; i < points.Length; i++) {~~//if (x == resolution) {~~~~// x = 0;~~~~// z += 1;~~~~//}~~Transform point = Instantiate(pointPrefab);~~//position.x = (x + 0.5f) * step - 1f;~~~~//position.z = (z + 0.5f) * step - 1f;~~~~//point.localPosition = position;~~point.localScale = scale; point.SetParent(transform, false); points[i] = point; } }

### Creating a Sphere

To demonstrate that we indeed are no longer limited to one point per (X, Z) coordinate pair, let's create a function that defines a sphere. Add a `Sphere`

method to `FunctionLibrary`

for this purpose. Also add an entry for it to the `FunctionName`

enum and the `functions`

array. Start with always returning a point at the origin.

public enum FunctionName { Wave, MultiWave, Ripple, Sphere } static Function[] functions = { Wave, MultiWave, Ripple, Sphere }; … public static Vector3 Sphere (float u, float v, float t) { Vector3 p; p.x = 0f; p.y = 0f; p.z = 0f; return p; }

The first step to creating a sphere is to describe a circle, laying flat in the XZ plane. We can use `[[sin(piu)],[0],[cos(piu)]]` for this, relying solely on `u`.

p.x = Sin(PI * u); p.y = 0f; p.z = Cos(PI * u);

We now have multiple circles that overlap perfectly. We can extrude them along Y based on `v`, which gives us a cylinder.

p.x = Sin(PI * u); p.y = v; p.z = Cos(PI * u);

We can adjust the radius of the cylinder by scaling X and Z by some value `r`. If we use `r=cos(pi/2v)` then the cylinder's top and bottom get collapsed to single points.

float r = Cos(0.5f * PI * v); Vector3 p; p.x = r * Sin(PI * u); p.y = v; p.z = r * Cos(PI * u);

This gets us close, but the reduction of the cylinder's radius isn't circular yet. That's because a circle is made with both a sine and a cosine, and we're only using the cosine at this point. The other part of the equation is Y, which is currently still equal to `v`. To complete the circle we have to use `y=sin(pi/2v)`.

p.y = Mathf.Sin(pi * 0.5f * v);

The result is a sphere, created with a pattern that's usually known as a UV-sphere. While this approach creates a correct sphere, note that the distribution of points isn't uniform, because the sphere is created by stacking circles with varying radius. Alternatively, we can consider it to consists of multiple half-circles that are rotated around the Y axis.

### Perturbing the Sphere

To be able to control the radius of the sphere we have to adjust our formula somewhat. We'll use `f(u,v)=[[ssin(piu)],[rsin(pi/2v)],[scos(piu)]]`, where `s=rcos(pi/2v)`, and `r` is the radius.

That approach makes it possible to animate the radius. For example, we could animate it by using `r=(1+sin(pit))/2` to scale it based on time.

float r = 0.5f + 0.5f * Sin(PI * t); float s = r * Cos(0.5f * PI * v); Vector3 p; p.x = s * Sin(PI * u); p.y = r * Sin(0.5f * PI * v); p.z = s * Cos(PI * u); return p; }

We don't have to use a uniform radius. We could vary it based on `u`, like `r=(9+sin(8piu))/10`.

float r = 0.9f + 0.1f * Sin(8f * PI * u);

This gives the sphere the appearance of having vertical bands. We can switch to horizontal bands by using `v` instead of `u`.

float r = 0.9f + 0.1f * Sin(8f * PI * v);

And by using both we get twisting bands. Let's add time as well to make them rotate, finishing with `r=(9+sin(pi(6u+4v+t)))/10`.

float r = 0.9f + 0.1f * Sin(PI * (6f * u + 4f * v + t));

### Creating a Torus

Let's wrap up by adding a torus surface to `FunctionLibrary`

. Duplicate `Sphere`

, rename it to `Torus`

and set its radius to 1. Also update the names and function array.

public enum FunctionName { Wave, MultiWave, Ripple, Sphere, Torus } static Function[] functions = { Wave, MultiWave, Ripple, Sphere, Torus }; … public static Vector3 Torus (float u, float v, float t) { float r = 1f; float s = r * Cos(0.5f * PI * v); Vector3 p; p.x = s * Sin(PI * u); p.y = r * Sin(0.5f * PI * v); p.z = s * Cos(PI * u); return p; }

We can morph our sphere into a torus by pulling its vertical half-circles away from each other and turning them into full circles. Let's begin with by switching to `s=1/2+rcos(pi/2v)`.

float s = 0.5f + r * Cos(0.5f * PI * v);

This gives us half a torus, with only the outer portion of its ring accounted for. To complete the torus we have to use `v` to describe an entire circle instead of half a circle. That can be done by using `piv` instead of `pi/2v` in `s` and `y`.

float s = 0.5f + r * Cos(PI * v); Vector3 p; p.x = s * Sin(PI * u); p.y = r * Sin(PI * v); p.z = s * Cos(PI * u);

Because we've pulled the sphere apart by half a unit this produces a self-intersecting shape, known as a spindle torus. Had we pulled it apart by one unit instead, we would've gotten a torus that doesn't self-intersect, but doesn't have a hole either, which is known as a horn torus. So how far we pull the sphere apart influences the shape of the torus. Specifically, it defines the major radius of the torus. The other radius is the minor radius and determines the thickness of the ring. Let's define the major radius as `r_1` and rename the other to `r_2` so `s=r_2cos(piv)+r_1`. Then use 0.75 for the major and 0.25 for the minor radius to keep the points inside the −1–1 domain.

float r1 = 1f; float r2 = 0.5f; float s = r1 + r2 * Cos(PI * v); Vector3 p; p.x = s * Sin(PI * u); p.y = r2 * Sin(PI * v); p.z = s * Cos(PI * u);

Now we have two radii to play with to make a more interesting torus. For example, we can turn it into a rotating star pattern by using `r_1=(7+sin(pi(6u+t/2)))/10`, while also twisting the ring by using `r_2=(3+sin(pi(8u+4v+2t)))/20`.

float r1 = 0.7f + 0.1f * Sin(PI * (6f * u + 0.5f * t)); float r2 = 0.15f + 0.05f * Sin(PI * (8f * u + 4f * v + 2f * t));

You now have some experience working with nontrivial functions that describe surfaces, plus how to visualize them. You can experiment with your own functions to get a better grasp of how it works. There are many seemingly complex parametric surfaces that can be created with a few sine waves.

Want to know when the next tutorial gets released? Keep tabs on my Patreon page!