新增动态水物理插件
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dcbce59eb9e305848972c15acdf24261
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,88 @@
|
||||
// ╔════════════════════════════════════════════════════════════════╗
|
||||
// ║ Copyright © 2025 NWH Coding d.o.o. All rights reserved. ║
|
||||
// ║ Licensed under Unity Asset Store Terms of Service: ║
|
||||
// ║ https://unity.com/legal/as-terms ║
|
||||
// ║ Use permitted only in compliance with the License. ║
|
||||
// ║ Distributed "AS IS", without warranty of any kind. ║
|
||||
// ╚════════════════════════════════════════════════════════════════╝
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
#region
|
||||
|
||||
using NWH.NUI;
|
||||
using NWH.DWP2.WaterObjects;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace NWH.DWP2.WaterEffects
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom inspector for WaterParticleSystem.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(WaterParticleSystem))]
|
||||
[CanEditMultipleObjects]
|
||||
public class WaterParticleSystemEditor : DWP2NUIEditor
|
||||
{
|
||||
private WaterParticleSystem wps;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Draws custom inspector GUI for WaterParticleSystem.
|
||||
/// </summary>
|
||||
public override bool OnInspectorNUI()
|
||||
{
|
||||
if (!base.OnInspectorNUI())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
wps = (WaterParticleSystem)target;
|
||||
|
||||
// Draw logo texture
|
||||
Rect logoRect = drawer.positionRect;
|
||||
logoRect.height = 60f;
|
||||
drawer.DrawEditorTexture(logoRect, "Dynamic Water Physics 2/Logos/WaterParticleSystemLogo");
|
||||
drawer.AdvancePosition(logoRect.height);
|
||||
|
||||
drawer.BeginSubsection("Particle Settings");
|
||||
drawer.Field("emit");
|
||||
drawer.Field("renderQueue");
|
||||
drawer.Field("startSize");
|
||||
drawer.Field("sleepThresholdVelocity");
|
||||
drawer.Field("initialVelocityModifier");
|
||||
drawer.Field("maxInitialAlpha");
|
||||
drawer.Field("initialAlphaModifier");
|
||||
drawer.Field("emitPerCycle");
|
||||
drawer.Field("emitTimeInterval");
|
||||
drawer.Field("positionExtrapolationFrames");
|
||||
drawer.Field("surfaceElevation");
|
||||
drawer.EndSubsection();
|
||||
|
||||
/* // TODO - move this from editor script
|
||||
if(!wps.GetComponent<ParticleSystem>())
|
||||
{
|
||||
GameObject waterParticleSystemPrefab = Resources.Load<GameObject>("Dynamic Water Physics
|
||||
2/WaterParticleSystemPrefab");
|
||||
if (waterParticleSystemPrefab == null)
|
||||
{
|
||||
Debug.LogError("Could not load WaterParticleSystemPrefab from Resources.");
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEditorInternal.ComponentUtility.CopyComponent(waterParticleSystemPrefab
|
||||
.GetComponent<ParticleSystem>());
|
||||
UnityEditorInternal.ComponentUtility.PasteComponentAsNew(wps.gameObject);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
drawer.EndEditor(this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53231489df986aa4cb71dddc0b72afaf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
Shader "WaterFX/WaterParticle" {
|
||||
Properties{
|
||||
_TintColor("Tint Color", Color) = (0.5,0.5,0.5,0.5)
|
||||
_MainTex("Particle Texture", 2D) = "white" {}
|
||||
}
|
||||
|
||||
Category
|
||||
{
|
||||
Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" }
|
||||
|
||||
Blend SrcAlpha OneMinusSrcAlpha
|
||||
ColorMask RGB
|
||||
Cull Off Lighting Off ZWrite Off
|
||||
|
||||
SubShader
|
||||
{
|
||||
Pass
|
||||
{
|
||||
CGPROGRAM
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag
|
||||
|
||||
#include "UnityCG.cginc"
|
||||
|
||||
sampler2D _MainTex;
|
||||
fixed4 _TintColor;
|
||||
|
||||
struct appdata_t {
|
||||
float4 vertex : POSITION;
|
||||
fixed4 color : COLOR;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
UNITY_VERTEX_INPUT_INSTANCE_ID
|
||||
};
|
||||
|
||||
struct v2f {
|
||||
float4 vertex : SV_POSITION;
|
||||
fixed4 color : COLOR;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
UNITY_FOG_COORDS(1)
|
||||
UNITY_VERTEX_OUTPUT_STEREO
|
||||
};
|
||||
|
||||
float4 _MainTex_ST;
|
||||
|
||||
v2f vert(appdata_t v)
|
||||
{
|
||||
v2f o;
|
||||
UNITY_SETUP_INSTANCE_ID(v);
|
||||
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
|
||||
o.vertex = UnityObjectToClipPos(v.vertex);
|
||||
o.color = v.color * _TintColor;
|
||||
o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
|
||||
return o;
|
||||
}
|
||||
|
||||
fixed4 frag(v2f i) : SV_Target
|
||||
{
|
||||
fixed4 col = 2 * i.color * tex2D(_MainTex, i.texcoord);
|
||||
return col;
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44e1bf9aba10f314fbc1aacd1e602f07
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,320 @@
|
||||
// ╔════════════════════════════════════════════════════════════════╗
|
||||
// ║ Copyright © 2025 NWH Coding d.o.o. All rights reserved. ║
|
||||
// ║ Licensed under Unity Asset Store Terms of Service: ║
|
||||
// ║ https://unity.com/legal/as-terms ║
|
||||
// ║ Use permitted only in compliance with the License. ║
|
||||
// ║ Distributed "AS IS", without warranty of any kind. ║
|
||||
// ╚════════════════════════════════════════════════════════════════╝
|
||||
|
||||
#region
|
||||
|
||||
using NWH.Common.Utility;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace NWH.DWP2.WaterObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates water spray and splash particles based on WaterObject simulation data.
|
||||
/// Emits particles at the waterline where triangles intersect the water surface.
|
||||
/// Particle emission is controlled by object velocity and triangle forces.
|
||||
/// Requires a ParticleSystem component.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(ParticleSystem))]
|
||||
public class WaterParticleSystem : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Should the particle system emit?
|
||||
/// </summary>
|
||||
[Tooltip("Should the particle system emit?")]
|
||||
public bool emit = true;
|
||||
|
||||
/// <summary>
|
||||
/// How many particles should be emitted each 'emitTimeInterval' seconds.
|
||||
/// </summary>
|
||||
[Tooltip("How many particles should be emitted each 'emitTimeInterval' seconds.")]
|
||||
[Range(0f, 20f)]
|
||||
public int emitPerCycle = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how often the particles will be emitted.
|
||||
/// </summary>
|
||||
[Tooltip("Determines how often the particles will be emitted.")]
|
||||
[Range(0f, 0.1f)]
|
||||
public float emitTimeInterval = 0.04f;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies initial alpha by this value. Alpha cannot be higher than maxInitialAlpha.
|
||||
/// </summary>
|
||||
[Tooltip("Multiplies initial alpha by this value. Alpha cannot be higher than maxInitialAlpha.")]
|
||||
[Range(0f, 10f)]
|
||||
public float initialAlphaModifier = 0.4f;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how much velocity of the object will affect initial particle speed.
|
||||
/// </summary>
|
||||
[Tooltip("Determines how much velocity of the object will affect initial particle speed.")]
|
||||
[Range(0f, 5f)]
|
||||
public float initialVelocityModifier = 0.01f;
|
||||
|
||||
/// <summary>
|
||||
/// Limit initial alpha to this value.
|
||||
/// </summary>
|
||||
[Tooltip("Limit initial alpha to this value.")]
|
||||
[Range(0f, 1f)]
|
||||
public float maxInitialAlpha = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// Number of frames ahead to predict particle emission position.
|
||||
/// Higher values spawn particles ahead of the object for better visual continuity.
|
||||
/// </summary>
|
||||
[Tooltip("Script will try to predict where the object will be in the next n frames.")]
|
||||
public int positionExtrapolationFrames = 4;
|
||||
|
||||
/// <summary>
|
||||
/// WaterObject to read simulation data from.
|
||||
/// Will auto-detect from parent if not assigned.
|
||||
/// </summary>
|
||||
public WaterObject ReferenceWaterObject;
|
||||
|
||||
/// <summary>
|
||||
/// Render queue of the particle material.
|
||||
/// </summary>
|
||||
[Tooltip("Render queue of the particle material.")]
|
||||
public int renderQueue = 2700;
|
||||
|
||||
/// <summary>
|
||||
/// Velocity object has to have to emit particles.
|
||||
/// </summary>
|
||||
[FormerlySerializedAs("sleepTresholdVelocity")]
|
||||
[Tooltip("Velocity object has to have to emit particles.")]
|
||||
[Range(0.1f, 5f)]
|
||||
public float sleepThresholdVelocity = 1.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Initial size of the particle.
|
||||
/// </summary>
|
||||
[Tooltip("Initial size of the particle.")]
|
||||
[Range(0f, 64f)]
|
||||
public float startSize = 4f;
|
||||
|
||||
/// <summary>
|
||||
/// Elevation above water at which the particles will spawn. Used to avoid clipping.
|
||||
/// </summary>
|
||||
[Tooltip("Elevation above water at which the particles will spawn. Used to avoid clipping.")]
|
||||
[Range(0f, 0.1f)]
|
||||
public float surfaceElevation = 0.016f;
|
||||
|
||||
private ParticleSystem.NoiseModule _noiseModule;
|
||||
private ParticleSystem _particleSystem;
|
||||
private int _prevTriCount;
|
||||
private WaterObject _targetWaterObject;
|
||||
|
||||
private float _timeElapsed;
|
||||
private int _waterlineCount;
|
||||
private int[] _waterlineIndices;
|
||||
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (ReferenceWaterObject == null)
|
||||
{
|
||||
ReferenceWaterObject = GetComponentInParent<WaterObject>();
|
||||
}
|
||||
|
||||
_targetWaterObject = transform.GetComponentInParentsOrChildren<WaterObject>();
|
||||
if (_targetWaterObject == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"{name}: WaterParticleSystem requires WaterObject attached to the same object or one of parent objects to function.");
|
||||
return;
|
||||
}
|
||||
|
||||
_particleSystem = GetComponent<ParticleSystem>();
|
||||
if (_particleSystem == null)
|
||||
{
|
||||
Debug.LogError("No ParticleSystem found.");
|
||||
}
|
||||
|
||||
_particleSystem.GetComponent<Renderer>().material.renderQueue = renderQueue;
|
||||
_noiseModule = _particleSystem.noise;
|
||||
|
||||
_prevTriCount = -999;
|
||||
}
|
||||
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!emit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int triCount = _targetWaterObject.triangleCount;
|
||||
if (triCount > 0 && _prevTriCount != triCount)
|
||||
{
|
||||
_waterlineIndices = new int[triCount];
|
||||
}
|
||||
|
||||
if (_targetWaterObject.targetRigidbody.linearVelocity.magnitude > sleepThresholdVelocity)
|
||||
{
|
||||
EmitNew();
|
||||
}
|
||||
|
||||
_timeElapsed += Time.deltaTime;
|
||||
_prevTriCount = triCount;
|
||||
}
|
||||
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (_waterlineIndices == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Gizmos.color = Color.magenta;
|
||||
for (int i = 0; i < _waterlineIndices.Length; i++)
|
||||
{
|
||||
if (_targetWaterObject.ResultStates[_waterlineIndices[i]] != 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 a = _targetWaterObject.ResultP0s[_waterlineIndices[i] * 6 + 2];
|
||||
Vector3 b = _targetWaterObject.ResultP0s[_waterlineIndices[i] * 6 + 1];
|
||||
Gizmos.DrawLine(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
DestroyImmediate(_particleSystem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void EmitNew()
|
||||
{
|
||||
if (_targetWaterObject == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int triCount = _targetWaterObject.triangleCount;
|
||||
if (emit && _timeElapsed >= emitTimeInterval && triCount > 0f)
|
||||
{
|
||||
_timeElapsed = 0;
|
||||
|
||||
int emitted = 0;
|
||||
|
||||
// Emit allowed number of particles
|
||||
float elevation = 0;
|
||||
if (ReferenceWaterObject != null)
|
||||
{
|
||||
elevation = ReferenceWaterObject.GetWaterHeightSingle(Vector3.zero);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Will not emit. WaterDataProvider is not present in the scene.");
|
||||
}
|
||||
|
||||
_waterlineCount = 0;
|
||||
for (int i = 0; i < triCount; i++)
|
||||
{
|
||||
if (_targetWaterObject.ResultStates[i] != 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_waterlineIndices[_waterlineCount] = i;
|
||||
_waterlineCount++;
|
||||
}
|
||||
|
||||
if (_waterlineCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float noise = startSize > 1f ? Mathf.Sqrt(startSize) * 0.1f : startSize * 0.1f;
|
||||
_noiseModule.strengthX = noise;
|
||||
_noiseModule.strengthY = 0f;
|
||||
_noiseModule.strengthZ = noise;
|
||||
|
||||
while (emitted < emitPerCycle)
|
||||
{
|
||||
int i = Random.Range(0, _waterlineCount);
|
||||
int waterLineTriIndex = _waterlineIndices[i];
|
||||
|
||||
EmitParticle(
|
||||
_targetWaterObject.ResultP0s[waterLineTriIndex * 6 + 2],
|
||||
_targetWaterObject.ResultP0s[waterLineTriIndex * 6 + 1],
|
||||
elevation,
|
||||
_targetWaterObject.ResultVelocities[waterLineTriIndex],
|
||||
_targetWaterObject.ResultNormals[waterLineTriIndex],
|
||||
_targetWaterObject.ResultForces[waterLineTriIndex],
|
||||
_targetWaterObject.ResultAreas[waterLineTriIndex]);
|
||||
|
||||
emitted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Emits a single particle at the waterline between two points.
|
||||
/// Particle properties are calculated from triangle simulation data.
|
||||
/// </summary>
|
||||
/// <param name="p0">First waterline point.</param>
|
||||
/// <param name="p1">Second waterline point.</param>
|
||||
/// <param name="elevation">Water surface elevation.</param>
|
||||
/// <param name="velocity">Triangle velocity.</param>
|
||||
/// <param name="normal">Triangle normal.</param>
|
||||
/// <param name="force">Force acting on triangle.</param>
|
||||
/// <param name="area">Triangle area.</param>
|
||||
private void EmitParticle(Vector3 p0, Vector3 p1, float elevation, Vector3 velocity, Vector3 normal,
|
||||
Vector3 force, float area)
|
||||
{
|
||||
if (area < 0.0001f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Start velocity
|
||||
Vector3 startVelocity = normal * velocity.magnitude;
|
||||
startVelocity.y = 0f;
|
||||
startVelocity *= initialVelocityModifier;
|
||||
|
||||
// Start position
|
||||
Vector3 emissionPoint = (p0 + p1) / 2f;
|
||||
emissionPoint += Time.deltaTime * positionExtrapolationFrames * velocity;
|
||||
emissionPoint.y = elevation + surfaceElevation;
|
||||
|
||||
float normalizedForce = force.magnitude / area;
|
||||
float startAlpha = Mathf.Clamp(normalizedForce * 0.00005f * initialAlphaModifier, 0f, maxInitialAlpha);
|
||||
Color startColor = new(1f, 1f, 1f, startAlpha);
|
||||
float size = startSize;
|
||||
|
||||
if (startAlpha < 0.001f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ParticleSystem.EmitParams emitParams = new()
|
||||
{
|
||||
startColor = startColor,
|
||||
position = emissionPoint,
|
||||
velocity = startVelocity,
|
||||
startSize = size,
|
||||
};
|
||||
_particleSystem.Emit(emitParams, 1);
|
||||
_particleSystem.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd49d8bec9d80fd4ba7c4b663a177c6e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user