You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

426 lines
18 KiB
C#

using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Rendering;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace FluffyGroomingTool {
[ExecuteAlways, RequireComponent(typeof(Renderer))]
public class FurRenderer : MonoBehaviour {
[Tooltip(
"The distance to this camera will be used to calculate the LOD. If this is not set the Camera with the MainCamera tag will be used.")]
public Camera lodCamera;
public Material material;
private Material windMaterial, motionVectorMaterial;
public bool drawWindContribution;
public ComputeShader computeShader;
public FurContainer furContainer;
public FurRendererSettings furRendererSettings;
public bool motionVectors = true;
public HeadersExpanded headerExpanded = new HeadersExpanded();
public Renderer CurrentRenderer { get; set; }
internal readonly FluffyRenderersController renderersController = new FluffyRenderersController();
private ComputeBuffer emptyFloatBuffer;
public MeshBaker meshBaker;
private SDFColliderCommon sdfColliderCommon;
private VerletSimulation verletSimulation;
private int verticesCount, updateFurMeshKernel, updateClumpsKernel;
public bool IsCreateMeshPass { get; set; }
internal int currentFurContainerID;
[SerializeField] int instanceID;
public string materialPostfix = "Strands";
public bool isHdrp, isUrp;
//Only needed for the editor UI
public string activeLodCameraName;
public string activeLodName;
private void OnValidate() {
if (furContainer == null) {
furContainer = ScriptableObject.CreateInstance<FurContainer>();
currentFurContainerID = furContainer.id;
}
addRecreateAllListener();
#if UNITY_EDITOR
if (isActiveAndEnabled && instanceID != 0 && instanceID != GetInstanceID()) {
StartCoroutine(recreateOnObjectDuplication());
}
instanceID = GetInstanceID();
#endif
}
private IEnumerator recreateOnObjectDuplication() {
yield return new WaitForEndOfFrame();
renderersController.clearObjects();
recreateAll();
}
private void addRecreateAllListener() {
if (furContainer != null) {
furContainer.recreateAll.RemoveListener(recreateAll);
furContainer.recreateAll.AddListener(recreateAll);
}
}
void OnEnable() {
#if UNITY_EDITOR
addRecreateAllListener();
if (Application.isPlaying && !ColliderHelper.collidersAssigned(sphereColliders, capsuleColliders)) {
ErrorLogger.logNoColliders();
}
ErrorLogger.logMeshDistortionUponBuilds();
DuplicateCleaner.checkDuplicates();
#endif
RenderPipelineManager.beginFrameRendering += beginFrameRendering;
Camera.onPreRender += cameraPreRender;
initialize();
}
public CullAndSortController cullAndSortController;
private void initialize() {
#if UNITY_EDITOR
updateInEditModeTimeStamp = EditorApplication.timeSinceStartup + 3f;
#endif
if (furRendererSettings == null) {
furRendererSettings = new FurRendererSettings();
furRendererSettings.verletSimulationSettings = new VerletSimulationSettings();
}
meshBaker = new MeshBaker(gameObject, Instantiate(Resources.Load<ComputeShader>(ShaderID.MESH_BAKER_CS_NAME)));
ErrorLogger.checkGpuSkinning(meshBaker, this);
sdfColliderCommon = new SDFColliderCommon(GetComponent<Renderer>(), meshBaker, furRendererSettings.verletSimulationSettings);
loadDefaultMaterial();
if (furContainer != null) {
furContainer.recreateCardMeshes();
computeShader = Instantiate(Resources.Load<ComputeShader>(ShaderID.FUR_RENDERER_CS_NAME));
verticesCount = furContainer.getCombinedVerticesCount();
verletSimulation = new VerletSimulation(verticesCount, furRendererSettings);
checkAlphaSortingPerformaceWarning();
currentFurContainerID = furContainer.id;
furContainer.NeedsUpdate = true;
updateFurMeshKernel = computeShader.FindKernel(ShaderID.UPDATE_FUR_MESH_KERNEL);
updateClumpsKernel = computeShader.FindKernel(ShaderID.UPDATE_CLUMPS_POSITION_KERNEL);
furContainer.recreateHairStrandsBuffers();
furContainer.update();
renderersController.createRendererObject(isHdrp, isUrp, motionVectorMaterial, verticesCount, furContainer.TriangleIndexArray);
var furMeshBufferStride = renderersController.getVertexBufferStride();
computeShader.SetInt(ShaderID.FUR_MESH_BUFFER_STRIDE, furMeshBufferStride);
computeShader.SetBuffer(updateFurMeshKernel, ShaderID.FUR_MESH_BUFFER, renderersController.hairMeshBuffer);
verletSimulation.setFurMeshBuffer(renderersController.hairMeshBuffer, furMeshBufferStride);
createCullAndSortController();
/*
* Without this the shader complains about the ComputeBuffers not being set even though they aren't used.
* So instead of adding an extra pragma keyword for each clump layer, we set an empty buffer instead.
*/
emptyFloatBuffer = new ComputeBuffer(1, sizeof(float) * 4);
computeShader.SetBuffer(updateFurMeshKernel, ShaderID.CLUMP_POINTS_POSITION, emptyFloatBuffer);
ColliderHelper.setupCollidersBuffer(ref colliderBuffer, ref collidersStruct, sphereColliders, capsuleColliders);
}
}
private void loadDefaultMaterial() {
DefaultMaterialLoader.loadDefaultMaterial(out isHdrp, out isUrp, ref material, out windMaterial, ref motionVectorMaterial,
materialPostfix, CurrentRenderer ? CurrentRenderer : GetComponent<Renderer>());
}
private const int PerformanceWarningPolyCount = 128000;
private void checkAlphaSortingPerformaceWarning() {
if (furRendererSettings.isAlphaSortingEnabled && verticesCount > PerformanceWarningPolyCount) {
Debug.LogWarning("This FurRenderer has a high polygon count and ALPHA SORTING enabled. This is a potential performance bottleneck.");
}
}
private void createCullAndSortController() {
if (renderersController.hairMesh.triangles.Length > 0) {
var indexBuffer = renderersController.hairMesh.GetIndexBuffer();
cullAndSortController = new CullAndSortController(
renderersController.getVertexBufferStride(),
indexBuffer,
renderersController.hairMeshBuffer,
getTrianglesCount(),
furRendererSettings
);
}
}
public void recreateMaterial() {
material = null;
loadDefaultMaterial();
}
void OnDisable() {
furContainer.recreateAll.RemoveListener(recreateAll);
RenderPipelineManager.beginFrameRendering -= beginFrameRendering;
Camera.onPreRender -= cameraPreRender;
clearResources();
}
private void clearResources() {
meshBaker?.dispose();
sdfColliderCommon?.dispose();
verletSimulation?.dispose();
cullAndSortController?.dispose();
emptyFloatBuffer?.Dispose();
colliderBuffer?.Dispose();
renderersController.destroy();
if (furContainer != null) furContainer.disposeBuffers();
colliderBuffer = null;
}
private void beginFrameRendering(ScriptableRenderContext rc, Camera[] cameras) {
if (Application.isPlaying) {
updateAndRenderFur();
}
forceUpdateOnStart();
}
private void cameraPreRender(Camera cam) {
if (lodCamera.getCamera() == cam && Application.isPlaying) {
updateAndRenderFur();
}
forceUpdateOnStart();
}
//In edit mode we update the fur using lateupdate to avoid flickering
private void LateUpdate() {
#if UNITY_EDITOR
if (!Application.isPlaying) {
updateAndRenderFur();
}
#endif
if (isReadyToRender() && !cullAndSortController.IsCulled) {
if (furRendererSettings.isAlphaSortingEnabled) {
var rp = new RenderParams(material) {
worldBounds = new Bounds(Vector3.zero, Vector3.one * 1000000),
shadowCastingMode = ShadowCastingMode.TwoSided,
receiveShadows = true,
motionVectorMode = MotionVectorGenerationMode.Camera,
layer = gameObject.layer
};
Graphics.RenderMeshIndirect(rp, renderersController.hairMesh, cullAndSortController.RenderMeshArguments);
renderersController.disableRenderer();
} else {
renderersController.enableRenderer();
}
} else if (cullAndSortController?.IsCulled == true) {
renderersController.disableRenderer();
}
}
internal void updateAndRenderFur() {
checkForNewFurContainer();
var mainCamera = lodCamera.getCamera();
if (mainCamera == null) {
ErrorLogger.logNoCamera();
} else if (isReadyToRender() && isNotPaused() && !cullAndSortController.IsCulled) {
meshBaker.bakeSkinnedMesh(IsCreateMeshPass);
if (furRendererSettings.verletSimulationSettings.isVerletColliderEnabled() && atLeastOneLayerHasMotionEnabled()) {
sdfColliderCommon.createSDF(transform, verletSimulation.compute, verletSimulation.verletKernel);
}
furContainer.update();
passCommonValuesToCs();
furContainer.dispatchClumpsKernel(computeShader, updateClumpsKernel, this);
dispatchStrandsKernel();
renderersController.setupRenderers(IsCreateMeshPass, getMaterial(), transform.position, CurrentRenderer, isUrp, motionVectors);
collideWithOtherSDFColliders();
}
activeLodCameraName = mainCamera ? mainCamera.name : "";
activeLodName = cullAndSortController?.getCurrentLodName();
cullAndSortController?.update(mainCamera, furContainer, meshBaker.getObjectPosition());
forceUpdateOnStart();
}
private bool atLeastOneLayerHasMotionEnabled() {
if (!furRendererSettings.perLayerMotionSettings) {
return furRendererSettings.verletSimulationSettings.enableMovement;
}
foreach (var layer in furContainer.layerStrandsList) {
if (layer.verletSimulationSettings.enableMovement) {
return true;
}
}
return false;
}
private void collideWithOtherSDFColliders() {
if (furRendererSettings.verletSimulationSettings.isSDFCollisionEnabled() && sdfColliders != null) {
foreach (var sdfCollider in sdfColliders) {
if (sdfCollider != null) {
sdfCollider.collideWith(verletSimulation.verletNodesBuffer, renderersController.getRendererBounds(),
renderersController.hairMeshBuffer,
renderersController.getVertexBufferStride());
}
}
}
}
private static bool isNotPaused() {
return Time.deltaTime > 0f;
}
private void passCommonValuesToCs() {
if (computeShader == null) furContainer.recreateAll.Invoke();
var thisTransform = transform;
var localToWorldMatrix = thisTransform.localToWorldMatrix;
var objectRotationMatrix = Matrix4x4.Rotate(thisTransform.rotation);
if (IsCreateMeshPass) {
localToWorldMatrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one);
objectRotationMatrix = Matrix4x4.Rotate(Quaternion.identity);
}
computeShader.SetBuffer(updateFurMeshKernel, ShaderID.SOURCE_MESH, meshBaker.bakedMesh);
computeShader.SetMatrix(ShaderID.LOCAL_TO_WORLD_MATRIX, localToWorldMatrix);
computeShader.SetMatrix(ShaderID.OBJECT_ROTATION_MATRIX, objectRotationMatrix);
computeShader.SetFloat(ShaderID.DELTA_TIME, Time.deltaTime);
var lossyScale = transform.lossyScale;
computeShader.SetFloat(ShaderID.OBJECT_GLOBAL_SCALE, lossyScale.getValueFurthestFromOne());
computeShader.SetFloat(ShaderID.EXTRA_SCALE, furContainer.furLods[cullAndSortController.getCurrentLodIndex()].strandsScale);
verletSimulation.setupWind(this);
ColliderHelper.setupColliderProperties(ref colliderBuffer, ref collidersStruct, sphereColliders, capsuleColliders,
verletSimulation.compute, verletSimulation.getKernel());
}
private float getNormalPercent(HairStrandLayer layer) {
if (furRendererSettings.perLayerNormals) return layer.sourceMeshNormalToStrandNormalPercent;
return furRendererSettings.sourceMeshNormalToStrandNormalPercent;
}
private void dispatchStrandsKernel() {
int layerVertexStartIndex = 0;
foreach (var layer in furContainer.layerStrandsList) {
computeShader.DisableKeyword(ShaderID.HAS_CLUMPS);
if (layer.hasClumps() && layer.getLastClumpModifierBuffer() != null) {
computeShader.SetBuffer(updateFurMeshKernel, ShaderID.CLUMP_POINTS_POSITION, layer.getLastClumpModifierBuffer());
computeShader.SetBuffer(updateFurMeshKernel, ShaderID.CLUMP_ATTRACTION_CURVE, layer.getLastClumpAttractionBuffer());
computeShader.SetInt(ShaderID.CLUMP_Y_COORDINATES, layer.cardMeshProperties.getCardMeshVerticesY());
computeShader.EnableKeyword(ShaderID.HAS_CLUMPS);
}
layer.passCardMeshPropertiesToComputeShader(updateFurMeshKernel, computeShader);
var layerVerticesCount = layer.hairStrandsBuffer.count * layer.CardMesh.vertexCount;
computeShader.SetInt(ShaderID.LAYER_VERTEX_START_INDEX, layerVertexStartIndex);
computeShader.SetInt(ShaderID.LAYER_VERTICES_COUNT, layerVerticesCount);
var normalPercent = getNormalPercent(layer);
computeShader.SetFloat(ShaderID.SOURCE_MESH_NORMAL_TO_STRAND_NORMAL_PERCENT, normalPercent);
var currentVerletSettings = getCurrentVerletSettings(layer);
verletSimulation.setupVerlet(computeShader, updateFurMeshKernel, currentVerletSettings);
computeShader.Dispatch(updateFurMeshKernel, layerVerticesCount.toCsGroups(), 1, 1);
verletSimulation.update(layer.hairStrandsBuffer.count, layer.cardMeshProperties.getCardMeshVerticesY(), layerVertexStartIndex / 2,
lodCamera.getCamera().transform.position, currentVerletSettings, normalPercent);
layerVertexStartIndex += layerVerticesCount;
}
}
private VerletSimulationSettings getCurrentVerletSettings(HairStrandLayer layer) {
if (furRendererSettings.perLayerMotionSettings) return layer.verletSimulationSettings;
return furRendererSettings.verletSimulationSettings;
}
private void checkForNewFurContainer() {
if (currentFurContainerID != furContainer.id) {
var existingFurCreator = GetComponent<FurCreator>();
if (existingFurCreator != null) {
DestroyImmediate(existingFurCreator);
}
clearResources();
initialize();
}
}
private double updateInEditModeTimeStamp;
public UnityAction UpdatedInEditModeAction { get; set; }
void forceUpdateOnStart() {
#if UNITY_EDITOR
if (!Application.isPlaying && EditorApplication.timeSinceStartup < updateInEditModeTimeStamp) {
EditorApplication.QueuePlayerLoopUpdate();
UpdatedInEditModeAction?.Invoke();
}
#endif
}
public bool isReadyToRender() {
if (CurrentRenderer == null) {
CurrentRenderer = GetComponent<Renderer>();
//renderer.isVisible will always return false on the first frame so we ignore it here.
return areResourcesReadyToRender();
}
return (CurrentRenderer.isVisible || IsCreateMeshPass) && areResourcesReadyToRender();
}
private bool areResourcesReadyToRender() {
return furContainer != null && furContainer.layerStrandsList.Length > 0 && furRendererSettings != null &&
renderersController.isReady();
}
private Material getMaterial() {
return drawWindContribution ? windMaterial : material;
}
private void recreateAll() {
clearResources();
initialize();
}
public int getTrianglesCount() {
if (furContainer.TriangleIndexArray == null) return 0;
return (int)(furContainer.TriangleIndexArray.Length / 3f);
}
public int getVerticesCount() {
return verticesCount;
}
public SphereCollider[] sphereColliders;
public CapsuleCollider[] capsuleColliders;
public SDFCollider[] sdfColliders;
private ComputeBuffer colliderBuffer;
private ColliderStruct[] collidersStruct;
public void updateLod() {
cullAndSortController?.update(lodCamera.getCamera(), furContainer, meshBaker.getObjectPosition());
}
public void recreateSdfCollider() {
sdfColliderCommon?.dispose();
sdfColliderCommon = new SDFColliderCommon(GetComponent<Renderer>(), meshBaker, furRendererSettings.verletSimulationSettings);
}
}
}