Files
2025-07-04 20:33:06 +03:00

359 lines
13 KiB
C#

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* Licensed under the Oculus SDK License Agreement (the "License");
* you may not use the Oculus SDK except in compliance with the License,
* which is provided at the time of installation or download, or which
* otherwise accompanies this software in either electronic or hard copy form.
*
* You may obtain a copy of the License at
*
* https://developer.oculus.com/licenses/oculussdk/
*
* Unless required by applicable law or agreed to in writing, the Oculus SDK
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using Oculus.Interaction.HandGrab;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace Oculus.Interaction.Demo
{
public class WaterSpray : MonoBehaviour, IHandGrabUseDelegate
{
public enum NozzleMode
{
Spray,
Stream
}
[Header("Input")]
[SerializeField]
private Transform _trigger;
[SerializeField]
private Transform _nozzle;
[SerializeField]
private AnimationCurve _triggerRotationCurve;
[SerializeField]
private SnapAxis _axis = SnapAxis.X;
[SerializeField]
[Range(0f, 1f)]
private float _releaseThresold = 0.3f;
[SerializeField]
[Range(0f, 1f)]
private float _fireThresold = 0.9f;
[SerializeField]
private float _triggerSpeed = 3f;
[SerializeField]
private AnimationCurve _strengthCurve = AnimationCurve.EaseInOut(0f,0f,1f,1f);
[Header("Output")]
[SerializeField, Tooltip("Masks the Raycast used to find objects to make wet")]
private LayerMask _raycastLayerMask = ~0;
[SerializeField, Tooltip("The spread angle when spraying, larger values will make a larger area wet")]
private float _spraySpreadAngle = 40;
[SerializeField, Tooltip("The spread angle when using stream, larger values will make a larger area wet")]
private float _streamSpreadAngle = 4;
[SerializeField]
private float _sprayStrength = 1.5f;
[SerializeField]
private int _sprayHits = 6;
[SerializeField]
private float _sprayRandomness = 6f;
[SerializeField, Tooltip("The max distance of the spray, controls the raycast and shader")]
private float _maxDistance = 2;
[SerializeField]
private float _dryingSpeed = 0.1f;
[SerializeField, Tooltip("Material for applying a stamp, should using the MeshBlitStamp shader or similar")]
private Material _sprayStampMaterial = null;
[SerializeField, Tooltip("When not null, will be set as the '_WetBumpMap' property on wet renderers")]
private Texture _waterBumpOverride = null;
[SerializeField]
private UnityEvent WhenSpray;
[SerializeField]
private UnityEvent WhenStream;
private static readonly int WET_MAP_PROPERTY = Shader.PropertyToID("_WetMap");
private static readonly int STAMP_MULTIPLIER_PROPERTY = Shader.PropertyToID("_StampMultipler");
private static readonly int SUBTRACT_PROPERTY = Shader.PropertyToID("_Subtract");
private static readonly int WET_BUMPMAP_PROPERTY = Shader.PropertyToID("_WetBumpMap");
private static readonly int STAMP_MATRIX_PROPERTY = Shader.PropertyToID("_StampMatrix");
private static readonly WaitForSeconds WAIT_TIME = new WaitForSeconds(0.1f);
private bool _wasFired = false;
private float _dampedUseStrength = 0;
private float _lastUseTime;
#region input
private void SprayWater()
{
NozzleMode mode = GetNozzleMode();
if (mode == NozzleMode.Spray)
{
Spray();
WhenSpray?.Invoke();
}
else if (mode == NozzleMode.Stream)
{
Stream();
WhenStream?.Invoke();
}
}
private void UpdateTriggerRotation(float progress)
{
float value = _triggerRotationCurve.Evaluate(progress);
Vector3 angles = _trigger.localEulerAngles;
if ((_axis & SnapAxis.X) != 0)
{
angles.x = value;
}
if ((_axis & SnapAxis.Y) != 0)
{
angles.y = value;
}
if ((_axis & SnapAxis.Z) != 0)
{
angles.z = value;
}
_trigger.localEulerAngles = angles;
}
private NozzleMode GetNozzleMode()
{
int rotations = ((int)_nozzle.localEulerAngles.z + 45) / 90;
if (rotations % 2 == 0)
{
return NozzleMode.Spray;
}
return NozzleMode.Stream;
}
#endregion
#region output
private void Spray()
{
StartCoroutine(StampRoutine(_sprayHits, _sprayRandomness, _spraySpreadAngle, _sprayStrength));
}
private void Stream()
{
StartCoroutine(StampRoutine(_sprayHits, 0, _streamSpreadAngle, _sprayStrength));
}
private IEnumerator StampRoutine(int stampCount, float randomness, float spread, float strength)
{
StartStamping();
Pose originalPose = _nozzle.GetPose();
for (int i = 0; i < stampCount; i++)
{
yield return WAIT_TIME;
Pose randomPose = originalPose;
randomPose.rotation =
randomPose.rotation *
Quaternion.Euler(
Random.Range(-randomness, randomness),
Random.Range(-randomness, randomness),
0f);
Stamp(randomPose, _maxDistance, spread, strength);
}
StartDrying();
}
private void StartStamping()
{
_sprayStampMaterial.SetFloat(SUBTRACT_PROPERTY, 0);
}
private void StartDrying()
{
_sprayStampMaterial.SetMatrix(STAMP_MATRIX_PROPERTY, Matrix4x4.zero);
_sprayStampMaterial.SetFloat(SUBTRACT_PROPERTY, _dryingSpeed);
}
private void Stamp(Pose pose, float maxDistance, float angle, float strength)
{
_sprayStampMaterial.SetMatrix(STAMP_MATRIX_PROPERTY, CreateStampMatrix(pose, angle));
_sprayStampMaterial.SetFloat(STAMP_MULTIPLIER_PROPERTY, strength);
float radius = Mathf.Tan(Mathf.Deg2Rad * angle / 2) * maxDistance;
Vector3 startPoint = pose.position + pose.forward * radius;
Vector3 endPoint = pose.position + pose.forward * maxDistance;
int hitCount = Physics.OverlapCapsuleNonAlloc(startPoint, endPoint, radius, NonAlloc._overlapResults, _raycastLayerMask.value, QueryTriggerInteraction.Ignore);
HashSet<Transform> roots = NonAlloc.GetRootsFromOverlapResults(hitCount);
foreach (Transform rootObject in roots)
{
RenderSplash(rootObject);
}
roots.Clear();
}
/// <summary>
/// Finds Meshes that are part of the rootObject and blits the material on them
/// </summary>
private void RenderSplash(Transform rootObject)
{
List<MeshFilter> meshFilters = NonAlloc.GetMeshFiltersInChildren(rootObject);
for (int i = 0; i < meshFilters.Count; i++)
{
int id = meshFilters[i].GetInstanceID();
if (!NonAlloc._blits.ContainsKey(id)) { NonAlloc._blits[id] = CreateMeshBlit(meshFilters[i]); }
NonAlloc._blits[id].Blit();
}
}
/// <summary>
/// Sets up a new mesh blit on the mesh filter for the water spray
/// </summary>
private MeshBlit CreateMeshBlit(MeshFilter meshFilter)
{
MeshBlit newBlit = meshFilter.gameObject.AddComponent<MeshBlit>();
newBlit.material = _sprayStampMaterial;
newBlit.renderTexture = new RenderTexture(512, 512, 0, RenderTextureFormat.RHalf);
newBlit.BlitsPerSecond = 30;
if (meshFilter.TryGetComponent(out Renderer renderer))
{
renderer.GetPropertyBlock(NonAlloc.PropertyBlock);
NonAlloc.PropertyBlock.SetTexture(WET_MAP_PROPERTY, newBlit.renderTexture);
if (_waterBumpOverride)
{
NonAlloc.PropertyBlock.SetTexture(WET_BUMPMAP_PROPERTY, _waterBumpOverride);
}
renderer.SetPropertyBlock(NonAlloc.PropertyBlock);
}
return newBlit;
}
private Matrix4x4 CreateStampMatrix(Pose pose, float angle)
{
Matrix4x4 viewMatrix = Matrix4x4.TRS(pose.position, pose.rotation, Vector3.one).inverse;
viewMatrix.m20 *= -1f;
viewMatrix.m21 *= -1f;
viewMatrix.m22 *= -1f;
viewMatrix.m23 *= -1f;
return GL.GetGPUProjectionMatrix(Matrix4x4.Perspective(angle, 1, 0, _maxDistance), true) * viewMatrix;
}
/// <summary>
/// Cleans destroyed MeshBlits form the dictionary
/// </summary>
private void OnDestroy()
{
NonAlloc.CleanUpDestroyedBlits();
}
public void BeginUse()
{
_dampedUseStrength = 0f;
_lastUseTime = Time.realtimeSinceStartup;
}
public void EndUse()
{
}
public float ComputeUseStrength(float strength)
{
float delta = Time.realtimeSinceStartup - _lastUseTime;
_lastUseTime = Time.realtimeSinceStartup;
if (strength > _dampedUseStrength)
{
_dampedUseStrength = Mathf.Lerp(_dampedUseStrength, strength, _triggerSpeed * delta);
}
else
{
_dampedUseStrength = strength;
}
float progress = _strengthCurve.Evaluate(_dampedUseStrength);
UpdateTriggerProgress(progress);
return progress;
}
private void UpdateTriggerProgress(float progress)
{
UpdateTriggerRotation(progress);
if (progress >= _fireThresold && !_wasFired)
{
_wasFired = true;
SprayWater();
}
else if (progress <= _releaseThresold)
{
_wasFired = false;
}
}
#endregion
/// <summary>
/// Allocation helpers
/// </summary>
static class NonAlloc
{
public static readonly Collider[] _overlapResults = new Collider[12];
public static readonly Dictionary<int, MeshBlit> _blits = new Dictionary<int, MeshBlit>();
public static MaterialPropertyBlock PropertyBlock => _block != null ? _block : _block = new MaterialPropertyBlock();
private static readonly List<MeshFilter> _meshFilters = new List<MeshFilter>();
private static readonly HashSet<Transform> _roots = new HashSet<Transform>();
private static MaterialPropertyBlock _block;
public static List<MeshFilter> GetMeshFiltersInChildren(Transform root)
{
root.GetComponentsInChildren(_meshFilters);
return _meshFilters;
}
public static HashSet<Transform> GetRootsFromOverlapResults(int hitCount)
{
_roots.Clear();
for (int i = 0; i < hitCount; i++)
{
Transform root = GetRoot(_overlapResults[i]);
_roots.Add(root);
}
return _roots;
}
/// <summary>
/// Returns the most likely 'root object' for the hit e.g. the rigidbody
/// </summary>
static Transform GetRoot(Collider hit)
{
return hit.attachedRigidbody ? hit.attachedRigidbody.transform :
hit.transform.parent ? hit.transform.parent :
hit.transform;
}
public static void CleanUpDestroyedBlits()
{
if (!_blits.ContainsValue(null)) { return; }
foreach (int key in new List<int>(_blits.Keys))
{
if (_blits[key] == null) _blits.Remove(key);
}
}
}
}
}