/******************************************************************************* The content of this file includes portions of the proprietary AUDIOKINETIC Wwise Technology released in source code form as part of the game integration package. The content of this file may not be used without valid licenses to the AUDIOKINETIC Wwise Technology. Note that the use of the game engine is subject to the Unreal(R) Engine End User License Agreement at https://www.unrealengine.com/en-US/eula/unreal License Usage Licensees holding valid licenses to the AUDIOKINETIC Wwise Technology may use this file in accordance with the end user license agreement provided with the software or, alternatively, in accordance with the terms contained in a written agreement between you and Audiokinetic Inc. Copyright (c) 2023 Audiokinetic Inc. *******************************************************************************/ /*============================================================================= AkSpatialAudioVolume.cpp: =============================================================================*/ #include "AkSpatialAudioVolume.h" #include "AkSpatialAudioHelper.h" #include "AkAudioDevice.h" #include "AkLateReverbComponent.h" #include "AkRoomComponent.h" #include "AkSurfaceReflectorSetComponent.h" #include "AkAcousticPortal.h" #include "AkSettings.h" #include "WwiseUnrealDefines.h" #include "WwiseUnrealEngineHelper.h" #include "WwiseUnrealObjectHelper.h" #include "Components/BrushComponent.h" #include "Model.h" #include "Engine/BrushBuilder.h" // Geometric Tools #if WITH_EDITOR #include "AkAudioStyle.h" #include "Mathematics/Math.h" #include "Mathematics/UIntegerAP32.h" #include "Mathematics/BSRational.h" #include "Mathematics/MinimumVolumeBox3.h" #if UE_5_1_OR_LATER #include "Misc/TransactionObjectEvent.h" #endif #endif static const float kScaleEpsilon = 0.001; static const float kConvexHullEpsilon = 0.001; static const FName NAME_SAV_Fit = TEXT("AkSpatialAudioVolumeRaycast"); #if WITH_EDITOR bool IntersectPlanes(FVector n0, float d0, FVector n1, float d1, FVector n2, float d2, FVector &p) { FVector u = FVector::CrossProduct(n1, n2); float denom = FVector::DotProduct(n0, u); if (std::abs(denom) < 0.1) return false; // Planes do not intersect in a point p = (d0*u + FVector::CrossProduct(n0, (d2 * n1) - (d1 * n2))) / denom; return true; } WwiseGTE::Vector3< float > ToGTEVector(FVector& In) { WwiseGTE::Vector3< float > Out; Out[0] = In.X; Out[1] = In.Y; Out[2] = In.Z; return Out; } FVector ToFVector(WwiseGTE::Vector3< float >& In) { return FVector(In[0], In[1], In[2]); } #endif /*------------------------------------------------------------------------------------ AAkSpatialAudioVolume ------------------------------------------------------------------------------------*/ AAkSpatialAudioVolume::AAkSpatialAudioVolume(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { // Property initialization UBrushComponent* BrushComp = GetBrushComponent(); if (BrushComp) { BrushComp->SetGenerateOverlapEvents(false); BrushComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly); BrushComp->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic); BrushComp->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore); } static const FName SurfReflSetName = TEXT("SurfaceReflector"); SurfaceReflectorSet = ObjectInitializer.CreateDefaultSubobject(this, SurfReflSetName); SurfaceReflectorSet->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); static const FName LateReverbame = TEXT("LateReverb"); LateReverb = ObjectInitializer.CreateDefaultSubobject(this, LateReverbame); LateReverb->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); static const FName RoomName = TEXT("Room"); Room = ObjectInitializer.CreateDefaultSubobject(this, RoomName); Room->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); bColored = true; BrushColor = FColor(109, 187, 255, 255); #if WITH_EDITOR CollisionChannel = EAkCollisionChannel::EAKCC_UseIntegrationSettingsDefault; PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.bStartWithTickEnabled = true; #endif } #if WITH_EDITOR ECollisionChannel AAkSpatialAudioVolume::GetCollisionChannel() { return UAkSettings::ConvertFitToGeomCollisionChannel(CollisionChannel.GetValue()); } void AAkSpatialAudioVolume::FitRaycast() { UWorld* World = GEngine->GetWorldFromContextObjectChecked(this); if (!World) return; TArray hits; hits.Reserve(kNumRaycasts); const float kRayLength = 10000000.f; FCollisionQueryParams CollisionParams(NAME_SAV_Fit, true, this); CollisionParams.bReturnPhysicalMaterial = true; FVector RaycastOrigin = GetActorLocation(); float Offset = 2.f / kNumRaycasts; float Increment = PI * (3.f - sqrtf(5.f)); for (int i = 0; i < kNumRaycasts; ++i) { float y = ((i * Offset) - 1) + (Offset / 2); float r = sqrtf(1.f - powf(y, 2.f)); float phi = ((i + 1) % kNumRaycasts) * Increment; float x = cosf(phi) * r; float z = sinf(phi) * r; FVector to = RaycastOrigin + FVector(x, y, z) * kRayLength; TArray< FHitResult > OutHits; OutHits.Empty(); World->LineTraceMultiByObjectType(OutHits, RaycastOrigin, to, (int)GetCollisionChannel(), CollisionParams); for (auto& res : OutHits) { AActor* HitActor = WwiseUnrealHelper::GetActorFromHitResult(res); if (HitActor != nullptr) { UAkPortalComponent* PortalComponent = (UAkPortalComponent*)HitActor->FindComponentByClass(UAkPortalComponent::StaticClass()); if (PortalComponent != nullptr) { // We hit a portal. The portals are a good reference point for the SAV, but we need to extend the ray to the center of the portal FVector PortalNorm = PortalComponent->GetComponentTransform().GetRotation().RotateVector(FVector(0.f, 1.f, 0.f)); FVector PortalPos = PortalComponent->GetComponentLocation(); float d = FVector::DotProduct(PortalPos, PortalNorm); FVector ab = to - res.ImpactPoint; float t = (d - FVector::DotProduct(PortalNorm, res.ImpactPoint)) / FVector::DotProduct(PortalNorm, ab); if (t >= 0.f && t < 1.0f) { FVector ImpactPointOnPoralPlane = res.ImpactPoint + t * ab; FHitResult ModifiedHitResult = res; ModifiedHitResult.ImpactPoint = ImpactPointOnPoralPlane; ModifiedHitResult.ImpactNormal = PortalNorm; if (FVector::DotProduct(PortalNorm, res.ImpactNormal) < 0.f) ModifiedHitResult.ImpactNormal = -PortalNorm; hits.Emplace(ModifiedHitResult); break; } } if (!res.bStartPenetrating && HitActor->GetClass() == AAkSpatialAudioVolume::StaticClass()) { hits.Emplace(res); break; } } if (res.IsValidBlockingHit() ) { hits.Emplace(res); break; } } } auto SortPredicate = [](FHitResult& A, FHitResult& B){ return A.Distance < B.Distance; }; Algo::Sort(hits, SortPredicate); FitPoints.Empty(); FitPoints.Reserve(hits.Num()); FitMaterials.Empty(); FitMaterials.Reserve(hits.Num()); FitNormals.Empty(); FitNormals.Reserve(hits.Num()); for (int i = 0; i < hits.Num(); ++i) { FitPoints.Emplace(hits[i].ImpactPoint); FitNormals.Emplace(hits[i].ImpactNormal); FitMaterials.Emplace(hits[i].PhysMaterial); } } void AAkSpatialAudioVolume::PostRebuildBrush() { UnregisterAllComponents(true); RegisterAllComponents(); FAkAudioDevice* AkAudioDevice = FAkAudioDevice::Get(); if (AkAudioDevice != nullptr) AkAudioDevice->PortalsNeedRoomUpdate(GetWorld()); if (SurfaceReflectorSet != nullptr) { SurfaceReflectorSet->UpdatePolys(); SurfaceReflectorSet->UpdateSurfaceReflectorSet(); } ClearTextComponents(); } void AAkSpatialAudioVolume::ClearTextComponents() { for (int i = 0; i < PreviewTextureNameComponents.Num(); ++i) { UTextRenderComponent* textComp = PreviewTextureNameComponents[i]; if (textComp != nullptr) { textComp->DestroyComponent(); } } PreviewTextureNameComponents.Empty(); } void AAkSpatialAudioVolume::UpdatePreviewTextComponents(TArray positions) { ClearTextComponents(); int index = 0; UMaterialInterface* mat = Cast(FAkAudioStyle::GetAkForegroundTextMaterial()); ensure(positions.Num() == PreviewPolys.Num()); for (FAkSurfacePoly& Poly : PreviewPolys) { FString VizName = GetName() + TEXT("PolyPreviewText ") + FString::FromInt(index); if (Poly.EnableSurface) { int32 idx = PreviewTextureNameComponents.Add(NewObject(GetOuter(), *VizName)); UTextRenderComponent* textComp = PreviewTextureNameComponents[idx]; if (textComp != nullptr) { if (mat != nullptr) textComp->SetTextMaterial(mat); textComp->RegisterComponentWithWorld(GetWorld()); textComp->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform); textComp->bIsEditorOnly = true; textComp->bSelectable = false; textComp->bAlwaysRenderAsText = true; textComp->SetHorizontalAlignment(EHTA_Center); textComp->SetVerticalAlignment(EVRTA_TextCenter); bool displayOcclusion = true; if (SurfaceReflectorSet != nullptr) displayOcclusion = SurfaceReflectorSet->bEnableSurfaceReflectors; textComp->SetText(Poly.GetPolyText(displayOcclusion)); textComp->SetWorldLocation(positions[index]); } } else { PreviewTextureNameComponents.Add(nullptr); } ++index; } } void AAkSpatialAudioVolume::UpdatePreviewPolys(TArray, int>> materialVotes) { PreviewPolys.Empty(); PreviewPolys.AddDefaulted(materialVotes.Num()); for (int i = 0; i < materialVotes.Num(); ++i) { PreviewPolys[i].EnableSurface = false; TMap, int>& votes = materialVotes[i]; // Tally the votes if (votes.Num() > 0) { auto MaxVotes = *votes.begin(); auto it = votes.begin(); ++it; while (it != votes.end()) { if (it->Value > MaxVotes.Value) MaxVotes = *it; ++it; } // Use the material with the max number of points. if (MaxVotes.Key.IsValid()) { GetDefault()->GetAssociatedAcousticTexture(MaxVotes.Key.Get(), PreviewPolys[i].Texture); GetDefault()->GetAssociatedOcclusionValue(MaxVotes.Key.Get(), PreviewPolys[i].Occlusion); PreviewPolys[i].EnableSurface = true; } } } } void AddOrIncrementMaterialVote(TMap, int>& votes, TWeakObjectPtr& material) { int* Count = votes.Find(material); if (Count == nullptr) { Count = &votes.Add(material); *Count = 0; } if (Count != nullptr) { *Count += 1; }; } void AAkSpatialAudioVolume::FitBox(bool bPreviewOnly) { ClearTextComponents(); LongestEdgeLength = 0.0f; if (FitPoints.Num() * FilterHitPoints == 0) { FitFailed = true; return; } static const float kExtent = 100.f; using MinimumVolumeBox3 = WwiseGTE::MinimumVolumeBox3>; using OBB = WwiseGTE::OrientedBox3 < float >; using Vector3 = WwiseGTE::Vector3< float >; PreviewOutline.Empty(); const float kNormalAgreement = 0.866f; // ~30 degrees if (Shape == EAkFitToGeometryMode::OrientedBox || Shape == EAkFitToGeometryMode::AlignedBox ) { FTransform XF = GetActorTransform(); if (Shape == EAkFitToGeometryMode::OrientedBox) //allow rotation. { TArray Points; Points.Reserve(FitPoints.Num()); for (int i = 0; i < FitPoints.Num() && i < FitPoints.Num()*FilterHitPoints; ++i) { Points.Emplace(ToGTEVector(FitPoints[i])); } MinimumVolumeBox3 mvb(std::min(8U, std::thread::hardware_concurrency()-1), true); OBB obb = mvb(Points.Num(), &Points[0], kConvexHullEpsilon); FVector Location(obb.center[0], obb.center[1], obb.center[2]); FVector Front(obb.axis[1][0], obb.axis[1][1], obb.axis[1][2]); FVector Top(obb.axis[2][0], obb.axis[2][1], obb.axis[2][2]); FQuat Rotation = FRotationMatrix::MakeFromYZ(Front, Top).ToQuat(); FVector Scale(obb.extent[0], obb.extent[1], obb.extent[2]); if (Scale.X > ::kScaleEpsilon && Scale.Y > ::kScaleEpsilon && Scale.Z > ::kScaleEpsilon) { LongestEdgeLength = Scale.GetAbsMax(); // Compensate for the standard AAkSpatialAudioVolume, based on a cube brush with verts at [+/-]100 X,Y,Z. Scale /= kExtent; XF.SetLocation(Location); XF.SetRotation(Rotation); XF.SetScale3D(Scale); } TArray textPositions = TArray(); textPositions.AddDefaulted(6); // Box face planes // The material votes for each plane. TArray, int>> MaterialVotes; MaterialVotes.AddDefaulted(6); for (int boxAxis = 0; boxAxis < 3; ++boxAxis) { int faceIndex = boxAxis * 2; int oppositeFaceIndex = faceIndex + 1; FVector axis(obb.axis[boxAxis][0], obb.axis[boxAxis][1], obb.axis[boxAxis][2]); FVector faceCenter = Location + axis * obb.extent[boxAxis]; FVector oppositeFaceCenter = Location - axis * obb.extent[boxAxis]; textPositions[faceIndex] = faceCenter; textPositions[oppositeFaceIndex] = oppositeFaceCenter; for (int i = 0; i < FitPoints.Num() && i < FitPoints.Num() * FilterHitPoints; ++i) { // The sign on the axis is flipped in the comparisons because the normals face in towards the cuboid... if (FVector::DotProduct(FitNormals[i], -axis) >= kNormalAgreement && FMath::IsNearlyZero(FVector::DotProduct(FitPoints[i] - faceCenter, axis), 0.01f)) { TMap, int>& votes = MaterialVotes[faceIndex]; AddOrIncrementMaterialVote(votes, FitMaterials[i]); } if (FVector::DotProduct(FitNormals[i], axis) >= kNormalAgreement && FMath::IsNearlyZero(FVector::DotProduct(FitPoints[i] - oppositeFaceCenter, -axis), 0.01f)) { TMap, int>& votes = MaterialVotes[oppositeFaceIndex]; AddOrIncrementMaterialVote(votes, FitMaterials[i]); } } } if (bPreviewOnly) { UpdatePreviewPolys(MaterialVotes); UpdatePreviewTextComponents(textPositions); } } else if (Shape == EAkFitToGeometryMode::AlignedBox) { USceneComponent* RC = GetRootComponent(); if (RC) { FRotator Rotation = RC->GetComponentRotation(); FVector Min(FLT_MAX, FLT_MAX, FLT_MAX); FVector Max(-FLT_MAX, -FLT_MAX, -FLT_MAX); for (int i = 0; i < FitPoints.Num() && i < FitPoints.Num()*FilterHitPoints; ++i) { FVector PtWorld(FitPoints[i][0], FitPoints[i][1], FitPoints[i][2]); FVector PtLocal = Rotation.UnrotateVector(PtWorld); Min = Min.ComponentMin(PtLocal); Max = Max.ComponentMax(PtLocal); } FVector Scale = Max - Min; if (Scale.X > ::kScaleEpsilon && Scale.Y > ::kScaleEpsilon && Scale.Z > ::kScaleEpsilon) { LongestEdgeLength = Scale.GetAbsMax(); FVector Location = Rotation.RotateVector((Min + Max) / 2.f); XF.SetLocation(Location); XF.SetScale3D(Scale / (2.f*kExtent)); } FVector centre(XF.GetLocation()); FVector extent(Scale / 2.0f); TArray textPositions = // Box face planes { centre + FVector(extent.X, 0.0f, 0.0f), centre - FVector(extent.X, 0.0f, 0.0f), centre + FVector(0.0f, extent.Y, 0.0f), centre - FVector(0.0f, extent.Y, 0.0f), centre + FVector(0.0f, 0.0f, extent.Z), centre - FVector(0.0f, 0.0f, extent.Z) }; TArray faceNormals = { FVector(1, 0, 0), FVector(-1, 0, 0), FVector(0, 1, 0), FVector(0, -1, 0), FVector(0, 0, 1), FVector(0, 0, -1) }; // The material votes for each plane. TArray, int>> materialVotes; materialVotes.AddDefaulted(6); for (int axisIndex = 0; axisIndex < textPositions.Num(); ++axisIndex) { for (int i = 0; i < FitPoints.Num() && i < FitPoints.Num() * FilterHitPoints; ++i) { if (FVector::DotProduct(-FitNormals[i], faceNormals[axisIndex]) >= kNormalAgreement && FMath::IsNearlyZero(FVector::DotProduct(FitPoints[i] - textPositions[axisIndex], faceNormals[axisIndex]), 0.01f)) { TMap, int>& votes = materialVotes[axisIndex]; AddOrIncrementMaterialVote(votes, FitMaterials[i]); } } } if (bPreviewOnly) { UpdatePreviewPolys(materialVotes); UpdatePreviewTextComponents(textPositions); } } } if (!bPreviewOnly && BrushBuilder) { BrushBuilder->BeginBrush(true, this->GetFName()); const FTransform& ActorTransform = GetActorTransform(); for (int32 i = -1; i < 2; i += 2) for (int32 j = -1; j < 2; j += 2) for (int32 k = -1; k < 2; k += 2) BrushBuilder->Vertexv(ActorTransform.InverseTransformPosition( XF.TransformPosition(FVector(i*kExtent, j*kExtent, k*kExtent)) )); BrushBuilder->Poly4i(+1, 0, 1, 3, 2); BrushBuilder->Poly4i(+1, 2, 3, 7, 6); BrushBuilder->Poly4i(+1, 6, 7, 5, 4); BrushBuilder->Poly4i(+1, 4, 5, 1, 0); BrushBuilder->Poly4i(+1, 3, 1, 5, 7); BrushBuilder->Poly4i(+1, 0, 2, 6, 4); BrushBuilder->EndBrush(GetWorld(), this); SetNeedRebuild(GetLevel()); GEditor->RebuildAlteredBSP(); PostRebuildBrush(); } PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(+kExtent, +kExtent, -kExtent)), XF.TransformPosition(FVector(+kExtent, +kExtent, +kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(+kExtent, -kExtent, -kExtent)), XF.TransformPosition(FVector(+kExtent, -kExtent, +kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(-kExtent, +kExtent, -kExtent)), XF.TransformPosition(FVector(-kExtent, +kExtent, +kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(-kExtent, -kExtent, -kExtent)), XF.TransformPosition(FVector(-kExtent, -kExtent, +kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(-kExtent, -kExtent, -kExtent)), XF.TransformPosition(FVector(-kExtent, +kExtent, -kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(+kExtent, -kExtent, -kExtent)), XF.TransformPosition(FVector(+kExtent, +kExtent, -kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(-kExtent, -kExtent, -kExtent)), XF.TransformPosition(FVector(+kExtent, -kExtent, -kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(-kExtent, +kExtent, -kExtent)), XF.TransformPosition(FVector(+kExtent, +kExtent, -kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(-kExtent, -kExtent, +kExtent)), XF.TransformPosition(FVector(-kExtent, +kExtent, +kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(+kExtent, -kExtent, +kExtent)), XF.TransformPosition(FVector(+kExtent, +kExtent, +kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(-kExtent, -kExtent, +kExtent)), XF.TransformPosition(FVector(+kExtent, -kExtent, +kExtent)))); PreviewOutline.Emplace(TPair(XF.TransformPosition(FVector(-kExtent, +kExtent, +kExtent)), XF.TransformPosition(FVector(+kExtent, +kExtent, +kExtent)))); } else if (Shape == EAkFitToGeometryMode::ConvexPolyhedron) { static const int kMaxAllowedPointsBehindPlane = 1; // Allows for some leniency - we cant expect geometry to be completely convex. static const float kDotEpsilon = 0.1f; // To determine if points are infront/behind a given plane. static const float kDotThreshold = 0.866f; //~ 30 degrees, enough for a polygonal cross section with 12 sides. Used for comparing normals. using ConvexHull3 = ::WwiseGTE::ConvexHull3>; using ETManifoldMesh = ::WwiseGTE::ETManifoldMesh; FVector Origin = GetActorLocation(); TArray Points; Points.Reserve(FitPoints.Num()); TArray Normals; // The material votes for each plane. TArray, int>> MaterialVotes; TArray Ds; for (int i = 0; i < FitPoints.Num()*FilterHitPoints; ++i) { // If the room is convex, every other point should be in front of the plane defined by the hit point and normal. int PointsBehindPlane = 0; for (int j = 0; j < FitPoints.Num()*FilterHitPoints; ++j) { if (i != j) { FVector ToPt = FitPoints[j] - FitPoints[i]; ToPt.Normalize(); float Proj = FVector::DotProduct(ToPt, FitNormals[i]); if (Proj < -kDotEpsilon) PointsBehindPlane++; } } // But in practice we will allow 1 point to be behind. if (PointsBehindPlane <= kMaxAllowedPointsBehindPlane) { // Calculate a representative plane for this point to be used in the convex hull algorithm. float d = FVector::DotProduct(Origin - FitPoints[i], FitNormals[i]); FVector HullPoint = Origin - FitNormals[i] * d; FVector& Normal = FitNormals[i]; TWeakObjectPtr& Material = FitMaterials[i]; bool Found = false; for (int n = 0; n < Normals.Num(); ++n) { // Check to see if a plane with the same normal has already been found. if (FVector::DotProduct(Normals[n], Normal) > kDotThreshold) { // If so, take the one with the largest d value. Found = true; if (Ds[n] < d) { Normals[n] = Normal; Ds[n] = d; Points[n] = ToGTEVector(HullPoint); } TMap, int>& votes = MaterialVotes[n]; AddOrIncrementMaterialVote(votes, Material); } } if (!Found) { MaterialVotes.Add(TMap, int>()); TMap, int>& votes = MaterialVotes.Last(); AddOrIncrementMaterialVote(votes, Material); Normals.Add(Normal); Ds.Add(d); Points.Emplace(ToGTEVector(HullPoint)); } } } if (Points.Num() < 4) { FitFailed = true; return; } // Build a convex hull with the planes found from the raycasts. ConvexHull3 ConvexHull; if (ConvexHull(Points.Num(), &Points[0], kConvexHullEpsilon)) { ETManifoldMesh RoughMesh = ConvexHull.GetHullMesh(); //At this point the polyhedron mesh is missing 'corners'. Iterate through the triangles, checking the normals of the vertices. TArray MeshPoints; MeshPoints.Reserve(FitPoints.Num()); ETManifoldMesh::TMap RoughTriangles = RoughMesh.GetTriangles(); for (auto& Triangle : RoughTriangles) { auto& Indexes = Triangle.second->V; FVector& n0 = Normals[Indexes[0]]; FVector& n1 = Normals[Indexes[1]]; FVector& n2 = Normals[Indexes[2]]; FVector p0 = ToFVector(Points[Indexes[0]]); FVector p1 = ToFVector(Points[Indexes[1]]); FVector p2 = ToFVector(Points[Indexes[2]]); float d0 = FVector::DotProduct(p0, n0); float d1 = FVector::DotProduct(p1, n1); float d2 = FVector::DotProduct(p2, n2); FVector CornerPoint; if (IntersectPlanes(n0, d0, n1, d1, n2, d2, CornerPoint)) { // Punch out the corner MeshPoints.Emplace(ToGTEVector(CornerPoint)); } else { FitFailed = true; return; } } // Now generate a convex hull with the new corner points. ConvexHull3 ConvexPolyhedron; if (ConvexPolyhedron(MeshPoints.Num(), &MeshPoints[0], kConvexHullEpsilon)) { ETManifoldMesh Mesh = ConvexPolyhedron.GetHullMesh(); // Build a new brush with the polyhedron mesh. if (!bPreviewOnly && BrushBuilder) { BrushBuilder->BeginBrush(true, this->GetFName()); FVector Location = GetActorLocation(); for (int p = 0; p < ConvexPolyhedron.GetNumPoints(); ++p) { const Vector3& Vert = ConvexPolyhedron.GetPoints()[p]; BrushBuilder->Vertex3f( Vert[0] - Location.X, Vert[1] - Location.Y, Vert[2] - Location.Z); } ETManifoldMesh::TMap Triangles = Mesh.GetTriangles(); for (auto& Triangle : Triangles) { auto& Indexes = Triangle.second->V; BrushBuilder->Poly3i(+1, Indexes[0], Indexes[1], Indexes[2]); } BrushBuilder->EndBrush(GetWorld(), this); auto* RC = GetRootComponent(); if (RC) { RC->SetWorldRotation(FQuat::Identity); RC->SetWorldScale3D(FVector::OneVector); } SetNeedRebuild(GetLevel()); GEditor->RebuildAlteredBSP(); PostRebuildBrush(); } // Add the edges to the preview outline - skipping the edges that are 'flat'. ETManifoldMesh::EMap Edges = Mesh.GetEdges(); for (auto& E : Edges) { auto& Edge = *E.second; auto T0 = Edge.T[0].lock(); auto T1 = Edge.T[1].lock(); if (T0 != nullptr && T1 != nullptr) { FVector N0 = FVector::CrossProduct(ToFVector(MeshPoints[T0->V[1]]) - ToFVector(MeshPoints[T0->V[0]]), ToFVector(MeshPoints[T0->V[2]]) - ToFVector(MeshPoints[T0->V[0]])); FVector N1 = FVector::CrossProduct(ToFVector(MeshPoints[T1->V[1]]) - ToFVector(MeshPoints[T1->V[0]]), ToFVector(MeshPoints[T1->V[2]]) - ToFVector(MeshPoints[T1->V[0]])); N0.Normalize(); N1.Normalize(); if (FVector::DotProduct(N0, N1) < kDotThreshold) { PreviewOutline.Add(TPair(ToFVector(MeshPoints[Edge.V[0]]), ToFVector(MeshPoints[Edge.V[1]]))); float edgeLength = (PreviewOutline.Last().Value - PreviewOutline.Last().Key).Size(); if (edgeLength > LongestEdgeLength) LongestEdgeLength = edgeLength; } } } } } if (bPreviewOnly) { UpdatePreviewPolys(MaterialVotes); TArray textPositions = TArray(); textPositions.AddDefaulted(Points.Num()); for (int posIndex = 0; posIndex < Points.Num(); ++posIndex) { textPositions[posIndex] = ToFVector(Points[posIndex]); } UpdatePreviewTextComponents(textPositions); } } else { check(false); } // Map physics materials to surfaces. if ((!bPreviewOnly) && SurfaceReflectorSet) { SurfaceReflectorSet->AssignAcousticTexturesFromSamples(FitPoints, FitNormals, FitMaterials, std::min(FitPoints.Num(), (int32)(FitPoints.Num()*FilterHitPoints))); } FitFailed = false; } bool AAkSpatialAudioVolume::ShouldTickIfViewportsOnly() const { return bBrushNeedsRebuild || ((GetBounds() != PreviousBounds) && PreviousBounds.SphereRadius != 0.0f); } void AAkSpatialAudioVolume::Tick(float DeltaSeconds) { if (ShouldTickIfViewportsOnly()) { SetNeedRebuild(GetLevel()); GEditor->RebuildAlteredBSP(); PostRebuildBrush(); PreviousBounds = GetBounds(); bBrushNeedsRebuild = false; } } void AAkSpatialAudioVolume::PostTransacted(const FTransactionObjectEvent& TransactionEvent) { Super::PostTransacted(TransactionEvent); const TArray& ChangedProperties = TransactionEvent.GetChangedProperties(); if (TransactionEvent.GetEventType() == ETransactionObjectEventType::UndoRedo && ChangedProperties.Contains(FName("FitToGeometry"))) { bBrushNeedsRebuild = true; } } void AAkSpatialAudioVolume::PostEditMove(bool bFinished) { Super::PostEditMove(bFinished); IsDragging = !bFinished; if (FitToGeometry) { FitRaycast(); if (bFinished && Shape == EAkFitToGeometryMode::AlignedBox) { USceneComponent* RC = GetRootComponent(); if (RC) SavedRotation = RC->GetComponentRotation(); } FitBox(!bFinished); } } void AAkSpatialAudioVolume::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); IsDragging = PropertyChangedEvent.ChangeType == EPropertyChangeType::Interactive; if (PropertyChangedEvent.Property) { if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(AAkSpatialAudioVolume, FitToGeometry)) { if (FitToGeometry) { FitRaycast(); FitBox(); } else { FitPoints.Empty(); } } if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(AAkSpatialAudioVolume, FilterHitPoints)) { if (FitToGeometry) // only fit box continuously on value set for performance reasons. { FitBox(PropertyChangedEvent.ChangeType != EPropertyChangeType::ValueSet); } } if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(AAkSpatialAudioVolume, Shape)) { USceneComponent* RC = GetRootComponent(); if (RC) { if (Shape != EAkFitToGeometryMode::AlignedBox) { // We just disabled 'ConstrainRotation'. Save the old rotation. SavedRotation = RC->GetComponentRotation(); } else { // We just enabled 'ConstrainRotation'. Restore the old rotation. RC->SetWorldRotation(SavedRotation); } } FitBox(); } if (PropertyChangedEvent.Property->GetFName() == "ActorLabel") { if (Room != nullptr) { Room->OnParentNameChanged(); } } } } #endif // WITH_EDITOR