-
-
Save graphnode/c2705fbc17cbb8d3e4e1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Linq; | |
using ProBuilder2.Common; | |
using ProBuilder2.MeshOperations; | |
using ProBuilder2.Math; | |
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. | |
foreach(pb_Face f in shell) | |
f.SetMaterial( material ); | |
pb_Face[] connectingFaces; | |
// 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, startingExtrusion, false, out connectingFaces); | |
// 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; | |
waveform.SetVertexCount(WAVEFORM_SAMPLES); | |
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); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment