// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= HairStrandsInterface.h: Hair manager implementation. =============================================================================*/ #include "HairStrandsInterface.h" #include "HairStrandsRendering.h" #include "HairStrandsData.h" #include "DataDrivenShaderPlatformInfo.h" #include "LightSceneProxy.h" #include "Rendering/SkeletalMeshRenderData.h" #include "Rendering/SkinWeightVertexBuffer.h" #include "CommonRenderResources.h" #include "Components/SkeletalMeshComponent.h" #include "SkeletalRenderPublic.h" #include "SceneRendering.h" #include "SystemTextures.h" #include "ShaderPrint.h" #include "ScenePrivate.h" DEFINE_LOG_CATEGORY_STATIC(LogHairRendering, Log, All); static TAutoConsoleVariable CVarHairStrandsRaytracingEnable( TEXT("r.HairStrands.Raytracing"), 1, TEXT("Enable/Disable hair strands raytracing geometry. This is anopt-in option per groom asset/groom instance."), ECVF_RenderThreadSafe | ECVF_Scalability); static TAutoConsoleVariable CVarHairStrandsEnable( TEXT("r.HairStrands.Strands"), 1, TEXT("Enable/Disable hair strands rendering"), ECVF_RenderThreadSafe | ECVF_Scalability); static TAutoConsoleVariable CVarHairCardsEnable( TEXT("r.HairStrands.Cards"), 1, TEXT("Enable/Disable hair cards rendering. This variable needs to be turned on when the engine starts."), ECVF_RenderThreadSafe | ECVF_Scalability); static TAutoConsoleVariable CVarHairMeshesEnable( TEXT("r.HairStrands.Meshes"), 1, TEXT("Enable/Disable hair meshes rendering. This variable needs to be turned on when the engine starts."), ECVF_RenderThreadSafe | ECVF_Scalability); static TAutoConsoleVariable CVarHairStrandsBinding( TEXT("r.HairStrands.Binding"), 1, TEXT("Enable/Disable hair binding, i.e., hair attached to skeletal meshes."), ECVF_RenderThreadSafe | ECVF_Scalability); static TAutoConsoleVariable CVarHairStrandsSimulation( TEXT("r.HairStrands.Simulation"), 1, TEXT("Enable/disable hair simulation"), ECVF_RenderThreadSafe | ECVF_Scalability); static TAutoConsoleVariable CVarHairStrandsNonVisibleShadowCasting( TEXT("r.HairStrands.Shadow.CastShadowWhenNonVisible"), 1, TEXT("Enable shadow casting for hair strands even when culled out from the primary view"), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarHairStrandsNonVisibleShadowCasting_CullDistance( TEXT("r.HairStrands.Visibility.NonVisibleShadowCasting.CullDistance"), 2000, TEXT("Cull distance at which shadow casting starts to be disabled for non-visible hair strands instances."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarHairStrandsNonVisibleShadowCasting_Debug( TEXT("r.HairStrands.Visibility.NonVisibleShadowCasting.Debug"), 0, TEXT("Enable debug rendering for non-visible hair strands instance, casting shadow."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarHairStrandsContinuousDecimationReordering( TEXT("r.HairStrands.ContinuousDecimationReordering"), 0, TEXT("Enable strand reordering to allow Continuous LOD. Experimental"), ECVF_RenderThreadSafe | ECVF_ReadOnly); static TAutoConsoleVariable CVarHairStrandsVisibilityComputeRaster( TEXT("r.HairStrands.Visibility.ComputeRaster"), 0, TEXT("Hair Visiblity uses raster compute. Experimental"), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarHairStrandsVisibilityComputeRaster_ContinuousLOD( TEXT("r.HairStrands.Visibility.ComputeRaster.ContinuousLOD"), 1, TEXT("Enable Continuos LOD when using compute rasterization. Experimental"), ECVF_RenderThreadSafe); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Import/export utils function for hair resources void FRDGExternalBuffer::Release() { Buffer = nullptr; SRV = nullptr; UAV = nullptr; } FRDGImportedBuffer Register(FRDGBuilder& GraphBuilder, const FRDGExternalBuffer& In, ERDGImportedBufferFlags Flags, ERDGUnorderedAccessViewFlags UAVFlags) { FRDGImportedBuffer Out; if (!In.Buffer) { return Out; } const uint32 uFlags = uint32(Flags); Out.Buffer = GraphBuilder.RegisterExternalBuffer(In.Buffer); if (In.Format != PF_Unknown) { if (uFlags & uint32(ERDGImportedBufferFlags::CreateSRV)) { Out.SRV = GraphBuilder.CreateSRV(Out.Buffer, In.Format); } if (uFlags & uint32(ERDGImportedBufferFlags::CreateUAV)) { Out.UAV = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(Out.Buffer, In.Format), UAVFlags); } } else { if (uFlags & uint32(ERDGImportedBufferFlags::CreateSRV)) { Out.SRV = GraphBuilder.CreateSRV(Out.Buffer); } if (uFlags & uint32(ERDGImportedBufferFlags::CreateUAV)) { Out.UAV = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(Out.Buffer), UAVFlags); } } return Out; } FRDGBufferSRVRef RegisterAsSRV(FRDGBuilder& GraphBuilder, const FRDGExternalBuffer& In) { if (!In.Buffer) { return nullptr; } FRDGBufferSRVRef Out = nullptr; FRDGBufferRef Buffer = GraphBuilder.RegisterExternalBuffer(In.Buffer); if (In.Format != PF_Unknown) { Out = GraphBuilder.CreateSRV(Buffer, In.Format); } else { Out = GraphBuilder.CreateSRV(Buffer); } return Out; } FRDGBufferUAVRef RegisterAsUAV(FRDGBuilder& GraphBuilder, const FRDGExternalBuffer& In, ERDGUnorderedAccessViewFlags Flags) { if (!In.Buffer) { return nullptr; } FRDGBufferUAVRef Out = nullptr; FRDGBufferRef Buffer = GraphBuilder.RegisterExternalBuffer(In.Buffer); if (In.Format != PF_Unknown) { Out = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(Buffer, In.Format), Flags); } else { Out = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(Buffer), Flags); } return Out; } bool IsHairRayTracingEnabled() { if (GIsRHIInitialized && !IsRunningCookCommandlet()) { return IsRayTracingEnabled() && CVarHairStrandsRaytracingEnable.GetValueOnAnyThread(); } else { return false; } } bool IsHairStrandsSupported(EHairStrandsShaderType Type, EShaderPlatform Platform) { if (!IsGroomEnabled()) return false; const bool Cards_Meshes_All = true; switch (Type) { case EHairStrandsShaderType::Strands: return IsHairStrandsGeometrySupported(Platform); case EHairStrandsShaderType::Cards: return Cards_Meshes_All; case EHairStrandsShaderType::Meshes: return Cards_Meshes_All; case EHairStrandsShaderType::Tool: return (IsD3DPlatform(Platform) || IsVulkanPlatform(Platform)) && IsPCPlatform(Platform) && IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM5); case EHairStrandsShaderType::All: return Cards_Meshes_All; } return false; } bool IsHairStrandsEnabled(EHairStrandsShaderType Type, EShaderPlatform Platform) { const bool HairStrandsGlobalEnable = IsGroomEnabled(); if (!HairStrandsGlobalEnable) return false; const int32 HairStrandsEnable = CVarHairStrandsEnable.GetValueOnAnyThread(); const int32 HairCardsEnable = CVarHairCardsEnable.GetValueOnAnyThread(); const int32 HairMeshesEnable = CVarHairMeshesEnable.GetValueOnAnyThread(); switch (Type) { case EHairStrandsShaderType::Strands: return HairStrandsEnable > 0 && (Platform != EShaderPlatform::SP_NumPlatforms ? IsHairStrandsGeometrySupported(Platform) : true); case EHairStrandsShaderType::Cards: return HairCardsEnable > 0; case EHairStrandsShaderType::Meshes: return HairMeshesEnable > 0; #if PLATFORM_DESKTOP && PLATFORM_WINDOWS case EHairStrandsShaderType::Tool: return (HairCardsEnable > 0 || HairMeshesEnable > 0 || HairStrandsEnable > 0); #else case EHairStrandsShaderType::Tool: return false; #endif case EHairStrandsShaderType::All : return HairStrandsGlobalEnable && (HairCardsEnable > 0 || HairMeshesEnable > 0 || HairStrandsEnable > 0); } return false; } bool IsHairStrandsBindingEnable() { return CVarHairStrandsBinding.GetValueOnAnyThread() > 0; } bool IsHairStrandsSimulationEnable() { return CVarHairStrandsSimulation.GetValueOnAnyThread() > 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FHairTransientResources FVector3f FHairTransientResources::GetTranslatedWorldOffsetCorrection(const FViewInfo& View) const { return FVector3f(View.ViewMatrices.GetPreViewTranslation() - TranslatedWorldOffsetView0); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FHairInstanceCullingResults void FHairInstanceCullingResults::InitVisibility(uint32 InstanceCount) { InstancesVisibilityType.Init(0u, InstanceCount); } void FHairInstanceCullingResults::Add(const FViewInfo& View, const FHairStrandsInstance& Instance, EHairInstanceVisibilityType Flag) { if (InstancesVisibilityType.IsValidIndex(Instance.RegisteredIndex)) { const uint32 ViewIndex = FMath::Min(View.SceneRendererPrimaryViewId, 7); const uint32 ViewMask = 0xF << (ViewIndex * 4u); const uint32 BitsToAdd = uint32(Flag) << (ViewIndex * 4u); InstancesVisibilityType[Instance.RegisteredIndex] &= ~ViewMask; InstancesVisibilityType[Instance.RegisteredIndex] |= BitsToAdd; } } EHairInstanceVisibilityType FHairInstanceCullingResults::Is(const FViewInfo& View, const FHairStrandsInstance& Instance) const { EHairInstanceVisibilityType Out = EHairInstanceVisibilityType::NotVisible; if (InstancesVisibilityType.IsValidIndex(Instance.RegisteredIndex)) { const uint32 ViewIndex = FMath::Min(View.SceneRendererPrimaryViewId, 7); const uint32 ViewBits = (InstancesVisibilityType[Instance.RegisteredIndex] >> (4u * ViewIndex)) & 0xF; // const uint32 BitIndex = 31u - FMath::CountLeadingZeros(ViewBits); Out = EHairInstanceVisibilityType(ViewBits); } return Out; } EHairInstanceVisibilityType FHairInstanceCullingResults::Is(const FSceneView& View, const FHairStrandsInstance& Instance) const { return Is(*(FViewInfo*)(&View), Instance); } bool FHairInstanceCullingResults::Is(const FViewInfo& View, const FHairStrandsInstance& Instance, EHairInstanceVisibilityType Flag) const { return Is(View, Instance) == Flag; } bool FHairInstanceCullingResults::IsVisibleInAnyViews(const FHairStrandsInstance& Instance) const { return InstancesVisibilityType.IsValidIndex(Instance.RegisteredIndex) && InstancesVisibilityType[Instance.RegisteredIndex] != 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FHairGroupPublicData::FHairGroupPublicData(uint32 InGroupIndex, const FName& InOwnerName) { GroupIndex = InGroupIndex; } /////////////////////////////////////////////////////////////////////////////////////////////////// bool IsHairStrandsNonVisibleShadowCastingEnable() { return CVarHairStrandsNonVisibleShadowCasting.GetValueOnAnyThread() > 0; } bool IsHairStrandsVisibleInShadows(const FViewInfo& View, const FHairStrandsInstance& Instance) { const bool bDebugEnable = CVarHairStrandsNonVisibleShadowCasting_Debug.GetValueOnRenderThread() > 0; FHairStrandsDebugData::FCullData* CullingData = nullptr; if (bDebugEnable) { CullingData = const_cast(&View.HairStrandsViewData.DebugData.CullData); CullingData->bIsValid = true; CullingData->ViewFrustum = View.ViewFrustum; } bool bIsVisibleInShadow = false; if (const FHairGroupPublicData* HairData = Instance.GetHairData()) { // Run simulation if the instance is either Strands geometry, have simulation, or has global interpolation enabled. // This ensures that the groom is correctly updated if visible in shadows const bool bNeedUpdate = HairData->LODIndex >= 0 && (Instance.GetHairGeometry() == EHairGeometryType::Strands || HairData->IsSimulationEnable(HairData->LODIndex) || HairData->IsGlobalInterpolationEnable(HairData->LODIndex)); if (!bNeedUpdate) { return false; } const FBoxSphereBounds& Bounds = Instance.GetBounds(); { // Local lights for (const FLightSceneInfo* LightInfo : View.HairStrandsViewData.VisibleShadowCastingLights) { if (LightInfo->Proxy->AffectsBounds(Bounds)) { bIsVisibleInShadow = true; break; } } // Directional lights if (!bIsVisibleInShadow) { const float CullDistance = FMath::Max(0.f, CVarHairStrandsNonVisibleShadowCasting_CullDistance.GetValueOnAnyThread()); for (const FHairStrandsViewData::FDirectionalLightCullData& CullData : View.HairStrandsViewData.VisibleShadowCastingDirectionalLights) { // Transform frustum into light space const FMatrix& WorldToLight = CullData.LightInfo->Proxy->GetWorldToLight(); // Transform groom bound into light space, and extend it along the light direction FBoxSphereBounds BoundsInLightSpace = Bounds.TransformBy(WorldToLight); BoundsInLightSpace.BoxExtent.X += CullDistance; const bool bIntersect = CullData.ViewFrustumInLightSpace.IntersectBox(BoundsInLightSpace.Origin, BoundsInLightSpace.BoxExtent); if (bDebugEnable) { FHairStrandsDebugData::FCullData::FLight& LightData = CullingData->DirectionalLights.AddDefaulted_GetRef(); LightData.WorldToLight = WorldToLight; LightData.LightToWorld = CullData.LightInfo->Proxy->GetLightToWorld(); LightData.Center = FVector3f(CullData.LightInfo->Proxy->GetBoundingSphere().Center); LightData.Extent = FVector3f(CullData.LightInfo->Proxy->GetBoundingSphere().W); LightData.ViewFrustumInLightSpace = CullData.ViewFrustumInLightSpace; LightData.InstanceBoundInLightSpace.Add({FVector3f(BoundsInLightSpace.GetBox().Min), FVector3f(BoundsInLightSpace.GetBox().Max)}); LightData.InstanceBoundInWorldSpace.Add({ FVector3f(Bounds.GetBox().Min), FVector3f(Bounds.GetBox().Max)}); LightData.InstanceIntersection.Add(bIntersect ? 1u : 0u); } // Ensure the extended groom bound interest the frustum if (bIntersect) { bIsVisibleInShadow = true; if (!bDebugEnable) { break; } } } } } } return bIsVisibleInShadow; } bool IsHairStrandContinuousDecimationReorderingEnabled() { //NB this is a readonly cvar - and causes changes in platform data and runtime allocations return CVarHairStrandsContinuousDecimationReordering.GetValueOnAnyThread() > 0; } bool IsHairVisibilityComputeRasterEnabled() { return CVarHairStrandsVisibilityComputeRaster.GetValueOnAnyThread() == 1; } bool IsHairVisibilityComputeRasterForwardEnabled(EShaderPlatform InPlatform) { return IsFeatureLevelSupported(InPlatform, ERHIFeatureLevel::SM6) && CVarHairStrandsVisibilityComputeRaster.GetValueOnAnyThread() == 2; } bool IsHairVisibilityComputeRasterContinuousLODEnabled() { return IsHairStrandContinuousDecimationReorderingEnabled() && IsHairVisibilityComputeRasterEnabled() && (CVarHairStrandsVisibilityComputeRaster_ContinuousLOD.GetValueOnAnyThread() > 0); } uint32 FHairGroupPublicData::GetActiveStrandsPointCount(bool bPrevious) const { return bPrevious ? ContinuousLODPreviousPointCount : ContinuousLODPointCount; } uint32 FHairGroupPublicData::GetActiveStrandsCurveCount(bool bPrevious) const { return bPrevious ? ContinuousLODPreviousCurveCount : ContinuousLODCurveCount; } float FHairGroupPublicData::GetActiveStrandsCoverageScale() const { return ContinuousLODCoverageScale; } float FHairGroupPublicData::GetActiveStrandsRadiusScale() const { return ContinuousLODRadiusScale; } /////////////////////////////////////////////////////////////////////////////////////////////////// // Bookmark API THairStrandsBookmarkFunction GHairStrandsBookmarkFunction = nullptr; void RegisterBookmarkFunction(THairStrandsBookmarkFunction Bookmark) { if (Bookmark) { GHairStrandsBookmarkFunction = Bookmark; } } void RunHairStrandsBookmark(FRDGBuilder& GraphBuilder, EHairStrandsBookmark Bookmark, FHairStrandsBookmarkParameters& Parameters) { if (GHairStrandsBookmarkFunction) { GHairStrandsBookmarkFunction(&GraphBuilder, Bookmark, Parameters); } } void RunHairStrandsBookmark(EHairStrandsBookmark Bookmark, FHairStrandsBookmarkParameters& Parameters) { if (GHairStrandsBookmarkFunction) { GHairStrandsBookmarkFunction(nullptr, Bookmark, Parameters); } } void CullHairInstance(FScene* Scene, FViewInfo& View, FHairInstanceCullingResults& Out) { // Only compute visible instances when required as this is expensive. { const int32 ActiveInstanceCount = Scene->HairStrandsSceneData.RegisteredProxies.Num(); Out.InitVisibility(ActiveInstanceCount); // 1. Strands - Add all visible strands instances Out.VisibleInAnyViews_Strands.Reserve(View.HairStrandsMeshElements.Num()); for (const FMeshBatchAndRelevance& MeshBatch : View.HairStrandsMeshElements) { check(MeshBatch.PrimitiveSceneProxy && MeshBatch.PrimitiveSceneProxy->ShouldRenderInMainPass()); if (MeshBatch.Mesh && MeshBatch.Mesh->Elements.Num() > 0) { FHairGroupPublicData* HairData = HairStrands::GetHairData(MeshBatch.Mesh); if (HairData && HairData->Instance) { if (!Out.IsVisibleInAnyViews(*HairData->Instance)) { Out.VisibleInAnyViews_Strands.Add(HairData->Instance); } Out.Add(View, *HairData->Instance, EHairInstanceVisibilityType::StrandsPrimaryView); } } } // 2. Cards/Meshes - Add all visible cards instances Out.VisibleInAnyViews_CardsOrMeshes_Primary.Reserve(View.HairCardsMeshElements.Num()); for (const FMeshBatchAndRelevance& MeshBatch : View.HairCardsMeshElements) { check(MeshBatch.PrimitiveSceneProxy && MeshBatch.PrimitiveSceneProxy->ShouldRenderInMainPass()); if (MeshBatch.Mesh && MeshBatch.Mesh->Elements.Num() > 0) { FHairGroupPublicData* HairData = HairStrands::GetHairData(MeshBatch.Mesh); if (HairData && HairData->Instance) { if (!Out.IsVisibleInAnyViews(*HairData->Instance)) { Out.VisibleInAnyViews_CardsOrMeshes_Primary.Add(HairData->Instance); } Out.Add(View, *HairData->Instance, EHairInstanceVisibilityType::CardsOrMeshesPrimaryView); } } } } } void CullHairInstanceShadow(FScene* Scene, TArray& Views, FHairInstanceCullingResults& Out) { const int32 ActiveInstanceCount = Scene->HairStrandsSceneData.RegisteredProxies.Num(); for (FHairStrandsInstance* Instance : Scene->HairStrandsSceneData.RegisteredProxies) { // 2. Strands - Add all instances non-visible primary view(s) but visible in shadow view(s) const bool bStrands = Instance->GetHairGeometry() == EHairGeometryType::Strands; const bool bCardsOrMeshes = Instance->GetHairGeometry() == EHairGeometryType::Cards || Instance->GetHairGeometry() == EHairGeometryType::Meshes; const bool bCompatible = bStrands || bCardsOrMeshes; if (bCompatible) { for (const FViewInfo& View : Views) { if (Out.Is(View, *Instance) == EHairInstanceVisibilityType::NotVisible && IsHairStrandsVisibleInShadows(View, *Instance)) { if (bStrands) { if (!Out.IsVisibleInAnyViews(*Instance)) { Out.VisibleInAnyViews_Strands.Add(Instance); } Out.Add(View, *Instance, EHairInstanceVisibilityType::StrandsShadowView); } else if (bCardsOrMeshes) { if (!Out.IsVisibleInAnyViews(*Instance)) { Out.VisibleInAnyViews_CardsOrMeshes_Shadow.Add(Instance); } Out.Add(View, *Instance, EHairInstanceVisibilityType::CardsOrMeshesShadowView); } } } } } } void CreateHairStrandsBookmarkParameters(FScene* Scene, FViewInfo& View, FHairStrandsBookmarkParameters& Out, bool bComputeVisibleInstances) { // Only compute visible instances when required as this is expensive. if (bComputeVisibleInstances) { CullHairInstance(Scene, View, Out.CullingResults); } Out.ShaderPrintData = ShaderPrint::IsEnabled(View.ShaderPrintData) ? &View.ShaderPrintData : nullptr; Out.ShaderMap = View.ShaderMap; Out.Instances = &Scene->HairStrandsSceneData.RegisteredProxies; Out.View = &View; Out.ViewRect = View.ViewRect; Out.ViewUniqueID = View.ViewState ? View.ViewState->UniqueID : ~0; Out.SceneColorTexture = nullptr; Out.SceneDepthTexture = nullptr; Out.Scene = Scene; Out.TransientResources = Scene->HairStrandsSceneData.TransientResources; } void UpdateHairStrandsBookmarkParameters(FScene* Scene, TArray& Views, FHairStrandsBookmarkParameters& Out) { if (Views.Num() == 0) { return; } if (IsHairStrandsNonVisibleShadowCastingEnable()) { CullHairInstanceShadow(Scene, Views, Out.CullingResults); } } void CreateHairStrandsBookmarkParameters(FScene* Scene, TArray& Views, TConstArrayView AllViews, FHairStrandsBookmarkParameters& Out, bool bComputeVisibleInstances) { CreateHairStrandsBookmarkParameters(Scene, Views[0], Out, bComputeVisibleInstances); Out.AllViews.Reserve(AllViews.Num()); for (FViewInfo* View : AllViews) { Out.AllViews.Emplace(View); } } namespace HairStrands { bool IsHairStrandsVF(const FMeshBatch* Mesh) { if (Mesh) { static const FHashedName& VFTypeRef = FVertexFactoryType::GetVFByName(TEXT("FHairStrandsVertexFactory"))->GetHashedName(); const FHashedName& VFType = Mesh->VertexFactory->GetType()->GetHashedName(); return VFType == VFTypeRef; } return false; } bool IsHairCardsVF(const FMeshBatch* Mesh) { if (Mesh) { static const FHashedName& VFTypeRef = FVertexFactoryType::GetVFByName(TEXT("FHairCardsVertexFactory"))->GetHashedName(); const FHashedName& VFType = Mesh->VertexFactory->GetType()->GetHashedName(); return VFType == VFTypeRef; } return false; } bool IsHairCompatible(const FMeshBatch* Mesh) { return IsHairStrandsVF(Mesh) || IsHairCardsVF(Mesh); } bool IsHairVisible(const FMeshBatchAndRelevance& MeshBatch, bool bCheckLengthScale) { if (MeshBatch.Mesh && MeshBatch.PrimitiveSceneProxy && MeshBatch.PrimitiveSceneProxy->ShouldRenderInMainPass()) { const FHairGroupPublicData* Data = HairStrands::GetHairData(MeshBatch.Mesh); switch (Data->VFInput.GeometryType) { case EHairGeometryType::Strands: return bCheckLengthScale ? Data->VFInput.Strands.Common.LengthScale > 0 : true; case EHairGeometryType::Cards: return true; case EHairGeometryType::Meshes: return true; } } return false; } FHairGroupPublicData* GetHairData(const FMeshBatch* Mesh) { return reinterpret_cast(Mesh->Elements[0].VertexFactoryUserData); } void AddVisibleShadowCastingLight(const FScene& Scene, TArray& Views, const FLightSceneInfo* LightSceneInfo) { const uint8 LightType = LightSceneInfo->Proxy->GetLightType(); for (FViewInfo& View : Views) { // If any hair data are registered, track which lights are visible so that hair strands can cast shadow even if not visible in primary view // // Actual intersection test is done in IsHairStrandsVisibleInShadows(): // * For local lights, shadow casting is determined based on light influence radius // * For directional light, view frustum and hair instance bounds are transformed in light space to determined potential intersections if (Scene.HairStrandsSceneData.RegisteredProxies.Num() > 0) { if (LightType == ELightComponentType::LightType_Directional) { FHairStrandsViewData::FDirectionalLightCullData& Data = View.HairStrandsViewData.VisibleShadowCastingDirectionalLights.AddDefaulted_GetRef(); // Transform view frustum into light space const FMatrix& WorldToLight = LightSceneInfo->Proxy->GetWorldToLight(); const uint32 PlaneCount = View.ViewFrustum.Planes.Num(); Data.ViewFrustumInLightSpace.Planes.SetNum(PlaneCount); for (uint32 PlaneIt = 0; PlaneIt < PlaneCount; ++PlaneIt) { Data.ViewFrustumInLightSpace.Planes[PlaneIt] = View.ViewFrustum.Planes[PlaneIt].TransformBy(WorldToLight); } Data.ViewFrustumInLightSpace.Init(); Data.LightInfo = LightSceneInfo; } else { View.HairStrandsViewData.VisibleShadowCastingLights.Add(LightSceneInfo); } } } } } // namespace HairStrands