// Stylized Water 3 by Staggart Creations (http://staggart.xyz) // COPYRIGHT PROTECTED UNDER THE UNITY ASSET STORE EULA (https://unity.com/legal/as-terms) // • Copying or referencing source code for the production of new asset store, or public, content is strictly prohibited! // • Uploading this file to a public repository will subject it to an automated DMCA takedown request. using System; using Unity.Mathematics; using UnityEngine; namespace StylizedWater3 { /// /// Samples the water height at 4 points around the Transform, and snaps the Y-position to the average. /// From these 4 points a normal direction can also be derived, which is used to orient the transform's rotation /// - This script is a prime example of how the Height Query System may be used. /// [ExecuteInEditMode] [AddComponentMenu("Stylized Water 3/Align Transform To Water")] public class AlignToWater : MonoBehaviour { //Because there two completely different methods of sampling the water's height, this Interface class provides a way to specify which is to be used. //In the case of the CPU-method, it contains everything else needed for such a query (eg. water level and material) public HeightQuerySystem.Interface heightInterface = new HeightQuerySystem.Interface(); public Vector2 surfaceSize = new Vector2(0.5f, 0.5f); [Tooltip("Assign an optional transform to follow on the XZ axis.\n\nThis may be used if you want to use this component to read the water height at another transform's position.")] public Transform followTarget; public float heightOffset; [Range(0f, 8f)] [Tooltip("Controls how strongly the transform should rotate to align with the wave curvature")] public float rollAmount = 0.1f; [Tooltip("Add a Y-axis rotation to the transform. Note that this technically results in an incorrect alignment.")] [Range(0f, 360f)] public float rotation = 0f; [Tooltip("Smoothly blend towards the newly calculated position and rotation. May be used to combat jittering")] [Min(0f)] public float smoothing = 0.1f; private Vector3 normal; private float height; public enum HeightValue { Average, Maximum } public HeightValue heightValue; //A sampler defines: // • An array of world-space input positions to sample the height at. // • A float array (of equal size) that stores the output height values. // Samplers must always be initialized by specifying how many sample points are needed. private HeightQuerySystem.Sampler heightSampler; //A request ask the Height Query System to return the water height at every one of the sampler's positions from the GPU //It contains a callback event that needs to be subscribed to, to let you know when the request is completed private HeightQuerySystem.AsyncRequest heightRequest; #pragma warning disable 108,114 //New keyword public Rigidbody rigidbody; #pragma warning restore 108,114 private Vector3 m_targetNormal; private float m_targetHeight; #if UNITY_EDITOR public static bool EnableInEditor { get { return UnityEditor.EditorPrefs.GetBool("SW3_BUOYANCY_EDITOR_ENABLED", true); } set { UnityEditor.EditorPrefs.SetBool("SW3_BUOYANCY_EDITOR_ENABLED", value); } } #endif private void Start() { rigidbody = GetComponent(); if (rigidbody && rigidbody.useGravity) { Debug.LogWarning($"[Align Transform To Water] Disabled gravity on RigidBody \"{rigidbody.name}\". Otherwise its position can't be set without it struggling back"); rigidbody.useGravity = false; } } private void OnEnable() { #if UNITY_EDITOR if(Application.isPlaying == false) UnityEditor.SceneView.duringSceneGui += DuringSceneViewUpdate; #endif //Sampler uses 4 sampling points, one for each corner of the rectangle/plane //In the context of a physics-based buoyancy system a sampler will use more than 4 sampling points heightSampler = new HeightQuerySystem.Sampler(); heightSampler.SetSampleCount(4); //Only when using the GPU-readback method does the HeightQuerySystem come into play if (heightInterface.method == HeightQuerySystem.Interface.Method.GPU) { //Create a new request with the sampler created above. //Do this just once! It'll keep going until you call "Dispose()" on it. //Using the object's hashcode ensures the request gets a unique ID heightRequest = new HeightQuerySystem.AsyncRequest(this.GetHashCode(), heightSampler, this.name); //Issue the request so the system starts populating the "heightSampler" with data heightRequest.Issue(); //True by default. This checks if the returned water height values are valid. If not, the value remains the same as last frame //This avoids objects that are not above any water surface, or fall outside the camera frustum, from falling down to -1000 heights heightRequest.invalidateMisses = true; //You may also call .Withdraw() if you wish to only temporarily remove the request from the system. Then call .Issue() again when needed. //This is also necessary when looking to call heightSampler.SetSampleCount() again. //Doing so avoids reallocating its resources but only temporarily takes it out of the running. //Subscribe to the event that indicates that data was retrieved from the GPU //Important to note that this may be a few (rendering) frames later than when the request was issued (at least 1) heightRequest.onCompleted += OnHeightRequestComplete; } else { prevHeight = this.transform.position.y; height = prevHeight; } } #if UNITY_EDITOR void DuringSceneViewUpdate(UnityEditor.SceneView sceneView) { FixedUpdate(); } #endif private void OnHeightRequestComplete() { //Debug.Log("Height request returned"); //Use the data. It pre-presents the water geometry's height value at each of the requested sample positions float xNeg = heightSampler.heightValues[0]; float xPos = heightSampler.heightValues[1]; float zNeg = heightSampler.heightValues[2]; float zPos = heightSampler.heightValues[3]; float newHeight = 0f; if (heightValue == HeightValue.Average) { newHeight = xNeg + xPos + zNeg + zPos; newHeight /= 4f; } if (heightValue == HeightValue.Maximum) { newHeight = Mathf.Max(Mathf.Max(xNeg, xPos), Mathf.Max(zNeg, zPos)); } newHeight += heightOffset; if (float.IsNaN(newHeight)) { #if SWS_DEV Debug.LogError("Height is NaN"); #endif //May occur during the first run newHeight = 0f; } if (heightInterface.method == HeightQuerySystem.Interface.Method.GPU) { //If all the samples were taking at a point where no water was visible, the height values would be -1000f. //Avoid setting an invalid height to prevent objects from sinking way down, instead keep them at the last valid height if (HeightQuerySystem.EqualsVoid(newHeight)) { UpdateSamplePositions(); return; } } height = newHeight; //Using 4 samples in a plus-shape pattern, a normal can be derived from the height differences normal = HeightQuerySystem.DeriveNormal( xNeg, xPos, zNeg, zPos, rollAmount); //Note: In a physics-based buoyancy system, calculating the normal will not be necessary. The varying upward forces will naturally align an object to the water. //Reading back the water surface normals directly will not be possible, since no such information is available (requires a deferred rendering water system) //Only updating the sampling positions here, for the next query. There would be no point in doing so for frames where the height request may not be returned UpdateSamplePositions(); } private void Reset() { //Auto-assign water object if there is only one if (heightInterface.waterObject == null && WaterObject.Instances.Count > 0) { heightInterface.waterObject = WaterObject.Instances[0]; #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(this); #endif } AutoCalculateSurfaceSizeFromMeshes(); } private void OnDisable() { #if UNITY_EDITOR if(Application.isPlaying == false) UnityEditor.SceneView.duringSceneGui -= DuringSceneViewUpdate; #endif if (heightRequest != null) { //Unsubscribe heightRequest.onCompleted -= OnHeightRequestComplete; //Important! Remove the request from the system so that it de-allocates the memory it uses. heightRequest.Dispose(); heightRequest = null; } //Also dispose the height sampler. This stores two arrays that need to be de-allocated //Note: When using the GPU method, disposing 'heightRequest' will already do this! heightSampler.Dispose(); heightSampler = null; } public void FixedUpdate() { if (!this.enabled) return; #if UNITY_EDITOR if (!EnableInEditor && Application.isPlaying == false) return; #endif //When using the CPU (wave pattern replication) method if (heightInterface.method == HeightQuerySystem.Interface.Method.CPU) { //The water object contains the material, and may be used to define a water level heightInterface.GetWaterObject(this.transform.position); //A reference to a Water Object is required, which in turn contains a material if (heightInterface.HasMissingReferences()) return; //This function reproduces the water shader's wave vertex animation. Gerstner.ComputeHeight(heightSampler, heightInterface); //At this point the 'heightSampler' has new height values, so can go right ahead and use them OnHeightRequestComplete(); ApplyTransform(); } else { ApplyTransform(); } } //Translates the corners of the bounds to the 4 sampling points (plus-shaped pattern) //From local-space to world-space. private void UpdateSamplePositions() { //Note: Should the number of sample positions change in realtime, that creates an issue where resources have to be destroyed and recreated. Avoid doing so! //Should it be needed, do so by: //• heightRequest.Withdraw(); //Remove the active request for data //• heightSampler.SetSampleCount(count). //This will automatically reallocate the arrays if the count was changed. //• heightRequest.Issue(); //Re-issue the request with the new number of samples // ┤ heightSampler.SetSamplePosition(0, ConvertToWorldSpace(new Vector3(-surfaceSize.x * 0.5f, 0, 0))); // ├ heightSampler.SetSamplePosition(1, ConvertToWorldSpace(new Vector3(surfaceSize.x * 0.5f, 0, 0))); // ┬ heightSampler.SetSamplePosition(2, ConvertToWorldSpace(new Vector3(0, 0, -surfaceSize.y * 0.5f))); // ┴ heightSampler.SetSamplePosition(3, ConvertToWorldSpace(new Vector3(0, 0, surfaceSize.y * 0.5f))); } private Vector3 prevNormal = new Vector3(0, 1, 0); private float prevHeight; private void ApplyTransform() { //Smooth transition to new normal of this frame, particularly reduces jittering on rigid bodies if (smoothing > 0) { m_targetHeight = Mathf.Lerp(prevHeight, height, Time.deltaTime / smoothing); prevHeight = m_targetHeight; m_targetNormal = Vector3.Lerp(prevNormal, normal, Time.smoothDeltaTime / smoothing); prevNormal = m_targetNormal; } else { prevHeight = height; prevNormal = normal; m_targetHeight = height; m_targetNormal = normal; } var position = this.transform.position; if (followTarget) { position.x = followTarget.position.x; position.z = followTarget.position.z; } position.y = m_targetHeight; //Setting the normal of a transform directly overrides any and all external rotations //If the component is attached an object with a mesh, move the mesh into a child object and rotate that instead var newRotation = Quaternion.FromToRotation(Vector3.up, m_targetNormal); newRotation *= quaternion.RotateY(rotation * Mathf.Deg2Rad); if (Application.isPlaying && rigidbody) { if (rollAmount > 0) { rigidbody.Move(position, newRotation); } else { rigidbody.MovePosition(position); } } else { if (rollAmount > 0) { this.transform.SetPositionAndRotation(position, newRotation); } else { this.transform.position = position; } } } private Vector3 ConvertToWorldSpace(Vector3 position) { return this.transform.TransformPoint(position); } /// /// Automatically grab a starting point for the area size, based on the attached mesh(es) /// public void AutoCalculateSurfaceSizeFromMeshes() { MeshFilter[] meshFilters = GetComponentsInChildren(); int meshCount = meshFilters.Length; if (meshCount > 0) { surfaceSize.x = 0f; surfaceSize.y = 0f; Bounds bounds = new Bounds(); Vector3 minSum = Vector3.one * Mathf.Infinity; Vector3 maxSum = Vector3.one * Mathf.NegativeInfinity; for (int i = 0; i < meshCount; i++) { minSum = Vector3.Min(minSum, meshFilters[i].sharedMesh.bounds.min); maxSum = Vector3.Max(maxSum, meshFilters[i].sharedMesh.bounds.max); } bounds.SetMinMax(minSum, maxSum); surfaceSize.x = bounds.size.x; surfaceSize.y = bounds.size.z; } } private void OnDrawGizmosSelected() { if (heightSampler != null && heightSampler.IsCreated()) { foreach (Vector3 p in heightSampler.positions) { Gizmos.DrawWireCube(p - (Vector3.up * heightOffset), Vector3.one * 0.2f); } } Gizmos.DrawLine(this.transform.position, this.transform.position + (m_targetNormal * 2f)); Gizmos.matrix = this.transform.localToWorldMatrix; Gizmos.DrawWireCube(Vector3.zero - (Vector3.up * heightOffset), new Vector3(surfaceSize.x, 0f, surfaceSize.y)); } } }