Files
aibis-dream/Assets/Shapes/Scripts/Runtime/Components/ShapeRenderer.cs
T
2025-05-07 15:07:44 +08:00

613 lines
23 KiB
C#

using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.Rendering;
// Shapes © Freya Holmér - https://twitter.com/FreyaHolmer/
// Website & Documentation - https://acegikmo.com/shapes/
namespace Shapes {
/// <summary>The base type of all shape components</summary>
[DisallowMultipleComponent]
public abstract class ShapeRenderer : MonoBehaviour {
bool initializedComponents = false;
MeshRenderer rnd;
MeshFilter mf;
int meshOwnerID;
MaterialPropertyBlock mpb;
MaterialPropertyBlock Mpb => mpb ?? ( mpb = new MaterialPropertyBlock() ); // hecking, gosh, I want the C#8 ??= operator
Material[] instancedMaterials = null; // used when pass tags are anything but the default (eg ZTest != Less Equal, or scale offset is set, or a weird blend mode)
internal bool IsUsingUniqueMaterials => IsInstanced == false;
/// <summary>Used to mark this mesh as changed. Only required when manually editing the point lists of polygons and polylines, otherwise you shouldn't ever need to use this</summary>
[System.NonSerialized] public bool meshOutOfDate = true;
/// <summary>The current mesh in use by this shape. Note: Do not directly modify this mesh, it may not update properly or your changes will be overwritten or you'll modify assets. It's here for reading purposes only</summary>
public Mesh Mesh {
get => mf.sharedMesh;
private set => mf.sharedMesh = value;
}
/// <summary>The sorting layer ID of this renderer</summary>
public int SortingLayerID {
get => MakeSureComponentExists( ref rnd, out _ ).sortingLayerID;
set => MakeSureComponentExists( ref rnd, out _ ).sortingLayerID = value;
}
/// <summary>The sorting order of this renderer</summary>
public int SortingOrder {
get => MakeSureComponentExists( ref rnd, out _ ).sortingOrder;
set => MakeSureComponentExists( ref rnd, out _ ).sortingOrder = value;
}
/// <summary>Gets the name of the current sorting layer of this renderer</summary>
public string SortingLayerName => SortingLayer.IDToName( SortingLayerID );
// Properties
[SerializeField] ShapesBlendMode blendMode = ShapesBlendMode.Transparent;
/// <summary>What blending mode to use</summary>
public ShapesBlendMode BlendMode {
get => blendMode;
set {
blendMode = value;
UpdateMaterial();
}
}
[SerializeField] ScaleMode scaleMode = ScaleMode.Uniform;
/// <summary>Sets how this shape should behave when scaled</summary>
public ScaleMode ScaleMode {
get => scaleMode;
set => SetIntNow( ShapesMaterialUtils.propScaleMode, (int)( scaleMode = value ) );
}
[SerializeField] [ShapesColorField( true )] private protected Color color = Color.white;
/// <summary>The color of this shape. The alpha channel is used for opacity/intensity in all blend modes</summary>
public virtual Color Color {
get => color;
set => SetColorNow( ShapesMaterialUtils.propColor, color = value );
}
[SerializeField] private protected DetailLevel detailLevel = DetailLevel.Medium;
/// <summary>What detail level to use for 3D primitives (3D Lines/Sphere/Torus/Cone)</summary>
public virtual DetailLevel DetailLevel {
get => detailLevel;
set {
detailLevel = value;
UpdateMesh( force: true );
}
}
#region instancing breaking pass tags
// if and only if all are set to their default values
bool IsInstanced => UsingDefaultZTests && UsingDefaultMasking && UsingDefaultRenderQueue;
// render queue offset
bool UsingDefaultRenderQueue => renderQueue == DEFAULT_RENDER_QUEUE_AUTO;
public int RenderQueue {
get => renderQueue;
set {
renderQueue = value;
if( IsUsingUniqueMaterials ) {
UpdateMaterial();
foreach( Material instancedMaterial in instancedMaterials )
instancedMaterial.renderQueue = renderQueue;
}
}
}
[SerializeField] int renderQueue = DEFAULT_RENDER_QUEUE_AUTO;
/// <summary>The default render queue, which is auto-set based on blend mode</summary>
public const int DEFAULT_RENDER_QUEUE_AUTO = -1;
// Z testing properties
/// <summary>The default Z test (depth test) value of LessEqual</summary>
public const CompareFunction DEFAULT_ZTEST = CompareFunction.LessEqual;
/// <summary>The default Z offset factor of 0</summary>
public const float DEFAULT_ZOFS_FACTOR = 0f;
/// <summary>The default Z offset units of 0</summary>
public const int DEFAULT_ZOFS_UNITS = 0;
/// <summary>The default ColorMask of 15 (RGBA)</summary>
public const ColorWriteMask DEFAULT_COLOR_MASK = ColorWriteMask.All;
bool UsingDefaultZTests => zTest == DEFAULT_ZTEST && zOffsetFactor == DEFAULT_ZOFS_FACTOR && zOffsetUnits == DEFAULT_ZOFS_UNITS;
[SerializeField] CompareFunction zTest = DEFAULT_ZTEST;
/// <inheritdoc cref="RenderState.zTest"/>
public CompareFunction ZTest {
get => zTest;
set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propZTest, (int)( zTest = value ) );
}
[SerializeField] float zOffsetFactor = DEFAULT_ZOFS_FACTOR;
/// <inheritdoc cref="RenderState.zOffsetFactor"/>
public float ZOffsetFactor {
get => zOffsetFactor;
set => SetFloatOnAllInstancedMaterials( ShapesMaterialUtils.propZOffsetFactor, zOffsetFactor = value );
}
[SerializeField] int zOffsetUnits = DEFAULT_ZOFS_UNITS;
/// <inheritdoc cref="RenderState.zOffsetUnits"/>
public int ZOffsetUnits {
get => zOffsetUnits;
set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propZOffsetUnits, zOffsetUnits = value );
}
[SerializeField] ColorWriteMask colorMask = DEFAULT_COLOR_MASK;
/// <inheritdoc cref="RenderState.colorMask"/>
public ColorWriteMask ColorMask {
get => colorMask;
set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propColorMask, (int)( colorMask = value ) );
}
// stencil
/// <summary>The default stencil compare function of CompareFunction.Always</summary>
public const CompareFunction DEFAULT_STENCIL_COMP = CompareFunction.Always;
/// <summary>The default stencil operation of StencilOp.Keep</summary>
public const StencilOp DEFAULT_STENCIL_OP = StencilOp.Keep;
/// <summary>The default stencil ref id 0</summary>
public const byte DEFAULT_STENCIL_REF_ID = 0;
/// <summary>The default stencil mask of 255</summary>
public const byte DEFAULT_STENCIL_MASK = 255; // read & write
bool UsingDefaultMasking => stencilComp == DEFAULT_STENCIL_COMP && stencilOpPass == DEFAULT_STENCIL_OP && stencilRefID == DEFAULT_STENCIL_REF_ID && stencilReadMask == DEFAULT_STENCIL_MASK && stencilWriteMask == DEFAULT_STENCIL_MASK && colorMask == DEFAULT_COLOR_MASK;
[SerializeField] CompareFunction stencilComp = DEFAULT_STENCIL_COMP;
/// <inheritdoc cref="RenderState.colorMask"/>
public CompareFunction StencilComp {
get => stencilComp;
set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilComp, (int)( stencilComp = value ) );
}
[SerializeField] StencilOp stencilOpPass = DEFAULT_STENCIL_OP;
/// <inheritdoc cref="RenderState.stencilComp"/>
public StencilOp StencilOpPass {
get => stencilOpPass;
set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilOpPass, (int)( stencilOpPass = value ) );
}
[SerializeField] byte stencilRefID = DEFAULT_STENCIL_REF_ID;
/// <inheritdoc cref="RenderState.stencilOpPass"/>
public byte StencilRefID {
get => stencilRefID;
set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilID, stencilRefID = value );
}
[SerializeField] byte stencilReadMask = DEFAULT_STENCIL_MASK;
/// <inheritdoc cref="RenderState.stencilRefID"/>
public byte StencilReadMask {
get => stencilReadMask;
set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilReadMask, stencilReadMask = value );
}
[SerializeField] byte stencilWriteMask = DEFAULT_STENCIL_MASK;
/// <inheritdoc cref="RenderState.stencilReadMask"/>
public byte StencilWriteMask {
get => stencilWriteMask;
set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilWriteMask, stencilWriteMask = value );
}
#endregion
#if UNITY_EDITOR
public virtual void OnValidate() {
// OnValidate can get called before awake in editor, so make sure the required things are initialized
if( rnd == null ) rnd = GetComponent<MeshRenderer>(); // Needed for ApplyProperties
if( mf == null ) mf = GetComponent<MeshFilter>(); // Needed for UpdateMesh
ShapeClampRanges();
UpdateAllMaterialProperties();
ApplyProperties();
if( MeshUpdateMode == MeshUpdateMode.SelfGenerated )
meshOutOfDate = true; // we can't update the mesh in OnValidate because of https://issuetracker.unity3d.com/issues/warning-appears-when-changing-meshfilter-dot-sharedmesh-during-onvalidate
// UpdateMesh( force:true ); gosh I wish I could do this it would solve so many problems but Unity has some WEIRD quirks here
}
internal void HideMeshFilterRenderer() {
VerifyComponents();
const HideFlags flags = HideFlags.HideInInspector; // Hide mesh renderer and filter
rnd.hideFlags = flags;
mf.hideFlags = flags;
}
#endif
T MakeSureComponentExists<T>( ref T field, out bool created ) where T : Component {
if( field == null ) {
field = GetComponent<T>();
if( field == null ) {
field = gameObject.AddComponent<T>();
created = true;
}
field.hideFlags = HideFlags.HideInInspector;
}
created = false;
return field;
}
void VerifyComponents() {
if( initializedComponents == false ) {
initializedComponents = true;
MakeSureComponentExists( ref mf, out _ );
MakeSureComponentExists( ref rnd, out bool createdRnd );
}
// these have to be turned off aggressively, otherwise batching can break
if( rnd.receiveShadows )
rnd.receiveShadows = false;
if( rnd.shadowCastingMode != ShadowCastingMode.Off )
rnd.shadowCastingMode = ShadowCastingMode.Off;
if( rnd.lightProbeUsage != LightProbeUsage.Off )
rnd.lightProbeUsage = LightProbeUsage.Off;
if( rnd.reflectionProbeUsage != ReflectionProbeUsage.Off )
rnd.reflectionProbeUsage = ReflectionProbeUsage.Off;
}
public virtual void Awake() {
VerifyComponents();
UpdateMaterial();
UpdateMesh();
UpdateAllMaterialProperties();
}
bool HasGeneratedOrCopyOfMesh => MeshUpdateMode == MeshUpdateMode.SelfGenerated || MeshUpdateMode == MeshUpdateMode.UseAssetCopy;
public virtual void OnEnable() {
UpdateMesh();
rnd.enabled = true;
#if UNITY_EDITOR
if( HasGeneratedOrCopyOfMesh )
UnityEditor.Undo.undoRedoPerformed += UpdateMeshOnUndoRedo;
#endif
if( UseCamOnPreCull )
SubscribeCamPreCull();
}
void OnDisable() {
if( rnd != null )
rnd.enabled = false;
#if UNITY_EDITOR
if( HasGeneratedOrCopyOfMesh )
UnityEditor.Undo.undoRedoPerformed -= UpdateMeshOnUndoRedo;
#endif
if( UseCamOnPreCull )
UnsubscribeCamPreCull();
}
// These are used for polyline and polygon to detect mesh changes and lazily update before rendering
void OnPreCamCullWithCam( Camera cam ) => CamOnPreCull();
#if UNITY_2019_1_OR_NEWER
void OnPreCamCullWithCam( ScriptableRenderContext ctx, Camera cam ) => CamOnPreCull();
#endif
void SubscribeCamPreCull() {
if( UnityInfo.UsingSRP ) {
#if UNITY_2019_1_OR_NEWER
RenderPipelineManager.beginCameraRendering += OnPreCamCullWithCam;
#else
UnityEngine.Experimental.Rendering.RenderPipeline.beginCameraRendering += OnPreCamCullWithCam;
#endif
} else
Camera.onPreCull += OnPreCamCullWithCam;
}
void UnsubscribeCamPreCull() {
if( UnityInfo.UsingSRP ) {
#if UNITY_2019_1_OR_NEWER
RenderPipelineManager.beginCameraRendering -= OnPreCamCullWithCam;
#else
UnityEngine.Experimental.Rendering.RenderPipeline.beginCameraRendering -= OnPreCamCullWithCam;
#endif
} else
Camera.onPreCull -= OnPreCamCullWithCam;
}
#if UNITY_EDITOR
void UpdateMeshOnUndoRedo() => UpdateMesh( true );
#endif
void Reset() {
UpdateAllMaterialProperties();
UpdateMesh( true );
}
void OnDestroy() {
if( HasGeneratedOrCopyOfMesh && Mesh != null )
DestroyImmediate( Mesh );
this.TryDestroyInOnDestroy( rnd );
this.TryDestroyInOnDestroy( mf );
TryDestroyInstancedMaterials( inOnDestroy: true );
}
private protected abstract Bounds GetBounds_Internal();
private protected abstract void SetAllMaterialProperties();
private protected virtual void ShapeClampRanges() => _ = 0;
private protected abstract Material[] GetMaterials();
private protected virtual void GenerateMesh() => _ = 0;
private protected virtual Mesh GetInitialMeshAsset() => ShapesMeshUtils.QuadMesh[HasDetailLevels ? (int)DetailLevel.Medium : 0];
private protected virtual MeshUpdateMode MeshUpdateMode => MeshUpdateMode.UseAsset;
internal virtual bool HasScaleModes => true;
internal virtual bool HasDetailLevels => true;
private protected virtual bool UseCamOnPreCull => false;
internal virtual void CamOnPreCull() => _ = 0;
void UpdateMeshBounds() => Mesh.bounds = GetBounds_Internal();
void TryDestroyInstancedMaterials( bool inOnDestroy = false ) {
if( instancedMaterials != null ) {
for( int i = 0; i < instancedMaterials.Length; i++ ) {
if( instancedMaterials[i] != null ) {
if( inOnDestroy )
this.TryDestroyInOnDestroy( instancedMaterials[i] );
else
instancedMaterials[i].DestroyBranched();
}
}
}
}
void MakeSureMaterialInstancesAreGood( Material[] sourceMats ) {
Material InstantiateMaterial( int index ) => new Material( sourceMats[index] ) { name = sourceMats[index].name + " (instance)" };
void PopulateAll() {
instancedMaterials = new Material[sourceMats.Length];
for( int i = 0; i < sourceMats.Length; i++ )
instancedMaterials[i] = InstantiateMaterial( i );
}
if( instancedMaterials == null ) {
// no instanced materials exist, create all
PopulateAll();
} else {
if( instancedMaterials.Length != sourceMats.Length ) {
// length mismatch, regenerate all
TryDestroyInstancedMaterials();
PopulateAll();
} else {
// same length! make sure they are all matching
for( int i = 0; i < sourceMats.Length; i++ ) {
if( instancedMaterials[i] == null ) {
// if null, create instance
instancedMaterials[i] = InstantiateMaterial( i );
} else {
// if not null, then make sure the shader is matching
if( instancedMaterials[i].shader != sourceMats[i].shader ) {
// mismatch, destroy instance and assign new one
instancedMaterials[i].DestroyBranched();
instancedMaterials[i] = InstantiateMaterial( i );
} else {
// they do use the same shader, but, make sure they also use the same keywords just to be safe
instancedMaterials[i].shaderKeywords = sourceMats[i].shaderKeywords;
}
}
}
}
}
}
private protected void UpdateMaterial() {
Material[] targetMats = GetMaterials();
// this means we have unique material properties for this shape, so, instantiate them
// but! only when they're in the scene
if( IsUsingUniqueMaterials ) {
MakeSureMaterialInstancesAreGood( targetMats );
targetMats = instancedMaterials;
}
VerifyComponents();
#if UNITY_EDITOR
if( EditorApplication.isPlaying == false )
UpdateMaterialsEditorMode( targetMats );
else
#endif
rnd.sharedMaterials = targetMats;
}
#if UNITY_EDITOR
void UpdateMaterialsEditorMode( Material[] targetMats ) {
bool needsUpdate = false;
if( rnd.sharedMaterials.Length != targetMats.Length ) {
needsUpdate = true;
} else {
for( int i = 0; i < targetMats.Length; i++ ) {
if( rnd.sharedMaterials[i] != targetMats[i] ) {
needsUpdate = true;
break;
}
}
}
if( needsUpdate ) {
Undo.RecordObject( rnd, "" );
rnd.sharedMaterials = targetMats;
}
}
#endif
/// <summary>Updates the mesh this object is using.
/// If this mesh generates procedural meshes, such as the polygon or polyline, it will regenerate it if force is set to true</summary>
/// <param name="force"></param>
public void UpdateMesh( bool force = false ) {
MeshUpdateMode mode = MeshUpdateMode;
// if we're using a mesh asset, we only assign if it's null or mismatching
if( mode == MeshUpdateMode.UseAsset && ( Mesh == null || Mesh != GetInitialMeshAsset() ) ) {
Mesh = GetInitialMeshAsset();
return;
}
// the next two modes are copy-sensitive, meaning that if we duplicate this object,
// we also have to duplicate the mesh and update which mesh the duplicate is pointing to
int id = gameObject.GetInstanceID();
bool createMesh = Mesh == null || meshOwnerID != id;
// create new mesh
if( createMesh ) {
meshOwnerID = id;
if( mode == MeshUpdateMode.UseAssetCopy ) {
Mesh = Instantiate( GetInitialMeshAsset() );
Mesh.hideFlags = HideFlags.HideAndDontSave;
Mesh.MarkDynamic();
} else if( mode == MeshUpdateMode.SelfGenerated ) {
Mesh = new Mesh() { hideFlags = HideFlags.HideAndDontSave };
Mesh.MarkDynamic();
GenerateMesh();
}
} else if( force && mode == MeshUpdateMode.SelfGenerated ) {
GenerateMesh(); // update existing mesh
}
}
/// <summary><para>Returns the local space bounds</para>
/// <para>Note: This does not take into account screen space sizing, so it will only behave as you expect when using meters only</para></summary>
public Bounds GetBounds() => GetBounds_Internal();
/// <summary><para>Returns the world space bounds of the local space bounds</para>
/// <para>Note: This does not take into account screen space sizing, so it will only behave as you expect when using meters only</para></summary>
public Bounds GetWorldBounds() {
Bounds localBounds = GetBounds_Internal();
Vector3 min = Vector3.one * float.MaxValue;
Vector3 max = Vector3.one * float.MinValue;
Transform tf = transform;
for( int x = -1; x <= 1; x += 2 )
for( int y = -1; y <= 1; y += 2 )
for( int z = -1; z <= 1; z += 2 ) {
Vector3 wPt = tf.TransformPoint( localBounds.center + Vector3.Scale( localBounds.extents, new Vector3( x, y, z ) ) );
min = Vector3.Min( min, wPt );
max = Vector3.Max( max, wPt );
}
return new Bounds( ( max + min ) / 2f, max - min );
}
void OnDidApplyAnimationProperties() => UpdateAllMaterialProperties(); // so this is not great but it works don't judge
void SetIntOnAllInstancedMaterials( int property, int value ) {
if( IsUsingUniqueMaterials ) {
UpdateMaterial();
foreach( Material instancedMaterial in instancedMaterials )
instancedMaterial.SetInt_Shapes( property, value );
}
}
void SetFloatOnAllInstancedMaterials( int property, float value ) {
if( IsUsingUniqueMaterials ) {
UpdateMaterial();
foreach( Material instancedMaterial in instancedMaterials )
instancedMaterial.SetFloat( property, value );
}
}
internal void UpdateAllMaterialProperties() {
if( gameObject.scene.IsValid() == false )
return; // not in a scene :c
UpdateMaterial();
if( IsUsingUniqueMaterials ) // I wish we could material property block these ;-;
foreach( Material instancedMaterial in instancedMaterials ) {
instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propZTest, (int)zTest );
instancedMaterial.SetFloat( ShapesMaterialUtils.propZOffsetFactor, zOffsetFactor );
instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propZOffsetUnits, zOffsetUnits );
instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propColorMask, (int)colorMask );
instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilComp, (int)stencilComp );
instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilOpPass, (int)stencilOpPass );
instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilID, stencilRefID );
instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilReadMask, stencilReadMask );
instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilWriteMask, stencilWriteMask );
instancedMaterial.renderQueue = renderQueue;
}
SetColor( ShapesMaterialUtils.propColor, color );
if( HasScaleModes )
SetInt( ShapesMaterialUtils.propScaleMode, (int)scaleMode );
SetAllMaterialProperties();
ApplyProperties();
}
private protected void ApplyProperties() {
VerifyComponents(); // make sure components exists. rnd can be uninitialized if you modify an object that has never had awake called
rnd.SetPropertyBlock( Mpb );
if( MeshUpdateMode == MeshUpdateMode.UseAssetCopy )
UpdateMeshBounds();
}
private protected void SetAllDashValues( DashStyle style, bool dashed, bool matchSpacingToSize, float thickness, bool setType, bool now ) {
float netDashSize = style.GetNetAbsoluteSize( dashed, thickness );
if( dashed ) {
SetFloat( ShapesMaterialUtils.propDashSpacing, GetNetDashSpacing( style, true, matchSpacingToSize, thickness ) );
SetFloat( ShapesMaterialUtils.propDashOffset, style.offset );
SetInt( ShapesMaterialUtils.propDashSpace, (int)style.space );
SetInt( ShapesMaterialUtils.propDashSnap, (int)style.snap );
if( setType ) {
SetInt( ShapesMaterialUtils.propDashType, (int)style.type );
if( style.type.HasModifier() )
SetFloat( ShapesMaterialUtils.propDashShapeModifier, style.shapeModifier );
}
}
if( now )
SetFloatNow( ShapesMaterialUtils.propDashSize, netDashSize );
else
SetFloat( ShapesMaterialUtils.propDashSize, netDashSize );
}
private protected float GetNetDashSpacing( DashStyle style, bool dashed, bool matchSpacingToSize, float thickness ) {
if( matchSpacingToSize && style.space == DashSpace.FixedCount )
return 0.5f;
return matchSpacingToSize ? style.GetNetAbsoluteSize( dashed, thickness ) : style.GetNetAbsoluteSpacing( dashed, thickness );
}
private protected void SetColor( int prop, Color value ) {
if( ShapeGroup.shapeGroupsInScene > 0 ) { // if color tint groups exist, see if we have any
ShapeGroup[] groups = GetComponentsInParent<ShapeGroup>();
if( groups != null )
foreach( ShapeGroup shapeGroup in groups.Where( g => g.IsEnabled ) )
value *= shapeGroup.Color;
}
Mpb.SetColor( prop, value );
}
private protected void SetFloat( int prop, float value ) => Mpb.SetFloat( prop, value );
private protected void SetInt( int prop, int value ) => Mpb.SetInt_Shapes( prop, value );
private protected void SetVector3( int prop, Vector3 value ) => Mpb.SetVector( prop, value );
private protected void SetVector4( int prop, Vector4 value ) => Mpb.SetVector( prop, value );
private protected void SetColorNow( int prop, Color value ) {
SetColor( prop, value );
ApplyProperties();
}
private protected void SetFloatNow( int prop, float value ) {
Mpb.SetFloat( prop, value );
ApplyProperties();
}
private protected void SetIntNow( int prop, int value ) {
Mpb.SetInt_Shapes( prop, value );
ApplyProperties();
}
private protected void SetVector3Now( int prop, Vector3 value ) {
Mpb.SetVector( prop, value );
ApplyProperties();
}
private protected void SetVector4Now( int prop, Vector4 value ) {
Mpb.SetVector( prop, value );
ApplyProperties();
}
}
}