Compare commits
2 Commits
28a99dc373
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fd45af39a | |||
| d2fc5a8fbb |
BIN
Unreal/PS_StratVision/Content/NewMap.umap
Normal file
BIN
Unreal/PS_StratVision/Content/NewMap.umap
Normal file
Binary file not shown.
BIN
Unreal/PS_StratVision/Content/testStatVis.uasset
Normal file
BIN
Unreal/PS_StratVision/Content/testStatVis.uasset
Normal file
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,7 @@
|
||||
"FriendlyName": "PS Strategic Vision",
|
||||
"Description": "Affichage strategique pour vues spectateur et replays : cones de vision, et autres visualisations a venir.",
|
||||
"Category": "Visualization",
|
||||
"CreatedBy": "j.foucher",
|
||||
"CreatedBy": "ASTERION VR",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "",
|
||||
"MarketplaceURL": "",
|
||||
|
||||
@@ -19,12 +19,14 @@ avec le projet).
|
||||
|
||||
## Contenu
|
||||
|
||||
| Classe | Rôle |
|
||||
| Élément | Rôle |
|
||||
|---|---|
|
||||
| `UPS_VisionConeComponent` | Component à poser sur un personnage : affiche son cône de vision (mesh procédural, bloqué par les murs). |
|
||||
| `UPS_VisionConeComponent` | Component à poser sur un personnage : affiche son cône de vision (mesh procédural, bloqué par les murs, plan toujours horizontal). |
|
||||
| `UPS_StrategicDisplayComponent` | Classe de base commune à tous les affichages stratégiques (camp, activation, couleur). Les futurs components en hériteront. |
|
||||
| `IPS_VisionSource` (interface) | À implémenter sur l'acteur pour injecter l'origine/direction de vision réelles (ex : caméra VR). |
|
||||
| `UPS_StrategicVisionSubsystem` | Subsystem monde : interrupteurs globaux (tout, par catégorie, par camp). |
|
||||
| `UPS_StrategicVisionSettings` | Réglages projet : couleurs des camps, matériau par défaut. |
|
||||
| `UPS_StrategicVisionSettings` | Réglages projet : activation au démarrage, style par camp (couleur, hachures), matériau. |
|
||||
| `M_VisionCone` (Content) | Matériau translucide unlit avec hachures procédurales, utilisé par défaut. |
|
||||
|
||||
## Utilisation
|
||||
|
||||
@@ -33,60 +35,81 @@ avec le projet).
|
||||
Dans le Blueprint du personnage (ou en C++), ajouter le component **PS Vision Cone**
|
||||
(idéalement attaché au root / à la capsule). Régler :
|
||||
|
||||
- **Team** : `Player`, `Ally`, `Enemy` ou `Neutral` — détermine la couleur
|
||||
(configurable dans *Project Settings > Plugins > PS Strategic Vision*).
|
||||
- **Team** : `Player`, `Ally`, `Enemy` ou `Neutral` — détermine la couleur et
|
||||
l'angle des hachures (configurables dans *Project Settings > Plugins >
|
||||
PS Strategic Vision*).
|
||||
- **View Distance** / **View Angle Degrees** : portée et ouverture du cône.
|
||||
- **Trace Channel** : canal de collision utilisé pour bloquer le cône sur les murs
|
||||
(`Visibility` par défaut).
|
||||
- **Flatten On Ground** : le cône reste à plat et se projette au sol (recommandé
|
||||
pour une vue spectateur top-down).
|
||||
|
||||
Le cône suit automatiquement la position et le yaw du component. Pour qu'il suive
|
||||
le regard de la tête plutôt que le corps, attacher le component au bone/socket voulu.
|
||||
Le plan du cône est **toujours horizontal**, même si l'acteur est incliné, et il
|
||||
est projeté à la hauteur du sol détecté (`Project To Ground`).
|
||||
|
||||
### 2. Activer / désactiver selon l'état de jeu
|
||||
### 2. Cas VR : injecter la position/direction de la caméra
|
||||
|
||||
Par component :
|
||||
Si l'acteur reste à l'origine pendant que la caméra bouge (pawn VR), implémenter
|
||||
l'interface **PS Vision Source** sur l'acteur (Class Settings > Interfaces en
|
||||
Blueprint) et surcharger **Get Vision View Point** :
|
||||
|
||||
- `SetDisplayEnabled(bool)` — interrupteur local du component.
|
||||
- `Out Location` = position monde de la caméra ;
|
||||
- `Out Forward Direction` = direction du regard (seul le cap horizontal est utilisé) ;
|
||||
- retourner `true`.
|
||||
|
||||
Globalement, via le subsystem (Blueprint : nœud `Get PS_StrategicVisionSubsystem`,
|
||||
ou `UPS_StrategicVisionSubsystem::Get(WorldContext)` en C++) :
|
||||
Le component l'utilise automatiquement (`Use Owner Vision Source`, activé par
|
||||
défaut). Retourner `false` pour repasser sur le transform du component. Penser à
|
||||
mettre `Trace Height` à 0 si la position fournie est déjà à hauteur des yeux.
|
||||
|
||||
- `SetGlobalDisplayEnabled(bool)` — tout l'affichage stratégique (ex : entrée/sortie
|
||||
du mode spectateur ou replay).
|
||||
- `SetVisionConesEnabled(bool)` — uniquement les cônes de vision.
|
||||
- `SetCategoryEnabled(FName, bool)` — par catégorie (pour les futurs components).
|
||||
- `SetTeamEnabled(EPS_StrategicTeam, bool)` — par camp (ex : ne montrer que les ennemis).
|
||||
### 3. Activer / désactiver selon l'état de jeu
|
||||
|
||||
Un component n'est visible que si son interrupteur local ET les interrupteurs
|
||||
globaux le permettent. Quand un cône est masqué, il ne fait plus aucune trace
|
||||
(coût nul).
|
||||
**Tout est masqué par défaut** : au démarrage d'une partie, aucun affichage
|
||||
stratégique n'est visible (modifiable via `bEnabledAtStartup` dans les settings).
|
||||
L'activation se fait via le subsystem (Blueprint : nœud
|
||||
`Get PS_StrategicVisionSubsystem`, ou `UPS_StrategicVisionSubsystem::Get()` en C++) :
|
||||
|
||||
### 3. Matériau (optionnel)
|
||||
- `SetGlobalDisplayEnabled(true)` — active l'affichage stratégique (ex : entrée
|
||||
en mode spectateur ou replay) ;
|
||||
- `SetVisionConesEnabled(bool)` — uniquement les cônes de vision ;
|
||||
- `SetCategoryEnabled(FName, bool)` — par catégorie (pour les futurs components) ;
|
||||
- `SetTeamEnabled(EPS_StrategicTeam, bool)` — par camp (ex : ne montrer que les
|
||||
ennemis).
|
||||
|
||||
Sans configuration, le cône utilise un matériau translucide du moteur
|
||||
(`M_SimpleUnlitTranslucent`) teinté à la couleur du camp — fonctionnel
|
||||
immédiatement, suffisant en développement.
|
||||
Chaque component a aussi son interrupteur local `SetDisplayEnabled(bool)` (actif
|
||||
par défaut — c'est l'état global qui masque tout au départ). Un component n'est
|
||||
visible que si son interrupteur local ET les interrupteurs globaux le permettent.
|
||||
Quand un cône est masqué, il ne fait plus aucune trace (coût nul).
|
||||
|
||||
Pour un rendu personnalisé (dégradé, scanlines, bord lumineux...), créer un
|
||||
matériau dans le Content du plugin à `/PS_StrategicVision/Materials/M_VisionCone`
|
||||
(chemin par défaut des settings) ou l'assigner via **Material Override** sur le
|
||||
component. Le component fournit au matériau :
|
||||
### 4. Lisibilité des cônes superposés
|
||||
|
||||
Deux mécanismes, configurables par component (catégorie *Vision Cone > Rendering*) :
|
||||
|
||||
- **Contour** (`Show Outline`, `Outline Width`, `Outline Opacity`) : bande plus
|
||||
opaque sur le pourtour du cône — chaque cône reste délimité même superposé.
|
||||
- **Hachures procédurales** (`Hatching Enabled`, `Hatching Scale`,
|
||||
`Hatching Intensity`) : rayures en espace monde dont **l'angle dépend du camp**
|
||||
(45° joueur, 90° allié, 135° ennemi par défaut — réglable dans les settings).
|
||||
Deux cônes superposés restent identifiables par l'orientation de leurs rayures.
|
||||
|
||||
Le matériau `M_VisionCone` fourni par le plugin gère ces options. Il est
|
||||
régénérable via `Tools/generate_vision_cone_material.py` (commandlet Python).
|
||||
|
||||
### 5. Matériau personnalisé (optionnel)
|
||||
|
||||
Assigner un matériau via **Material Override** (component) ou
|
||||
**Default Cone Material** (settings). Le component fournit au matériau :
|
||||
|
||||
- paramètre vecteur `Color` : couleur du camp (alpha = opacité) ;
|
||||
- paramètre scalaire `Opacity` : opacité ;
|
||||
- vertex colors : couleur du camp ;
|
||||
- paramètres scalaires `Opacity`, `HatchAngle`, `HatchScale`, `HatchIntensity` ;
|
||||
- vertex colors : couleur du camp (alpha = opacité) ;
|
||||
- UV : U = position angulaire (0..1), V = distance normalisée (0..1) — pratique
|
||||
pour un fondu vers le bord du cône.
|
||||
|
||||
Recette minimale : matériau *Translucent* / *Unlit*, node **VertexColor** → Emissive
|
||||
Recette minimale : matériau *Translucent* / *Unlit*, **VertexColor** → Emissive
|
||||
Color, **VertexColor.A** → Opacity. Laisser *Two Sided* décoché (la géométrie est
|
||||
déjà double face).
|
||||
|
||||
> Note packaging : si le matériau n'est référencé que par les settings (soft
|
||||
> reference), l'ajouter à la liste des assets toujours cookés, ou le référencer en
|
||||
> dur via Material Override.
|
||||
> Note packaging : le matériau par défaut n'est référencé que par soft reference ;
|
||||
> pour un build packagé, vérifier qu'il est bien cooké (Additional Assets to Cook,
|
||||
> ou référence en dur via Material Override).
|
||||
|
||||
### Performance
|
||||
|
||||
@@ -97,5 +120,7 @@ déjà double face).
|
||||
## Roadmap
|
||||
|
||||
- [x] Cône de vision bloqué par les obstacles
|
||||
- [x] Source de vision injectable (VR) via interface
|
||||
- [x] Contour + hachures procédurales pour les superpositions
|
||||
- [ ] Autres affichages stratégiques (trajectoires, zones d'alerte, ...) sous forme
|
||||
de components héritant de `UPS_StrategicDisplayComponent`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#include "PS_StrategicDisplayComponent.h"
|
||||
|
||||
@@ -47,7 +47,7 @@ bool UPS_StrategicDisplayComponent::IsDisplayVisible() const
|
||||
}
|
||||
}
|
||||
|
||||
// Monde editeur (preview) : on suit uniquement l'activation locale.
|
||||
// Editor world (preview): only the local activation is considered.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#include "PS_StrategicVisionSettings.h"
|
||||
|
||||
#include "Materials/MaterialInterface.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
FPS_StrategicTeamStyle MakeStyle(const FLinearColor& Color, float HatchAngleDegrees)
|
||||
{
|
||||
FPS_StrategicTeamStyle Style;
|
||||
Style.Color = Color;
|
||||
Style.HatchAngleDegrees = HatchAngleDegrees;
|
||||
return Style;
|
||||
}
|
||||
}
|
||||
|
||||
UPS_StrategicVisionSettings::UPS_StrategicVisionSettings()
|
||||
{
|
||||
TeamColors.Add(EPS_StrategicTeam::Player, FLinearColor(0.05f, 0.45f, 1.0f));
|
||||
TeamColors.Add(EPS_StrategicTeam::Ally, FLinearColor(0.1f, 0.9f, 0.5f));
|
||||
TeamColors.Add(EPS_StrategicTeam::Enemy, FLinearColor(1.0f, 0.12f, 0.08f));
|
||||
TeamColors.Add(EPS_StrategicTeam::Neutral, FLinearColor(0.75f, 0.75f, 0.75f));
|
||||
TeamStyles.Add(EPS_StrategicTeam::Player, MakeStyle(FLinearColor(0.05f, 0.45f, 1.0f), 45.f));
|
||||
TeamStyles.Add(EPS_StrategicTeam::Ally, MakeStyle(FLinearColor(0.1f, 0.9f, 0.5f), 90.f));
|
||||
TeamStyles.Add(EPS_StrategicTeam::Enemy, MakeStyle(FLinearColor(1.0f, 0.12f, 0.08f), 135.f));
|
||||
TeamStyles.Add(EPS_StrategicTeam::Neutral, MakeStyle(FLinearColor(0.75f, 0.75f, 0.75f), 0.f));
|
||||
|
||||
// Materiau optionnel a creer dans le Content du plugin (voir README).
|
||||
// Material shipped in the plugin content (procedural hatching).
|
||||
DefaultConeMaterial = TSoftObjectPtr<UMaterialInterface>(
|
||||
FSoftObjectPath(TEXT("/PS_StrategicVision/Materials/M_VisionCone.M_VisionCone")));
|
||||
}
|
||||
|
||||
FLinearColor UPS_StrategicVisionSettings::GetTeamColor(EPS_StrategicTeam Team) const
|
||||
{
|
||||
if (const FLinearColor* Color = TeamColors.Find(Team))
|
||||
return GetTeamStyle(Team).Color;
|
||||
}
|
||||
|
||||
FPS_StrategicTeamStyle UPS_StrategicVisionSettings::GetTeamStyle(EPS_StrategicTeam Team) const
|
||||
{
|
||||
if (const FPS_StrategicTeamStyle* Style = TeamStyles.Find(Team))
|
||||
{
|
||||
return *Color;
|
||||
return *Style;
|
||||
}
|
||||
return FLinearColor::White;
|
||||
return FPS_StrategicTeamStyle();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#include "PS_StrategicVisionSubsystem.h"
|
||||
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/World.h"
|
||||
#include "PS_StrategicVisionSettings.h"
|
||||
|
||||
void UPS_StrategicVisionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
bGlobalEnabled = GetDefault<UPS_StrategicVisionSettings>()->bEnabledAtStartup;
|
||||
}
|
||||
|
||||
UPS_StrategicVisionSubsystem* UPS_StrategicVisionSubsystem::Get(const UObject* WorldContextObject)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#include "PS_StrategicVisionTypes.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#include "PS_VisionConeComponent.h"
|
||||
|
||||
@@ -6,27 +6,89 @@
|
||||
#include "Engine/HitResult.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialInstanceDynamic.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "ProceduralMeshComponent.h"
|
||||
#include "PS_StrategicVisionSettings.h"
|
||||
#include "PS_VisionSourceInterface.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Materiaux moteur utilises en dernier recours si aucun materiau n'est configure.
|
||||
// Engine materials used as a last resort when no material is configured.
|
||||
const TCHAR* GFallbackTranslucentMaterialPath = TEXT("/Engine/EngineDebugMaterials/M_SimpleUnlitTranslucent.M_SimpleUnlitTranslucent");
|
||||
const TCHAR* GFallbackOpaqueMaterialPath = TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial");
|
||||
|
||||
const FName GColorParamName(TEXT("Color"));
|
||||
const FName GOpacityParamName(TEXT("Opacity"));
|
||||
const FName GColorParam(TEXT("Color"));
|
||||
const FName GOpacityParam(TEXT("Opacity"));
|
||||
const FName GHatchAngleParam(TEXT("HatchAngle"));
|
||||
const FName GHatchScaleParam(TEXT("HatchScale"));
|
||||
const FName GHatchIntensityParam(TEXT("HatchIntensity"));
|
||||
|
||||
// Small lift of the outline above the fill to avoid z-fighting.
|
||||
constexpr float GOutlineZLift = 0.4f;
|
||||
|
||||
// Ground search distance below the vision origin.
|
||||
constexpr float GGroundSearchDepth = 1000.f;
|
||||
|
||||
/** Inward inset of a closed polygon (XY), used to build the outline strip. */
|
||||
void InsetPolygon(const TArray<FVector2D>& Points, float Width, TArray<FVector2D>& OutInner)
|
||||
{
|
||||
const int32 Num = Points.Num();
|
||||
OutInner.SetNum(Num);
|
||||
|
||||
// Polygon orientation (signed area, shoelace formula).
|
||||
double SignedArea = 0.0;
|
||||
for (int32 Index = 0; Index < Num; ++Index)
|
||||
{
|
||||
const FVector2D& A = Points[Index];
|
||||
const FVector2D& B = Points[(Index + 1) % Num];
|
||||
SignedArea += A.X * B.Y - B.X * A.Y;
|
||||
}
|
||||
const float Orient = (SignedArea >= 0.0) ? 1.f : -1.f;
|
||||
|
||||
for (int32 Index = 0; Index < Num; ++Index)
|
||||
{
|
||||
const FVector2D& Prev = Points[(Index - 1 + Num) % Num];
|
||||
const FVector2D& Cur = Points[Index];
|
||||
const FVector2D& Next = Points[(Index + 1) % Num];
|
||||
|
||||
const FVector2D DirPrev = (Cur - Prev).GetSafeNormal();
|
||||
const FVector2D DirNext = (Next - Cur).GetSafeNormal();
|
||||
// Outward normal of each adjacent edge.
|
||||
const FVector2D NormPrev = FVector2D(DirPrev.Y, -DirPrev.X) * Orient;
|
||||
const FVector2D NormNext = FVector2D(DirNext.Y, -DirNext.X) * Orient;
|
||||
|
||||
FVector2D Bisector = (NormPrev + NormNext).GetSafeNormal();
|
||||
if (Bisector.IsNearlyZero())
|
||||
{
|
||||
Bisector = !NormPrev.IsNearlyZero() ? NormPrev : NormNext;
|
||||
}
|
||||
if (Bisector.IsNearlyZero())
|
||||
{
|
||||
OutInner[Index] = Cur;
|
||||
continue;
|
||||
}
|
||||
|
||||
const FVector2D Reference = !NormPrev.IsNearlyZero() ? NormPrev : NormNext;
|
||||
// Miter length, clamped for very sharp angles.
|
||||
const float CosHalf = FMath::Max(0.35f, FVector2D::DotProduct(Bisector, Reference));
|
||||
OutInner[Index] = Cur - Bisector * (Width / CosHalf);
|
||||
}
|
||||
}
|
||||
|
||||
/** Appends a double-sided quad (4 triangles) to the index list. */
|
||||
void AddDoubleSidedQuad(TArray<int32>& Triangles, int32 Outer0, int32 Inner0, int32 Outer1, int32 Inner1)
|
||||
{
|
||||
Triangles.Append({ Outer0, Outer1, Inner0, Inner0, Outer1, Inner1 });
|
||||
Triangles.Append({ Outer0, Inner0, Outer1, Inner0, Inner1, Outer1 });
|
||||
}
|
||||
}
|
||||
|
||||
UPS_VisionConeComponent::UPS_VisionConeComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.bStartWithTickEnabled = true;
|
||||
// Apres le mouvement des acteurs, pour que le cone suive sans retard.
|
||||
// After actor movement, so that the cone follows without lag.
|
||||
PrimaryComponentTick.TickGroup = TG_PostUpdateWork;
|
||||
bTickInEditor = true;
|
||||
}
|
||||
@@ -37,7 +99,7 @@ void UPS_VisionConeComponent::OnRegister()
|
||||
|
||||
EnsureConeMesh();
|
||||
EnsureMaterial();
|
||||
ApplyTeamColor();
|
||||
ApplyTeamStyle();
|
||||
}
|
||||
|
||||
void UPS_VisionConeComponent::OnComponentDestroyed(bool bDestroyingHierarchy)
|
||||
@@ -76,18 +138,53 @@ void UPS_VisionConeComponent::OnDisplayStateChanged()
|
||||
ConeMesh->SetVisibility(bShouldDisplay);
|
||||
}
|
||||
|
||||
// Pas de traces ni de rebuild quand le cone est cache.
|
||||
// No traces nor rebuilds while the cone is hidden.
|
||||
SetComponentTickEnabled(bShouldDisplay);
|
||||
SetComponentTickInterval(UpdateInterval);
|
||||
|
||||
if (bShouldDisplay)
|
||||
{
|
||||
EnsureMaterial();
|
||||
ApplyTeamColor();
|
||||
ApplyTeamStyle();
|
||||
RebuildCone();
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_VisionConeComponent::ResolveVisionSource(FVector& OutOrigin, FVector& OutForward) const
|
||||
{
|
||||
OutOrigin = GetComponentLocation();
|
||||
// Only the yaw is used: the cone plane stays horizontal.
|
||||
OutForward = FRotator(0.f, GetComponentRotation().Yaw, 0.f).Vector();
|
||||
|
||||
if (!bUseOwnerVisionSource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AActor* Owner = GetOwner();
|
||||
const UWorld* World = GetWorld();
|
||||
if (!Owner || !World || !World->IsGameWorld() || !Owner->Implements<UPS_VisionSource>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FVector SourceLocation = FVector::ZeroVector;
|
||||
FVector SourceForward = FVector::ForwardVector;
|
||||
if (!IPS_VisionSource::Execute_GetVisionViewPoint(Owner, SourceLocation, SourceForward))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OutOrigin = SourceLocation;
|
||||
|
||||
const FVector FlatForward(SourceForward.X, SourceForward.Y, 0.f);
|
||||
if (!FlatForward.IsNearlyZero())
|
||||
{
|
||||
OutForward = FlatForward.GetSafeNormal();
|
||||
}
|
||||
// Otherwise (near-vertical gaze): keep the component yaw.
|
||||
}
|
||||
|
||||
void UPS_VisionConeComponent::EnsureConeMesh()
|
||||
{
|
||||
if (ConeMesh || !GetWorld())
|
||||
@@ -97,8 +194,9 @@ void UPS_VisionConeComponent::EnsureConeMesh()
|
||||
|
||||
ConeMesh = NewObject<UProceduralMeshComponent>(this, NAME_None, RF_Transient);
|
||||
ConeMesh->SetupAttachment(this);
|
||||
// Rotation absolue : le fan est construit en espace monde, seul le point
|
||||
// d'ancrage suit le personnage entre deux mises a jour.
|
||||
// Absolute transform: the fan is built in world space around the vision
|
||||
// origin, which can differ from the component transform.
|
||||
ConeMesh->SetUsingAbsoluteLocation(true);
|
||||
ConeMesh->SetUsingAbsoluteRotation(true);
|
||||
ConeMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
ConeMesh->SetGenerateOverlapEvents(false);
|
||||
@@ -106,7 +204,7 @@ void UPS_VisionConeComponent::EnsureConeMesh()
|
||||
ConeMesh->bReceivesDecals = false;
|
||||
ConeMesh->SetTranslucentSortPriority(100);
|
||||
ConeMesh->RegisterComponent();
|
||||
ConeMesh->SetWorldRotation(FQuat::Identity);
|
||||
ConeMesh->SetWorldLocationAndRotation(GetComponentLocation(), FQuat::Identity);
|
||||
}
|
||||
|
||||
void UPS_VisionConeComponent::EnsureMaterial()
|
||||
@@ -135,24 +233,41 @@ void UPS_VisionConeComponent::EnsureMaterial()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ConeMID || ConeMID->Parent != BaseMaterial)
|
||||
if (!FillMID || FillMID->Parent != BaseMaterial)
|
||||
{
|
||||
ConeMID = UMaterialInstanceDynamic::Create(BaseMaterial, this);
|
||||
FillMID = UMaterialInstanceDynamic::Create(BaseMaterial, this);
|
||||
}
|
||||
ConeMesh->SetMaterial(0, ConeMID);
|
||||
if (!OutlineMID || OutlineMID->Parent != BaseMaterial)
|
||||
{
|
||||
OutlineMID = UMaterialInstanceDynamic::Create(BaseMaterial, this);
|
||||
}
|
||||
ConeMesh->SetMaterial(0, FillMID);
|
||||
ConeMesh->SetMaterial(1, OutlineMID);
|
||||
}
|
||||
|
||||
void UPS_VisionConeComponent::ApplyTeamColor()
|
||||
void UPS_VisionConeComponent::ApplyTeamStyle()
|
||||
{
|
||||
if (!ConeMID)
|
||||
const FPS_StrategicTeamStyle Style = GetDefault<UPS_StrategicVisionSettings>()->GetTeamStyle(GetTeam());
|
||||
|
||||
if (FillMID)
|
||||
{
|
||||
return;
|
||||
FLinearColor FillColor = Style.Color;
|
||||
FillColor.A = Opacity;
|
||||
FillMID->SetVectorParameterValue(GColorParam, FillColor);
|
||||
FillMID->SetScalarParameterValue(GOpacityParam, Opacity);
|
||||
FillMID->SetScalarParameterValue(GHatchAngleParam, Style.HatchAngleDegrees);
|
||||
FillMID->SetScalarParameterValue(GHatchScaleParam, HatchingScale);
|
||||
FillMID->SetScalarParameterValue(GHatchIntensityParam, bHatchingEnabled ? HatchingIntensity : 0.f);
|
||||
}
|
||||
|
||||
FLinearColor Color = GetTeamColor();
|
||||
Color.A = Opacity;
|
||||
ConeMID->SetVectorParameterValue(GColorParamName, Color);
|
||||
ConeMID->SetScalarParameterValue(GOpacityParamName, Opacity);
|
||||
if (OutlineMID)
|
||||
{
|
||||
FLinearColor OutlineColor = Style.Color;
|
||||
OutlineColor.A = OutlineOpacity;
|
||||
OutlineMID->SetVectorParameterValue(GColorParam, OutlineColor);
|
||||
OutlineMID->SetScalarParameterValue(GOpacityParam, OutlineOpacity);
|
||||
OutlineMID->SetScalarParameterValue(GHatchIntensityParam, 0.f);
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_VisionConeComponent::RebuildCone()
|
||||
@@ -163,39 +278,38 @@ void UPS_VisionConeComponent::RebuildCone()
|
||||
return;
|
||||
}
|
||||
|
||||
const FVector CompLocation = GetComponentLocation();
|
||||
const FRotator CompRotation = GetComponentRotation();
|
||||
|
||||
const FVector Forward = bFlattenOnGround
|
||||
? FRotator(0.f, CompRotation.Yaw, 0.f).Vector()
|
||||
: GetForwardVector();
|
||||
const FVector Up = bFlattenOnGround ? FVector::UpVector : GetUpVector();
|
||||
|
||||
FCollisionQueryParams QueryParams(TEXT("PS_VisionCone"), bTraceComplex);
|
||||
if (const AActor* Owner = GetOwner())
|
||||
{
|
||||
QueryParams.AddIgnoredActor(Owner);
|
||||
}
|
||||
|
||||
// Hauteur de base du fan : projete au sol si demande, sinon a la hauteur du component.
|
||||
float BaseZ = GroundOffset;
|
||||
if (bFlattenOnGround)
|
||||
FVector Origin, Forward;
|
||||
ResolveVisionSource(Origin, Forward);
|
||||
|
||||
// Cone plane: always horizontal, projected onto the ground when requested.
|
||||
float PlaneZ = GetComponentLocation().Z + GroundOffset;
|
||||
if (bProjectToGround)
|
||||
{
|
||||
FHitResult GroundHit;
|
||||
const FVector GroundStart = CompLocation + FVector(0.f, 0.f, TraceHeight);
|
||||
const FVector GroundEnd = CompLocation - FVector(0.f, 0.f, 500.f);
|
||||
const FVector GroundStart(Origin.X, Origin.Y, Origin.Z + TraceHeight);
|
||||
const FVector GroundEnd(Origin.X, Origin.Y, Origin.Z - GGroundSearchDepth);
|
||||
if (World->LineTraceSingleByChannel(GroundHit, GroundStart, GroundEnd, TraceChannel, QueryParams))
|
||||
{
|
||||
BaseZ = (GroundHit.ImpactPoint.Z - CompLocation.Z) + GroundOffset;
|
||||
PlaneZ = GroundHit.ImpactPoint.Z + GroundOffset;
|
||||
}
|
||||
}
|
||||
|
||||
// The mesh is anchored on the cone plane; vertices are local to that point.
|
||||
ConeMesh->SetWorldLocation(FVector(Origin.X, Origin.Y, PlaneZ));
|
||||
|
||||
const int32 RayCount = FMath::Clamp(ArcSegments, 4, 256) + 1;
|
||||
const float HalfAngle = FMath::Clamp(ViewAngleDegrees, 1.f, 360.f) * 0.5f;
|
||||
const FVector TraceOrigin = CompLocation + FVector(0.f, 0.f, TraceHeight);
|
||||
const FVector TraceOrigin = Origin + FVector(0.f, 0.f, TraceHeight);
|
||||
|
||||
FLinearColor VertexColor = GetTeamColor();
|
||||
VertexColor.A = Opacity;
|
||||
const FLinearColor TeamColor = GetTeamColor();
|
||||
FLinearColor FillColor = TeamColor;
|
||||
FillColor.A = Opacity;
|
||||
|
||||
TArray<FVector> Vertices;
|
||||
TArray<FVector> Normals;
|
||||
@@ -206,17 +320,17 @@ void UPS_VisionConeComponent::RebuildCone()
|
||||
UVs.Reserve(RayCount + 1);
|
||||
Colors.Reserve(RayCount + 1);
|
||||
|
||||
// Sommet central (origine du cone).
|
||||
Vertices.Add(bFlattenOnGround ? FVector(0.f, 0.f, BaseZ) : Up * GroundOffset);
|
||||
Normals.Add(Up);
|
||||
// Center vertex (cone origin).
|
||||
Vertices.Add(FVector::ZeroVector);
|
||||
Normals.Add(FVector::UpVector);
|
||||
UVs.Add(FVector2D(0.5f, 0.f));
|
||||
Colors.Add(VertexColor);
|
||||
Colors.Add(FillColor);
|
||||
|
||||
for (int32 RayIndex = 0; RayIndex < RayCount; ++RayIndex)
|
||||
{
|
||||
const float Alpha = static_cast<float>(RayIndex) / static_cast<float>(RayCount - 1);
|
||||
const float AngleDeg = FMath::Lerp(-HalfAngle, HalfAngle, Alpha);
|
||||
const FVector Direction = Forward.RotateAngleAxis(AngleDeg, Up);
|
||||
const FVector Direction = Forward.RotateAngleAxis(AngleDeg, FVector::UpVector);
|
||||
|
||||
float HitDistance = ViewDistance;
|
||||
FHitResult Hit;
|
||||
@@ -225,46 +339,108 @@ void UPS_VisionConeComponent::RebuildCone()
|
||||
HitDistance = Hit.Distance;
|
||||
}
|
||||
|
||||
FVector LocalPoint = Direction * HitDistance;
|
||||
if (bFlattenOnGround)
|
||||
{
|
||||
LocalPoint.Z = BaseZ;
|
||||
}
|
||||
else
|
||||
{
|
||||
LocalPoint += Up * GroundOffset;
|
||||
}
|
||||
|
||||
Vertices.Add(LocalPoint);
|
||||
Normals.Add(Up);
|
||||
Vertices.Add(FVector(Direction.X * HitDistance, Direction.Y * HitDistance, 0.f));
|
||||
Normals.Add(FVector::UpVector);
|
||||
UVs.Add(FVector2D(Alpha, HitDistance / ViewDistance));
|
||||
Colors.Add(VertexColor);
|
||||
Colors.Add(FillColor);
|
||||
}
|
||||
|
||||
const TArray<FProcMeshTangent> NoTangents;
|
||||
|
||||
if (Vertices.Num() == LastVertexCount)
|
||||
if (Vertices.Num() == LastFillVertexCount)
|
||||
{
|
||||
// Meme topologie : mise a jour des sommets uniquement (moins couteux).
|
||||
// Same topology: only update the vertices (cheaper).
|
||||
ConeMesh->UpdateMeshSection_LinearColor(0, Vertices, Normals, UVs, Colors, NoTangents);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Triangles dans les deux sens pour rester visible sous tous les angles de camera.
|
||||
// Triangles in both windings to stay visible from any camera angle.
|
||||
TArray<int32> Triangles;
|
||||
Triangles.Reserve((RayCount - 1) * 6);
|
||||
for (int32 SegmentIndex = 0; SegmentIndex < RayCount - 1; ++SegmentIndex)
|
||||
{
|
||||
Triangles.Add(0);
|
||||
Triangles.Add(1 + SegmentIndex);
|
||||
Triangles.Add(2 + SegmentIndex);
|
||||
|
||||
Triangles.Add(0);
|
||||
Triangles.Add(2 + SegmentIndex);
|
||||
Triangles.Add(1 + SegmentIndex);
|
||||
Triangles.Append({ 0, 1 + SegmentIndex, 2 + SegmentIndex });
|
||||
Triangles.Append({ 0, 2 + SegmentIndex, 1 + SegmentIndex });
|
||||
}
|
||||
|
||||
ConeMesh->CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UVs, Colors, NoTangents, false);
|
||||
LastVertexCount = Vertices.Num();
|
||||
LastFillVertexCount = Vertices.Num();
|
||||
}
|
||||
|
||||
if (bShowOutline && OutlineWidth > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
BuildOutlineSection(Vertices, TeamColor);
|
||||
}
|
||||
else if (LastOutlineVertexCount > 0)
|
||||
{
|
||||
ConeMesh->ClearMeshSection(1);
|
||||
LastOutlineVertexCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_VisionConeComponent::BuildOutlineSection(const TArray<FVector>& FillVertices, const FLinearColor& TeamColor)
|
||||
{
|
||||
// Closed cone perimeter: center + arc (the fill already provides these points).
|
||||
const int32 NumPerimeter = FillVertices.Num();
|
||||
if (NumPerimeter < 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FVector2D> Perimeter;
|
||||
Perimeter.Reserve(NumPerimeter);
|
||||
for (const FVector& Vertex : FillVertices)
|
||||
{
|
||||
Perimeter.Add(FVector2D(Vertex.X, Vertex.Y));
|
||||
}
|
||||
|
||||
TArray<FVector2D> Inner;
|
||||
InsetPolygon(Perimeter, OutlineWidth, Inner);
|
||||
|
||||
FLinearColor OutlineColor = TeamColor;
|
||||
OutlineColor.A = OutlineOpacity;
|
||||
|
||||
TArray<FVector> Vertices;
|
||||
TArray<FVector> Normals;
|
||||
TArray<FVector2D> UVs;
|
||||
TArray<FLinearColor> Colors;
|
||||
Vertices.Reserve(NumPerimeter * 2);
|
||||
Normals.Reserve(NumPerimeter * 2);
|
||||
UVs.Reserve(NumPerimeter * 2);
|
||||
Colors.Reserve(NumPerimeter * 2);
|
||||
|
||||
for (int32 Index = 0; Index < NumPerimeter; ++Index)
|
||||
{
|
||||
const float U = static_cast<float>(Index) / static_cast<float>(NumPerimeter - 1);
|
||||
|
||||
Vertices.Add(FVector(Perimeter[Index].X, Perimeter[Index].Y, GOutlineZLift));
|
||||
Normals.Add(FVector::UpVector);
|
||||
UVs.Add(FVector2D(U, 0.f));
|
||||
Colors.Add(OutlineColor);
|
||||
|
||||
Vertices.Add(FVector(Inner[Index].X, Inner[Index].Y, GOutlineZLift));
|
||||
Normals.Add(FVector::UpVector);
|
||||
UVs.Add(FVector2D(U, 1.f));
|
||||
Colors.Add(OutlineColor);
|
||||
}
|
||||
|
||||
const TArray<FProcMeshTangent> NoTangents;
|
||||
|
||||
if (Vertices.Num() == LastOutlineVertexCount)
|
||||
{
|
||||
ConeMesh->UpdateMeshSection_LinearColor(1, Vertices, Normals, UVs, Colors, NoTangents);
|
||||
}
|
||||
else
|
||||
{
|
||||
TArray<int32> Triangles;
|
||||
Triangles.Reserve(NumPerimeter * 12);
|
||||
for (int32 Index = 0; Index < NumPerimeter; ++Index)
|
||||
{
|
||||
const int32 NextIndex = (Index + 1) % NumPerimeter;
|
||||
AddDoubleSidedQuad(Triangles, Index * 2, Index * 2 + 1, NextIndex * 2, NextIndex * 2 + 1);
|
||||
}
|
||||
|
||||
ConeMesh->CreateMeshSection_LinearColor(1, Vertices, Triangles, Normals, UVs, Colors, NoTangents, false);
|
||||
LastOutlineVertexCount = Vertices.Num();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#include "PS_StrategicDisplayComponent.generated.h"
|
||||
|
||||
/**
|
||||
* Classe de base des affichages strategiques poses sur les personnages
|
||||
* (cone de vision, et autres visualisations a venir).
|
||||
* Base class of the strategic displays placed on characters
|
||||
* (vision cone, and other visualizations to come).
|
||||
*
|
||||
* Gere le camp (couleur), l'activation locale du component et la
|
||||
* synchronisation avec l'etat global du UPS_StrategicVisionSubsystem.
|
||||
* Handles the team (color), the local activation of the component and the
|
||||
* synchronization with the global state of the UPS_StrategicVisionSubsystem.
|
||||
*/
|
||||
UCLASS(Abstract, ClassGroup = (PS_StrategicVision), HideCategories = (Physics, Collision, Mobility))
|
||||
class PS_STRATEGICVISION_API UPS_StrategicDisplayComponent : public USceneComponent
|
||||
@@ -22,29 +22,29 @@ class PS_STRATEGICVISION_API UPS_StrategicDisplayComponent : public USceneCompon
|
||||
public:
|
||||
UPS_StrategicDisplayComponent();
|
||||
|
||||
/** Change le camp (met a jour la couleur de l'affichage). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Strategic Display")
|
||||
/** Changes the team (updates the display color). */
|
||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Display")
|
||||
void SetTeam(EPS_StrategicTeam NewTeam);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Strategic Display")
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
|
||||
EPS_StrategicTeam GetTeam() const { return Team; }
|
||||
|
||||
/** Active / desactive cet affichage precis (independamment de l'etat global). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Strategic Display")
|
||||
/** Enables / disables this specific display (independently of the global state). */
|
||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Display")
|
||||
void SetDisplayEnabled(bool bEnabled);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Strategic Display")
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
|
||||
bool IsDisplayEnabled() const { return bDisplayEnabled; }
|
||||
|
||||
/** Visibilite effective : activation locale ET etat global du subsystem. */
|
||||
UFUNCTION(BlueprintPure, Category = "Strategic Display")
|
||||
/** Effective visibility: local activation AND global subsystem state. */
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
|
||||
bool IsDisplayVisible() const;
|
||||
|
||||
/** Couleur du camp courant, lue dans les settings du plugin. */
|
||||
UFUNCTION(BlueprintPure, Category = "Strategic Display")
|
||||
/** Color of the current team, read from the plugin settings. */
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
|
||||
FLinearColor GetTeamColor() const;
|
||||
|
||||
/** Categorie declaree par chaque type d'affichage (cf. FPS_StrategicDisplayCategories). */
|
||||
/** Category declared by each display type (see FPS_StrategicDisplayCategories). */
|
||||
virtual FName GetDisplayCategory() const { return NAME_None; }
|
||||
|
||||
protected:
|
||||
@@ -55,17 +55,17 @@ protected:
|
||||
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||
#endif
|
||||
|
||||
/** Appele quand la visibilite effective ou le camp change. */
|
||||
/** Called when the effective visibility or the team changes. */
|
||||
virtual void OnDisplayStateChanged() {}
|
||||
|
||||
/** Reevalue la visibilite effective et previent la classe fille. */
|
||||
/** Re-evaluates the effective visibility and notifies the child class. */
|
||||
void RefreshDisplayState();
|
||||
|
||||
/** Camp du personnage porteur, determine la couleur de l'affichage. */
|
||||
/** Team of the owning character, drives the display color. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strategic Display")
|
||||
EPS_StrategicTeam Team = EPS_StrategicTeam::Enemy;
|
||||
|
||||
/** Etat d'activation local de cet affichage. */
|
||||
/** Local activation state of this display. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strategic Display")
|
||||
bool bDisplayEnabled = true;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -9,9 +9,27 @@
|
||||
|
||||
class UMaterialInterface;
|
||||
|
||||
/** Display style of a team. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct PS_STRATEGICVISION_API FPS_StrategicTeamStyle
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Team color. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style")
|
||||
FLinearColor Color = FLinearColor::White;
|
||||
|
||||
/**
|
||||
* Procedural hatching angle, in degrees. Giving each team a different
|
||||
* angle keeps overlapping cones readable.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (ClampMin = "0.0", ClampMax = "180.0"))
|
||||
float HatchAngleDegrees = 0.f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reglages du plugin PS Strategic Vision.
|
||||
* Accessibles dans Project Settings > Plugins > PS Strategic Vision.
|
||||
* PS Strategic Vision plugin settings.
|
||||
* Available in Project Settings > Plugins > PS Strategic Vision.
|
||||
*/
|
||||
UCLASS(Config = Game, DefaultConfig, meta = (DisplayName = "PS Strategic Vision"))
|
||||
class PS_STRATEGICVISION_API UPS_StrategicVisionSettings : public UDeveloperSettings
|
||||
@@ -23,20 +41,34 @@ public:
|
||||
|
||||
virtual FName GetCategoryName() const override { return TEXT("Plugins"); }
|
||||
|
||||
/** Couleur associee a chaque camp. */
|
||||
/**
|
||||
* If false (default), no strategic display is visible when a game starts:
|
||||
* activation is done explicitly through the UPS_StrategicVisionSubsystem
|
||||
* (spectator view, replay...).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Config, BlueprintReadOnly, Category = "Activation")
|
||||
bool bEnabledAtStartup = false;
|
||||
|
||||
/** Style (color, hatching) associated with each team. */
|
||||
UPROPERTY(EditAnywhere, Config, BlueprintReadOnly, Category = "Teams")
|
||||
TMap<EPS_StrategicTeam, FLinearColor> TeamColors;
|
||||
TMap<EPS_StrategicTeam, FPS_StrategicTeamStyle> TeamStyles;
|
||||
|
||||
/**
|
||||
* Materiau par defaut des cones de vision. La couleur est appliquee via le
|
||||
* parametre vecteur "Color" (et le parametre scalaire "Opacity" s'il existe),
|
||||
* ainsi que via les vertex colors du mesh.
|
||||
* Si vide ou introuvable, un materiau translucide du moteur est utilise.
|
||||
* Default material of the vision cones. The plugin ships
|
||||
* /PS_StrategicVision/Materials/M_VisionCone (translucent, procedural
|
||||
* hatching). Expected parameters: vector "Color", scalars "Opacity",
|
||||
* "HatchAngle", "HatchScale", "HatchDuty", "HatchIntensity"; the color is
|
||||
* also provided through the mesh vertex colors.
|
||||
* If empty or missing, a translucent engine material is used instead.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Config, BlueprintReadOnly, Category = "Rendering")
|
||||
TSoftObjectPtr<UMaterialInterface> DefaultConeMaterial;
|
||||
|
||||
/** Couleur du camp demande (blanc si non configuree). */
|
||||
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision")
|
||||
/** Color of the requested team (white if not configured). */
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
|
||||
FLinearColor GetTeamColor(EPS_StrategicTeam Team) const;
|
||||
|
||||
/** Full style of the requested team (default style if not configured). */
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
|
||||
FPS_StrategicTeamStyle GetTeamStyle(EPS_StrategicTeam Team) const;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPS_StrategicDisplayStateChanged);
|
||||
|
||||
/**
|
||||
* Subsystem monde pilotant l'affichage strategique global.
|
||||
* Permet d'activer / desactiver tous les affichages, une categorie (ex : cones
|
||||
* de vision) ou un camp entier selon l'etat de jeu (vue spectateur, replay...).
|
||||
* Tous les UPS_StrategicDisplayComponent s'y abonnent automatiquement.
|
||||
* World subsystem driving the global strategic display state.
|
||||
* Allows enabling / disabling every display, one category (e.g. vision cones)
|
||||
* or a whole team depending on the game state (spectator view, replay...).
|
||||
* Every UPS_StrategicDisplayComponent subscribes to it automatically.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_STRATEGICVISION_API UPS_StrategicVisionSubsystem : public UWorldSubsystem
|
||||
@@ -21,52 +21,55 @@ class PS_STRATEGICVISION_API UPS_StrategicVisionSubsystem : public UWorldSubsyst
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Raccourci d'acces au subsystem depuis n'importe quel contexte monde. */
|
||||
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision", meta = (WorldContext = "WorldContextObject"))
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
|
||||
/** Shortcut to access the subsystem from any world context. */
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision", meta = (WorldContext = "WorldContextObject"))
|
||||
static UPS_StrategicVisionSubsystem* Get(const UObject* WorldContextObject);
|
||||
|
||||
/** Interrupteur general de tous les affichages strategiques. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Strategic Vision")
|
||||
/** Master switch of every strategic display. */
|
||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision")
|
||||
void SetGlobalDisplayEnabled(bool bEnabled);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision")
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
|
||||
bool IsGlobalDisplayEnabled() const { return bGlobalEnabled; }
|
||||
|
||||
/** Active / desactive une categorie d'affichage (ex : "VisionCone"). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Strategic Vision")
|
||||
/** Enables / disables one display category (e.g. "VisionCone"). */
|
||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision")
|
||||
void SetCategoryEnabled(FName Category, bool bEnabled);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision")
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
|
||||
bool IsCategoryEnabled(FName Category) const { return !DisabledCategories.Contains(Category); }
|
||||
|
||||
/** Active / desactive les affichages d'un camp (ex : ne montrer que les ennemis). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Strategic Vision")
|
||||
/** Enables / disables the displays of one team (e.g. only show enemies). */
|
||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision")
|
||||
void SetTeamEnabled(EPS_StrategicTeam Team, bool bEnabled);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision")
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
|
||||
bool IsTeamEnabled(EPS_StrategicTeam Team) const { return !DisabledTeams.Contains(Team); }
|
||||
|
||||
/** Raccourci : active / desactive tous les cones de vision. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Strategic Vision|Vision Cone")
|
||||
/** Shortcut: enables / disables every vision cone. */
|
||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision|Vision Cone")
|
||||
void SetVisionConesEnabled(bool bEnabled) { SetCategoryEnabled(FPS_StrategicDisplayCategories::VisionCone, bEnabled); }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision|Vision Cone")
|
||||
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision|Vision Cone")
|
||||
bool AreVisionConesEnabled() const { return IsCategoryEnabled(FPS_StrategicDisplayCategories::VisionCone); }
|
||||
|
||||
/** Etat combine utilise par les components pour savoir s'ils doivent s'afficher. */
|
||||
/** Combined state used by the components to know whether they should be shown. */
|
||||
bool ShouldDisplay(FName Category, EPS_StrategicTeam Team) const
|
||||
{
|
||||
return bGlobalEnabled && IsCategoryEnabled(Category) && IsTeamEnabled(Team);
|
||||
}
|
||||
|
||||
/** Notifie a chaque changement d'etat global (les components s'y abonnent). */
|
||||
UPROPERTY(BlueprintAssignable, Category = "PS Strategic Vision")
|
||||
/** Fired on every global state change (components subscribe to it). */
|
||||
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Strategic Vision")
|
||||
FPS_StrategicDisplayStateChanged OnDisplayStateChanged;
|
||||
|
||||
private:
|
||||
void NotifyStateChanged();
|
||||
|
||||
bool bGlobalEnabled = true;
|
||||
// Initial state read from UPS_StrategicVisionSettings::bEnabledAtStartup (false by default).
|
||||
bool bGlobalEnabled = false;
|
||||
TSet<FName> DisabledCategories;
|
||||
TSet<EPS_StrategicTeam> DisabledTeams;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "PS_StrategicVisionTypes.generated.h"
|
||||
|
||||
/** Camp d'un personnage, utilise pour colorer / filtrer les affichages strategiques. */
|
||||
/** Team of a character, used to color / filter the strategic displays. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_StrategicTeam : uint8
|
||||
{
|
||||
@@ -16,8 +16,8 @@ enum class EPS_StrategicTeam : uint8
|
||||
};
|
||||
|
||||
/**
|
||||
* Categories d'affichage strategique. Chaque type de component declare sa categorie,
|
||||
* ce qui permet de l'activer / desactiver globalement via le subsystem.
|
||||
* Strategic display categories. Each component type declares its category,
|
||||
* which allows enabling / disabling it globally through the subsystem.
|
||||
*/
|
||||
struct PS_STRATEGICVISION_API FPS_StrategicDisplayCategories
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2026 j.foucher. All rights reserved.
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -12,13 +12,15 @@ class UMaterialInterface;
|
||||
class UProceduralMeshComponent;
|
||||
|
||||
/**
|
||||
* Affiche le cone de vision d'un personnage sous forme de mesh procedural.
|
||||
* Le cone est decoupe par les obstacles (murs) via des line traces, colore
|
||||
* selon le camp, et activable / desactivable localement ou globalement
|
||||
* (vue spectateur, replay) via le UPS_StrategicVisionSubsystem.
|
||||
* Displays the vision cone of a character as a procedural mesh.
|
||||
* The cone is cut by obstacles (walls) through line traces, colored by team,
|
||||
* and its plane always stays horizontal whatever the actor tilt is.
|
||||
* Hidden by default: activation is done through the
|
||||
* UPS_StrategicVisionSubsystem (spectator view, replay).
|
||||
*
|
||||
* A attacher sur le personnage : le cone suit la position du component et
|
||||
* s'oriente selon son yaw (rotation horizontale).
|
||||
* The vision origin and direction come from the component transform, or from
|
||||
* the PS_VisionSource interface when the owning actor implements it
|
||||
* (e.g. a VR pawn whose camera moves under a static actor).
|
||||
*/
|
||||
UCLASS(ClassGroup = (PS_StrategicVision), meta = (BlueprintSpawnableComponent, DisplayName = "PS Vision Cone"))
|
||||
class PS_STRATEGICVISION_API UPS_VisionConeComponent : public UPS_StrategicDisplayComponent
|
||||
@@ -30,57 +32,97 @@ public:
|
||||
|
||||
virtual FName GetDisplayCategory() const override { return FPS_StrategicDisplayCategories::VisionCone; }
|
||||
|
||||
/** Portee de vision, en cm. */
|
||||
/** Vision range, in cm. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (ClampMin = "10.0", Units = "cm"))
|
||||
float ViewDistance = 1500.f;
|
||||
|
||||
/** Angle total du cone, en degres (ex : 90 = 45 de chaque cote). */
|
||||
/** Total cone angle, in degrees (e.g. 90 = 45 on each side). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (ClampMin = "1.0", ClampMax = "360.0", Units = "deg"))
|
||||
float ViewAngleDegrees = 90.f;
|
||||
|
||||
/** Opacite du cone (0 = invisible, 1 = opaque). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float Opacity = 0.35f;
|
||||
|
||||
/**
|
||||
* Si vrai, le cone reste a plat au sol : seul le yaw du component est pris
|
||||
* en compte et le mesh est projete a la hauteur du sol detecte.
|
||||
* If true and the owning actor implements the PS_VisionSource interface,
|
||||
* the vision origin and direction come from the interface (e.g. VR camera)
|
||||
* instead of this component transform.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone")
|
||||
bool bFlattenOnGround = true;
|
||||
bool bUseOwnerVisionSource = true;
|
||||
|
||||
/** Decalage vertical du mesh au-dessus du sol, pour eviter le z-fighting. */
|
||||
/** If true, the cone is drawn at the height of the ground detected under the vision origin. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone")
|
||||
bool bProjectToGround = true;
|
||||
|
||||
/** Vertical offset of the mesh above the ground, to avoid z-fighting. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (Units = "cm"))
|
||||
float GroundOffset = 3.f;
|
||||
|
||||
/** Nombre de segments de l'arc : plus eleve = plus precis mais plus de traces. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Quality", meta = (ClampMin = "4", ClampMax = "256"))
|
||||
int32 ArcSegments = 48;
|
||||
// --- Rendering ---
|
||||
|
||||
/** Intervalle de mise a jour en secondes (0 = chaque frame). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Quality", meta = (ClampMin = "0.0", Units = "s"))
|
||||
float UpdateInterval = 0.f;
|
||||
/** Opacity of the cone fill (0 = invisible, 1 = opaque). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float Opacity = 0.35f;
|
||||
|
||||
/** Canal de collision utilise pour bloquer le cone sur les obstacles. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision")
|
||||
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
|
||||
/** Draws a more opaque outline around the cone (readability when cones overlap). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering")
|
||||
bool bShowOutline = true;
|
||||
|
||||
/** Trace contre la geometrie complexe (plus precis, plus couteux). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision")
|
||||
bool bTraceComplex = false;
|
||||
/** Outline width, in cm. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.5", Units = "cm", EditCondition = "bShowOutline"))
|
||||
float OutlineWidth = 6.f;
|
||||
|
||||
/** Hauteur (relative au component) a laquelle les traces de vision sont lancees. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision", meta = (Units = "cm"))
|
||||
float TraceHeight = 50.f;
|
||||
/** Outline opacity. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.0", ClampMax = "1.0", EditCondition = "bShowOutline"))
|
||||
float OutlineOpacity = 0.9f;
|
||||
|
||||
/**
|
||||
* Materiau du cone pour ce component (prioritaire sur celui des settings).
|
||||
* La couleur du camp est envoyee dans le parametre vecteur "Color", l'opacite
|
||||
* dans le parametre scalaire "Opacity", et dans les vertex colors du mesh.
|
||||
* Procedural hatching in the fill: the angle depends on the team
|
||||
* (plugin settings), which keeps overlapping cones readable.
|
||||
* Requires the plugin M_VisionCone material (or a compatible one).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering")
|
||||
bool bHatchingEnabled = true;
|
||||
|
||||
/** Hatching spacing, in cm. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "1.0", Units = "cm", EditCondition = "bHatchingEnabled"))
|
||||
float HatchingScale = 30.f;
|
||||
|
||||
/** Hatching intensity (0 = flat fill, 1 = hard stripes). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.0", ClampMax = "1.0", EditCondition = "bHatchingEnabled"))
|
||||
float HatchingIntensity = 0.5f;
|
||||
|
||||
/**
|
||||
* Cone material for this component (takes precedence over the settings one).
|
||||
* Provided parameters: vector "Color" (alpha = opacity), scalars "Opacity",
|
||||
* "HatchAngle", "HatchScale", "HatchIntensity"; the color is also written
|
||||
* into the mesh vertex colors.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering")
|
||||
TObjectPtr<UMaterialInterface> MaterialOverride;
|
||||
|
||||
// --- Quality ---
|
||||
|
||||
/** Number of arc segments: higher = more precise but more traces. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Quality", meta = (ClampMin = "4", ClampMax = "256"))
|
||||
int32 ArcSegments = 48;
|
||||
|
||||
/** Update interval in seconds (0 = every frame). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Quality", meta = (ClampMin = "0.0", Units = "s"))
|
||||
float UpdateInterval = 0.f;
|
||||
|
||||
// --- Collision ---
|
||||
|
||||
/** Collision channel used to block the cone on obstacles. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision")
|
||||
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
|
||||
|
||||
/** Traces against complex geometry (more precise, more expensive). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision")
|
||||
bool bTraceComplex = false;
|
||||
|
||||
/** Height (relative to the vision origin) at which the traces are cast. Set to 0 if the origin is already at eye level (VR camera). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision", meta = (Units = "cm"))
|
||||
float TraceHeight = 50.f;
|
||||
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
protected:
|
||||
@@ -90,17 +132,25 @@ protected:
|
||||
virtual void OnDisplayStateChanged() override;
|
||||
|
||||
private:
|
||||
/** Mesh procedural interne portant le fan du cone. */
|
||||
/** Internal procedural mesh: section 0 = fill, section 1 = outline. */
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UProceduralMeshComponent> ConeMesh;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UMaterialInstanceDynamic> ConeMID;
|
||||
TObjectPtr<UMaterialInstanceDynamic> FillMID;
|
||||
|
||||
int32 LastVertexCount = 0;
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UMaterialInstanceDynamic> OutlineMID;
|
||||
|
||||
int32 LastFillVertexCount = 0;
|
||||
int32 LastOutlineVertexCount = 0;
|
||||
|
||||
/** Vision origin and (horizontal) direction: PS_VisionSource interface or component transform. */
|
||||
void ResolveVisionSource(FVector& OutOrigin, FVector& OutForward) const;
|
||||
|
||||
void EnsureConeMesh();
|
||||
void EnsureMaterial();
|
||||
void ApplyTeamColor();
|
||||
void ApplyTeamStyle();
|
||||
void RebuildCone();
|
||||
void BuildOutlineSection(const TArray<FVector>& FillVertices, const FLinearColor& TeamColor);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "PS_VisionSourceInterface.generated.h"
|
||||
|
||||
UINTERFACE(BlueprintType, Blueprintable)
|
||||
class PS_STRATEGICVISION_API UPS_VisionSource : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Implement this (C++ or Blueprint) on an actor to provide the real vision
|
||||
* origin and direction when the actor transform does not reflect them.
|
||||
* Typical case: a VR pawn whose actor stays at the origin while the camera
|
||||
* moves underneath — return the camera position/direction here.
|
||||
*/
|
||||
class PS_STRATEGICVISION_API IPS_VisionSource
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Vision origin and direction to use for the strategic displays.
|
||||
* Return false to fall back to the component transform (default behavior).
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|Strategic Vision")
|
||||
bool GetVisionViewPoint(FVector& OutLocation, FVector& OutForwardDirection);
|
||||
|
||||
virtual bool GetVisionViewPoint_Implementation(FVector& OutLocation, FVector& OutForwardDirection) { return false; }
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
# Copyright (c) 2026 ASTERION VR. All rights reserved.
|
||||
#
|
||||
# Generates the /PS_StrategicVision/Materials/M_VisionCone material:
|
||||
# translucent, unlit, colored through vertex color, with world-space
|
||||
# procedural hatching (parameters: HatchAngle, HatchScale, HatchDuty,
|
||||
# HatchIntensity).
|
||||
#
|
||||
# Run from the editor (PythonScriptPlugin enabled):
|
||||
# UnrealEditor-Cmd.exe <project.uproject> -run=pythonscript -script="<this file>"
|
||||
|
||||
import unreal
|
||||
|
||||
PKG_DIR = "/PS_StrategicVision/Materials"
|
||||
NAME = "M_VisionCone"
|
||||
FULL_PATH = f"{PKG_DIR}/{NAME}"
|
||||
|
||||
mel = unreal.MaterialEditingLibrary
|
||||
asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
|
||||
|
||||
if asset_subsystem.does_asset_exist(FULL_PATH):
|
||||
unreal.log(f"[PS_StrategicVision] Deleting previous {FULL_PATH}")
|
||||
asset_subsystem.delete_asset(FULL_PATH)
|
||||
|
||||
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||
material = asset_tools.create_asset(NAME, PKG_DIR, unreal.Material, unreal.MaterialFactoryNew())
|
||||
if material is None:
|
||||
raise SystemExit("[PS_StrategicVision] FAILED: create_asset returned None")
|
||||
|
||||
material.set_editor_property("shading_model", unreal.MaterialShadingModel.MSM_UNLIT)
|
||||
material.set_editor_property("blend_mode", unreal.BlendMode.BLEND_TRANSLUCENT)
|
||||
|
||||
|
||||
def scalar_param(name, value, x, y):
|
||||
param = mel.create_material_expression(material, unreal.MaterialExpressionScalarParameter, x, y)
|
||||
param.set_editor_property("parameter_name", name)
|
||||
param.set_editor_property("default_value", value)
|
||||
return param
|
||||
|
||||
|
||||
vertex_color = mel.create_material_expression(material, unreal.MaterialExpressionVertexColor, -700, -200)
|
||||
world_pos = mel.create_material_expression(material, unreal.MaterialExpressionWorldPosition, -1050, 40)
|
||||
hatch_angle = scalar_param("HatchAngle", 45.0, -1050, 160)
|
||||
hatch_scale = scalar_param("HatchScale", 30.0, -1050, 260)
|
||||
hatch_duty = scalar_param("HatchDuty", 0.5, -1050, 360)
|
||||
hatch_intensity = scalar_param("HatchIntensity", 0.0, -1050, 460)
|
||||
|
||||
# Procedural hatching: world-space (XY) oriented stripes.
|
||||
custom = mel.create_material_expression(material, unreal.MaterialExpressionCustom, -650, 150)
|
||||
custom.set_editor_property("description", "ProceduralHatch")
|
||||
custom.set_editor_property("output_type", unreal.CustomMaterialOutputType.CMOT_FLOAT1)
|
||||
custom.set_editor_property(
|
||||
"code",
|
||||
"float a = radians(AngleDeg);\n"
|
||||
"float2 dir = float2(cos(a), sin(a));\n"
|
||||
"float d = dot(WorldPos.xy, dir) / max(Scale, 1.0);\n"
|
||||
"float stripe = step(frac(d), saturate(Duty));\n"
|
||||
"return lerp(1.0, stripe, saturate(Intensity));\n",
|
||||
)
|
||||
|
||||
inputs = []
|
||||
for input_name in ("WorldPos", "AngleDeg", "Scale", "Duty", "Intensity"):
|
||||
custom_input = unreal.CustomInput()
|
||||
custom_input.set_editor_property("input_name", input_name)
|
||||
inputs.append(custom_input)
|
||||
custom.set_editor_property("inputs", inputs)
|
||||
|
||||
# Final opacity = vertex color alpha * hatching factor.
|
||||
multiply = mel.create_material_expression(material, unreal.MaterialExpressionMultiply, -300, 60)
|
||||
|
||||
connections = [
|
||||
mel.connect_material_expressions(world_pos, "", custom, "WorldPos"),
|
||||
mel.connect_material_expressions(hatch_angle, "", custom, "AngleDeg"),
|
||||
mel.connect_material_expressions(hatch_scale, "", custom, "Scale"),
|
||||
mel.connect_material_expressions(hatch_duty, "", custom, "Duty"),
|
||||
mel.connect_material_expressions(hatch_intensity, "", custom, "Intensity"),
|
||||
mel.connect_material_expressions(vertex_color, "A", multiply, "A"),
|
||||
mel.connect_material_expressions(custom, "", multiply, "B"),
|
||||
mel.connect_material_property(vertex_color, "", unreal.MaterialProperty.MP_EMISSIVE_COLOR),
|
||||
mel.connect_material_property(multiply, "", unreal.MaterialProperty.MP_OPACITY),
|
||||
]
|
||||
|
||||
if not all(connections):
|
||||
raise SystemExit(f"[PS_StrategicVision] FAILED: graph connections = {connections}")
|
||||
|
||||
mel.recompile_material(material)
|
||||
|
||||
if not asset_subsystem.save_asset(FULL_PATH, only_if_is_dirty=False):
|
||||
raise SystemExit("[PS_StrategicVision] FAILED: save_asset")
|
||||
|
||||
unreal.log(f"[PS_StrategicVision] OK: {FULL_PATH} generated and saved")
|
||||
Reference in New Issue
Block a user