273 lines
8.7 KiB
C#
273 lines
8.7 KiB
C#
#if UNITY_EDITOR || UNITY_STANDALONE
|
|
using UnityEngine;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using ProBuilder2.Common;
|
|
using ProBuilder2.MeshOperations;
|
|
|
|
namespace ProBuilder2.Examples
|
|
{
|
|
|
|
[RequireComponent(typeof(AudioSource))]
|
|
public class IcoBumpin : MonoBehaviour
|
|
{
|
|
pb_Object ico; // A reference to the icosphere pb_Object component
|
|
Mesh icoMesh; // A reference to the icosphere mesh (cached because we access the vertex array every frame)
|
|
Transform icoTransform; // A reference to the icosphere transform component. Cached because I can't remember if GameObject.transform is still a performance drain :|
|
|
AudioSource audioSource;// Cached reference to the audiosource.
|
|
|
|
/**
|
|
* Holds a pb_Face, the normal of that face, and the index of every vertex that touches it (sharedIndices).
|
|
*/
|
|
struct FaceRef
|
|
{
|
|
public pb_Face face;
|
|
public Vector3 nrm; // face normal
|
|
public int[] indices; // all vertex indices (including shared connected vertices)
|
|
|
|
public FaceRef(pb_Face f, Vector3 n, int[] i)
|
|
{
|
|
face = f;
|
|
nrm = n;
|
|
indices = i;
|
|
}
|
|
}
|
|
|
|
// All faces that have been extruded
|
|
FaceRef[] outsides;
|
|
|
|
// Keep a copy of the original vertex array to calculate the distance from origin.
|
|
Vector3[] original_vertices, displaced_vertices;
|
|
|
|
// The radius of the mesh icosphere on instantiation.
|
|
[Range(1f, 10f)]
|
|
public float icoRadius = 2f;
|
|
|
|
// The number of subdivisions to give the icosphere.
|
|
[Range(0, 3)]
|
|
public int icoSubdivisions = 2;
|
|
|
|
// How far along the normal should each face be extruded when at idle (no audio input).
|
|
[Range(0f, 1f)]
|
|
public float startingExtrusion = .1f;
|
|
|
|
// The material to apply to the icosphere.
|
|
public Material material;
|
|
|
|
// The max distance a frequency range will extrude a face.
|
|
[Range(1f, 50f)]
|
|
public float extrusion = 30f;
|
|
|
|
// An FFT returns a spectrum including frequencies that are out of human hearing range -
|
|
// this restricts the number of bins used from the spectrum to the lower @fftBounds.
|
|
[Range(8, 128)]
|
|
public int fftBounds = 32;
|
|
|
|
// How high the icosphere transform will bounce (sample volume determines height).
|
|
[Range(0f, 10f)]
|
|
public float verticalBounce = 4f;
|
|
|
|
// Optionally weights the frequency amplitude when calculating extrude distance.
|
|
public AnimationCurve frequencyCurve;
|
|
|
|
// A reference to the line renderer that will be used to render the raw waveform.
|
|
public LineRenderer waveform;
|
|
|
|
// The y size of the waveform.
|
|
public float waveformHeight = 2f;
|
|
|
|
// How far from the icosphere should the waveform be.
|
|
public float waveformRadius = 20f;
|
|
|
|
// If @rotateWaveformRing is true, this is the speed it will travel.
|
|
public float waveformSpeed = .1f;
|
|
|
|
// If true, the waveform ring will randomly orbit the icosphere.
|
|
public bool rotateWaveformRing = false;
|
|
|
|
// If true, the waveform will bounce up and down with the icosphere.
|
|
public bool bounceWaveform = false;
|
|
|
|
public GameObject missingClipWarning;
|
|
|
|
// Icosphere's starting position.
|
|
Vector3 icoPosition = Vector3.zero;
|
|
float faces_length;
|
|
|
|
const float TWOPI = 6.283185f; // 2 * PI
|
|
const int WAVEFORM_SAMPLES = 1024; // How many samples make up the waveform ring.
|
|
const int FFT_SAMPLES = 4096; // How many samples are used in the FFT. More means higher resolution.
|
|
|
|
// Keep copy of the last frame's sample data to average with the current when calculating
|
|
// deformation amounts. Smoothes the visual effect.
|
|
float[] fft = new float[FFT_SAMPLES],
|
|
fft_history = new float[FFT_SAMPLES],
|
|
data = new float[WAVEFORM_SAMPLES],
|
|
data_history = new float[WAVEFORM_SAMPLES];
|
|
|
|
// Root mean square of raw data (volume, but not in dB).
|
|
float rms = 0f, rms_history = 0f;
|
|
|
|
/**
|
|
* Creates the icosphere, and loads all the cache information.
|
|
*/
|
|
void Start()
|
|
{
|
|
audioSource = GetComponent<AudioSource>();
|
|
|
|
if( audioSource.clip == null )
|
|
missingClipWarning.SetActive(true);
|
|
|
|
// Create a new icosphere.
|
|
ico = pb_ShapeGenerator.IcosahedronGenerator(icoRadius, icoSubdivisions);
|
|
|
|
// Shell is all the faces on the new icosphere.
|
|
pb_Face[] shell = ico.faces;
|
|
|
|
// Materials are set per-face on pb_Object meshes. pb_Objects will automatically
|
|
// condense the mesh to the smallest set of subMeshes possible based on materials.
|
|
#if !PROTOTYPE
|
|
foreach(pb_Face f in shell)
|
|
f.material = material;
|
|
#else
|
|
ico.gameObject.GetComponent<MeshRenderer>().sharedMaterial = material;
|
|
#endif
|
|
|
|
// Extrude all faces on the icosphere by a small amount. The third boolean parameter
|
|
// specifies that extrusion should treat each face as an individual, not try to group
|
|
// all faces together.
|
|
ico.Extrude(shell, ExtrudeMethod.IndividualFaces, startingExtrusion);
|
|
|
|
// ToMesh builds the mesh positions, submesh, and triangle arrays. Call after adding
|
|
// or deleting vertices, or changing face properties.
|
|
ico.ToMesh();
|
|
|
|
// Refresh builds the normals, tangents, and UVs.
|
|
ico.Refresh();
|
|
|
|
outsides = new FaceRef[shell.Length];
|
|
Dictionary<int, int> lookup = ico.sharedIndices.ToDictionary();
|
|
|
|
// Populate the outsides[] cache. This is a reference to the tops of each extruded column, including
|
|
// copies of the sharedIndices.
|
|
for(int i = 0; i < shell.Length; ++i)
|
|
outsides[i] = new FaceRef( shell[i],
|
|
pb_Math.Normal(ico, shell[i]),
|
|
ico.sharedIndices.AllIndicesWithValues(lookup, shell[i].distinctIndices).ToArray()
|
|
);
|
|
|
|
// Store copy of positions array un-modified
|
|
original_vertices = new Vector3[ico.vertices.Length];
|
|
System.Array.Copy(ico.vertices, original_vertices, ico.vertices.Length);
|
|
|
|
// displaced_vertices should mirror icosphere mesh vertices.
|
|
displaced_vertices = ico.vertices;
|
|
|
|
icoMesh = ico.msh;
|
|
icoTransform = ico.transform;
|
|
|
|
faces_length = (float)outsides.Length;
|
|
|
|
// Build the waveform ring.
|
|
icoPosition = icoTransform.position;
|
|
#if UNITY_4_5 || UNITY_4_6 || UNITY_4_7 || UNITY_5_0 || UNITY_5_1 || UNITY_5_2 || UNITY_5_3 || UNITY_5_4
|
|
waveform.SetVertexCount(WAVEFORM_SAMPLES);
|
|
#elif UNITY_5_5
|
|
waveform.numPositions = WAVEFORM_SAMPLES;
|
|
#else
|
|
waveform.positionCount = WAVEFORM_SAMPLES;
|
|
#endif
|
|
|
|
|
|
if( bounceWaveform )
|
|
waveform.transform.parent = icoTransform;
|
|
|
|
audioSource.Play();
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
// fetch the fft spectrum
|
|
audioSource.GetSpectrumData(fft, 0, FFTWindow.BlackmanHarris);
|
|
|
|
// get raw data for waveform
|
|
audioSource.GetOutputData(data, 0);
|
|
|
|
// calculate root mean square (volume)
|
|
rms = RMS(data);
|
|
|
|
/**
|
|
* For each face, translate the vertices some distance depending on the frequency range assigned.
|
|
* Not using the TranslateVertices() pb_Object extension method because as a convenience, that method
|
|
* gathers the sharedIndices per-face on every call, which while not tremondously expensive in most
|
|
* contexts, is far too slow for use when dealing with audio, and especially so when the mesh is
|
|
* somewhat large.
|
|
*/
|
|
for(int i = 0; i < outsides.Length; i++)
|
|
{
|
|
float normalizedIndex = (i/faces_length);
|
|
|
|
int n = (int)(normalizedIndex*fftBounds);
|
|
|
|
Vector3 displacement = outsides[i].nrm * ( ((fft[n]+fft_history[n]) * .5f) * (frequencyCurve.Evaluate(normalizedIndex) * .5f + .5f)) * extrusion;
|
|
|
|
foreach(int t in outsides[i].indices)
|
|
{
|
|
displaced_vertices[t] = original_vertices[t] + displacement;
|
|
}
|
|
}
|
|
|
|
Vector3 vec = Vector3.zero;
|
|
|
|
// Waveform ring
|
|
for(int i = 0; i < WAVEFORM_SAMPLES; i++)
|
|
{
|
|
int n = i < WAVEFORM_SAMPLES-1 ? i : 0;
|
|
vec.x = Mathf.Cos((float)n/WAVEFORM_SAMPLES * TWOPI) * (waveformRadius + (((data[n] + data_history[n]) * .5f) * waveformHeight));
|
|
vec.z = Mathf.Sin((float)n/WAVEFORM_SAMPLES * TWOPI) * (waveformRadius + (((data[n] + data_history[n]) * .5f) * waveformHeight));
|
|
|
|
vec.y = 0f;
|
|
|
|
waveform.SetPosition(i, vec);
|
|
}
|
|
|
|
// Ring rotation
|
|
if( rotateWaveformRing )
|
|
{
|
|
Vector3 rot = waveform.transform.localRotation.eulerAngles;
|
|
|
|
rot.x = Mathf.PerlinNoise(Time.time * waveformSpeed, 0f) * 360f;
|
|
rot.y = Mathf.PerlinNoise(0f, Time.time * waveformSpeed) * 360f;
|
|
|
|
waveform.transform.localRotation = Quaternion.Euler(rot);
|
|
}
|
|
|
|
icoPosition.y = -verticalBounce + ((rms + rms_history) * verticalBounce);
|
|
icoTransform.position = icoPosition;
|
|
|
|
// Keep copy of last FFT samples so we can average with the current. Smoothes the movement.
|
|
System.Array.Copy(fft, fft_history, FFT_SAMPLES);
|
|
System.Array.Copy(data, data_history, WAVEFORM_SAMPLES);
|
|
rms_history = rms;
|
|
|
|
icoMesh.vertices = displaced_vertices;
|
|
}
|
|
|
|
/**
|
|
* Root mean square is a good approximation of perceived loudness.
|
|
*/
|
|
float RMS(float[] arr)
|
|
{
|
|
float v = 0f,
|
|
len = (float)arr.Length;
|
|
|
|
for(int i = 0; i < len; i++)
|
|
v += Mathf.Abs(arr[i]);
|
|
|
|
return Mathf.Sqrt(v / (float)len);
|
|
}
|
|
}
|
|
}
|
|
#endif
|