新增动态水物理插件

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,56 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.DWP2
{
/// <summary>
/// Custom inspector for MassFromChildren.
/// </summary>
[CustomEditor(typeof(MassFromChildren))]
[CanEditMultipleObjects]
public class MassFromChildrenEditor : DWP2NUIEditor
{
private MassFromChildren _massFromChildren;
/// <summary>
/// Draws custom inspector GUI for MassFromChildren.
/// </summary>
public override bool OnInspectorNUI()
{
_massFromChildren = (MassFromChildren)target;
if (!base.OnInspectorNUI() || _massFromChildren == null)
{
return false;
}
drawer.Info("Sums mass of all 'MassFromVolume's attached to this and child objects.");
if (drawer.Button("Calculate Mass From Children"))
{
_massFromChildren.Calculate();
}
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6e2a38dc6b814caa8f2638c49a2972c7
timeCreated: 1593013502

View File

@@ -0,0 +1,84 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.DWP2
{
/// <summary>
/// Custom inspector for MassFromVolume.
/// </summary>
[CustomEditor(typeof(MassFromVolume))]
[CanEditMultipleObjects]
public class MassFromVolumeEditor : DWP2NUIEditor
{
private MassFromVolume _massFromVolume;
public void OnEnable()
{
_massFromVolume = (MassFromVolume)target;
}
/// <summary>
/// Draws custom inspector GUI for MassFromVolume.
/// </summary>
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI() || _massFromVolume == null)
{
return false;
}
_massFromVolume = (MassFromVolume)target;
// Material settings
drawer.Field("mass", true, "kg");
drawer.Field("volume", false, "m3");
drawer.Info("Volume is auto-calculated from the mesh when either Calculate option is used.");
drawer.BeginSubsection("Density");
drawer.Field("density", true, "kg/m3");
if (drawer.Button("Calculate Mass From Density"))
{
foreach (MassFromVolume mfm in targets)
{
mfm.CalculateAndApplyFromDensity(mfm.density);
}
}
drawer.EndSubsection();
drawer.BeginSubsection("Material");
drawer.Field("material");
if (drawer.Button("Calculate Mass From Material"))
{
foreach (MassFromVolume mfm in targets)
{
mfm.CalculateAndApplyFromMaterial();
}
}
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e4d7cea0eaeb4875948ee3242c6d65fe
timeCreated: 1593873668

View File

@@ -0,0 +1,365 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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
{
/// <summary>
/// Custom inspector for WaterObject.
/// </summary>
[CustomEditor(typeof(WaterObject))]
[CanEditMultipleObjects]
[ExecuteAlways]
public class WaterObjectEditor : DWP2NUIEditor
{
private bool _editorHasWarnings;
private Texture2D _greyTexture;
private Texture2D _originalMeshPreviewTexture;
private float _previewHeight;
private Texture2D _simMeshPreviewTexture;
private WaterObject _waterObject;
private void OnEnable()
{
_greyTexture = new Texture2D(10, 10);
FillInTexture(ref _greyTexture, new Color32(66, 66, 66, 255));
}
private static void FillInTexture(ref Texture2D tex, Color color)
{
Color[] fillColorArray = tex.GetPixels();
for (int i = 0; i < fillColorArray.Length; i++)
{
fillColorArray[i] = color;
}
tex.SetPixels(fillColorArray);
tex.Apply();
}
/// <summary>
/// Draws custom inspector GUI for WaterObject.
/// </summary>
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
// Get water object and make sure it is initialized
_waterObject = (WaterObject)target;
// Draw logo texture
Rect logoRect = drawer.positionRect;
logoRect.height = 60f;
drawer.DrawEditorTexture(logoRect, "Dynamic Water Physics 2/Logos/WaterObjectLogo");
drawer.AdvancePosition(logoRect.height);
if (_waterObject.originalMesh == null || _waterObject.serializedSimulationMesh == null ||
_waterObject.SimulationMesh == null)
{
drawer.Space();
drawer.Info("Simulation mesh not generated. Click 'Update Simulation Mesh' button below to fix this.",
MessageType.Error);
}
if (_waterObject.CurrentWaterDataProvider)
{
drawer.Info($"Current water data source: '{_waterObject.CurrentWaterDataProvider.GetType().Name}' " +
$"on object '{_waterObject.CurrentWaterDataProvider.name}'.");
}
if (Application.isPlaying)
{
drawer.Info($"Forces are being applied to '{_waterObject.targetRigidbody}'.");
}
drawer.BeginSubsection("Buoyancy");
if (Application.isPlaying)
{
drawer.Field("submergedVolume", false, "l");
}
drawer.Field("buoyantForceCoefficient");
drawer.Field("fluidDensity");
drawer.EndSubsection();
drawer.BeginSubsection("Hydrodynamics");
drawer.Field("hydrodynamicForceCoefficient");
drawer.Field("slamForceCoefficient");
drawer.Field("suctionForceCoefficient");
drawer.Field("skinDragCoefficient");
drawer.Field("velocityDotPower");
drawer.EndSubsection();
drawer.BeginSubsection("Water");
drawer.Field("calculateWaterHeights");
drawer.Field("calculateWaterNormals");
drawer.Field("calculateWaterFlows");
drawer.Field("defaultWaterHeight");
drawer.Field("defaultWaterNormal");
drawer.Field("defaultWaterFlow");
drawer.EndSubsection();
// Simulation mesh
drawer.BeginSubsection("Simulation Mesh Settings");
if (drawer.Field("simplifyMesh").boolValue)
{
drawer.Field("targetTriangleCount");
}
drawer.Field("convexifyMesh");
drawer.Field("weldColocatedVertices");
if (drawer.Button("Update Simulation Mesh"))
{
UpdateSimulationMesh();
}
if (drawer.Button("Toggle In-Scene Preview"))
{
ToggleInScenePreview();
}
if (targets.Length == 1)
{
drawer.Label("Simulation Mesh Preview:");
drawer.Space();
if (Event.current.type == EventType.Repaint)
{
DrawPreviewTexture(_waterObject, drawer.positionRect, out _previewHeight);
}
drawer.Space(_previewHeight);
}
drawer.EndSubsection();
// Warnings
drawer.BeginSubsection("Messages");
DrawWarnings();
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
/// <summary>
/// Draws validation warnings and errors.
/// </summary>
private void DrawWarnings()
{
if (targets.Length == 1)
{
_editorHasWarnings = false;
// Missing rigidbody
if (_waterObject.targetRigidbody == null)
{
_waterObject.targetRigidbody = _waterObject.transform.GetComponentInParent<Rigidbody>(true);
if (_waterObject.targetRigidbody == null)
{
drawer.Info("WaterObject requires a rigidbody attached to the object or its parent(s) to " +
$"function. Add a rigidbody to object {_waterObject.name} or one of its parents.",
MessageType.Error);
_editorHasWarnings = true;
if (drawer.Button("Add a Rigidbody"))
{
foreach (WaterObject wo in targets)
{
wo.gameObject.AddComponent<Rigidbody>();
}
}
}
}
// Collider count
if (_waterObject.targetRigidbody != null)
{
int colliderCount = _waterObject.targetRigidbody.transform.GetComponentsInChildren<Collider>()
.Length;
if (_waterObject.targetRigidbody.transform.GetComponentsInChildren<Collider>().Length == 0)
{
drawer.Info(
$"Found {colliderCount} colliders attached to rigidbody {_waterObject.targetRigidbody.name} " +
"and its children. At least one collider is required for a rigidbody to work properly.",
MessageType.Error);
_editorHasWarnings = true;
if (drawer.Button("Add a MeshCollider"))
{
foreach (WaterObject wo in targets)
{
MeshCollider mc = wo.gameObject.AddComponent<MeshCollider>();
mc.convex = true;
mc.isTrigger = false;
}
}
}
}
// Excessive triangle count
if (_waterObject.triangleCount > 150)
{
drawer.Info($"Possible excessive number of triangles detected ({_waterObject.triangleCount})." +
" Use simplify mesh option to reduce the number of triangles, or if this is intentional ignore this message." +
" Recommended number is 16-128.", MessageType.Warning);
}
// Scale error
if (_waterObject.transform.localScale.x <= 0
|| _waterObject.transform.localScale.y <= 0
|| _waterObject.transform.localScale.z <= 0)
{
drawer.Info(
"Scale of this object is negative or zero on one or more of axes. Scale less than or equal to zero is not supported." +
" WaterObject will still be calculated but with unpredictable results. ", MessageType.Error);
}
if (!_editorHasWarnings)
{
drawer.Info($"No warnings or errors for object {_waterObject.name}.");
}
}
}
/// <summary>
/// Regenerates simulation mesh for selected objects.
/// </summary>
private void UpdateSimulationMesh()
{
foreach (WaterObject wo in targets)
{
wo.StopSimMeshPreview();
wo.GenerateSimMesh();
Undo.RecordObject(wo, "Updated Simulation Mesh");
EditorUtility.SetDirty(wo);
}
}
/// <summary>
/// Toggles in-scene preview of simulation mesh.
/// </summary>
private void ToggleInScenePreview()
{
foreach (WaterObject wo in targets)
{
if (wo.PreviewEnabled)
{
wo.StopSimMeshPreview();
}
else
{
wo.StartSimMeshPreview();
}
}
}
/// <summary>
/// Draws side-by-side preview of original and simulation meshes.
/// </summary>
private void DrawPreviewTexture(WaterObject waterObject, Rect rect, out float previewHeight)
{
previewHeight = 0;
if (waterObject == null)
{
return;
}
if (waterObject.originalMesh == null)
{
return;
}
if (waterObject.serializedSimulationMesh.vertices == null)
{
return;
}
if (waterObject.SimulationMesh == null)
{
return;
}
// Tri count
int originalTriCount = waterObject.originalMesh == null ? 0 : waterObject.originalMesh.triangles.Length / 3;
int originalVertCount = waterObject.originalMesh == null ? 0 : waterObject.originalMesh.vertices.Length;
int simulationTriCount = waterObject.serializedSimulationMesh.triangles.Length / 3;
int simulationVertCount = waterObject.serializedSimulationMesh.vertices.Length;
_originalMeshPreviewTexture = AssetPreview.GetAssetPreview(waterObject.originalMesh);
_simMeshPreviewTexture = waterObject.SimulationMesh == null
? AssetPreview.GetAssetPreview(waterObject.originalMesh)
: AssetPreview.GetAssetPreview(waterObject.SimulationMesh);
float startY = rect.y;
float previewWidth = rect.width;
float maxPreviewWidth = 480f;
previewWidth = Mathf.Clamp(previewWidth, 240f, maxPreviewWidth);
float margin = (rect.width - previewWidth) * 0.5f;
float halfWidth = previewWidth * 0.5f;
Rect leftRect = new(rect.x + margin, startY, halfWidth, halfWidth);
Rect rightRect = new(rect.x + halfWidth + margin, startY, halfWidth, halfWidth);
Material previewMaterial = new(Shader.Find("UI/Default"));
GUI.DrawTexture(leftRect, _originalMeshPreviewTexture == null ? _greyTexture : _originalMeshPreviewTexture);
GUI.DrawTexture(rightRect, _simMeshPreviewTexture == null ? _greyTexture : _simMeshPreviewTexture);
GUIStyle centeredStyle = GUI.skin.GetStyle("Label");
centeredStyle.alignment = TextAnchor.MiddleCenter;
centeredStyle.normal.textColor = Color.white;
Rect leftLabelRect = leftRect;
leftLabelRect.height = 20f;
GUI.Label(leftLabelRect, "ORIGINAL", centeredStyle);
Rect rightLabelRect = rightRect;
rightLabelRect.height = 20f;
GUI.Label(rightLabelRect, "SIMULATION", centeredStyle);
Rect leftBottomLabelRect = leftRect;
leftBottomLabelRect.y = leftRect.y + halfWidth - 20f;
leftBottomLabelRect.height = 20f;
GUI.Label(leftBottomLabelRect, $"{originalTriCount} tris, {originalVertCount} verts");
Rect rightBottomLabelrect = rightRect;
rightBottomLabelrect.y = rightRect.y + halfWidth - 20f;
rightBottomLabelrect.height = 20f;
GUI.Label(rightBottomLabelrect, $"{simulationTriCount} tris, {simulationVertCount} verts");
previewHeight = halfWidth;
}
}
}
#endif

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,88 @@
fileFormatVersion: 2
guid: 93c55739f324f5540b9c04732218a774
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,88 @@
fileFormatVersion: 2
guid: ff137dafe58a6324c908e3cb796b45b9
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,88 @@
fileFormatVersion: 2
guid: eeffff81a7107a9498f73a9ac807c6e8
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant: