新增动态水物理插件

This commit is contained in:
Bob.Song
2026-02-27 17:44:21 +08:00
parent a6e061d9ce
commit 60744d113d
2218 changed files with 698551 additions and 189 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dcbce59eb9e305848972c15acdf24261
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 53231489df986aa4cb71dddc0b72afaf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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
}
}
}
}

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 44e1bf9aba10f314fbc1aacd1e602f07
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd49d8bec9d80fd4ba7c4b663a177c6e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: