#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(); 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().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 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