Streamingle_URP/Assets/NiloToonURP/Runtime/NiloToonRendererRedrawer.cs

392 lines
16 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.Rendering;
using UnityEngine.Serialization;
namespace NiloToon.NiloToonURP
{
[ExecuteAlways]
[RequireComponent(typeof(Renderer))]
public class NiloToonRendererRedrawer : MonoBehaviour
{
[Serializable]
public class DrawData
{
public bool enabled = true;
/// <summary>
/// If you want to edit the material in runtime(e.g., control tint color), consider calling
/// - "ActualMaterialUsedForRendering"
/// instead of this "sharedMaterial".
/// </summary>
[FormerlySerializedAs("material")] // DO NOT REMOVE THIS LINE! it will produce material reference lost if the prefab is very old which is still saved with 'material' instead of 'sharedMaterial'
public Material sharedMaterial;
public int subMeshIndex;
Material _materialInstance;
Material _materialInstanceSource;
/// <summary>
/// Call this will create a material instance, similar to calling Renderer.material
/// </summary>
public Material MaterialInstance
{
get
{
if (!sharedMaterial)
{
Debug.LogError("because sharedMaterial is null, calling NiloToonRendererRedrawer.MaterialInstance will now return null");
return null;
}
// a safe way to edit material in playmode,
// is to convert sharedMaterial to material instance,
// similar to calling Renderer.material
// *Don't create material instance in Edit mode.
if (!Application.isPlaying)
{
Debug.LogError("Tried to instantiating material due to calling NiloToonRendererRedrawer.MaterialInstance during edit mode. This will leak materials into the scene. You most likely want to use renderer.sharedMaterial instead. *Now returning sharedMaterial instead of MaterialInstance to prevent material leak*.");
return sharedMaterial;
}
// mimic the result of calling Renderer.material
if ((!_materialInstance && sharedMaterial) || // not yet clone material
(_materialInstanceSource != sharedMaterial)) // sharedMaterial changed to another material
{
_materialInstance = Instantiate(sharedMaterial);
_materialInstanceSource = sharedMaterial;
_materialInstance.name = sharedMaterial.name + " (Instance)";
_materialInstance.hideFlags = HideFlags.DontSave;
}
return _materialInstance;
}
}
public Material ActualMaterialUsedForRendering
{
get
{
if (_materialInstance) return _materialInstance;
return sharedMaterial;
}
}
}
public List<DrawData> DrawList = new();
[Header("Sync options")]
[Tooltip("Should this script also stop rendering when the Renderer of this GameObject is disabled?")]
[Revertible]
public bool deactivateWithRenderer = true;
[Header("Optimization")]
[Tooltip("Should this script also stop rendering when the Renderer of this GameObject is not visible by any camera?\n" +
"*Please note that the editor scene window camera is considered also. To profile the game window CPU/GPU performance, you should hide the scene window.")]
[Revertible]
public bool deactivateWhenRendererIsNotVisible = true;
private NiloToonPerCharacterRenderController _niloToonPerCharacterRenderController;
public NiloToonPerCharacterRenderController NiloToonPerCharacterRenderController
{
get
{
if (!_niloToonPerCharacterRenderController)
{
// search parent for the closest NiloToonPerCharacterRenderController
_niloToonPerCharacterRenderController = GetComponentInParent<NiloToonPerCharacterRenderController>();
}
return _niloToonPerCharacterRenderController;
}
}
private Renderer _Renderer;
public Renderer Renderer
{
get
{
if (!_Renderer)
_Renderer = GetComponent<Renderer>();
return _Renderer;
}
}
private MaterialPropertyBlock _MPB;
public MaterialPropertyBlock MPB
{
get
{
if (_MPB == null)
_MPB = new MaterialPropertyBlock();
return _MPB;
}
}
private Mesh _bakedMeshHolder;
public Mesh BakedMeshHolder
{
get
{
if (_bakedMeshHolder == null)
_bakedMeshHolder = new Mesh();
return _bakedMeshHolder;
}
}
public void GetMaterials(List<Material> outList)
{
outList.Clear();
foreach (var drawData in DrawList)
{
if(drawData == null) continue;
if (drawData.sharedMaterial)
{
outList.Add(drawData.MaterialInstance);
}
}
}
Mesh GetMeshFromRenderer(Renderer renderer)
{
if (renderer is MeshRenderer meshRenderer)
{
MeshFilter meshFilter = meshRenderer.GetComponent<MeshFilter>();
if (!meshFilter)
{
Debug.LogError($"No MeshFilter found on GameObject {renderer.gameObject.name}.");
return null;
}
return meshFilter.sharedMesh;
}
if (renderer is SkinnedMeshRenderer skinnedMeshRenderer)
{
// TODO:
// SkinnedMeshRenderer.BakeMesh() is extremely slow but works perfectly,
// we should provide a new option to use an extra SkinnedMeshRenderer instead of SkinnedMeshRenderer.BakeMesh()+Graphics.RenderMesh()
skinnedMeshRenderer.BakeMesh(BakedMeshHolder);
return BakedMeshHolder;
}
Debug.LogError($"No Mesh or SkinnedMeshRenderer found on GameObject {renderer.gameObject.name}.");
return null;
}
void FindParentNiloToonPerCharacterRenderController()
{
// add a link with that NiloToonPerCharacterRenderController
if (NiloToonPerCharacterRenderController)
{
if (!NiloToonPerCharacterRenderController.nilotoonRendererRedrawerList.Contains(this))
{
NiloToonPerCharacterRenderController.nilotoonRendererRedrawerList.Add(this);
}
}
}
Vector3 CalculateWorldScale(Transform current)
{
if (current.parent == null)
{
// This is a root object, return its scale directly
return current.localScale;
}
else
{
// This is not a root object, calculate the total scale
Vector3 parentScale = CalculateWorldScale(current.parent);
return new Vector3(
current.localScale.x * parentScale.x,
current.localScale.y * parentScale.y,
current.localScale.z * parentScale.z);
}
}
void SyncSkinnedMeshRenderer(SkinnedMeshRenderer from, SkinnedMeshRenderer to)
{
to.sharedMesh = from.sharedMesh;
to.bones = from.bones;
to.rootBone = from.rootBone;
to.quality = from.quality;
to.updateWhenOffscreen = from.updateWhenOffscreen;
to.skinnedMotionVectors = from.skinnedMotionVectors;
to.localBounds = from.localBounds;
// Renderer properties
to.enabled = from.enabled;
to.shadowCastingMode = from.shadowCastingMode;
to.receiveShadows = from.receiveShadows;
to.motionVectorGenerationMode = from.motionVectorGenerationMode;
to.lightProbeUsage = from.lightProbeUsage;
to.reflectionProbeUsage = from.reflectionProbeUsage;
to.probeAnchor = from.probeAnchor;
to.lightProbeProxyVolumeOverride = from.lightProbeProxyVolumeOverride;
to.rendererPriority = from.rendererPriority;
to.sortingLayerID = from.sortingLayerID;
to.sortingLayerName = from.sortingLayerName;
to.sortingOrder = from.sortingOrder;
to.allowOcclusionWhenDynamic = from.allowOcclusionWhenDynamic;
to.sharedMaterials = from.sharedMaterials;
// Copy blendShape weights
for (int i = 0; i < from.sharedMesh.blendShapeCount; i++)
{
to.SetBlendShapeWeight(i, from.GetBlendShapeWeight(i));
}
}
private void OnValidate()
{
foreach (var drawData in DrawList)
{
// prevent negative index in UI
drawData.subMeshIndex = Mathf.Max(0, drawData.subMeshIndex);
}
LateUpdate();
}
private void OnEnable()
{
FindParentNiloToonPerCharacterRenderController();
}
private void LateUpdate()
{
if (!Application.isPlaying)
{
// keep update to parent NiloToonPerCharacterRenderController in edit mode
FindParentNiloToonPerCharacterRenderController();
}
// Cache Renderer
Renderer renderer = Renderer;
if (!renderer) return;
if (deactivateWhenRendererIsNotVisible && !renderer.isVisible) return;
if (deactivateWithRenderer && !renderer.enabled) return;
Mesh meshToDraw = GetMeshFromRenderer(renderer);
Matrix4x4 ObjToWorldMatrix = renderer.transform.localToWorldMatrix;
if (renderer is SkinnedMeshRenderer)
{
// in case the SkinnedMeshRenderer transform for any parent transform is not using (1,1,1) scale, cancel out the scale for Graphics.RenderMesh()
Vector3 worldScale = CalculateWorldScale(renderer.transform);
worldScale.x = 1f / worldScale.x;
worldScale.y = 1f / worldScale.y;
worldScale.z = 1f / worldScale.z;
ObjToWorldMatrix = ObjToWorldMatrix * Matrix4x4.Scale(worldScale);
}
renderer.GetPropertyBlock(MPB);
foreach (var drawData in DrawList)
{
// safe check
if (!drawData.enabled) continue;
if (!drawData.ActualMaterialUsedForRendering) continue;
// https://docs.unity3d.com/ScriptReference/RenderParams.html
RenderParams renderParams = new RenderParams(drawData.ActualMaterialUsedForRendering)
{
layer = renderer.gameObject.layer,
renderingLayerMask = renderer.renderingLayerMask,
rendererPriority = renderer.rendererPriority,
worldBounds = renderer.bounds,
camera = null,
motionVectorMode = renderer.motionVectorGenerationMode,
reflectionProbeUsage = renderer.reflectionProbeUsage,
material = drawData.ActualMaterialUsedForRendering,
matProps = MPB,
shadowCastingMode = renderer.shadowCastingMode,
receiveShadows = renderer.receiveShadows,
lightProbeUsage = renderer.lightProbeUsage,
//lightProbeProxyVolume = renderer.lightProbeProxyVolumeOverride.GetComponent<LightProbeProxyVolume>(), // TODO: not sure how to sync
};
int safeSubMeshIndex = drawData.subMeshIndex;
if (drawData.subMeshIndex >= meshToDraw.subMeshCount)
{
Debug.LogError($"SubMeshIndex out of bounds. Current index: {drawData.subMeshIndex}, Maximum index (subMeshCount - 1): {meshToDraw.subMeshCount - 1}. Skipping draw. Click to highlight the GameObject for fixing.", this);
continue;
}
if (drawData.subMeshIndex < 0)
{
Debug.LogError($"SubMeshIndex out of bounds. Current index: {drawData.subMeshIndex}, Minimum index : {0}. Skipping draw. Click to highlight the GameObject for fixing.", this);
continue;
}
// use Graphics.RenderMesh() instead of RendererFeature's cmd.DrawRenderer(),
// since Graphics.RenderMesh() can ensure lighting, shadow, multi shader passes, and RenderQueue are all working correct in both editor and build.
// To Unity/URP, Graphics.RenderMesh() is a high-level method, it is very similar to adding a regular Renderer GameObject(but without GameObject).
// while cmd.DrawRenderer() is more low-level, it is not a good method to solve this high-level redraw material problem.
Graphics.RenderMesh(renderParams, meshToDraw, safeSubMeshIndex, ObjToWorldMatrix);
}
}
private void OnDisable()
{
// remove any link with NiloToonPerCharacterRenderController
if (NiloToonPerCharacterRenderController)
{
NiloToonPerCharacterRenderController.nilotoonRendererRedrawerList?.Remove(this);
}
}
private void OnDestroy()
{
if (_bakedMeshHolder)
{
if (Application.isPlaying)
{
// We're in play mode, so use Destroy().
Destroy(_bakedMeshHolder);
}
else
{
// We're in edit mode, so use DestroyImmediate().
DestroyImmediate(_bakedMeshHolder);
}
}
}
}
#if UNITY_EDITOR
[CustomEditor(typeof(NiloToonRendererRedrawer))]
[CanEditMultipleObjects]
public class NiloToonRendererRedrawerUI : Editor
{
public override void OnInspectorGUI()
{
EditorGUILayout.HelpBox("Render extra materials using this GameObject's Renderer.\n" +
"- Same as manually adding materials to Renderer's Materials list, but here you can decide which SubMesh(material slot) is used for rendering each material\n" +
"- Automatically be affected by the closest parent NiloToonPerCharacterRenderController\n" +
"- This script can be used independently without any other NiloToon's material, script or renderer feature", MessageType.Info);
EditorGUILayout.HelpBox("This script uses SkinnedMeshRenderer.BakeMesh() to redraw a SkinnedMeshRenderer. This process can be extremely slow, so please use this script with caution when working with SkinnedMeshRenderers.", MessageType.Info);
EditorGUILayout.HelpBox("This script doesn't work with MotionVector pass, so effects like TAA and motion blur may not work", MessageType.Info);
if (Application.isPlaying && EditorApplication.isPaused)
{
EditorGUILayout.HelpBox("This script will stop rendering if you pause the game while in editor's play mode", MessageType.Warning);
}
DrawDefaultInspector();
}
}
#endif
}