From 2fd45af39a045b33dfd1c86e4db08091aaad3392 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Wed, 22 Jul 2026 13:24:05 +0200 Subject: [PATCH] Integre le plugin directement dans le repo unique (integration par lien symbolique) Suppression du depot git imbrique du plugin : les fichiers du plugin sont suivis normalement ici. L'integration dans les autres projets se fait via un lien symbolique Windows vers Plugins/PS_StrategicVision. Co-Authored-By: Claude Fable 5 --- .../PS_StratVision/Plugins/PS_StrategicVision | 1 - .../Plugins/PS_StrategicVision/.gitignore | 3 + .../Content/Materials/M_VisionCone.uasset | Bin 0 -> 9977 bytes .../PS_StrategicVision.uplugin | 30 ++ .../Plugins/PS_StrategicVision/README.md | 126 +++++ .../PS_StrategicVision.Build.cs | 18 + .../Private/PS_StrategicDisplayComponent.cpp | 97 ++++ .../Private/PS_StrategicVisionModule.cpp | 5 + .../Private/PS_StrategicVisionSettings.cpp | 42 ++ .../Private/PS_StrategicVisionSubsystem.cpp | 65 +++ .../Private/PS_StrategicVisionTypes.cpp | 5 + .../Private/PS_VisionConeComponent.cpp | 446 ++++++++++++++++++ .../Public/PS_StrategicDisplayComponent.h | 75 +++ .../Public/PS_StrategicVisionSettings.h | 74 +++ .../Public/PS_StrategicVisionSubsystem.h | 75 +++ .../Public/PS_StrategicVisionTypes.h | 25 + .../Public/PS_VisionConeComponent.h | 156 ++++++ .../Public/PS_VisionSourceInterface.h | 34 ++ .../Tools/generate_vision_cone_material.py | 90 ++++ 19 files changed, 1366 insertions(+), 1 deletion(-) delete mode 160000 Unreal/PS_StratVision/Plugins/PS_StrategicVision create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/.gitignore create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Content/Materials/M_VisionCone.uasset create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/PS_StrategicVision.uplugin create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/README.md create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/PS_StrategicVision.Build.cs create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicDisplayComponent.cpp create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionModule.cpp create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionSettings.cpp create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionSubsystem.cpp create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionTypes.cpp create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_VisionConeComponent.cpp create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicDisplayComponent.h create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionSettings.h create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionSubsystem.h create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionTypes.h create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_VisionConeComponent.h create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_VisionSourceInterface.h create mode 100644 Unreal/PS_StratVision/Plugins/PS_StrategicVision/Tools/generate_vision_cone_material.py diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision b/Unreal/PS_StratVision/Plugins/PS_StrategicVision deleted file mode 160000 index 199c140..0000000 --- a/Unreal/PS_StratVision/Plugins/PS_StrategicVision +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 199c14097e451995c57b4cc777c269f69ac01d02 diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/.gitignore b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/.gitignore new file mode 100644 index 0000000..156d316 --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/.gitignore @@ -0,0 +1,3 @@ +# Artefacts de build du plugin (regeneres a la compilation) +Binaries/ +Intermediate/ diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Content/Materials/M_VisionCone.uasset b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Content/Materials/M_VisionCone.uasset new file mode 100644 index 0000000000000000000000000000000000000000..7c1cdb986e47a3606d4e86cb7c3799f57af9f5b6 GIT binary patch literal 9977 zcmb_i3wRVo5^i}&L^u#pRN!0%Av{B3NFs=keT9UCguL)c7?xRrPf5_9oeUNk04gc6z3&x~jYS z@9OGKw%yx**>C61pP$&8koK(zISn>83w0rhUSDwqvKm5bEoVAHBZ+;^Tv}^Yi zKk-I>pJ%=8u37Zqte2#LKpSbD{a~iM)^(uk!wXGE&u-TN%{k%mz23a7y`Npzr6%0X ze_#aAj#buv`(NjxlNawxUYYrtcW@%m2EZ?=ps1|KG9*i`P<*9|spwi#9();!q?$>2 zWj4vBYcdFf3*a{dT!Q86PJ~yp-8;A_Bu63@^9g|aG3raWlq;B?9-!7CLmjBZ zNv<5THvcMko8*$TJjt9a`<*gKh61ubKPXKJ$)x|=D`0uyvx8Tw`{9GvvD#%Sg zWKO}EMa6ltSy><{npG@UTOmUx*3;xQ+GAjX!EDX6B+Vz=U2{v8M0$L@^GDt)tD5nMgDEczX_gX@OBKTkNh-6z`q%BA$0wuzu@gl;S(9D5svG1;aq4f~7`YT2 zELUf10QjJ;N*1sm>oH_J_ttpbQ2hnEsZgJnB=u~M9A~a)th=n(kTg>bfnJOB{J4jL zNOY>Q=FijpGCBQQtCj6I#U$gW?wx^4Zc>aNpuLlCo!ivTqh2YJ0zp+aV|0=CUQ6oJ6HSzuz|yhDrGP@+po7RSFE?e2{BQfd z+8$h}SPlerLsAQ6GpK7ObU_32Wb4%rKR|7VEjeQQ@Yq3bAqHJ}`Nd^kRhO(}vU*GU zD{Ya61AeNixCp^Oeaw57ts#!CIGB-4+fwY=w7jDeHlPu=TrP!Ft5i|}sGWD7ydDMJ zvPoAHdZKmzZ!SW;Zap+fmF*ExX#m-8%MPM_cL)~AeecvxMd!G~niNoc1(G2JWSG{m zP?;p{kzW_#GG*MNsfy*RWNI$^YUBQkf#-?N-seBpoJWLuY)tau*tjYV2W3*(eQH0% zmdCGvi}N)#Oo6+2`Ioc)ie%m-(hqU*`qrq$*w=_uQKjTpv5K9#}ma<7~8z?R>|vwXM+4QMyU0 zyD#02R_WYErv1z_`_G|budZ1IQiaT1HvFAoebHq|W+d;4G2LnVkVgBIt(EI0Ipb|b z1B_Rs7Fn>txC&k5`{(Bm!v1Bc`Xot>xQeu?*z_Q(&oU&_M0cB{{3*{Coa?NR;wP?x z)0M~{Ex|0(;7LlCEKb1{NJ$hB1@GOs5W7w(xWWP9fRm-~P5&j*w#X-`GVvD9Nw^Rs zvo*h5O(s6M03)|TO+=BkWg)gFa!Dhsd zt3FA~!A2}Req5lJBl>fdib@M6o{nn9nw1`GmkXPOWZ1E)tIPD_4#D9>j@6z>##PP< zG#>jSuJdS?5hh(bogR*XDn_VSsxZl@9iOi4RhLsz>e6)s00`4!LiF(CHbQopyRH;1 zMZqEfZ^NOt*W*IrmZC!T!nm2d`Rl%ejY-@>5{|w(2jg*l9n>wqb|V`Q_?#_5iD*A{ zp!noW=HKdHPR$dCX_I+NlWiJw#kL>Xs*^)dA8}=Lln7;#wY_jDu32s=B1gG7tNRQP zM=367uOh2{^8Nfm9dVLZlvh@wsftAwe*WAEgfvCf4LviyKt$k21U0ixB;-&DOutUH z-q8{9Y$q2HnanR=Jc0=o?Damx_rZ+=Y7pN-ta9v{aU0HLf#jPEYdBX2)GO0y9zmlc zw3JB~D?Il~D#QY3?D$P`@yzc6IK;@nqXH}h+?RI4TuASt66l&D*mbJPJDkJV5NS$> zoZEX-NAxHk3M82t#4`rJne7jEL=2CEZ5_syD_6-X1t*!O^w@)YX`x&$8!{vguyI15 z*CZ7;q9_7gDnt(m9hp}jFTmkP8_G$!% zt%4Ef|M=2#7^Cd?^{=r{Rbk)&S94#Ids5P!w3)cGJhR}AP3Y75=zMp{Gjz`pIVQQ) zllCS$rARNg>cH-{F&j=J8j6CFValbt3i&2;EAL}%_F@DkjrQ4o;$QXpqf{A0f5jKG z1FyK?t5@j&EF&_2;oFSf=yf=pgnZF)4IxjQ#hkR>6f&!`pILeup%&Yhhu)q1J1Q%I zn4FvzvGGd%t~eM*G9e_%!qC#R)G$P(w3H+cXMnsFeztaHtFL;(dlv8Gl(jAmlaY`^#azOZ{8Vx5#_ z;r*i?i3z{48BNmoW3OXe;NTV37ft6#kG^leOijSB}x1+6=$UfZA! z8`Y7xK*BlWN-%$_Z_UosVaY;@J~i*yMn{8E+?{s9{>b+A;}L=^@DVd>E^;dE|6a!)#*Vu79DTSYyB2MT?@h6X8gMMQzR{1EXt#wQ?v8luLLSa1emBHJd&tN3 zSmSqdf6#=qV=)Zd*4vAb$FB+PtH8m0o1>k=jzzB^8op@4m=*Wd3`ggMYg?J%E*C4t zuFj$&m?XvCi$!^ul>@JV%duD)-hHDb=PoM>8Rd8osh73Jo|>@Vu~{niHc=$e1J{V;ezYgiYdT+;AGhfZ9Ucja zb0J%zxhv%KKDdi>D~5fgBB>NKPl8aSU?z`+2XdU{K9mwg3;U#v38uSPCfij^!~TrM zVa7h~hDGcvz-m(vE)?FeF)uVcsqCwYQ=!Jd325f~F|G$JnDdFHbC7+B#?l#0G=wC) zwMlsSFK;Z(>Ul}_?6n_^+N@uio5+O6vl=U0jkE_Ye1Xzr?Fl<}Jy7z+j@;+-PfXu* z=Ai|=|F#<8iCHriQ8VU{Sn7H=1851j%CT^bT!_~;xBl~HMiLE`Gg{rY1A(4E2M;gY z5R+}7nmH01U=fvP+oXD<7>D#E#^gq@2cqYDBcBJwhJqdjI&92)$7^gy3$73>hF#I; z=lJ{te&u)v?2sHI90uH7!c}-8|0_nn;q56i#o<>B;M*UGKHW@|`VDlLiUu_oa`-{Q zuUm;cb01tC9*7P5Ig%U!sXCD!aR)k*2M>ufF=4R=WoTzwaxk&nkbH@LR4ltBUIaIjYPlWa$K^qE^k!wK|C%4cM#;)(aj8=Dk(Xj-N|5&cEs~AVoV1dg7 z&W~_K0X*=T56r51{!kp)Ok@;Nct$=>ug@-&$ff(JWX~ZY*AC;k79lVLA8bv;C1*nQ^L5*U;Z;p^AE9EPJ zD-{*;_qgu-6wa=3-FYK4Z?-!h{x-PmwWHOUYkpcWrmdd1s_E`@u-*>gy@r}Po{&`M z1V*eEhH#I?GRUrCCZo}LA9C{5>UF&ybIzLc!S4r429(70(P20r#`Vz(FxzY&jUM#l z%<13m&YHVi{dw9N#}9vskABLG>!ab!H_i4@$K9h|KGo^4d)2uOU1p3vb7{+b)CO?f zY#)v6vgJEx&0goKkN!Pl-TLFsKgCC6WLzKJ&{7|L>bibYXA;g_HKT8@l+YP#I-cy?`t152o9?5yZWz{5H$2_*!G){e-ID!}4rlj$=} z5;t7>@zgdeg1-D$_AgsDr~Q)Go9>3Vopd+0&1AqOL9_9E$LfoY7QTBbYxATj6|3*4 zTJk4!veCel*pk3>C}ne}^pp$6Ec>pzJmrsddz#$IZSYTQi+^1;bMWx(AK|vhb5({W SQnf#0T$|l*_T#Y~<^K=Lhl5@K literal 0 HcmV?d00001 diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/PS_StrategicVision.uplugin b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/PS_StrategicVision.uplugin new file mode 100644 index 0000000..047ab6a --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/PS_StrategicVision.uplugin @@ -0,0 +1,30 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "0.1.0", + "FriendlyName": "PS Strategic Vision", + "Description": "Affichage strategique pour vues spectateur et replays : cones de vision, et autres visualisations a venir.", + "Category": "Visualization", + "CreatedBy": "ASTERION VR", + "CreatedByURL": "", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": true, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "PS_StrategicVision", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "ProceduralMeshComponent", + "Enabled": true + } + ] +} diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/README.md b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/README.md new file mode 100644 index 0000000..b834138 --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/README.md @@ -0,0 +1,126 @@ +# PS Strategic Vision + +Plugin Unreal Engine 5.7 d'affichage stratégique pour vues spectateur et replays : +cônes de vision des personnages, et d'autres visualisations à venir. + +## Installation dans un projet + +Copier (ou cloner) ce dossier dans `Plugins/PS_StrategicVision/` du projet, puis +activer le plugin dans le `.uproject` ou via *Edit > Plugins*. + +En submodule git : + +```bash +git submodule add Plugins/PS_StrategicVision +``` + +Le plugin est en C++ : le projet hôte doit être un projet C++ (il sera compilé +avec le projet). + +## Contenu + +| Élément | Rôle | +|---|---| +| `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 : 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 + +### 1. Poser le cône de vision + +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 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). + +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. Cas VR : injecter la position/direction de la caméra + +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** : + +- `Out Location` = position monde de la caméra ; +- `Out Forward Direction` = direction du regard (seul le cap horizontal est utilisé) ; +- retourner `true`. + +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. + +### 3. Activer / désactiver selon l'état de jeu + +**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++) : + +- `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). + +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). + +### 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è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*, **VertexColor** → Emissive +Color, **VertexColor.A** → Opacity. Laisser *Two Sided* décoché (la géométrie est +déjà double face). + +> 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 + +- `ArcSegments` (48 par défaut) = nombre de line traces par mise à jour. +- `UpdateInterval` : 0 = chaque frame ; augmenter (ex : 0.05) si beaucoup de + personnages affichent leur cône simultanément. + +## 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` diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/PS_StrategicVision.Build.cs b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/PS_StrategicVision.Build.cs new file mode 100644 index 0000000..0924c3e --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/PS_StrategicVision.Build.cs @@ -0,0 +1,18 @@ +using UnrealBuildTool; + +public class PS_StrategicVision : ModuleRules +{ + public PS_StrategicVision(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new string[] + { + "Core", + "CoreUObject", + "Engine", + "DeveloperSettings", + "ProceduralMeshComponent", + }); + } +} diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicDisplayComponent.cpp b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicDisplayComponent.cpp new file mode 100644 index 0000000..4c9a063 --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicDisplayComponent.cpp @@ -0,0 +1,97 @@ +// Copyright (c) 2026 ASTERION VR. All rights reserved. + +#include "PS_StrategicDisplayComponent.h" + +#include "Engine/World.h" +#include "PS_StrategicVisionSettings.h" +#include "PS_StrategicVisionSubsystem.h" + +UPS_StrategicDisplayComponent::UPS_StrategicDisplayComponent() +{ + PrimaryComponentTick.bCanEverTick = false; +} + +void UPS_StrategicDisplayComponent::SetTeam(EPS_StrategicTeam NewTeam) +{ + if (Team != NewTeam) + { + Team = NewTeam; + RefreshDisplayState(); + } +} + +void UPS_StrategicDisplayComponent::SetDisplayEnabled(bool bEnabled) +{ + if (bDisplayEnabled != bEnabled) + { + bDisplayEnabled = bEnabled; + RefreshDisplayState(); + } +} + +bool UPS_StrategicDisplayComponent::IsDisplayVisible() const +{ + if (!bDisplayEnabled) + { + return false; + } + + if (const UWorld* World = GetWorld()) + { + if (World->IsGameWorld()) + { + if (const UPS_StrategicVisionSubsystem* Subsystem = World->GetSubsystem()) + { + return Subsystem->ShouldDisplay(GetDisplayCategory(), Team); + } + } + } + + // Editor world (preview): only the local activation is considered. + return true; +} + +FLinearColor UPS_StrategicDisplayComponent::GetTeamColor() const +{ + return GetDefault()->GetTeamColor(Team); +} + +void UPS_StrategicDisplayComponent::BeginPlay() +{ + Super::BeginPlay(); + + if (UPS_StrategicVisionSubsystem* Subsystem = UPS_StrategicVisionSubsystem::Get(this)) + { + Subsystem->OnDisplayStateChanged.AddUniqueDynamic(this, &UPS_StrategicDisplayComponent::HandleGlobalDisplayStateChanged); + } + + RefreshDisplayState(); +} + +void UPS_StrategicDisplayComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + if (UPS_StrategicVisionSubsystem* Subsystem = UPS_StrategicVisionSubsystem::Get(this)) + { + Subsystem->OnDisplayStateChanged.RemoveDynamic(this, &UPS_StrategicDisplayComponent::HandleGlobalDisplayStateChanged); + } + + Super::EndPlay(EndPlayReason); +} + +#if WITH_EDITOR +void UPS_StrategicDisplayComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + RefreshDisplayState(); +} +#endif + +void UPS_StrategicDisplayComponent::RefreshDisplayState() +{ + OnDisplayStateChanged(); +} + +void UPS_StrategicDisplayComponent::HandleGlobalDisplayStateChanged() +{ + RefreshDisplayState(); +} diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionModule.cpp b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionModule.cpp new file mode 100644 index 0000000..3703369 --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionModule.cpp @@ -0,0 +1,5 @@ +// Copyright (c) 2026 ASTERION VR. All rights reserved. + +#include "Modules/ModuleManager.h" + +IMPLEMENT_MODULE(FDefaultModuleImpl, PS_StrategicVision) diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionSettings.cpp b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionSettings.cpp new file mode 100644 index 0000000..01c12b9 --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionSettings.cpp @@ -0,0 +1,42 @@ +// 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() +{ + 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)); + + // Material shipped in the plugin content (procedural hatching). + DefaultConeMaterial = TSoftObjectPtr( + FSoftObjectPath(TEXT("/PS_StrategicVision/Materials/M_VisionCone.M_VisionCone"))); +} + +FLinearColor UPS_StrategicVisionSettings::GetTeamColor(EPS_StrategicTeam Team) const +{ + return GetTeamStyle(Team).Color; +} + +FPS_StrategicTeamStyle UPS_StrategicVisionSettings::GetTeamStyle(EPS_StrategicTeam Team) const +{ + if (const FPS_StrategicTeamStyle* Style = TeamStyles.Find(Team)) + { + return *Style; + } + return FPS_StrategicTeamStyle(); +} diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionSubsystem.cpp b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionSubsystem.cpp new file mode 100644 index 0000000..f6b29dc --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionSubsystem.cpp @@ -0,0 +1,65 @@ +// 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()->bEnabledAtStartup; +} + +UPS_StrategicVisionSubsystem* UPS_StrategicVisionSubsystem::Get(const UObject* WorldContextObject) +{ + if (GEngine) + { + if (const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull)) + { + return World->GetSubsystem(); + } + } + return nullptr; +} + +void UPS_StrategicVisionSubsystem::SetGlobalDisplayEnabled(bool bEnabled) +{ + if (bGlobalEnabled != bEnabled) + { + bGlobalEnabled = bEnabled; + NotifyStateChanged(); + } +} + +void UPS_StrategicVisionSubsystem::SetCategoryEnabled(FName Category, bool bEnabled) +{ + const bool bChanged = bEnabled ? (DisabledCategories.Remove(Category) > 0) : !DisabledCategories.Contains(Category); + if (!bEnabled) + { + DisabledCategories.Add(Category); + } + if (bChanged) + { + NotifyStateChanged(); + } +} + +void UPS_StrategicVisionSubsystem::SetTeamEnabled(EPS_StrategicTeam Team, bool bEnabled) +{ + const bool bChanged = bEnabled ? (DisabledTeams.Remove(Team) > 0) : !DisabledTeams.Contains(Team); + if (!bEnabled) + { + DisabledTeams.Add(Team); + } + if (bChanged) + { + NotifyStateChanged(); + } +} + +void UPS_StrategicVisionSubsystem::NotifyStateChanged() +{ + OnDisplayStateChanged.Broadcast(); +} diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionTypes.cpp b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionTypes.cpp new file mode 100644 index 0000000..6f8b7c7 --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_StrategicVisionTypes.cpp @@ -0,0 +1,5 @@ +// Copyright (c) 2026 ASTERION VR. All rights reserved. + +#include "PS_StrategicVisionTypes.h" + +const FName FPS_StrategicDisplayCategories::VisionCone(TEXT("VisionCone")); diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_VisionConeComponent.cpp b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_VisionConeComponent.cpp new file mode 100644 index 0000000..f429c2d --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Private/PS_VisionConeComponent.cpp @@ -0,0 +1,446 @@ +// Copyright (c) 2026 ASTERION VR. All rights reserved. + +#include "PS_VisionConeComponent.h" + +#include "CollisionQueryParams.h" +#include "Engine/HitResult.h" +#include "Engine/World.h" +#include "GameFramework/Actor.h" +#include "Materials/MaterialInstanceDynamic.h" +#include "Materials/MaterialInterface.h" +#include "ProceduralMeshComponent.h" +#include "PS_StrategicVisionSettings.h" +#include "PS_VisionSourceInterface.h" + +namespace +{ + // 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 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& Points, float Width, TArray& 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& 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; + // After actor movement, so that the cone follows without lag. + PrimaryComponentTick.TickGroup = TG_PostUpdateWork; + bTickInEditor = true; +} + +void UPS_VisionConeComponent::OnRegister() +{ + Super::OnRegister(); + + EnsureConeMesh(); + EnsureMaterial(); + ApplyTeamStyle(); +} + +void UPS_VisionConeComponent::OnComponentDestroyed(bool bDestroyingHierarchy) +{ + if (ConeMesh) + { + ConeMesh->DestroyComponent(); + ConeMesh = nullptr; + } + + Super::OnComponentDestroyed(bDestroyingHierarchy); +} + +void UPS_VisionConeComponent::BeginPlay() +{ + Super::BeginPlay(); + SetComponentTickInterval(UpdateInterval); +} + +void UPS_VisionConeComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + if (IsDisplayVisible()) + { + RebuildCone(); + } +} + +void UPS_VisionConeComponent::OnDisplayStateChanged() +{ + const bool bShouldDisplay = IsDisplayVisible(); + + if (ConeMesh) + { + ConeMesh->SetVisibility(bShouldDisplay); + } + + // No traces nor rebuilds while the cone is hidden. + SetComponentTickEnabled(bShouldDisplay); + SetComponentTickInterval(UpdateInterval); + + if (bShouldDisplay) + { + EnsureMaterial(); + 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()) + { + 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()) + { + return; + } + + ConeMesh = NewObject(this, NAME_None, RF_Transient); + ConeMesh->SetupAttachment(this); + // 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); + ConeMesh->SetCastShadow(false); + ConeMesh->bReceivesDecals = false; + ConeMesh->SetTranslucentSortPriority(100); + ConeMesh->RegisterComponent(); + ConeMesh->SetWorldLocationAndRotation(GetComponentLocation(), FQuat::Identity); +} + +void UPS_VisionConeComponent::EnsureMaterial() +{ + if (!ConeMesh) + { + return; + } + + UMaterialInterface* BaseMaterial = MaterialOverride; + + if (!BaseMaterial) + { + BaseMaterial = GetDefault()->DefaultConeMaterial.LoadSynchronous(); + } + if (!BaseMaterial) + { + BaseMaterial = LoadObject(nullptr, GFallbackTranslucentMaterialPath); + } + if (!BaseMaterial) + { + BaseMaterial = LoadObject(nullptr, GFallbackOpaqueMaterialPath); + } + if (!BaseMaterial) + { + return; + } + + if (!FillMID || FillMID->Parent != BaseMaterial) + { + FillMID = UMaterialInstanceDynamic::Create(BaseMaterial, this); + } + if (!OutlineMID || OutlineMID->Parent != BaseMaterial) + { + OutlineMID = UMaterialInstanceDynamic::Create(BaseMaterial, this); + } + ConeMesh->SetMaterial(0, FillMID); + ConeMesh->SetMaterial(1, OutlineMID); +} + +void UPS_VisionConeComponent::ApplyTeamStyle() +{ + const FPS_StrategicTeamStyle Style = GetDefault()->GetTeamStyle(GetTeam()); + + if (FillMID) + { + 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); + } + + 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() +{ + UWorld* World = GetWorld(); + if (!World || !ConeMesh) + { + return; + } + + FCollisionQueryParams QueryParams(TEXT("PS_VisionCone"), bTraceComplex); + if (const AActor* Owner = GetOwner()) + { + QueryParams.AddIgnoredActor(Owner); + } + + 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(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)) + { + 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 = Origin + FVector(0.f, 0.f, TraceHeight); + + const FLinearColor TeamColor = GetTeamColor(); + FLinearColor FillColor = TeamColor; + FillColor.A = Opacity; + + TArray Vertices; + TArray Normals; + TArray UVs; + TArray Colors; + Vertices.Reserve(RayCount + 1); + Normals.Reserve(RayCount + 1); + UVs.Reserve(RayCount + 1); + Colors.Reserve(RayCount + 1); + + // Center vertex (cone origin). + Vertices.Add(FVector::ZeroVector); + Normals.Add(FVector::UpVector); + UVs.Add(FVector2D(0.5f, 0.f)); + Colors.Add(FillColor); + + for (int32 RayIndex = 0; RayIndex < RayCount; ++RayIndex) + { + const float Alpha = static_cast(RayIndex) / static_cast(RayCount - 1); + const float AngleDeg = FMath::Lerp(-HalfAngle, HalfAngle, Alpha); + const FVector Direction = Forward.RotateAngleAxis(AngleDeg, FVector::UpVector); + + float HitDistance = ViewDistance; + FHitResult Hit; + if (World->LineTraceSingleByChannel(Hit, TraceOrigin, TraceOrigin + Direction * ViewDistance, TraceChannel, QueryParams)) + { + HitDistance = Hit.Distance; + } + + Vertices.Add(FVector(Direction.X * HitDistance, Direction.Y * HitDistance, 0.f)); + Normals.Add(FVector::UpVector); + UVs.Add(FVector2D(Alpha, HitDistance / ViewDistance)); + Colors.Add(FillColor); + } + + const TArray NoTangents; + + if (Vertices.Num() == LastFillVertexCount) + { + // Same topology: only update the vertices (cheaper). + ConeMesh->UpdateMeshSection_LinearColor(0, Vertices, Normals, UVs, Colors, NoTangents); + } + else + { + // Triangles in both windings to stay visible from any camera angle. + TArray Triangles; + Triangles.Reserve((RayCount - 1) * 6); + for (int32 SegmentIndex = 0; SegmentIndex < RayCount - 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); + 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& 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 Perimeter; + Perimeter.Reserve(NumPerimeter); + for (const FVector& Vertex : FillVertices) + { + Perimeter.Add(FVector2D(Vertex.X, Vertex.Y)); + } + + TArray Inner; + InsetPolygon(Perimeter, OutlineWidth, Inner); + + FLinearColor OutlineColor = TeamColor; + OutlineColor.A = OutlineOpacity; + + TArray Vertices; + TArray Normals; + TArray UVs; + TArray 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(Index) / static_cast(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 NoTangents; + + if (Vertices.Num() == LastOutlineVertexCount) + { + ConeMesh->UpdateMeshSection_LinearColor(1, Vertices, Normals, UVs, Colors, NoTangents); + } + else + { + TArray 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(); + } +} diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicDisplayComponent.h b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicDisplayComponent.h new file mode 100644 index 0000000..4a98319 --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicDisplayComponent.h @@ -0,0 +1,75 @@ +// Copyright (c) 2026 ASTERION VR. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/SceneComponent.h" +#include "PS_StrategicVisionTypes.h" +#include "PS_StrategicDisplayComponent.generated.h" + +/** + * Base class of the strategic displays placed on characters + * (vision cone, and other visualizations to come). + * + * 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 +{ + GENERATED_BODY() + +public: + UPS_StrategicDisplayComponent(); + + /** Changes the team (updates the display color). */ + UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Display") + void SetTeam(EPS_StrategicTeam NewTeam); + + UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display") + EPS_StrategicTeam GetTeam() const { return Team; } + + /** Enables / disables this specific display (independently of the global state). */ + UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Display") + void SetDisplayEnabled(bool bEnabled); + + UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display") + bool IsDisplayEnabled() const { return bDisplayEnabled; } + + /** Effective visibility: local activation AND global subsystem state. */ + UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display") + bool IsDisplayVisible() const; + + /** Color of the current team, read from the plugin settings. */ + UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display") + FLinearColor GetTeamColor() const; + + /** Category declared by each display type (see FPS_StrategicDisplayCategories). */ + virtual FName GetDisplayCategory() const { return NAME_None; } + +protected: + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + + /** Called when the effective visibility or the team changes. */ + virtual void OnDisplayStateChanged() {} + + /** Re-evaluates the effective visibility and notifies the child class. */ + void RefreshDisplayState(); + + /** Team of the owning character, drives the display color. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strategic Display") + EPS_StrategicTeam Team = EPS_StrategicTeam::Enemy; + + /** Local activation state of this display. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strategic Display") + bool bDisplayEnabled = true; + +private: + UFUNCTION() + void HandleGlobalDisplayStateChanged(); +}; diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionSettings.h b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionSettings.h new file mode 100644 index 0000000..022fab1 --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionSettings.h @@ -0,0 +1,74 @@ +// Copyright (c) 2026 ASTERION VR. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" +#include "PS_StrategicVisionTypes.h" +#include "PS_StrategicVisionSettings.generated.h" + +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; +}; + +/** + * 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 +{ + GENERATED_BODY() + +public: + UPS_StrategicVisionSettings(); + + virtual FName GetCategoryName() const override { return TEXT("Plugins"); } + + /** + * 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 TeamStyles; + + /** + * 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 DefaultConeMaterial; + + /** 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; +}; diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionSubsystem.h b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionSubsystem.h new file mode 100644 index 0000000..68191eb --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionSubsystem.h @@ -0,0 +1,75 @@ +// Copyright (c) 2026 ASTERION VR. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/WorldSubsystem.h" +#include "PS_StrategicVisionTypes.h" +#include "PS_StrategicVisionSubsystem.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPS_StrategicDisplayStateChanged); + +/** + * 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 +{ + GENERATED_BODY() + +public: + 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); + + /** Master switch of every strategic display. */ + UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision") + void SetGlobalDisplayEnabled(bool bEnabled); + + UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision") + bool IsGlobalDisplayEnabled() const { return bGlobalEnabled; } + + /** Enables / disables one display category (e.g. "VisionCone"). */ + UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision") + void SetCategoryEnabled(FName Category, bool bEnabled); + + UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision") + bool IsCategoryEnabled(FName Category) const { return !DisabledCategories.Contains(Category); } + + /** 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 = "ASTERION|Strategic Vision") + bool IsTeamEnabled(EPS_StrategicTeam Team) const { return !DisabledTeams.Contains(Team); } + + /** 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 = "ASTERION|Strategic Vision|Vision Cone") + bool AreVisionConesEnabled() const { return IsCategoryEnabled(FPS_StrategicDisplayCategories::VisionCone); } + + /** 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); + } + + /** Fired on every global state change (components subscribe to it). */ + UPROPERTY(BlueprintAssignable, Category = "ASTERION|Strategic Vision") + FPS_StrategicDisplayStateChanged OnDisplayStateChanged; + +private: + void NotifyStateChanged(); + + // Initial state read from UPS_StrategicVisionSettings::bEnabledAtStartup (false by default). + bool bGlobalEnabled = false; + TSet DisabledCategories; + TSet DisabledTeams; +}; diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionTypes.h b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionTypes.h new file mode 100644 index 0000000..27e493b --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_StrategicVisionTypes.h @@ -0,0 +1,25 @@ +// Copyright (c) 2026 ASTERION VR. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "PS_StrategicVisionTypes.generated.h" + +/** Team of a character, used to color / filter the strategic displays. */ +UENUM(BlueprintType) +enum class EPS_StrategicTeam : uint8 +{ + Player, + Ally, + Enemy, + Neutral +}; + +/** + * Strategic display categories. Each component type declares its category, + * which allows enabling / disabling it globally through the subsystem. + */ +struct PS_STRATEGICVISION_API FPS_StrategicDisplayCategories +{ + static const FName VisionCone; +}; diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_VisionConeComponent.h b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_VisionConeComponent.h new file mode 100644 index 0000000..6d4593b --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_VisionConeComponent.h @@ -0,0 +1,156 @@ +// Copyright (c) 2026 ASTERION VR. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/EngineTypes.h" +#include "PS_StrategicDisplayComponent.h" +#include "PS_VisionConeComponent.generated.h" + +class UMaterialInstanceDynamic; +class UMaterialInterface; +class UProceduralMeshComponent; + +/** + * 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). + * + * 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 +{ + GENERATED_BODY() + +public: + UPS_VisionConeComponent(); + + virtual FName GetDisplayCategory() const override { return FPS_StrategicDisplayCategories::VisionCone; } + + /** Vision range, in cm. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (ClampMin = "10.0", Units = "cm")) + float ViewDistance = 1500.f; + + /** 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; + + /** + * 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 bUseOwnerVisionSource = true; + + /** 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; + + // --- Rendering --- + + /** 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; + + /** Draws a more opaque outline around the cone (readability when cones overlap). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering") + bool bShowOutline = true; + + /** Outline width, in cm. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.5", Units = "cm", EditCondition = "bShowOutline")) + float OutlineWidth = 6.f; + + /** Outline opacity. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.0", ClampMax = "1.0", EditCondition = "bShowOutline")) + float OutlineOpacity = 0.9f; + + /** + * 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 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 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: + virtual void OnRegister() override; + virtual void OnComponentDestroyed(bool bDestroyingHierarchy) override; + virtual void BeginPlay() override; + virtual void OnDisplayStateChanged() override; + +private: + /** Internal procedural mesh: section 0 = fill, section 1 = outline. */ + UPROPERTY(Transient) + TObjectPtr ConeMesh; + + UPROPERTY(Transient) + TObjectPtr FillMID; + + UPROPERTY(Transient) + TObjectPtr 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 ApplyTeamStyle(); + void RebuildCone(); + void BuildOutlineSection(const TArray& FillVertices, const FLinearColor& TeamColor); +}; diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_VisionSourceInterface.h b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_VisionSourceInterface.h new file mode 100644 index 0000000..c6af62f --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Source/PS_StrategicVision/Public/PS_VisionSourceInterface.h @@ -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; } +}; diff --git a/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Tools/generate_vision_cone_material.py b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Tools/generate_vision_cone_material.py new file mode 100644 index 0000000..35aea3d --- /dev/null +++ b/Unreal/PS_StratVision/Plugins/PS_StrategicVision/Tools/generate_vision_cone_material.py @@ -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 -run=pythonscript -script="" + +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")