2025-10-21 14:18:31 +09:00

393 lines
16 KiB
C#

// 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
{
/// <summary>
/// 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.
/// </summary>
[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<Rigidbody>();
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);
}
/// <summary>
/// Automatically grab a starting point for the area size, based on the attached mesh(es)
/// </summary>
public void AutoCalculateSurfaceSizeFromMeshes()
{
MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
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));
}
}
}