You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#lockdown Nick.Penwarden Change 3196499 on 2016/11/14 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34.3 @ CL 3196473 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3196498 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3195674 on 2016/11/11 by Laurent.Delayen Fix for twinblast ult mesh not disappearing at lower lods. (curves not getting properly update with URO) Fixes https://jira.it.epicgames.net/browse/OR-31509 #rb lina.halper #tests twinblast ult multiPIE Change 3195245 on 2016/11/11 by Dan.Hertzka Card crafting progress - Device responds to UI actions appropriately and infinitely - Backing out of device screen goes to card details first (i.e. doesn't take you all the way out of the screen) - CraftingDevice and CraftingDeviceNode now have a few natively-controlled timelines for reversible states - Non-reversible timelines all now play from start (so they work more than once) Engine-side: - Added an overload to UTimelineComponent::SetOnTimelineFinishedFunc() that takes a native (non-dynamic) delegate - Added a way to set the owning player of a UUserWidget via a local PlayerController (since ULocalPlayer isn't a BP type) - used to establish the owner of a WidgetComponent's widget #rb none #tests PIE crafting Change 3194616 on 2016/11/11 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34.3 @ CL3194604#RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3194615 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3193875 on 2016/11/10 by Andrew.Grant Removed GameThreadWaitForTask fatal timeout if running in editor (assumption is that some operations can cause significant blocks, and that's ok.. #jira UE-38496 #tests compiled #rb none #c0dereview Marcus.Wassmer Change 3193368 on 2016/11/10 by Mieszko.Zielinski Fixed a bug in UCharacterMovementComponent::OnMovementModeChanged making UPathFollowingComponent::OnStartedFalling being sent too late #UE4 #rb Lukasz.Furman #test golden path Change 3193280 on 2016/11/10 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34.2 @ CL 3193232 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3193279 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3192376 on 2016/11/09 by Laurent.Delayen RootMotion Sources: removed second check, as that was a perfectly valid case. Fixes https://jira.it.epicgames.net/browse/OR-31490 #rb none #tests riftmage blackhold multiPIE Change 3192243 on 2016/11/09 by Laurent.Delayen UAbilityTask_ApplyRootMotionMoveToActorForce replicates TimeMappingCurve to potentially fix https://jira.it.epicgames.net/browse/OR-31266 #rb none #tests Jump pads on Sovereign2 multiPIE Change 3191985 on 2016/11/09 by Laurent.Delayen Additional debug info for https://jira.it.epicgames.net/browse/OR-31300 #rb none #tests compiles Change 3191565 on 2016/11/09 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34.2 @ CL 3191371 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3191564 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3190702 on 2016/11/08 by David.Ratti Fix PIE autologgin mcp problem #rb JoshM #tests PIE autologgin Change 3190591 on 2016/11/08 by Mieszko.Zielinski Added a way to RecastNavMesh to retrieve navigation links in a given tile #UE4 #rb Lukasz.Furman #test golden path Change 3190363 on 2016/11/08 by Frank.Gigliotti Root motion velocity clamping and bug fixes; * Added option to clamp velocity when ending MoveToActorForce, MoveToForce, and RadialForce root motions. * Disabled partial ticking on the FRootMotionSource_ConstantForce when applying the finishing velocity or clamping velocity on root motion ability tasks. This ensures the desired velocity is set correctly. * Added AbilityTask_ApplyRootMotion_Base as a base class for all root motion ability tasks. #RB David.Ratti, Zak.Middleton #c0dereview Zak.Middleton #Tests PIE - Multiple heroes with root motion abilities Change 3190344 on 2016/11/08 by Laurent.Delayen RootMotionSources: Tracking down https://jira.it.epicgames.net/browse/OR-31266 Testing for 'Matches' rule in more places, to track down where it breaks. Added more info. Switched check to ensure, so it doesn't prevent playtests. Minor tweaks: - Test for ID before TimeStamp - Changed auto to proper type. - Trim RootMotionIDMappings of outdated mappings since we can iterate over that array quite a bit. #rb none #tests multiPIE Change 3190217 on 2016/11/08 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34.2 @ CL 3190009 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3190216 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3188560 on 2016/11/07 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34.2 @ CL 3187796 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3188499 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3188012 on 2016/11/05 by Andrew.Grant Merging using ROBO://Orion/Main->//Orion/Dev-General #rb #tests na Change 3187818 on 2016/11/04 by Michael.Noland Editor: Refactored the game instance PIE creation hooks, allowing them to pass an error message back that will be displayed to the user explaining why PIE was aborted/failed to start - Fixed a potential crash trying to tear down a null world when UGameInstance::StartPlayInEditorGameInstance fails Upgrade notes: - UGameInstance::InitializePIE overrides should now override InitializeForPlayInEditor instead - UGameInstance::StartPIEGameInstance overrides should now override StartPlayInEditorGameInstance instead - These methods return a FGameInstancePIEResult which can be created via FGameInstancePIEResult::Success() or FGameInstancePIEResult::Failure(), and take a parameter struct to make it easier to send additional information in the future without breaking the signature again #rb matt.kuhlenscmidt #tests Tested various PIE configurations in Paragon Change 3187756 on 2016/11/04 by Michael.Noland Editor: Added support for games to respond to Play in Editor setting changes (by properly calling PostEditChange when the PIE/SIE menu options are picked/toggled) #tests Tested in Paragon by binding to FCoreUObjectDelegates::OnObjectPropertyChanged #rb ben.ziegler Change 3187258 on 2016/11/04 by Dan.Hertzka - UTextBlock::SetText is now virtual - OrionTextBlock has a property ("All Caps") that, if true, will always convert all text it's given to ALL CAPS #c0dereview Sean.Smith, Philip.Buuck, Marcel.Swanepoel, Bryan.Rathman #rb none #tests PIE Change 3187157 on 2016/11/04 by Jason.Bestimt #R0B0MERGE-AUTHOR: andrew.grant Case fix for staged files #rb Ben.Marsh #tests preflighted #R0B0MERGE-SOURCE: CL 3187153 in //Orion/Release-34/... via CL 3187154 via CL 3187155 via CL 3187156 #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3186870 on 2016/11/04 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34.2 @ CL 3186846 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3186855 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3186243 on 2016/11/03 by Michael.Noland Engine: Fixed reporting of ScreenPct when the cvar is set directly rather than via game user settings - Also fixed a place that was using ScreenPct as an integer instead of a float in the OSVR plugin #jira OR-23184 #tests Tested by setting r.screenpercentage directly to a different value and inspecting the chart results #rb ben.ziegler Change 3185134 on 2016/11/03 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34.2 @ CL 3185065 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3185131 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3183689 on 2016/11/02 by Aaron.McLeran OR-31091 Implementing 3175639 in Dev-General #rb zak.middleton #tests unplug headphones and observe no log spam or other issues Change 3183292 on 2016/11/02 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34.2 @ CL 3182926 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3183084 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3182323 on 2016/11/01 by Dan.Hertzka Fix crash in sequencer when opening a widget BP with the sequencer tab closed (failed to null check) #c0dereview Nick.Darnell #rb Stephan.Jiang #tests none Change 3182295 on 2016/11/01 by Daniel.Lamb Fixed up routing BeginDestroy. #rb Andrew.Grant #jira OR-31043 #test Paragon win64 -game Change 3181975 on 2016/11/01 by Daniel.Lamb Make sure to remove safe zone delegate when canvas is destroyed. Moved registration of safe zone delegate to constructor. #rb Michael.Noland #jira OR-31043 #test Paragon win64 -game Change 3181895 on 2016/11/01 by Daniel.Lamb Make sure to remove safe zone delegate when canvas is destroyed. Moved registration of safe zone delegate to constructor. #rb Michael.Noland #jira OR-31043 #test Paragon win64 -game Change 3181892 on 2016/11/01 by Michael.Trepka Copy of CL 3162466 By default, do not compile Mac OpenGL and Metal SM4 shaders while cooking #rb Mark.Satterthwaite #jira UE-37088 #tests Cooked MacNoEditor data on Windows Change 3181624 on 2016/11/01 by David.Ratti Pass attribute value by reference through SetNumericValue_Internal so that the final/clamped value is what is broadcasted to attribute change delegates #rb none #tests golden path w/ extra logging to verify final value is what is broadcasted #c0dereview Billy.Bramer, Fred.Kimberley Change 3181574 on 2016/11/01 by Jason.Bestimt #ORION_DG - Fixing up R0B0MERGE issue #RB:none #Tests:none Change 3180859 on 2016/10/31 by Brian.Karis Removed SSAO from hair Change 3180320 on 2016/10/31 by Daniel.Lamb Added support for rebuild lighting commandlet to read maps to rebuild from ini file. #rb Andrew.Grant #test Paragon rebuild lighting #jira OR-30841 Change 3180227 on 2016/10/31 by Laurent.Delayen Disabled Warning to fix https://jira.it.epicgames.net/browse/OR-30965 Will be turned into a warning message in the BP editor. #c0dereview benn.gallagher #rb none #tests none Change 3179903 on 2016/10/31 by jason.bestimt #ORION_MAIN - Merge 34.2 @ CL 3179886 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3179895 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3177903 on 2016/10/28 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34 @ CL 3177869 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3177901 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3175548 on 2016/10/26 by Michael.Noland GameplayAbilities: Change it so that delegates registered via RegisterGameplayAttributeEvent are still called on the client when there is no aggregator entry #rb david.ratti #c0dereview billy.bramer #tests Tested in multiplayer PIE client with a new minion callback and golden path in -game Change 3175544 on 2016/10/26 by Bart.Hawthorne Draft in replays improvements: - Marker added to timeline bar that shows when the level change happens - Added "Skip Draft" button when loading a replay which will bypass the draft - Only PvP matches will record replays - Added "Exit Replay" button to hamburger menu while in draft - No longer load hero data before viewing a replay if not skipping draft - Enable replay recording in PvP #rb john.pollard #c0dereview paul.moore #tests nomcp golden path up to spawnpoint, created and loaded several replays Change 3175533 on 2016/10/26 by Michael.Noland Fix for FMallocBinned::GetAllocationSize() for aligned allocations. #jira UE-37249 #jira UE-37243 #rb robert.manuszewski #lockdown robert.manuszewski [reimplementing CL# 3165739 from Release-4.13] #tests Compiled Change 3175311 on 2016/10/26 by Daniel.Lamb Added support for safe zone change. Messed up files in last checkin #test Ps4 paragon #jira OR-30506 #rb Matt.Kuhlenschmidt Change 3175298 on 2016/10/26 by Daniel.Lamb Added support for updating safe area #rb Matt.Kuhlenschmidt #c0dereview Andrew.Grant Nick.Darnell #test Ps4 paragon #jira OR-30506 Change 3175209 on 2016/10/26 by David.Ratti Fix a few cases where default gameplay cue parameters weren't initialized properly and could not be translated by the skin system. Fixes some mayan steel issues. #rb none #tests pie Change 3174858 on 2016/10/26 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34/33.2 @ CL 3174784 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3174857 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3174822 on 2016/10/26 by Marcus.Wassmer Duplicate 3174187 #jira UE-37020 #rb marc.audy #test create/destroy effects with HQ lights in editor. Change 3174344 on 2016/10/25 by Ryan.Gerleve Added a bIsNetStartupComponent flag to UActorComponent. This will be set for components that are owned by an actor when that actor's bNetStartup flag is set. #rb john.pollard #tests golden path Change 3174270 on 2016/10/25 by Marcus.Wassmer Add LightingChannel control to High Quality particle lights. #rb none #test tested different lighting channels. Change 3173855 on 2016/10/25 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 34 @ CL 3173292 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3173361 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3173843 on 2016/10/25 by Michael.Trepka Update custom window controls on toggle fullscreen and make sure that when we switch to windowed mode we don't use window size that wouldn't fit on desktop #rb Matt.Kuhlenschmidt #tests Tested in editor build on PC Change 3173783 on 2016/10/25 by Dan.Youhon Add VelocityOnFinish mode options to RootMotionRadialForce so that designers can control what happens to a character's velocity when the ability task ends (part of #OR-30249) #rb None #tests MultiPIE Change 3173734 on 2016/10/25 by Dan.Youhon Protect against invalid Duration in FRootMotionSource_MoveToDynamicForce on simulated clients #OR-27128 #rb None #tests MultiPIE Change 3173714 on 2016/10/25 by David.Ratti Add Game and Engine Compat versions for replays #coderview John.Pollard #rb Lietz #tests golden path, replays Change 3173681 on 2016/10/25 by Bart.Hawthorne Implement drafting in replays. A replay spectator is spawned in the draft lobby so that the draft is saved out to the replay server, and clients are now able to travel in replays both when they occur "naturally" (at the same time as if they were a client), and also by scrubbing to a place in the timeline that's a different level. The feature is implemented but currently disabled by default - turn on by setting CVarEnableDraftInReplays to 1. Replays should function the same as before. Also fixed a warning on the dedicated server related to abandoning a draft. #rb john.pollard #c0dereview josh.markiewicz, paul.moore #tests nomcp golden path (up to spawning), recorded multiple replays and played back with lots of scrubbing Change 3173677 on 2016/10/25 by Andrew.Grant Reenabled audio thread Added safety wrapper to prevent code accidentally using events after they are returned to the pool. #tests na #rb Gil.Gribb Change 3173588 on 2016/10/25 by Ryan.Gerleve Added a replication condition to skip replays. #tests golden path #rb john.pollard Change 3172692 on 2016/10/24 by Marcus.Wassmer Fix OR-30390 caused by missing mutex lock #rb none #test compile ps4 Change 3172025 on 2016/10/24 by Matt.Kuhlenschmidt Fix blur widget not respecting clip rects #rb none #tests paragon blur widget clipping bugs Change 3171570 on 2016/10/23 by Mieszko.Zielinski Moved Bots' enemy selection eqs query triggering to native code #Orion Did this to be able to manually trigger enemy selection when current enemy dies, to avoid having a "null" enemy in BB for couple of ticks. Also, made couple of tweaks to positioning and tower attacking behavior of melee bots #rb none #test golden path Change 3171100 on 2016/10/21 by Aaron.Eady FGameplayCueTagDetails; Adding a check for if the RawStructData.Num > 0 before trying to use it. There was a case where you could crash the editor if you create a new GC tag inside of a BP, compile the BP, click Add New, select a GC type, then when the file is created, click away from it in the content browser. This repro wasn't 100% but often enough to caus a problem. #rb David.Ratti (actually wrote the code) #tests PIE Change 3171060 on 2016/10/21 by Ryan.Gerleve Some cleanup and fixes for deathcam: The recording demo net driver for the deathcam replay now uses checkpoint amortization to smooth out spikes. Converted UOrionKillcamPlayback::IsKillcamWorld to IsKillcamActor, since IsKillWorld isn't useful for the new single-world implementation. Converted a GetValueOnGameThread to GetValueOnAnyThread (wasn't in the merge) so that deathcam replay recording can happen on a thread. Added comment to UAbilitySystemComponent::OnComponentDestroyed. #rb john.pollard #tests golden path, enabled deathcam Change 3171041 on 2016/10/21 by Ryan.Gerleve Make the CheckpointSaveMaxMSPerFrame value a member of UDemoNetDriver so it can be set per instance, and convert the demo.CheckpointSaveMaxMSPerFrame cvar into an override for this value. #rb john.pollard #tests golden path Change 3170917 on 2016/10/21 by Mieszko.Zielinski Made a change to OrionBTTask_ObjectiveGraphMove to make it fallback to regular move if the destination is in the same or second-closest objective graph node #Orion #rb none #test golden path Change 3170914 on 2016/10/21 by Mieszko.Zielinski Fixed EQS scoring bug resulting in some items getting NaN scored #UE4 The NaN was happening when scoring but not filtering using a bool-based test, or when unintentionally skipping items by moving item iterator without doing any work. Removed a bunch of deprecated code while there #rb Lukasz.Furman #test golden path Change 3170912 on 2016/10/21 by Mieszko.Zielinski Manual merge of crucial BT fixed over from //Fortnite/Main #UE4 Original CL#3159145 , CL#3159892 #rb Lukasz.Furman #test golden path Change 3170478 on 2016/10/21 by David.Ratti fix editor crash related to recompiling gamplay cue blueprint while a preview animation is playing that invokes that gameplay cue. #rb none #tests editor Change 3170231 on 2016/10/21 by Ryan.Gerleve Fix for an issue that was preventing moving your hero after spawning in PIE in Agora: set the net driver on static level collections as well. #tests golden path, PIE #c0dereview john.pollard #rb none Change 3170074 on 2016/10/20 by Ryan.Gerleve Merging support for recording client replays in a task parallel with Slate (optimization for deathcam) from UE4/Dev-Networking. CL 3169209 #tests golden path, replays #rb none Change 3170019 on 2016/10/20 by Ryan.Gerleve Merging support for deathcam memory optimizations (level collection work) from UE4/Main and UE4/Dev-Networking. CLs: 3134499 3134771 3135279 3137140 3138081 3140413 3150142 3142515 3162189 3162194 #tests golden path #rb none Change 3169686 on 2016/10/20 by Michael.Trepka Fixed a Windows-specific problem with parts of the custom window buttons not accepting mouse clicks when the window is maximized due to window region used by FWindowsWindow::IsPointInWindow() being offset by border size. #rb Jeff.Campeau #tests Tested in editor build on PC Change 3169668 on 2016/10/20 by Max.Chen Sequencer - Don't crash when a bool track or visibility track has a null runtime object. Copy from Odin #rb none #tests opened a recorded sequence Change 3169657 on 2016/10/20 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_33 - Fix for localization export of web data Fixed export of localized formatted text - Removed the super-specific GetSourceTextsFromFormatHistory, and replaced it with the more-generic GetHistoricFormatData to get information about an FText that was generated via FText::Format. - Added GetHistoricNumericData to get information about an FText that was generated via FText::AsNumber or FText::AsPercent. - Updated the translation picker to use GetHistoricFormatData. - Removed the code from FMultiLocHelper that used GetSourceTextsFromFormatHistory as it wasn't actually needed. - Added code to FGameDataExporter to correctly localize a formatted text for a given culture, and re-format the result for export. [c0dereviewed]: jamie.dale #RB:none #Tests:Exported game data! #R0B0MERGE-SOURCE: CL 3169653 in //Orion/Release-33/... via CL 3169654 via CL 3169655 via CL 3169656 #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3169616 on 2016/10/20 by David.Ratti missed file #rb none #tests none Change 3169597 on 2016/10/20 by David.Ratti Missed include #rb DanH #tests none Change 3169393 on 2016/10/20 by David.Ratti AbilitySystem.GlobalAbilityScale cvar to help design iterate on animation/duration based tweaks #rb none #tests pie Change 3168287 on 2016/10/19 by Mieszko.Zielinski Expanded EQS info logged with vlog #UE4 #rb none #test golden path Change 3168282 on 2016/10/19 by David.Ratti Restore warning when multiple GC notifies try to handle the same tag. #rb none #tests compile, launch editor, see warnings, cry Change 3168196 on 2016/10/19 by Jon.Lietz compile fix, removing the int version of FirstActiveIndex and leaving the in32 version. #RB none #tests compiles Change 3168041 on 2016/10/19 by Michael.Trepka Don't restore saved resolution on window activation in non-fullscreen modes #rb Matt.Kuhlenschmidt #tests Tested in editor build on PC Change 3167859 on 2016/10/19 by Aaron.McLeran UE-36288 Fixing concurrency resolution stop quietest Implementing in Dev-General for Joey since he needs the fix ASAP. #rb Jeff.Campeau #tests perform tests described in JIRA bug. Change 3167790 on 2016/10/19 by Andrew.Grant Duplication of 3167569 from //Odion/Main for Paragon cinematics #rb none #tests compiled Change 3167682 on 2016/10/19 by Laurent.Delayen Integrated #ORION_33.1 - Disabled WindSources on Cloth and AnimDynamics, as it's not safe to access from the GameThread. https://jira.it.epicgames.net/browse/OR-30473 #rb ori.cohen, benn.gallagher #tests Vamp, Kwang, Chains in Persona and PIE with WindActor in level. Change 3167466 on 2016/10/19 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 33.2 @ CL 3167368 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3167456 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3167312 on 2016/10/19 by Mieszko.Zielinski Fixed EQS template cache issues with multiple query run modes #UE4 #rb Lukasz.Furman #test golden path #jira UE-37496 Change 3166784 on 2016/10/18 by Laurent.Delayen Don't leave PhysicsBodies::bWindEnabled uninitialized in case Wind is not enabled (Persona) #rb none #c0dereview benn.gallagher #test Chains in Persona Change 3166641 on 2016/10/18 by Mieszko.Zielinski Made the value span used for EQS item score normalization configurable #UE4 #rb Lukasz.Furman #test golden path Change 3166632 on 2016/10/18 by Jason.Bestimt #R0B0MERGE-AUTHOR: andrew.grant Added support for multiple deployment sandboxes on PS4. BuildCookRun -deploy=SomeDir ps4.elf -deployedbuild=SomeDir Omitting name in -deploy/-deployedbuild falls back to previous default of using 'GameName' as the deployment sandbox. #tests BuildCookRun with -deploy and -deploy=Orion_v33, Ran PS4 with -deployedbuild and -deployedbuild=Orion_v33 [c0dereviewed] Marcus.Wassmer, Luke.Thatcher #rb none #R0B0MERGE-SOURCE: CL 3166622 in //Orion/Release-33/... via CL 3166629 via CL 3166630 via CL 3166631 #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change3166494on 2016/10/18 by Michael.Noland Engine: Removed irrelevant GPU stats from FPS chart server analytics reports #jira OR-13877 #rb david.ratti #tests Ran golden path on uncooked Win64, used forcewinmatch, and inspected the analytics arrays Change 3166476 on 2016/10/18 by Michael.Noland Cooker: Deleting dead GenerateManifestInfo methods #tests Compiled #rb none #c0dereview daniel.lamb Change 3166471 on 2016/10/18 by Michael.Noland Making sure DebugWorlds/DebugWorldNames are in sync, since we expect an index from one to match the other (fixes an issue where users could select a debug world and the wrong one was chosen). [reimplementing CL# 3157138 by Mike.Beach] #jira UE-37107 #rb Dan.OConnor #tests Tested multiplayer PIE and opening an anim BP Change 3166460 on 2016/10/18 by Michael.Noland Particles: Prevent log spam on servers about stripped emitters #rb graeme.thornton [reimplementing CL# 3157862 by Simon.Tovey] #tests Ran an uncooked server and tested golden path Change 3166339 on 2016/10/18 by Laurent.Delayen oops, this file got away. #rb none #tests none Change 3166337 on 2016/10/18 by Laurent.Delayen Fix for AnimDynamics Wind crash. https://jira.it.epicgames.net/browse/OR-30351 Access WindParameters on GameThread. Also minor optimization: don't iterate over bodies every frame if wind is off. #rb benn.gallagher #c0dereview lina.halper, thomas.sarkanen #tests Vamp Change 3166207 on 2016/10/18 by Mieszko.Zielinski Bot perception work #Orion Added a new sense that makes bots know about enemies visible on the minimap Made jungle minions do not register as sight sources Cleaned up bot perception component a bit #rb none #test golden path Change 3166138 on 2016/10/18 by Michael.Noland Blueprints: Converted a crash with the debug world name in the BP editor to an ensure until it can be fixed properly (see OR-29650) #c0dereview dan.oconnor #rb none #tests Tested opening an anim BP during multiplayer PIE Change 3165860 on 2016/10/18 by David.Ratti remove some debug code that wasn't intended to be checked in #rb none #tests compile Change 3165288 on 2016/10/17 by Ian.Fox #XMPP - Add correlation id attribute to outgoing stanzas #RB Rob.Cannaday #Tests Correlation IDs come back in responses to xmpp messages we send #JIRA OGS-409 Change 3165096 on 2016/10/17 by David.Ratti Fix issue where gameplay tags net indices would be out of sync on cooked PS4 client playing on uncooked windows servers. Would cause some effects to not play. #rb none #tests PS4/PC crossplay Change 3164973 on 2016/10/17 by Dan.Hertzka Fix link error #rb #tests compile Change 3164910 on 2016/10/17 by Lukasz.Furman fixed bug in merging behavior tree searches copy of CL 3164903 #ue4 #rb Mieszko.Zielinski #tests none Change 3164908 on 2016/10/17 by Dan.Hertzka Exposing the blur widget for use in Paragon ** Use OrionBlurWidget, not the base BackgroundBlurWidget - Added it to the hero and default tooltips for reference #rb none #c0dereview Marcel.Swanepoel, Sean.Smith, Bryan.Rathman #tests PIE Change 3164482 on 2016/10/17 by David.Ratti Editor loadtime improvements * Refactor GameplayCue manager to support two distinct object library sets: Runtime and Editor. Editor library operates on all valid gameplay cue paths but never loads or scans, only reflects what asset registry has found. Runtime library is the initial loaded paths + any explicit requests. These scan when needed and async load at startup. * Wrote UOrionAsyncLoadRequestQueue to feed the async load queue with requests at startup. This is to avoid submitting 300+ requests at startup and have them flushed by a sync load. The editor will wait until it is fully initialized before kicking these off. * Changed UOrionUIManagerWidget to weakly reference all of its state widgets. In non editor builds these are all loaded at startup like before. In editor builds, we sync load them on demand. This allows us to not load everything in order to PIE. * Added options for loading various pieces of data at editor startup: HUDwidget V4, last used hero data, and shared gameplay cues. * -game -nomcp will now properly async load initial set of data * BeginLoadAsyncData no longer takes netmode as parameter since it is confusing and can just use IsDedicatedServer() internally * Added new log category: LogOrionStartup #rb none #c0dereview Dan.Hertzka #tests pie, golden path, cooked PS4 Change 3163635 on 2016/10/14 by Laurent.Delayen AnimInstance: Pass a few FNames by reference instead of by value. Added CalcSlotMontageLocalWeight to get local a slot's local weight without a frame a lag. Fixed Montage update happening after native update on gamethread, but before native update on worker thread. Now happens before both, so we can reliably get montage weights without a frame of lag regardless of where we access it. #rb none #c0dereview martin.wilson, thomas.sarkanen #tests fixes Twinblast's primary fire blend out having a frame a lag. Change 3163620 on 2016/10/14 by Laurent.Delayen AnimNode_Slot debug: Show actual slot local weight, instead of always 1. #rb none #c0dereview martin.wilson #tests twinblast debug Change 3163061 on 2016/10/14 by Andrew.Grant Pulling test framework changes into seprate CL #rb #tests na Change 3162675 on 2016/10/13 by Jason.Bestimt #R0B0MERGE-AUTHOR: andrew.grant Merging test framework changes down early. #rb #tests na #R0B0MERGE-SOURCE: CL 3162674 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3162062 on 2016/10/13 by Michael.Trepka Replaced FWindowsCursor hack for warping the mouse cursor to the center of the viewport with a better fix for the original problem (users being able to resize the window while the cursor is hidden and the mouse controls the camera). This change removes round window corners in borderless window mode and disables window resizing when the cursor is hidden. #rb Matt.Kuhlenschmidt #tests Tested in editor build on PC Change 3161489 on 2016/10/13 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 33.2 @ CL 3161453 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3161473 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3160664 on 2016/10/12 by Ben.Salem Fix logic order error fuzzy matching on automated tests - we were only allowing one match per filter instead of one match per test. #rb adric.worley #tests Ran All Ftests that start with S Change 3159866 on 2016/10/12 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 33.2 @ CL3159727#RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3159865 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3158870 on 2016/10/11 by John.Barrett Fixed issue where some PacketHandler parsing errors, would not stop NetConnection processing of packets, and would not trigger a disconnect. #JIRA OR-29219 #rb none #tests compiles, client/server Change 3158336 on 2016/10/11 by Lukasz.Furman string pulling for local navigation grids #ue4 #rb Mieszko.Zielinski #tests PIE Change 3158203 on 2016/10/11 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 33.2 @ CL 3158043 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3158154 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3158162 on 2016/10/11 by Matt.Kuhlenschmidt Added a blur widget to umg that applies a blur effect to whatever is behind the widget - The widget has a content slot that can be used to display unblurred content on top of the blur - The widget has a low quality mode brush that can be applied instead of the background blur. This is enabled by the cvar Slate.ForceBackgroundBlurLowQualityOverride=1 - This widget is currently expermental and must be subclassed to be used #tests Tested on PS4, PC, Mac (opengl and metal) #rb nick.darnell Change 3157232 on 2016/10/10 by Lukasz.Furman added local navigation grids: dynamic obstacles on static navmesh #ue4 #rb Mieszko.Zielinski #tests none, disabled by default Change 3157112 on 2016/10/10 by Laurent.Delayen Removed my layer anim node fix, since Martin did a similar fix. #rb none #tests compiles #c0dereview martin.wilson Change 3156789 on 2016/10/10 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 33.2 @ CL 3156726 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3156788 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3156717 on 2016/10/10 by Jason.Bestimt #R0B0MERGE-AUTHOR: andrew.grant Merging 3156681 from //Orion/Release-33 to Main #rb #tests na #R0B0MERGE-SOURCE: CL 3156713 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3156596 on 2016/10/10 by Martin.Wilson Fix pose flickering on LOD change when using Layered Blend by Bone node (recreated from dev-framework CL 3112086) #Jira OR-30017 #rb Lina.Halper #tests Tested affected anim nodes in editor Change 3156149 on 2016/10/08 by Jason.Bestimt #R0B0MERGE-AUTHOR: andrew.grant Merging test framework changes from //Orion/Release-33.2 to Main (//Orion/Main) #rb #tests na #R0B0MERGE-SOURCE: CL 3156148 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3155444 on 2016/10/07 by David.Ratti -Fix crashes from FScalableFloats caching raw curve pointers by invalidating cache on curve table swaps -Removed the old code that was trying to do this in the editor on reimport, which never actually worked properly. #rb none #tests golden path Change 3155228 on 2016/10/07 by Michael.Trepka Partial (Windows implementation only) copy of CL 3151851 from //UE4/Main Added DesktopRect and WorkArea to FMonitorInfo for desktop platforms and used that to fix an issue in SceneViewport where windowed fullscreen mode would be forced to primary monitor and incorrectly positioned on desktops where a taskbar/dock/etc. was on the left #rb Dmitry.Rekman #tests Tested in editor build on PC Change 3154910 on 2016/10/07 by Lukasz.Furman added new accessors in TSimpleCellGrid and inlined bunch of functions #ue4 #rb none #tests none Change 3154906 on 2016/10/07 by Lukasz.Furman adjusted comments for FGraphAStar #ue4 #rb none #tests none Change 3154679 on 2016/10/07 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 33 @ CL 3154662 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3154677 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3153638 on 2016/10/06 by Andrew.Grant Duplicating fix for UE-36087 from UE4 #rb #tests na Change 3153325 on 2016/10/06 by David.Ratti CurveTableSets: support for multiple spread sheets -Sovereign data located in Sovereign subfolder, cloned from base data. #rb none #tests PIE, golden path Change 3153318 on 2016/10/06 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge DUI @ CL 3152667 #RB:none #Tests:none [c0dereviewed]: kerrington.smith, matt.schembari #R0B0MERGE-SOURCE: CL 3153310 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change3153268on 2016/10/06 by David.Ratti Missed file for engine changes #rb none #tests none Change 3153264 on 2016/10/06 by David.Ratti Move some DetailCustomziation classes to public folder so that games can override/extend them. Also made some virtual functions to override the things paragon needs to. #rb none #tests paragon editor Change 3153204 on 2016/10/06 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 32.2/33 @ CL 3152587 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3153171 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3152699 on 2016/10/05 by Andrew.Grant I apologize for the mega-checkin but there are a lot of dependencies here, the work spiralled, and I've been cranking to get this in for the v33 branch. Please review and comment on what you know about and don't worry about the rest :) -Added options-struct to MallocLeak detection to allow filtering open callstacks by frame range and size. This is now used in Paragon automatic tests to dump out memory left loaded by the previous map. -PS4StackWalk now uses lowercase filenames as this is how non-UFS files are staged -Renamed Orion.Foo.cs test scripts to OrionTest.Foo.cs -Split some Orion tests into seprate scripts -Added concept of "TestControllers". These are constructed by OrionEngine based on the -test= commandline and provide a super-simple way to implent state-based logic and checks by overriding base class functions. -Added controllers for Boot, Soak, and Leak checks -Renamed SimpleSolo bot to SimpleSoak. Moved a lot of logic about match composition and state to OrionTestControllerSoak -Added new MatchStarted/MatchEnded delegates to OrionGameState for clients -Fixed issues where OrionGameState_Base::HasMatchStarted would return true for WaitingForPlayers and MatchCountdown -OrionBot code no longer caches command line since some TestControllers set it at runtime -Added some ensures in Draft logic to catch/guard against a crash being triggered by bots. #rb none #tests verified all of the above and much more! #c0dereview David.Ratti, Marcus.Wasmer,Michael.Noland Change 3152605 on 2016/10/05 by Andrew.Grant Suppressed warning about missing parent if parent package was in the KnownMissingPackageList Added Editor ScaleRef stuff to Orion to suppress cooked warning #rb none #c0dereview Marcus.Wassmer #tests Verified warning about ScaleRef being missing is gone Change 3152596 on 2016/10/05 by Andrew.Grant Made ASLR an option that can be disabled. Disabled ASLR for Paragon PS4 Test builds so symbol lookup is available for diagnostics. Made "don't optimize adaptive unity files" an official feature, off by default but turned on in Paragon #c0dereview Luke.Thatcher #rb none #tests Verified test build has symbols that can be resolved. Verified an adaptive unity file is non-optimized, but non-adaptive files are optimized as normal Change 3152399 on 2016/10/05 by Josh.Markiewicz #UE4 - temporary fix for OSS R0B0MERGE issue #rb david.nikdel #test compiles Change 3150916 on 2016/10/04 by Daniel.Lamb Removed warning when shader compiler is in a bad state. #rb Andrew.Grant #jira OR-29580 #test Cook paragon Change 3150889 on 2016/10/04 by Ben.Salem Add log feedback to automation harness when processing commands, including syntax helpers when an unhandled command is added. #rb adric.worley #tests Ran several commands to see log output. Change 3150844 on 2016/10/04 by Lukasz.Furman compilation fix #rb none #tests none Change 3150759 on 2016/10/04 by Lukasz.Furman added "hidden" state to gameplay debugger category #ue4 #rb Mieszko.Zielinski #tests config changes and PIE Change 3150758 on 2016/10/04 by Lukasz.Furman pass on SimpleCellGrid template to make it usable for local navigation grids #orion #rb Mieszko.Zielinski #tests PIE on agora, AI tactics debug on agora Change 3150567 on 2016/10/04 by Dan.Hertzka Rough initial implementation of a generic UI layer for moving widgets around to and from arbitrary locations. Lots of possible uses. For example, equipping a card in the card shop that then animates down into the correct hand slot. - Not in actual use anywhere yet #rb none #tests PIE Change 3150307 on 2016/10/04 by Laurent.Delayen Removed check() not considering SimulatedRootMotion for RemoteClients. #rb none #tests compiles Change 3150236 on 2016/10/04 by Josh.Markiewicz #UE4 - added documentation to FNetworkNotify interface - fixed bad UE_LOG category while double checking the above #rb none #tests compiles Change3150206on 2016/10/04 by Josh.Markiewicz #UE4 - moved ClientTravelToSession out of AGameSession and into UGameInstance - removed similar function from UGameInstanceCommon - more common usage location #rb none #c0dereview paul.moore #tests rejoin vectors and golden path Change 3150073 on 2016/10/04 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 32.2/33 @ CL 3150010 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3150072 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3150031 on 2016/10/04 by Mieszko.Zielinski New AIData provider that generated random numbers #UE4 #rb Lukasz.Furman #test golden path Change 3149946 on 2016/10/04 by Ben.Woodhouse Make UPrimitiveComponents (and derived variants) take proxy memory into account in GetResourceSize() We do this by dereferencing the SceneProxy directly, but this should be safe, since we NULL it on the gamethread before the proxy is released. #jira OR-26778 #rb luke.thatcher #tests compile, run Win64 with -game, run editor Change 3149743 on 2016/10/03 by Ben.Salem Null check for blank test names when making functional tests to repair crash on server. #rb nick.darnell #tests Ran multiple FTests Change 3149460 on 2016/10/03 by Laurent.Delayen Refactored TickCharacterPose. Now calls 'ShouldTickPose' so it can get properly obey bPauseAnims, MeshComponentUpdateFlag and other conditions. Still forces updates when playing networked root motion montages, and that check is now done inside of USkeletalMeshComponent::ShouldTickPose(). Fixes human players always calling TickPose regardless of settings on dedicated servers. Also addresses Jira UE-34720 #rb martin.wilson #tests networked Vamp x2 + golden path Change 3149435 on 2016/10/03 by Mieszko.Zielinski Fixed a bug in EQS item score normalization for the purposes of drawing #UE4 Also, made printed out scores not normalized since seeing original EQS calculated score desirable #rb Lukasz.Furman #test golden path Change 3148550 on 2016/10/03 by John.Barrett Fixed bad/blocking ensure added in FBitReader. OR-29219 #tests compile #rb none Change 3147460 on 2016/09/30 by Laurent.Delayen Fixed AOrionChar::UpdateAnimationTicking never setting EMeshComponentUpdateFlag::OnlyTickPoseWhenRendered, because OnMontageEnded() is called before the MontageInstance is actually removed and deleted. Added OnAllMontageInstancesEnded to AnimInstance, and used that to call UpdateAnimationTicking. #rb michael.noland #tests Golden Path Change 3146677 on 2016/09/30 by Jamie.Dale Fixed UGatherTextFromSourceCommandlet::ParseSourceText being able to underflow while parsing #rb Andrew.Rodham #tests Ran the gather Change 3146555 on 2016/09/30 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 32.2 @ CL 3146524 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3146553 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3146129 on 2016/09/29 by Michael.Noland Engine: Fixed the spectator camera (used in ToggleDebugCamera) so it moves consistently regardless of the slomo value by using the raw frame delta rather than trying to counter-correct for time dilation #c0dereview marc.audy #rb none #tests Tested ToggleDebugCamera with slomo 0.00001 Change 3145574 on 2016/09/29 by Adric.Worley Fix FunctionalTestingManager not compiling when included #tests compile #rb mieszko.zielinski Change 3145224 on 2016/09/29 by Michael.Trepka Better check for whether or not PreFullscreenWindowPlacement in FWindowsWindow is valid #rb Dmitry.Rekman #tests Tested editor build on PC Change 3145132 on 2016/09/29 by Alexis.Matte Make sure we use GetMesh instead of the SkeletalMeshPtr variable. #jira OR-29617 #rb matt.kuhlenschmidt #test none Change 3144926 on 2016/09/29 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 32.2 @ CL 3144835 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3144925 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3144920 on 2016/09/29 by Benn.Gallagher Added "Reset Clothing Sim" anim notify to trigger a clothing reset from an animation, to help with issues arising from extreme movements in animations. #rb James.Golding #tests Editor + -game vamp RMB abilities using new notify Change 3144055 on 2016/09/28 by Jason.Bestimt #R0B0MERGE-AUTHOR: ben.marsh BuildGraph: Fix builds created with preconditions on nodes behind triggers, causing nightly builds to run forever due to conditions never evaluating to true. #rb none #tests Compared exported job definition before and after #R0B0MERGE-SOURCE: CL 3143992 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3143801 on 2016/09/28 by Mieszko.Zielinski Made UAIBlueprintHelperLibrary::CreateMoveToProxyObject deduce WorldContextObject from Pawn if not received from BP #Orion Also, made failing to do so not fails a check #rb Lukasz.Furman #test golden path #c0dereview Aaron.Eady Change 3142377 on 2016/09/27 by Jason.Bestimt #R0B0MERGE-AUTHOR: marcus.wassmer Fix crashes when using GBuffer resources in simpleforward mode. #rb Daniel.Wright #test vamp Q on low settings. #R0B0MERGE-SOURCE: CL 3142376 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3141628 on 2016/09/27 by David.Ratti Guard against recursion in WaitGameplayEffectApplied ability task #rb none #tests pie crash case Change 3141497 on 2016/09/27 by Marcus.Wassmer Duplicate 3123743 Separate skeletal/static mesh lod interfaces #rb none #test created an LOD for vamp locally. Change3140832on 2016/09/26 by Jason.Bestimt #R0B0MERGE-AUTHOR: marcus.wassmer Remove dubious non-threadsafe GBuffer reference adjustments. Possibly fix OR-29506 #rb none #test PC on all settings #R0B0MERGE-SOURCE: CL 3140831 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3140828 on 2016/09/26 by Uriel.Doyon Workaround (reverting previous attemp) at fixing issue with FTextRenderSceneProxy when running command let. #rb marcus.wassmer #tests running lighting build with command let & loading editor Change 3140331 on 2016/09/26 by Jason.Bestimt #R0B0MERGE-AUTHOR: andrew.grant Cloning fix for UE-36253 from //UE4/Dev-Framework/... #rb #tests na #R0B0MERGE-SOURCE: CL 3140329 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3139976 on 2016/09/26 by David.Ratti balance tweaker + some prep for multiple data tables support #rb none #tests pie, golden path Change 3139904 on 2016/09/26 by Jason.Bestimt #R0B0MERGE-AUTHOR: andrew.grant Adding code to log name of package that refuses to load... #rb none #tests compiled #R0B0MERGE-SOURCE: CL 3139902 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change3139871on 2016/09/26 by Jason.Bestimt #R0B0MERGE-AUTHOR: andrew.grant Fixes for OR-29229 and OR-29413 #rb #tests na #R0B0MERGE-SOURCE: CL 3139870 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3139751 on 2016/09/26 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 32.2 @ CL 3139692 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL3139740in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3139451 on 2016/09/25 by Uriel.Doyon Submitted a workaround for the lighting build command let crash. #rb none #tests loaded editor, built lighting command let Change 3138304 on 2016/09/23 by David.Ratti Fix checkslow in Debug editor #rb none #tests debug editor #c0dereview Martin.Wilson Change 3138068 on 2016/09/23 by Laurent.Delayen Don't try to match invalid GUIDs in FSmartNameMapping::GetNameByGuid. Fixes Steel's curves all getting matched to 'DistanceCurve' #rb martin.wilson #tests Steel's curve are not all 'DistanceCurve' Change 3137830 on 2016/09/23 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 32.2 @ CL 3137699 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3137746 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3137657 on 2016/09/23 by Ben.Marsh Fix initialization order warning. #rb none #tests none Change 3137628 on 2016/09/23 by bruce.nesbit Fixed non-unity compiles in LandscapeSplines and LandscapeSplineRaster #rb none #tests Compiled NU Change 3137538 on 2016/09/23 by Thomas.Sarkanen Fix crash rendering sequence with keyframed material parameters Ported Frank F's fix from Dev-Sequencer. Original CL 3136577: Sequencer - Always use a unique name when creating dynamic material instances for animation to prevent reuse and resource issues. #tests Rendered out problematic sequence successfully multiple times #rb none #jira UE-36175 - Keyframing material parameters can cause crashes when rendering #c0dereview Frank.Fella Change 3136580 on 2016/09/22 by Ben.Marsh Merging CL 3136158 to fix support for generating project files with Visual Studio Express. #rb none #tests none Change 3136574 on 2016/09/22 by Michael.Trepka Fixed a crash caused by trying to redraw window contents while switching from fullscreen to windowed mode #rb Marcus.Wassmer #tests Tested editor build on PC Change 3136293 on 2016/09/22 by Adric.Worley Add BlueprintType to EFunctionalTestResult #tests editor #rb ben.salem #c0dereview nick.darnell Change 3136240 on 2016/09/22 by Andrew.Grant Merging from //UE4/Main @ 3135156 #rb none #tests QA pass and local golden path Change 3136197 on 2016/09/22 by Jamie.Dale Merging CL# 3094477 and CL# 3111827 to fix some tesselated landscape crashes #rb Gareth.Martin #tests Loaded the map that was crashing Change 3135914 on 2016/09/22 by Dan.Youhon Fixed CharacterMovementComponent impulse net correction handling during additive root motion (part of #OR-5545) - Fixes #OR-28478, heroes tethered by Kwang cannot be knocked up - Fixes #OR-18985, Gideon R ability negating knockback/knockup effects (including Howitzer E) #rb None #tests MultiPIE #R0B0MERGE: MAIN, 32.2, 32.1 Change 3135893 on 2016/09/22 by David.Ratti GameplayCueeditir Change override type from a checkbox to a combobox to make things a little clearer #rb none #tests gameplaycue editor Change 3135843 on 2016/09/22 by jason.bestimt #ORION_MAIN - Merge 32.2 @ CL 3135756 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3135820 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) #R0B0MERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human. //Orion/Dev-General/OrionGame/Content/Balance/HeroData.uasset - can't integrate exclusive file already opened //Orion/Dev-General/OrionGame/Content/Cards/Effects/P_ThunderCleaver.uasset - can't integrate exclusive file already opened //Orion/Dev-General/OrionGame/DataTables/HeroData.xlsm - can't integrate exclusive file already opened #c0dereview: jason.bestimt Change 3134639 on 2016/09/21 by jason.bestimt #ORION_MAIN - Merge 32.2 @ CL3133910#RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3134086 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) #R0B0MERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human. #c0dereview: jason.bestimt Change 3134367 on 2016/09/21 by Ben.Woodhouse More complete fix for SSAO issues. Disable vertex fogging automatically if forward shading is disabled #rb daniel.wright #tests none Change 3134176 on 2016/09/21 by Jason.Bestimt #ORION_DG - UnrealPak speed improvements Moving shelved CL to DG and submitting for DanielL #RB:none #Tests:none #c0dereview: andrew.grant, daniel.lamb Change 3134129 on 2016/09/21 by Jamie.Dale Added the "unattended" flag when running the localzation commandlets via UAT #rb none #tests Built UAT Change 3133864 on 2016/09/21 by Ben.Woodhouse Default r.VertexFoggingForOpaque to 0, since it only makes sense for forward shading. This was causing fog to be modulated by SSAO in Orion. Note: this setting is overridden to 1 in Odin's DefaultEngine.ini, so it should work in that case. #c0dereview daniel.wright #rb luke.thatcher #jira OR-29262 #tests yes Change 3133849 on 2016/09/21 by Martin.Wilson Fix pose blending for on non-additive pose blending + remove normalising of weights for weights less than 1 #rb Jurre.DeBaare #tests Editor tests with mambo pose asset #jira UE-36189 Change 3133546 on 2016/09/20 by Jason.Bestimt #R0B0MERGE-AUTHOR: marcus.wassmer Null merge of3131588. Fix from 31.1 is unncecessary as a more complete fix came from the engine integration that's in v32. #rb none #tests none [c0dereviewed] Jason.Bestimt #R0B0MERGE-SOURCE: CL 3132617 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3133487 on 2016/09/20 by Michael.Noland Automation: Added Automation to the manual autocomplete list Change 3133363 on 2016/09/20 by Daniel.Lamb Added Jaymee Sanford and Tony Oliva to the rebuild lighting email list. #rb Trivial #test Compile automation tool Change 3132956 on 2016/09/20 by Benn.Gallagher Fixed crash when importing clothing with mismatching number of triangles when compared to the original render data #tests Editor, apex reimport #rb none Change 3132403 on 2016/09/20 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 32.2 @ CL 3132254 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3132353 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3132332 on 2016/09/20 by Andrew.Grant Replicated UE4/Main fix for missing materials pane #rb none #tests verified material pane shows Change 3132131 on 2016/09/20 by Jason.Bestimt #R0B0MERGE-AUTHOR: andrew.grant Merging automation work from //Orion/Release-32.2 to Main #rb none #tests verified functionality #R0B0MERGE-SOURCE: CL 3132130 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) Change 3131698 on 2016/09/19 by Andrew.Grant Qucik fix to unblock build. Will follow up correct way tomorrow #rb none #tests blueprint compiles Change 3131489 on 2016/09/19 by Andrew.Grant Merging from //UE4/Orion-Staging (Source: //UE4/Main @ 3111290) #rb none #tests QA pass in Orion-Staging, Golden path post merge Change 3131350 on 2016/09/19 by Adric.Worley Fix functional test reporting typo #tests PIE #rb ben.salem Change 3130959 on 2016/09/19 by Mieszko.Zielinski Compilation fix #UE4 #rb none #test compilation Change 3130904 on 2016/09/19 by Mieszko.Zielinski Couple of generic AI perception fixes #UE4 Made unregistering AI sight source broadcast "no longer visible" information to all observers currently "seeing" the source Fixed FActorPerceptionInfo::GetLastStimulusLocation not carying whether selected stimulus was successfully sensed Fixed dominant sense not really working if not set with UAIPerceptionComponent::SetDominantSense call #rb Lukasz.Furman #test golden path Change 3130304 on 2016/09/19 by Jason.Bestimt #R0B0MERGE-AUTHOR: jason.bestimt #ORION_MAIN - Merge 32.2 @ CL 3130115 #RB:none #Tests:none #R0B0MERGE-SOURCE: CL 3130164 in //Orion/Main/... #R0B0MERGE-BOT: ORION (Main -> Dev-General) [CL 3205566 by Andrew Grant in Main branch]
8818 lines
301 KiB
C++
8818 lines
301 KiB
C++
// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "MeshUtilitiesPrivate.h"
|
|
#include "StaticMeshResources.h"
|
|
#include "SkeletalMeshTypes.h"
|
|
#include "MeshBuild.h"
|
|
#include "TessellationRendering.h"
|
|
#include "NvTriStrip.h"
|
|
#include "forsythtriangleorderoptimizer.h"
|
|
#include "nvtess.h"
|
|
#include "SkeletalMeshTools.h"
|
|
#include "ImageUtils.h"
|
|
#include "Textures/TextureAtlas.h"
|
|
#include "LayoutUV.h"
|
|
#include "mikktspace.h"
|
|
#include "DistanceFieldAtlas.h"
|
|
#include "FbxErrors.h"
|
|
#include "Components/SplineMeshComponent.h"
|
|
#include "PhysicsEngine/BodySetup.h"
|
|
#include "MaterialUtilities.h"
|
|
#include "HierarchicalLODUtilities.h"
|
|
#include "HierarchicalLODUtilitiesModule.h"
|
|
#include "MeshBoneReduction.h"
|
|
#include "MeshMergeData.h"
|
|
#include "Editor/EditorPerProjectUserSettings.h"
|
|
#include "GPUSkinVertexFactory.h"
|
|
|
|
#include "Landscape.h"
|
|
#include "LandscapeProxy.h"
|
|
#include "LandscapeHeightfieldCollisionComponent.h"
|
|
#include "Engine/MeshMergeCullingVolume.h"
|
|
#include "ProxyMaterialUtilities.h"
|
|
#include "LevelEditor.h"
|
|
#include "IAnimationBlueprintEditorModule.h"
|
|
#include "IAnimationEditorModule.h"
|
|
#include "ISkeletalMeshEditorModule.h"
|
|
#include "ISkeletonEditorModule.h"
|
|
#include "IPersonaToolkit.h"
|
|
#include "Dialogs/DlgPickAssetPath.h"
|
|
#include "SkeletalRenderPublic.h"
|
|
#include "AssetRegistryModule.h"
|
|
#include "SNotificationList.h"
|
|
#include "NotificationManager.h"
|
|
#include "Toolkits/AssetEditorManager.h"
|
|
#include "StaticMeshResources.h"
|
|
#include "PropertyEditing.h"
|
|
#include "Engine/MeshSimplificationSettings.h"
|
|
|
|
//@todo - implement required vector intrinsics for other implementations
|
|
#if PLATFORM_ENABLE_VECTORINTRINSICS
|
|
#include "kDOP.h"
|
|
#endif
|
|
|
|
#if WITH_EDITOR
|
|
#include "Editor.h"
|
|
#endif
|
|
|
|
|
|
/*------------------------------------------------------------------------------
|
|
MeshUtilities module.
|
|
------------------------------------------------------------------------------*/
|
|
|
|
// The version string is a GUID. If you make a change to mesh utilities that
|
|
// causes meshes to be rebuilt you MUST generate a new GUID and replace this
|
|
// string with it.
|
|
|
|
#define MESH_UTILITIES_VER TEXT("228332BAE0224DD294E232B87D83948F")
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogMeshUtilities, Verbose, All);
|
|
|
|
#define LOCTEXT_NAMESPACE "MeshUtils"
|
|
|
|
// CVars
|
|
static TAutoConsoleVariable<int32> CVarTriangleOrderOptimization(
|
|
TEXT("r.TriangleOrderOptimization"),
|
|
1,
|
|
TEXT("Controls the algorithm to use when optimizing the triangle order for the post-transform cache.\n")
|
|
TEXT("0: Use NVTriStrip (slower)\n")
|
|
TEXT("1: Use Forsyth algorithm (fastest)(default)")
|
|
TEXT("2: No triangle order optimization. (least efficient, debugging purposes only)"),
|
|
ECVF_Default);
|
|
|
|
static FAutoConsoleVariable CVarMeshReductionModule(
|
|
TEXT("r.MeshReductionModule"),
|
|
TEXT("QuadricMeshReduction"),
|
|
TEXT("Name of what mesh reduction module to choose. If blank it chooses any that exist.\n"),
|
|
ECVF_ReadOnly);
|
|
|
|
class FMeshUtilities : public IMeshUtilities
|
|
{
|
|
public:
|
|
/** Default constructor. */
|
|
FMeshUtilities()
|
|
: StaticMeshReduction(NULL)
|
|
, SkeletalMeshReduction(NULL)
|
|
, MeshMerging(NULL)
|
|
, DistributedMeshMerging(NULL)
|
|
, Processor(NULL)
|
|
{
|
|
}
|
|
|
|
void UpdateMeshReductionModule()
|
|
{
|
|
TArray<FName> ModuleNames;
|
|
FModuleManager::Get().FindModules(TEXT("*MeshReduction"), ModuleNames);
|
|
|
|
for(int32 Index = 0; Index < ModuleNames.Num(); Index++)
|
|
{
|
|
FString String = CVarMeshReductionModule->GetString();
|
|
bool bIsChoosenModule = ModuleNames[Index].GetPlainNameString().Equals(String);
|
|
|
|
IMeshReductionModule& MeshReductionModule = FModuleManager::LoadModuleChecked<IMeshReductionModule>(ModuleNames[Index]);
|
|
|
|
// Look for MeshReduction interface
|
|
if(MeshReductionModule.GetStaticMeshReductionInterface())
|
|
{
|
|
if(bIsChoosenModule || StaticMeshReduction == NULL)
|
|
{
|
|
StaticMeshReduction = MeshReductionModule.GetStaticMeshReductionInterface();
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Using %s for automatic static mesh reduction"), *ModuleNames[Index].ToString());
|
|
}
|
|
}
|
|
|
|
// Look for MeshReduction interface
|
|
if(MeshReductionModule.GetSkeletalMeshReductionInterface())
|
|
{
|
|
if(bIsChoosenModule || SkeletalMeshReduction == NULL)
|
|
{
|
|
SkeletalMeshReduction = MeshReductionModule.GetSkeletalMeshReductionInterface();
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Using %s for automatic skeletal mesh reduction"), *ModuleNames[Index].ToString());
|
|
}
|
|
}
|
|
|
|
// Look for MeshMerging interface
|
|
if(MeshReductionModule.GetMeshMergingInterface())
|
|
{
|
|
if(bIsChoosenModule || MeshMerging == NULL)
|
|
{
|
|
MeshMerging = MeshReductionModule.GetMeshMergingInterface();
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Using %s for automatic mesh merging"), *ModuleNames[Index].ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private:
|
|
/** Cached pointer to the mesh reduction interface. */
|
|
IMeshReduction* StaticMeshReduction;
|
|
/** Cached pointer to the mesh reduction interface. */
|
|
IMeshReduction* SkeletalMeshReduction;
|
|
/** Cached pointer to the mesh merging interface. */
|
|
IMeshMerging* MeshMerging;
|
|
/** Cached pointer to the distributed mesh merging interface. */
|
|
IMeshMerging* DistributedMeshMerging;
|
|
/** Cached version string. */
|
|
FString VersionString;
|
|
/** True if Simplygon is being used for mesh reduction. */
|
|
bool bUsingSimplygon;
|
|
/** True if NvTriStrip is being used for tri order optimization. */
|
|
bool bUsingNvTriStrip;
|
|
/** True if we disable triangle order optimization. For debugging purposes only */
|
|
bool bDisableTriangleOrderOptimization;
|
|
|
|
class FProxyGenerationProcessor* Processor;
|
|
|
|
// IMeshUtilities interface.
|
|
virtual const FString& GetVersionString() const override
|
|
{
|
|
return VersionString;
|
|
}
|
|
|
|
virtual bool BuildStaticMesh(
|
|
FStaticMeshRenderData& OutRenderData,
|
|
TArray<FStaticMeshSourceModel>& SourceModels,
|
|
const FStaticMeshLODGroup& LODGroup,
|
|
int32 ImportVersion = EImportStaticMeshVersion::LastVersion
|
|
) override;
|
|
|
|
virtual void BuildStaticMeshVertexAndIndexBuffers(
|
|
TArray<FStaticMeshBuildVertex>& OutVertices,
|
|
TArray<TArray<uint32> >& OutPerSectionIndices,
|
|
TArray<int32>& OutWedgeMap,
|
|
const FRawMesh& RawMesh,
|
|
const TMultiMap<int32, int32>& OverlappingCorners,
|
|
const TMap<uint32, uint32>& MaterialToSectionMapping,
|
|
float ComparisonThreshold,
|
|
FVector BuildScale,
|
|
int32 ImportVersion
|
|
) override;
|
|
|
|
virtual bool GenerateStaticMeshLODs(TArray<FStaticMeshSourceModel>& Models, const FStaticMeshLODGroup& LODGroup) override;
|
|
|
|
virtual void GenerateSignedDistanceFieldVolumeData(
|
|
const FStaticMeshLODResources& LODModel,
|
|
class FQueuedThreadPool& ThreadPool,
|
|
const TArray<EBlendMode>& MaterialBlendModes,
|
|
const FBoxSphereBounds& Bounds,
|
|
float DistanceFieldResolutionScale,
|
|
float DistanceFieldBias,
|
|
bool bGenerateAsIfTwoSided,
|
|
FDistanceFieldVolumeData& OutData) override;
|
|
|
|
virtual bool BuildSkeletalMesh(FStaticLODModel& LODModel, const FReferenceSkeleton& RefSkeleton, const TArray<FVertInfluence>& Influences, const TArray<FMeshWedge>& Wedges, const TArray<FMeshFace>& Faces, const TArray<FVector>& Points, const TArray<int32>& PointToOriginalMap, const MeshBuildOptions& BuildOptions = MeshBuildOptions(), TArray<FText> * OutWarningMessages = NULL, TArray<FName> * OutWarningNames = NULL) override;
|
|
bool BuildSkeletalMesh_Legacy(FStaticLODModel& LODModel, const FReferenceSkeleton& RefSkeleton, const TArray<FVertInfluence>& Influences, const TArray<FMeshWedge>& Wedges, const TArray<FMeshFace>& Faces, const TArray<FVector>& Points, const TArray<int32>& PointToOriginalMap, bool bKeepOverlappingVertices = false, bool bComputeNormals = true, bool bComputeTangents = true, TArray<FText> * OutWarningMessages = NULL, TArray<FName> * OutWarningNames = NULL);
|
|
|
|
virtual IMeshReduction* GetStaticMeshReductionInterface() override;
|
|
virtual IMeshReduction* GetSkeletalMeshReductionInterface() override;
|
|
virtual IMeshMerging* GetMeshMergingInterface() override;
|
|
virtual void CacheOptimizeIndexBuffer(TArray<uint16>& Indices) override;
|
|
virtual void CacheOptimizeIndexBuffer(TArray<uint32>& Indices) override;
|
|
void CacheOptimizeVertexAndIndexBuffer(TArray<FStaticMeshBuildVertex>& Vertices, TArray<TArray<uint32> >& PerSectionIndices, TArray<int32>& WedgeMap);
|
|
|
|
virtual void BuildSkeletalAdjacencyIndexBuffer(
|
|
const TArray<FSoftSkinVertex>& VertexBuffer,
|
|
const uint32 TexCoordCount,
|
|
const TArray<uint32>& Indices,
|
|
TArray<uint32>& OutPnAenIndices
|
|
) override;
|
|
|
|
virtual void RechunkSkeletalMeshModels(USkeletalMesh* SrcMesh, int32 MaxBonesPerChunk) override;
|
|
|
|
virtual void CalcBoneVertInfos(USkeletalMesh* SkeletalMesh, TArray<FBoneVertInfo>& Infos, bool bOnlyDominant) override;
|
|
|
|
/**
|
|
* Convert a set of mesh components in their current pose to a static mesh.
|
|
* @param InMeshComponents The mesh components we want to convert
|
|
* @param InRootTransform The transform of the root of the mesh we want to output
|
|
* @param InPackageName The package name to create the static mesh in. If this is empty then a dialog will be displayed to pick the mesh.
|
|
* @return a new static mesh (specified by the user)
|
|
*/
|
|
virtual UStaticMesh* ConvertMeshesToStaticMesh(const TArray<UMeshComponent*>& InMeshComponents, const FTransform& InRootTransform = FTransform::Identity, const FString& InPackageName = FString()) override;
|
|
|
|
/**
|
|
* Builds a renderable skeletal mesh LOD model. Note that the array of chunks
|
|
* will be destroyed during this process!
|
|
* @param LODModel Upon return contains a renderable skeletal mesh LOD model.
|
|
* @param RefSkeleton The reference skeleton associated with the model.
|
|
* @param Chunks Skinned mesh chunks from which to build the renderable model.
|
|
* @param PointToOriginalMap Maps a vertex's RawPointIdx to its index at import time.
|
|
*/
|
|
void BuildSkeletalModelFromChunks(FStaticLODModel& LODModel, const FReferenceSkeleton& RefSkeleton, TArray<FSkinnedMeshChunk*>& Chunks, const TArray<int32>& PointToOriginalMap);
|
|
|
|
// IModuleInterface interface.
|
|
virtual void StartupModule() override;
|
|
virtual void ShutdownModule() override;
|
|
|
|
DEPRECATED(4.12, "Please use MergeActor with new signature instead")
|
|
virtual void MergeActors(
|
|
const TArray<AActor*>& SourceActors,
|
|
const FMeshMergingSettings& InSettings,
|
|
UPackage* InOuter,
|
|
const FString& InBasePackageName,
|
|
int32 UseLOD, // does not build all LODs but only use this LOD to create base mesh
|
|
TArray<UObject*>& OutAssetsToSync,
|
|
FVector& OutMergedActorLocation,
|
|
bool bSilent = false) const override;
|
|
|
|
virtual void MergeActors(
|
|
const TArray<AActor*>& SourceActors,
|
|
const FMeshMergingSettings& InSettings,
|
|
UPackage* InOuter,
|
|
const FString& InBasePackageName,
|
|
TArray<UObject*>& OutAssetsToSync,
|
|
FVector& OutMergedActorLocation,
|
|
bool bSilent = false) const override;
|
|
|
|
|
|
DEPRECATED(4.12, "Please use MergeStaticMeshComponents with new signature instead")
|
|
virtual void MergeStaticMeshComponents(
|
|
const TArray<UStaticMeshComponent*>& ComponentsToMerge,
|
|
UWorld* World,
|
|
const FMeshMergingSettings& InSettings,
|
|
UPackage* InOuter,
|
|
const FString& InBasePackageName,
|
|
int32 UseLOD, // does not build all LODs but only use this LOD to create base mesh
|
|
TArray<UObject*>& OutAssetsToSync,
|
|
FVector& OutMergedActorLocation,
|
|
const float ScreenSize,
|
|
bool bSilent = false) const override;
|
|
|
|
virtual void MergeStaticMeshComponents(
|
|
const TArray<UStaticMeshComponent*>& ComponentsToMerge,
|
|
UWorld* World,
|
|
const FMeshMergingSettings& InSettings,
|
|
UPackage* InOuter,
|
|
const FString& InBasePackageName,
|
|
TArray<UObject*>& OutAssetsToSync,
|
|
FVector& OutMergedActorLocation,
|
|
const float ScreenSize,
|
|
bool bSilent = false) const override;
|
|
|
|
virtual void CreateProxyMesh(const TArray<AActor*>& InActors, const struct FMeshProxySettings& InMeshProxySettings, UPackage* InOuter, const FString& InProxyBasePackageName, const FGuid InGuid, FCreateProxyDelegate InProxyCreatedDelegate, const bool bAllowAsync,
|
|
const float ScreenAreaSize = 1.0f) override;
|
|
|
|
DEPRECATED(4.11, "Please use CreateProxyMesh with new signature")
|
|
virtual void CreateProxyMesh(
|
|
const TArray<AActor*>& Actors,
|
|
const struct FMeshProxySettings& InProxySettings,
|
|
UPackage* InOuter,
|
|
const FString& ProxyBasePackageName,
|
|
TArray<UObject*>& OutAssetsToSync,
|
|
FVector& OutProxyLocation
|
|
) override;
|
|
|
|
virtual void CreateProxyMesh(
|
|
const TArray<AActor*>& Actors,
|
|
const struct FMeshProxySettings& InProxySettings,
|
|
UPackage* InOuter,
|
|
const FString& ProxyBasePackageName,
|
|
TArray<UObject*>& OutAssetsToSync,
|
|
const float ScreenAreaSize = 1.0f) override;
|
|
|
|
virtual void FlattenMaterialsWithMeshData(TArray<UMaterialInterface*>& InMaterials, TArray<FRawMeshExt>& InSourceMeshes, TMap<FMeshIdAndLOD, TArray<int32>>& InMaterialIndexMap, TArray<bool>& InMeshShouldBakeVertexData, const FMaterialProxySettings &InMaterialProxySettings, TArray<FFlattenMaterial> &OutFlattenedMaterials) const override;
|
|
|
|
bool ConstructRawMesh(
|
|
const UStaticMeshComponent* InMeshComponent,
|
|
int32 InLODIndex,
|
|
const bool bPropagateVertexColours,
|
|
FRawMesh& OutRawMesh,
|
|
TArray<FSectionInfo>& OutUniqueSections,
|
|
TArray<int32>& OutGlobalMaterialIndices
|
|
) const;
|
|
|
|
virtual void ExtractMeshDataForGeometryCache(FRawMesh& RawMesh, const FMeshBuildSettings& BuildSettings, TArray<FStaticMeshBuildVertex>& OutVertices, TArray<TArray<uint32> >& OutPerSectionIndices, int32 ImportVersion);
|
|
|
|
virtual bool PropagatePaintedColorsToRawMesh(const UStaticMeshComponent* StaticMeshComponent, int32 LODIndex, FRawMesh& RawMesh) const override;
|
|
|
|
virtual void CalculateTextureCoordinateBoundsForRawMesh(const FRawMesh& InRawMesh, TArray<FBox2D>& OutBounds) const override;
|
|
|
|
virtual void CalculateTextureCoordinateBoundsForSkeletalMesh(const FStaticLODModel& LODModel, TArray<FBox2D>& OutBounds) const override;
|
|
|
|
virtual bool GenerateUniqueUVsForStaticMesh(const FRawMesh& RawMesh, int32 TextureResolution, TArray<FVector2D>& OutTexCoords) const override;
|
|
virtual bool GenerateUniqueUVsForSkeletalMesh(const FStaticLODModel& LODModel, int32 TextureResolution, TArray<FVector2D>& OutTexCoords) const override;
|
|
|
|
virtual bool RemoveBonesFromMesh(USkeletalMesh* SkeletalMesh, int32 LODIndex, const TArray<FName>* BoneNamesToRemove) const override;
|
|
|
|
virtual void CalculateTangents(const TArray<FVector>& InVertices, const TArray<uint32>& InIndices, const TArray<FVector2D>& InUVs, const TArray<uint32>& InSmoothingGroupIndices, const uint32 InTangentOptions, TArray<FVector>& OutTangentX, TArray<FVector>& OutTangentY, TArray<FVector>& OutNormals) const override;
|
|
|
|
// Need to call some members from this class, (which is internal to this module)
|
|
friend class FStaticMeshUtilityBuilder;
|
|
|
|
protected:
|
|
void AddAnimationBlueprintEditorToolbarExtender();
|
|
|
|
void RemoveAnimationBlueprintEditorToolbarExtender();
|
|
|
|
TSharedRef<FExtender> GetAnimationBlueprintEditorToolbarExtender(const TSharedRef<FUICommandList> CommandList, TSharedRef<IAnimationBlueprintEditor> InAnimationBlueprintEditor);
|
|
|
|
void AddAnimationEditorToolbarExtender();
|
|
|
|
void RemoveAnimationEditorToolbarExtender();
|
|
|
|
TSharedRef<FExtender> GetAnimationEditorToolbarExtender(const TSharedRef<FUICommandList> CommandList, TSharedRef<IAnimationEditor> InAnimationEditor);
|
|
|
|
void AddSkeletalMeshEditorToolbarExtender();
|
|
|
|
void RemoveSkeletalMeshEditorToolbarExtender();
|
|
|
|
TSharedRef<FExtender> GetSkeletalMeshEditorToolbarExtender(const TSharedRef<FUICommandList> CommandList, TSharedRef<ISkeletalMeshEditor> InSkeletalMeshEditor);
|
|
|
|
void AddSkeletonEditorToolbarExtender();
|
|
|
|
void RemoveSkeletonEditorToolbarExtender();
|
|
|
|
TSharedRef<FExtender> GetSkeletonEditorToolbarExtender(const TSharedRef<FUICommandList> CommandList, TSharedRef<ISkeletonEditor> InSkeletonEditor);
|
|
|
|
void HandleAddConvertComponentToStaticMeshToToolbar(FToolBarBuilder& ParentToolbarBuilder, UMeshComponent* MeshComponent);
|
|
|
|
void AddLevelViewportMenuExtender();
|
|
|
|
void RemoveLevelViewportMenuExtender();
|
|
|
|
TSharedRef<FExtender> GetLevelViewportContextMenuExtender(const TSharedRef<FUICommandList> CommandList, const TArray<AActor*> InActors);
|
|
|
|
void ConvertActorMeshesToStaticMesh(const TArray<AActor*> InActors);
|
|
|
|
FDelegateHandle ModuleLoadedDelegateHandle;
|
|
FDelegateHandle LevelViewportExtenderHandle;
|
|
FDelegateHandle AnimationBlueprintEditorExtenderHandle;
|
|
FDelegateHandle AnimationEditorExtenderHandle;
|
|
FDelegateHandle SkeletalMeshEditorExtenderHandle;
|
|
FDelegateHandle SkeletonEditorExtenderHandle;
|
|
};
|
|
|
|
IMPLEMENT_MODULE(FMeshUtilities, MeshUtilities);
|
|
|
|
class FProxyGenerationProcessor : FTickerObjectBase
|
|
{
|
|
public:
|
|
FProxyGenerationProcessor()
|
|
{
|
|
#if WITH_EDITOR
|
|
FEditorDelegates::MapChange.AddRaw(this, &FProxyGenerationProcessor::OnMapChange);
|
|
FEditorDelegates::NewCurrentLevel.AddRaw(this, &FProxyGenerationProcessor::OnNewCurrentLevel);
|
|
#endif // WITH_EDITOR
|
|
}
|
|
|
|
~FProxyGenerationProcessor()
|
|
{
|
|
#if WITH_EDITOR
|
|
FEditorDelegates::MapChange.RemoveAll(this);
|
|
FEditorDelegates::NewCurrentLevel.RemoveAll(this);
|
|
#endif // WITH_EDITOR
|
|
}
|
|
|
|
void AddProxyJob(FGuid InJobGuid, FMergeCompleteData* InCompleteData)
|
|
{
|
|
FScopeLock Lock(&StateLock);
|
|
ProxyMeshJobs.Add(InJobGuid, InCompleteData);
|
|
}
|
|
|
|
virtual bool Tick(float DeltaTime) override
|
|
{
|
|
FScopeLock Lock(&StateLock);
|
|
for (const auto& Entry : ToProcessJobDataMap)
|
|
{
|
|
FGuid JobGuid = Entry.Key;
|
|
FProxyGenerationData* Data = Entry.Value;
|
|
|
|
// Process the job
|
|
ProcessJob(JobGuid, Data);
|
|
|
|
// Data retrieved so can now remove the job from the map
|
|
ProxyMeshJobs.Remove(JobGuid);
|
|
delete Data->MergeData;
|
|
delete Data;
|
|
}
|
|
|
|
ToProcessJobDataMap.Reset();
|
|
|
|
return true;
|
|
}
|
|
|
|
void ProxyGenerationComplete(FRawMesh& OutProxyMesh, struct FFlattenMaterial& OutMaterial, const FGuid OutJobGUID)
|
|
{
|
|
FScopeLock Lock(&StateLock);
|
|
FMergeCompleteData** FindData = ProxyMeshJobs.Find(OutJobGUID);
|
|
if (FindData && *FindData)
|
|
{
|
|
FMergeCompleteData* Data = *FindData;
|
|
|
|
FProxyGenerationData* GenerationData = new FProxyGenerationData();
|
|
GenerationData->Material = OutMaterial;
|
|
GenerationData->RawMesh = OutProxyMesh;
|
|
GenerationData->MergeData = Data;
|
|
|
|
ToProcessJobDataMap.Add(OutJobGUID, GenerationData);
|
|
}
|
|
}
|
|
|
|
//@third party BEGIN SIMPLYGON
|
|
void ProxyGenerationFailed(const FGuid OutJobGUID, const FString& ErrorMessage)
|
|
{
|
|
FScopeLock Lock(&StateLock);
|
|
FMergeCompleteData** FindData = ProxyMeshJobs.Find(OutJobGUID);
|
|
if (FindData && *FindData)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Failed to generate proxy mesh for cluster %s, %s"), *(*FindData)->ProxyBasePackageName, *ErrorMessage);
|
|
ProxyMeshJobs.Remove(OutJobGUID);
|
|
}
|
|
}
|
|
//@third party END SIMPLYGON
|
|
|
|
|
|
protected:
|
|
/** Called when the map has changed*/
|
|
void OnMapChange(uint32 MapFlags)
|
|
{
|
|
ClearProcessingData();
|
|
}
|
|
|
|
/** Called when the current level has changed */
|
|
void OnNewCurrentLevel()
|
|
{
|
|
ClearProcessingData();
|
|
}
|
|
|
|
/** Clears the processing data array/map */
|
|
void ClearProcessingData()
|
|
{
|
|
FScopeLock Lock(&StateLock);
|
|
ProxyMeshJobs.Empty();
|
|
ToProcessJobDataMap.Empty();
|
|
}
|
|
|
|
protected:
|
|
/** Structure storing the data required during processing */
|
|
struct FProxyGenerationData
|
|
{
|
|
FRawMesh RawMesh;
|
|
FFlattenMaterial Material;
|
|
FMergeCompleteData* MergeData;
|
|
};
|
|
|
|
void ProcessJob(const FGuid& JobGuid, FProxyGenerationData* Data)
|
|
{
|
|
TArray<UObject*> OutAssetsToSync;
|
|
const FString AssetBaseName = FPackageName::GetShortName(Data->MergeData->ProxyBasePackageName);
|
|
const FString AssetBasePath = Data->MergeData->InOuter ? TEXT("") : FPackageName::GetLongPackagePath(Data->MergeData->ProxyBasePackageName) + TEXT("/");
|
|
|
|
// Retrieve flattened material data
|
|
FFlattenMaterial& FlattenMaterial = Data->Material;
|
|
|
|
// Resize flattened material
|
|
FMaterialUtilities::ResizeFlattenMaterial(FlattenMaterial, Data->MergeData->InProxySettings);
|
|
|
|
// Optimize flattened material
|
|
FMaterialUtilities::OptimizeFlattenMaterial(FlattenMaterial);
|
|
|
|
// Create a new proxy material instance
|
|
UMaterialInstanceConstant* ProxyMaterial = ProxyMaterialUtilities::CreateProxyMaterialInstance(Data->MergeData->InOuter, Data->MergeData->InProxySettings.MaterialSettings, FlattenMaterial, AssetBasePath, AssetBaseName);
|
|
|
|
// Set material static lighting usage flag if project has static lighting enabled
|
|
static const auto AllowStaticLightingVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.AllowStaticLighting"));
|
|
const bool bAllowStaticLighting = (!AllowStaticLightingVar || AllowStaticLightingVar->GetValueOnGameThread() != 0);
|
|
if (bAllowStaticLighting)
|
|
{
|
|
ProxyMaterial->CheckMaterialUsage(MATUSAGE_StaticLighting);
|
|
}
|
|
|
|
// Construct proxy static mesh
|
|
UPackage* MeshPackage = Data->MergeData->InOuter;
|
|
FString MeshAssetName = TEXT("SM_") + AssetBaseName;
|
|
if (MeshPackage == nullptr)
|
|
{
|
|
MeshPackage = CreatePackage(NULL, *(AssetBasePath + MeshAssetName));
|
|
MeshPackage->FullyLoad();
|
|
MeshPackage->Modify();
|
|
}
|
|
|
|
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(MeshPackage, FName(*MeshAssetName), RF_Public | RF_Standalone);
|
|
StaticMesh->InitResources();
|
|
|
|
FString OutputPath = StaticMesh->GetPathName();
|
|
|
|
// make sure it has a new lighting guid
|
|
StaticMesh->LightingGuid = FGuid::NewGuid();
|
|
|
|
// Set it to use textured lightmaps. Note that Build Lighting will do the error-checking (texcoordindex exists for all LODs, etc).
|
|
StaticMesh->LightMapResolution = Data->MergeData->InProxySettings.LightMapResolution;
|
|
StaticMesh->LightMapCoordinateIndex = 1;
|
|
|
|
FStaticMeshSourceModel* SrcModel = new (StaticMesh->SourceModels) FStaticMeshSourceModel();
|
|
/*Don't allow the engine to recalculate normals*/
|
|
SrcModel->BuildSettings.bRecomputeNormals = false;
|
|
SrcModel->BuildSettings.bRecomputeTangents = false;
|
|
SrcModel->BuildSettings.bRemoveDegenerates = true;
|
|
SrcModel->BuildSettings.bUseHighPrecisionTangentBasis = false;
|
|
SrcModel->BuildSettings.bUseFullPrecisionUVs = false;
|
|
SrcModel->RawMeshBulkData->SaveRawMesh(Data->RawMesh);
|
|
|
|
//Assign the proxy material to the static mesh
|
|
StaticMesh->StaticMaterials.Add(FStaticMaterial(ProxyMaterial));
|
|
|
|
//Set the Imported version before calling the build
|
|
StaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion;
|
|
|
|
StaticMesh->Build();
|
|
StaticMesh->PostEditChange();
|
|
|
|
OutAssetsToSync.Add(StaticMesh);
|
|
|
|
// Execute the delegate received from the user
|
|
Data->MergeData->CallbackDelegate.ExecuteIfBound(JobGuid, OutAssetsToSync);
|
|
}
|
|
protected:
|
|
/** Holds Proxy mesh job data together with the job Guid */
|
|
TMap<FGuid, FMergeCompleteData*> ProxyMeshJobs;
|
|
/** Holds Proxy generation data together with the job Guid */
|
|
TMap<FGuid, FProxyGenerationData*> ToProcessJobDataMap;
|
|
/** Critical section to keep ProxyMeshJobs/ToProcessJobDataMap access thread-safe */
|
|
FCriticalSection StateLock;
|
|
};
|
|
|
|
//@todo - implement required vector intrinsics for other implementations
|
|
#if PLATFORM_ENABLE_VECTORINTRINSICS
|
|
|
|
class FMeshBuildDataProvider
|
|
{
|
|
public:
|
|
|
|
/** Initialization constructor. */
|
|
FMeshBuildDataProvider(
|
|
const TkDOPTree<const FMeshBuildDataProvider, uint32>& InkDopTree) :
|
|
kDopTree(InkDopTree)
|
|
{}
|
|
|
|
// kDOP data provider interface.
|
|
|
|
FORCEINLINE const TkDOPTree<const FMeshBuildDataProvider, uint32>& GetkDOPTree(void) const
|
|
{
|
|
return kDopTree;
|
|
}
|
|
|
|
FORCEINLINE const FMatrix& GetLocalToWorld(void) const
|
|
{
|
|
return FMatrix::Identity;
|
|
}
|
|
|
|
FORCEINLINE const FMatrix& GetWorldToLocal(void) const
|
|
{
|
|
return FMatrix::Identity;
|
|
}
|
|
|
|
FORCEINLINE FMatrix GetLocalToWorldTransposeAdjoint(void) const
|
|
{
|
|
return FMatrix::Identity;
|
|
}
|
|
|
|
FORCEINLINE float GetDeterminant(void) const
|
|
{
|
|
return 1.0f;
|
|
}
|
|
|
|
private:
|
|
|
|
const TkDOPTree<const FMeshBuildDataProvider, uint32>& kDopTree;
|
|
};
|
|
|
|
/** Generates unit length, stratified and uniformly distributed direction samples in a hemisphere. */
|
|
void GenerateStratifiedUniformHemisphereSamples(int32 NumThetaSteps, int32 NumPhiSteps, FRandomStream& RandomStream, TArray<FVector4>& Samples)
|
|
{
|
|
Samples.Empty(NumThetaSteps * NumPhiSteps);
|
|
for (int32 ThetaIndex = 0; ThetaIndex < NumThetaSteps; ThetaIndex++)
|
|
{
|
|
for (int32 PhiIndex = 0; PhiIndex < NumPhiSteps; PhiIndex++)
|
|
{
|
|
const float U1 = RandomStream.GetFraction();
|
|
const float U2 = RandomStream.GetFraction();
|
|
|
|
const float Fraction1 = (ThetaIndex + U1) / (float)NumThetaSteps;
|
|
const float Fraction2 = (PhiIndex + U2) / (float)NumPhiSteps;
|
|
|
|
const float R = FMath::Sqrt(1.0f - Fraction1 * Fraction1);
|
|
|
|
const float Phi = 2.0f * (float)PI * Fraction2;
|
|
// Convert to Cartesian
|
|
Samples.Add(FVector4(FMath::Cos(Phi) * R, FMath::Sin(Phi) * R, Fraction1));
|
|
}
|
|
}
|
|
}
|
|
|
|
class FMeshDistanceFieldAsyncTask : public FNonAbandonableTask
|
|
{
|
|
public:
|
|
FMeshDistanceFieldAsyncTask(TkDOPTree<const FMeshBuildDataProvider, uint32>* InkDopTree,
|
|
const TArray<FVector4>* InSampleDirections,
|
|
FBox InVolumeBounds,
|
|
FIntVector InVolumeDimensions,
|
|
float InVolumeMaxDistance,
|
|
float InDistanceFieldBias,
|
|
int32 InZIndex,
|
|
TArray<FFloat16>* DistanceFieldVolume)
|
|
:
|
|
kDopTree(InkDopTree),
|
|
SampleDirections(InSampleDirections),
|
|
VolumeBounds(InVolumeBounds),
|
|
VolumeDimensions(InVolumeDimensions),
|
|
VolumeMaxDistance(InVolumeMaxDistance),
|
|
DistanceFieldBias(InDistanceFieldBias),
|
|
ZIndex(InZIndex),
|
|
OutDistanceFieldVolume(DistanceFieldVolume),
|
|
bNegativeAtBorder(false)
|
|
{}
|
|
|
|
void DoWork();
|
|
|
|
FORCEINLINE TStatId GetStatId() const
|
|
{
|
|
RETURN_QUICK_DECLARE_CYCLE_STAT(FMeshDistanceFieldAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
|
|
}
|
|
|
|
bool WasNegativeAtBorder() const
|
|
{
|
|
return bNegativeAtBorder;
|
|
}
|
|
|
|
private:
|
|
|
|
// Readonly inputs
|
|
TkDOPTree<const FMeshBuildDataProvider, uint32>* kDopTree;
|
|
const TArray<FVector4>* SampleDirections;
|
|
FBox VolumeBounds;
|
|
FIntVector VolumeDimensions;
|
|
float VolumeMaxDistance;
|
|
float DistanceFieldBias;
|
|
int32 ZIndex;
|
|
|
|
// Output
|
|
TArray<FFloat16>* OutDistanceFieldVolume;
|
|
bool bNegativeAtBorder;
|
|
};
|
|
|
|
void FMeshDistanceFieldAsyncTask::DoWork()
|
|
{
|
|
FMeshBuildDataProvider kDOPDataProvider(*kDopTree);
|
|
const FVector DistanceFieldVoxelSize(VolumeBounds.GetSize() / FVector(VolumeDimensions.X, VolumeDimensions.Y, VolumeDimensions.Z));
|
|
const float VoxelDiameterSqr = DistanceFieldVoxelSize.SizeSquared();
|
|
|
|
for (int32 YIndex = 0; YIndex < VolumeDimensions.Y; YIndex++)
|
|
{
|
|
for (int32 XIndex = 0; XIndex < VolumeDimensions.X; XIndex++)
|
|
{
|
|
const FVector VoxelPosition = FVector(XIndex + .5f, YIndex + .5f, ZIndex + .5f) * DistanceFieldVoxelSize + VolumeBounds.Min;
|
|
const int32 Index = (ZIndex * VolumeDimensions.Y * VolumeDimensions.X + YIndex * VolumeDimensions.X + XIndex);
|
|
|
|
float MinDistance = VolumeMaxDistance;
|
|
int32 Hit = 0;
|
|
int32 HitBack = 0;
|
|
|
|
for (int32 SampleIndex = 0; SampleIndex < SampleDirections->Num(); SampleIndex++)
|
|
{
|
|
const FVector RayDirection = (*SampleDirections)[SampleIndex];
|
|
|
|
if (FMath::LineBoxIntersection(VolumeBounds, VoxelPosition, VoxelPosition + RayDirection * VolumeMaxDistance, RayDirection))
|
|
{
|
|
FkHitResult Result;
|
|
|
|
TkDOPLineCollisionCheck<const FMeshBuildDataProvider, uint32> kDOPCheck(
|
|
VoxelPosition,
|
|
VoxelPosition + RayDirection * VolumeMaxDistance,
|
|
true,
|
|
kDOPDataProvider,
|
|
&Result);
|
|
|
|
bool bHit = kDopTree->LineCheck(kDOPCheck);
|
|
|
|
if (bHit)
|
|
{
|
|
Hit++;
|
|
|
|
const FVector HitNormal = kDOPCheck.GetHitNormal();
|
|
|
|
if (FVector::DotProduct(RayDirection, HitNormal) > 0
|
|
// MaterialIndex on the build triangles was set to 1 if two-sided, or 0 if one-sided
|
|
&& kDOPCheck.Result->Item == 0)
|
|
{
|
|
HitBack++;
|
|
}
|
|
|
|
const float CurrentDistance = VolumeMaxDistance * Result.Time;
|
|
|
|
if (CurrentDistance < MinDistance)
|
|
{
|
|
MinDistance = CurrentDistance;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const float UnsignedDistance = MinDistance;
|
|
|
|
// Consider this voxel 'inside' an object if more than 50% of the rays hit back faces
|
|
MinDistance *= (Hit == 0 || HitBack < SampleDirections->Num() * .5f) ? 1 : -1;
|
|
|
|
// If we are very close to a surface and nearly all of our rays hit backfaces, treat as inside
|
|
// This is important for one sided planes
|
|
if (FMath::Square(UnsignedDistance) < VoxelDiameterSqr && HitBack > .95f * Hit)
|
|
{
|
|
MinDistance = -UnsignedDistance;
|
|
}
|
|
|
|
MinDistance = FMath::Min(MinDistance + DistanceFieldBias, VolumeMaxDistance);
|
|
const float VolumeSpaceDistance = MinDistance / VolumeBounds.GetExtent().GetMax();
|
|
|
|
if (MinDistance < 0 &&
|
|
(XIndex == 0 || XIndex == VolumeDimensions.X - 1 ||
|
|
YIndex == 0 || YIndex == VolumeDimensions.Y - 1 ||
|
|
ZIndex == 0 || ZIndex == VolumeDimensions.Z - 1))
|
|
{
|
|
bNegativeAtBorder = true;
|
|
}
|
|
|
|
(*OutDistanceFieldVolume)[Index] = FFloat16(VolumeSpaceDistance);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::GenerateSignedDistanceFieldVolumeData(
|
|
const FStaticMeshLODResources& LODModel,
|
|
class FQueuedThreadPool& ThreadPool,
|
|
const TArray<EBlendMode>& MaterialBlendModes,
|
|
const FBoxSphereBounds& Bounds,
|
|
float DistanceFieldResolutionScale,
|
|
float DistanceFieldBias,
|
|
bool bGenerateAsIfTwoSided,
|
|
FDistanceFieldVolumeData& OutData)
|
|
{
|
|
if (DistanceFieldResolutionScale > 0)
|
|
{
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
const FPositionVertexBuffer& PositionVertexBuffer = LODModel.PositionVertexBuffer;
|
|
FIndexArrayView Indices = LODModel.IndexBuffer.GetArrayView();
|
|
TArray<FkDOPBuildCollisionTriangle<uint32> > BuildTriangles;
|
|
|
|
FVector BoundsSize = Bounds.GetBox().GetExtent() * 2;
|
|
float MaxDimension = FMath::Max(FMath::Max(BoundsSize.X, BoundsSize.Y), BoundsSize.Z);
|
|
|
|
// Consider the mesh a plane if it is very flat
|
|
const bool bMeshWasPlane = BoundsSize.Z * 100 < MaxDimension
|
|
// And it lies mostly on the origin
|
|
&& Bounds.Origin.Z - Bounds.BoxExtent.Z < KINDA_SMALL_NUMBER
|
|
&& Bounds.Origin.Z + Bounds.BoxExtent.Z > -KINDA_SMALL_NUMBER;
|
|
|
|
for (int32 i = 0; i < Indices.Num(); i += 3)
|
|
{
|
|
FVector V0 = PositionVertexBuffer.VertexPosition(Indices[i + 0]);
|
|
FVector V1 = PositionVertexBuffer.VertexPosition(Indices[i + 1]);
|
|
FVector V2 = PositionVertexBuffer.VertexPosition(Indices[i + 2]);
|
|
|
|
if (bMeshWasPlane)
|
|
{
|
|
// Flatten out the mesh into an actual plane, this will allow us to manipulate the component's Z scale at runtime without artifacts
|
|
V0.Z = 0;
|
|
V1.Z = 0;
|
|
V2.Z = 0;
|
|
}
|
|
|
|
const FVector LocalNormal = ((V1 - V2) ^ (V0 - V2)).GetSafeNormal();
|
|
|
|
// No degenerates
|
|
if (LocalNormal.IsUnit())
|
|
{
|
|
bool bTriangleIsOpaqueOrMasked = false;
|
|
|
|
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
const FStaticMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
|
|
if ((uint32)i >= Section.FirstIndex && (uint32)i < Section.FirstIndex + Section.NumTriangles * 3)
|
|
{
|
|
if (MaterialBlendModes.IsValidIndex(Section.MaterialIndex))
|
|
{
|
|
bTriangleIsOpaqueOrMasked = !IsTranslucentBlendMode(MaterialBlendModes[Section.MaterialIndex]);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bTriangleIsOpaqueOrMasked)
|
|
{
|
|
BuildTriangles.Add(FkDOPBuildCollisionTriangle<uint32>(
|
|
bGenerateAsIfTwoSided,
|
|
V0,
|
|
V1,
|
|
V2));
|
|
}
|
|
}
|
|
}
|
|
|
|
TkDOPTree<const FMeshBuildDataProvider, uint32> kDopTree;
|
|
kDopTree.Build(BuildTriangles);
|
|
|
|
//@todo - project setting
|
|
const int32 NumVoxelDistanceSamples = 1200;
|
|
TArray<FVector4> SampleDirections;
|
|
const int32 NumThetaSteps = FMath::TruncToInt(FMath::Sqrt(NumVoxelDistanceSamples / (2.0f * (float)PI)));
|
|
const int32 NumPhiSteps = FMath::TruncToInt(NumThetaSteps * (float)PI);
|
|
FRandomStream RandomStream(0);
|
|
GenerateStratifiedUniformHemisphereSamples(NumThetaSteps, NumPhiSteps, RandomStream, SampleDirections);
|
|
TArray<FVector4> OtherHemisphereSamples;
|
|
GenerateStratifiedUniformHemisphereSamples(NumThetaSteps, NumPhiSteps, RandomStream, OtherHemisphereSamples);
|
|
|
|
for (int32 i = 0; i < OtherHemisphereSamples.Num(); i++)
|
|
{
|
|
FVector4 Sample = OtherHemisphereSamples[i];
|
|
Sample.Z *= -1;
|
|
SampleDirections.Add(Sample);
|
|
}
|
|
|
|
static const auto CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DistanceFields.MaxPerMeshResolution"));
|
|
const int32 PerMeshMax = CVar->GetValueOnAnyThread();
|
|
|
|
// Meshes with explicit artist-specified scale can go higher
|
|
const int32 MaxNumVoxelsOneDim = DistanceFieldResolutionScale <= 1 ? PerMeshMax / 2 : PerMeshMax;
|
|
const int32 MinNumVoxelsOneDim = 8;
|
|
|
|
static const auto CVarDensity = IConsoleManager::Get().FindTConsoleVariableDataFloat(TEXT("r.DistanceFields.DefaultVoxelDensity"));
|
|
const float VoxelDensity = CVarDensity->GetValueOnAnyThread();
|
|
|
|
const float NumVoxelsPerLocalSpaceUnit = VoxelDensity * DistanceFieldResolutionScale;
|
|
FBox MeshBounds(Bounds.GetBox());
|
|
|
|
{
|
|
const float MaxOriginalExtent = MeshBounds.GetExtent().GetMax();
|
|
// Expand so that the edges of the volume are guaranteed to be outside of the mesh
|
|
// Any samples outside the bounds will be clamped to the border, so they must be outside
|
|
const FVector NewExtent(MeshBounds.GetExtent() + FVector(.2f * MaxOriginalExtent).ComponentMax(4 * MeshBounds.GetExtent() / MinNumVoxelsOneDim));
|
|
FBox DistanceFieldVolumeBounds = FBox(MeshBounds.GetCenter() - NewExtent, MeshBounds.GetCenter() + NewExtent);
|
|
const float DistanceFieldVolumeMaxDistance = DistanceFieldVolumeBounds.GetExtent().Size();
|
|
|
|
const FVector DesiredDimensions(DistanceFieldVolumeBounds.GetSize() * FVector(NumVoxelsPerLocalSpaceUnit));
|
|
|
|
const FIntVector VolumeDimensions(
|
|
FMath::Clamp(FMath::TruncToInt(DesiredDimensions.X), MinNumVoxelsOneDim, MaxNumVoxelsOneDim),
|
|
FMath::Clamp(FMath::TruncToInt(DesiredDimensions.Y), MinNumVoxelsOneDim, MaxNumVoxelsOneDim),
|
|
FMath::Clamp(FMath::TruncToInt(DesiredDimensions.Z), MinNumVoxelsOneDim, MaxNumVoxelsOneDim));
|
|
|
|
OutData.Size = VolumeDimensions;
|
|
OutData.LocalBoundingBox = DistanceFieldVolumeBounds;
|
|
OutData.DistanceFieldVolume.AddZeroed(VolumeDimensions.X * VolumeDimensions.Y * VolumeDimensions.Z);
|
|
|
|
TIndirectArray<FAsyncTask<FMeshDistanceFieldAsyncTask>> AsyncTasks;
|
|
|
|
for (int32 ZIndex = 0; ZIndex < VolumeDimensions.Z; ZIndex++)
|
|
{
|
|
FAsyncTask<FMeshDistanceFieldAsyncTask>* Task = new FAsyncTask<class FMeshDistanceFieldAsyncTask>(
|
|
&kDopTree,
|
|
&SampleDirections,
|
|
DistanceFieldVolumeBounds,
|
|
VolumeDimensions,
|
|
DistanceFieldVolumeMaxDistance,
|
|
DistanceFieldBias,
|
|
ZIndex,
|
|
&OutData.DistanceFieldVolume);
|
|
|
|
Task->StartBackgroundTask(&ThreadPool);
|
|
|
|
AsyncTasks.Add(Task);
|
|
}
|
|
|
|
bool bNegativeAtBorder = false;
|
|
|
|
for (int32 TaskIndex = 0; TaskIndex < AsyncTasks.Num(); TaskIndex++)
|
|
{
|
|
FAsyncTask<FMeshDistanceFieldAsyncTask>& Task = AsyncTasks[TaskIndex];
|
|
Task.EnsureCompletion(false);
|
|
bNegativeAtBorder = bNegativeAtBorder || Task.GetTask().WasNegativeAtBorder();
|
|
}
|
|
|
|
OutData.bMeshWasClosed = !bNegativeAtBorder;
|
|
OutData.bBuiltAsIfTwoSided = bGenerateAsIfTwoSided;
|
|
OutData.bMeshWasPlane = bMeshWasPlane;
|
|
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Finished distance field build in %.1fs - %ux%ux%u distance field, %u triangles"),
|
|
(float)(FPlatformTime::Seconds() - StartTime),
|
|
VolumeDimensions.X,
|
|
VolumeDimensions.Y,
|
|
VolumeDimensions.Z,
|
|
Indices.Num() / 3);
|
|
|
|
// Toss distance field if mesh was not closed
|
|
if (bNegativeAtBorder)
|
|
{
|
|
OutData.Size = FIntVector(0, 0, 0);
|
|
OutData.DistanceFieldVolume.Empty();
|
|
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Discarded distance field as mesh was not closed! Assign a two-sided material to fix."));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
void FMeshUtilities::GenerateSignedDistanceFieldVolumeData(
|
|
const FStaticMeshLODResources& LODModel,
|
|
class FQueuedThreadPool& ThreadPool,
|
|
const TArray<EBlendMode>& MaterialBlendModes,
|
|
const FBoxSphereBounds& Bounds,
|
|
float DistanceFieldResolutionScale,
|
|
float DistanceFieldBias,
|
|
bool bGenerateAsIfTwoSided,
|
|
FDistanceFieldVolumeData& OutData)
|
|
{
|
|
if (DistanceFieldResolutionScale > 0)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Error, TEXT("Couldn't generate distance field for mesh, platform is missing required Vector intrinsics."));
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
/*------------------------------------------------------------------------------
|
|
NVTriStrip for cache optimizing index buffers.
|
|
------------------------------------------------------------------------------*/
|
|
|
|
namespace NvTriStrip
|
|
{
|
|
/**
|
|
* Converts 16 bit indices to 32 bit prior to passing them into the real GenerateStrips util method
|
|
*/
|
|
void GenerateStrips(
|
|
const uint8* Indices,
|
|
bool Is32Bit,
|
|
const uint32 NumIndices,
|
|
PrimitiveGroup** PrimGroups,
|
|
uint32* NumGroups
|
|
)
|
|
{
|
|
if (Is32Bit)
|
|
{
|
|
GenerateStrips((uint32*)Indices, NumIndices, PrimGroups, NumGroups);
|
|
}
|
|
else
|
|
{
|
|
// convert to 32 bit
|
|
uint32 Idx;
|
|
TArray<uint32> NewIndices;
|
|
NewIndices.AddUninitialized(NumIndices);
|
|
for (Idx = 0; Idx < NumIndices; ++Idx)
|
|
{
|
|
NewIndices[Idx] = ((uint16*)Indices)[Idx];
|
|
}
|
|
GenerateStrips(NewIndices.GetData(), NumIndices, PrimGroups, NumGroups);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Orders a triangle list for better vertex cache coherency.
|
|
*
|
|
* *** WARNING: This is safe to call for multiple threads IF AND ONLY IF all
|
|
* threads call SetListsOnly(true) and SetCacheSize(CACHESIZE_GEFORCE3). If
|
|
* NvTriStrip is ever used with different settings the library will need
|
|
* some modifications to be thread-safe. ***
|
|
*/
|
|
template<typename IndexDataType, typename Allocator>
|
|
void CacheOptimizeIndexBuffer(TArray<IndexDataType, Allocator>& Indices)
|
|
{
|
|
static_assert(sizeof(IndexDataType) == 2 || sizeof(IndexDataType) == 4, "Indices must be short or int.");
|
|
|
|
PrimitiveGroup* PrimitiveGroups = NULL;
|
|
uint32 NumPrimitiveGroups = 0;
|
|
bool Is32Bit = sizeof(IndexDataType) == 4;
|
|
|
|
SetListsOnly(true);
|
|
SetCacheSize(CACHESIZE_GEFORCE3);
|
|
|
|
GenerateStrips((uint8*)Indices.GetData(), Is32Bit, Indices.Num(), &PrimitiveGroups, &NumPrimitiveGroups);
|
|
|
|
Indices.Empty();
|
|
Indices.AddUninitialized(PrimitiveGroups->numIndices);
|
|
|
|
if (Is32Bit)
|
|
{
|
|
FMemory::Memcpy(Indices.GetData(), PrimitiveGroups->indices, Indices.Num() * sizeof(IndexDataType));
|
|
}
|
|
else
|
|
{
|
|
for (uint32 I = 0; I < PrimitiveGroups->numIndices; ++I)
|
|
{
|
|
Indices[I] = (uint16)PrimitiveGroups->indices[I];
|
|
}
|
|
}
|
|
|
|
delete[] PrimitiveGroups;
|
|
}
|
|
}
|
|
|
|
/*------------------------------------------------------------------------------
|
|
Forsyth algorithm for cache optimizing index buffers.
|
|
------------------------------------------------------------------------------*/
|
|
|
|
namespace Forsyth
|
|
{
|
|
/**
|
|
* Converts 16 bit indices to 32 bit prior to passing them into the real OptimizeFaces util method
|
|
*/
|
|
void OptimizeFaces(
|
|
const uint8* Indices,
|
|
bool Is32Bit,
|
|
const uint32 NumIndices,
|
|
uint32 NumVertices,
|
|
uint32* OutIndices,
|
|
uint16 CacheSize
|
|
)
|
|
{
|
|
if (Is32Bit)
|
|
{
|
|
OptimizeFaces((uint32*)Indices, NumIndices, NumVertices, OutIndices, CacheSize);
|
|
}
|
|
else
|
|
{
|
|
// convert to 32 bit
|
|
uint32 Idx;
|
|
TArray<uint32> NewIndices;
|
|
NewIndices.AddUninitialized(NumIndices);
|
|
for (Idx = 0; Idx < NumIndices; ++Idx)
|
|
{
|
|
NewIndices[Idx] = ((uint16*)Indices)[Idx];
|
|
}
|
|
OptimizeFaces(NewIndices.GetData(), NumIndices, NumVertices, OutIndices, CacheSize);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Orders a triangle list for better vertex cache coherency.
|
|
*/
|
|
template<typename IndexDataType, typename Allocator>
|
|
void CacheOptimizeIndexBuffer(TArray<IndexDataType, Allocator>& Indices)
|
|
{
|
|
static_assert(sizeof(IndexDataType) == 2 || sizeof(IndexDataType) == 4, "Indices must be short or int.");
|
|
bool Is32Bit = sizeof(IndexDataType) == 4;
|
|
|
|
// Count the number of vertices
|
|
uint32 NumVertices = 0;
|
|
for (int32 Index = 0; Index < Indices.Num(); ++Index)
|
|
{
|
|
if (Indices[Index] > NumVertices)
|
|
{
|
|
NumVertices = Indices[Index];
|
|
}
|
|
}
|
|
NumVertices += 1;
|
|
|
|
TArray<uint32> OptimizedIndices;
|
|
OptimizedIndices.AddUninitialized(Indices.Num());
|
|
uint16 CacheSize = 32;
|
|
OptimizeFaces((uint8*)Indices.GetData(), Is32Bit, Indices.Num(), NumVertices, OptimizedIndices.GetData(), CacheSize);
|
|
|
|
if (Is32Bit)
|
|
{
|
|
FMemory::Memcpy(Indices.GetData(), OptimizedIndices.GetData(), Indices.Num() * sizeof(IndexDataType));
|
|
}
|
|
else
|
|
{
|
|
for (int32 I = 0; I < OptimizedIndices.Num(); ++I)
|
|
{
|
|
Indices[I] = (uint16)OptimizedIndices[I];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::CacheOptimizeIndexBuffer(TArray<uint16>& Indices)
|
|
{
|
|
if (bUsingNvTriStrip)
|
|
{
|
|
NvTriStrip::CacheOptimizeIndexBuffer(Indices);
|
|
}
|
|
else if (!bDisableTriangleOrderOptimization)
|
|
{
|
|
Forsyth::CacheOptimizeIndexBuffer(Indices);
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::CacheOptimizeIndexBuffer(TArray<uint32>& Indices)
|
|
{
|
|
if (bUsingNvTriStrip)
|
|
{
|
|
NvTriStrip::CacheOptimizeIndexBuffer(Indices);
|
|
}
|
|
else if (!bDisableTriangleOrderOptimization)
|
|
{
|
|
Forsyth::CacheOptimizeIndexBuffer(Indices);
|
|
}
|
|
}
|
|
|
|
/*------------------------------------------------------------------------------
|
|
NVTessLib for computing adjacency used for tessellation.
|
|
------------------------------------------------------------------------------*/
|
|
|
|
/**
|
|
* Provides static mesh render data to the NVIDIA tessellation library.
|
|
*/
|
|
class FStaticMeshNvRenderBuffer : public nv::RenderBuffer
|
|
{
|
|
public:
|
|
|
|
/** Construct from static mesh render buffers. */
|
|
FStaticMeshNvRenderBuffer(
|
|
const FPositionVertexBuffer& InPositionVertexBuffer,
|
|
const FStaticMeshVertexBuffer& InVertexBuffer,
|
|
const TArray<uint32>& Indices)
|
|
: PositionVertexBuffer(InPositionVertexBuffer)
|
|
, VertexBuffer(InVertexBuffer)
|
|
{
|
|
check(PositionVertexBuffer.GetNumVertices() == VertexBuffer.GetNumVertices());
|
|
mIb = new nv::IndexBuffer((void*)Indices.GetData(), nv::IBT_U32, Indices.Num(), false);
|
|
}
|
|
|
|
/** Retrieve the position and first texture coordinate of the specified index. */
|
|
virtual nv::Vertex getVertex(unsigned int Index) const
|
|
{
|
|
nv::Vertex Vertex;
|
|
|
|
check(Index < PositionVertexBuffer.GetNumVertices());
|
|
|
|
const FVector& Position = PositionVertexBuffer.VertexPosition(Index);
|
|
Vertex.pos.x = Position.X;
|
|
Vertex.pos.y = Position.Y;
|
|
Vertex.pos.z = Position.Z;
|
|
|
|
if (VertexBuffer.GetNumTexCoords())
|
|
{
|
|
const FVector2D UV = VertexBuffer.GetVertexUV(Index, 0);
|
|
Vertex.uv.x = UV.X;
|
|
Vertex.uv.y = UV.Y;
|
|
}
|
|
else
|
|
{
|
|
Vertex.uv.x = 0.0f;
|
|
Vertex.uv.y = 0.0f;
|
|
}
|
|
|
|
return Vertex;
|
|
}
|
|
|
|
private:
|
|
|
|
/** The position vertex buffer for the static mesh. */
|
|
const FPositionVertexBuffer& PositionVertexBuffer;
|
|
|
|
/** The vertex buffer for the static mesh. */
|
|
const FStaticMeshVertexBuffer& VertexBuffer;
|
|
|
|
/** Copying is forbidden. */
|
|
FStaticMeshNvRenderBuffer(const FStaticMeshNvRenderBuffer&);
|
|
FStaticMeshNvRenderBuffer& operator=(const FStaticMeshNvRenderBuffer&);
|
|
};
|
|
|
|
/**
|
|
* Provides skeletal mesh render data to the NVIDIA tessellation library.
|
|
*/
|
|
class FSkeletalMeshNvRenderBuffer : public nv::RenderBuffer
|
|
{
|
|
public:
|
|
|
|
/** Construct from static mesh render buffers. */
|
|
FSkeletalMeshNvRenderBuffer(
|
|
const TArray<FSoftSkinVertex>& InVertexBuffer,
|
|
const uint32 InTexCoordCount,
|
|
const TArray<uint32>& Indices)
|
|
: VertexBuffer(InVertexBuffer)
|
|
, TexCoordCount(InTexCoordCount)
|
|
{
|
|
mIb = new nv::IndexBuffer((void*)Indices.GetData(), nv::IBT_U32, Indices.Num(), false);
|
|
}
|
|
|
|
/** Retrieve the position and first texture coordinate of the specified index. */
|
|
virtual nv::Vertex getVertex(unsigned int Index) const
|
|
{
|
|
nv::Vertex Vertex;
|
|
|
|
check(Index < (unsigned int)VertexBuffer.Num());
|
|
|
|
const FSoftSkinVertex& SrcVertex = VertexBuffer[Index];
|
|
|
|
Vertex.pos.x = SrcVertex.Position.X;
|
|
Vertex.pos.y = SrcVertex.Position.Y;
|
|
Vertex.pos.z = SrcVertex.Position.Z;
|
|
|
|
if (TexCoordCount > 0)
|
|
{
|
|
Vertex.uv.x = SrcVertex.UVs[0].X;
|
|
Vertex.uv.y = SrcVertex.UVs[0].Y;
|
|
}
|
|
else
|
|
{
|
|
Vertex.uv.x = 0.0f;
|
|
Vertex.uv.y = 0.0f;
|
|
}
|
|
|
|
return Vertex;
|
|
}
|
|
|
|
private:
|
|
/** The vertex buffer for the skeletal mesh. */
|
|
const TArray<FSoftSkinVertex>& VertexBuffer;
|
|
const uint32 TexCoordCount;
|
|
|
|
/** Copying is forbidden. */
|
|
FSkeletalMeshNvRenderBuffer(const FSkeletalMeshNvRenderBuffer&);
|
|
FSkeletalMeshNvRenderBuffer& operator=(const FSkeletalMeshNvRenderBuffer&);
|
|
};
|
|
|
|
static void BuildStaticAdjacencyIndexBuffer(
|
|
const FPositionVertexBuffer& PositionVertexBuffer,
|
|
const FStaticMeshVertexBuffer& VertexBuffer,
|
|
const TArray<uint32>& Indices,
|
|
TArray<uint32>& OutPnAenIndices
|
|
)
|
|
{
|
|
if (Indices.Num())
|
|
{
|
|
FStaticMeshNvRenderBuffer StaticMeshRenderBuffer(PositionVertexBuffer, VertexBuffer, Indices);
|
|
nv::IndexBuffer* PnAENIndexBuffer = nv::tess::buildTessellationBuffer(&StaticMeshRenderBuffer, nv::DBM_PnAenDominantCorner, true);
|
|
check(PnAENIndexBuffer);
|
|
const int32 IndexCount = (int32)PnAENIndexBuffer->getLength();
|
|
OutPnAenIndices.Empty(IndexCount);
|
|
OutPnAenIndices.AddUninitialized(IndexCount);
|
|
for (int32 Index = 0; Index < IndexCount; ++Index)
|
|
{
|
|
OutPnAenIndices[Index] = (*PnAENIndexBuffer)[Index];
|
|
}
|
|
delete PnAENIndexBuffer;
|
|
}
|
|
else
|
|
{
|
|
OutPnAenIndices.Empty();
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::BuildSkeletalAdjacencyIndexBuffer(
|
|
const TArray<FSoftSkinVertex>& VertexBuffer,
|
|
const uint32 TexCoordCount,
|
|
const TArray<uint32>& Indices,
|
|
TArray<uint32>& OutPnAenIndices
|
|
)
|
|
{
|
|
if (Indices.Num())
|
|
{
|
|
FSkeletalMeshNvRenderBuffer SkeletalMeshRenderBuffer(VertexBuffer, TexCoordCount, Indices);
|
|
nv::IndexBuffer* PnAENIndexBuffer = nv::tess::buildTessellationBuffer(&SkeletalMeshRenderBuffer, nv::DBM_PnAenDominantCorner, true);
|
|
check(PnAENIndexBuffer);
|
|
const int32 IndexCount = (int32)PnAENIndexBuffer->getLength();
|
|
OutPnAenIndices.Empty(IndexCount);
|
|
OutPnAenIndices.AddUninitialized(IndexCount);
|
|
for (int32 Index = 0; Index < IndexCount; ++Index)
|
|
{
|
|
OutPnAenIndices[Index] = (*PnAENIndexBuffer)[Index];
|
|
}
|
|
delete PnAENIndexBuffer;
|
|
}
|
|
else
|
|
{
|
|
OutPnAenIndices.Empty();
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::RechunkSkeletalMeshModels(USkeletalMesh* SrcMesh, int32 MaxBonesPerChunk)
|
|
{
|
|
#if WITH_EDITORONLY_DATA
|
|
TIndirectArray<FStaticLODModel> DestModels;
|
|
TIndirectArray<FSkinnedModelData> ModelData;
|
|
FReferenceSkeleton RefSkeleton = SrcMesh->RefSkeleton;
|
|
uint32 VertexBufferBuildFlags = SrcMesh->GetVertexBufferFlags();
|
|
FSkeletalMeshResource* SrcMeshResource = SrcMesh->GetImportedResource();
|
|
FVector TriangleSortCenter;
|
|
bool bHaveTriangleSortCenter = SrcMesh->GetSortCenterPoint(TriangleSortCenter);
|
|
|
|
for (int32 ModelIndex = 0; ModelIndex < SrcMeshResource->LODModels.Num(); ++ModelIndex)
|
|
{
|
|
FSkinnedModelData& TmpModelData = *new(ModelData)FSkinnedModelData();
|
|
SkeletalMeshTools::CopySkinnedModelData(TmpModelData, SrcMeshResource->LODModels[ModelIndex]);
|
|
}
|
|
|
|
for (int32 ModelIndex = 0; ModelIndex < ModelData.Num(); ++ModelIndex)
|
|
{
|
|
TArray<FSkinnedMeshChunk*> Chunks;
|
|
TArray<int32> PointToOriginalMap;
|
|
TArray<ETriangleSortOption> SectionSortOptions;
|
|
|
|
const FSkinnedModelData& SrcModel = ModelData[ModelIndex];
|
|
FStaticLODModel& DestModel = *new(DestModels)FStaticLODModel();
|
|
|
|
SkeletalMeshTools::UnchunkSkeletalModel(Chunks, PointToOriginalMap, SrcModel);
|
|
SkeletalMeshTools::ChunkSkinnedVertices(Chunks, MaxBonesPerChunk);
|
|
|
|
for (int32 ChunkIndex = 0; ChunkIndex < Chunks.Num(); ++ChunkIndex)
|
|
{
|
|
int32 SectionIndex = Chunks[ChunkIndex]->OriginalSectionIndex;
|
|
SectionSortOptions.Add(SrcModel.Sections[SectionIndex].TriangleSorting);
|
|
}
|
|
check(SectionSortOptions.Num() == Chunks.Num());
|
|
|
|
BuildSkeletalModelFromChunks(DestModel, RefSkeleton, Chunks, PointToOriginalMap);
|
|
check(DestModel.Sections.Num() == SectionSortOptions.Num());
|
|
|
|
DestModel.NumTexCoords = SrcModel.NumTexCoords;
|
|
DestModel.BuildVertexBuffers(VertexBufferBuildFlags);
|
|
for (int32 SectionIndex = 0; SectionIndex < DestModel.Sections.Num(); ++SectionIndex)
|
|
{
|
|
DestModel.SortTriangles(TriangleSortCenter, bHaveTriangleSortCenter, SectionIndex, SectionSortOptions[SectionIndex]);
|
|
}
|
|
}
|
|
|
|
//@todo-rco: Swap() doesn't seem to work
|
|
Exchange(SrcMeshResource->LODModels, DestModels);
|
|
|
|
// TODO: Also need to patch bEnableShadowCasting in the LODInfo struct.
|
|
#endif // #if WITH_EDITORONLY_DATA
|
|
}
|
|
|
|
void FMeshUtilities::CalcBoneVertInfos(USkeletalMesh* SkeletalMesh, TArray<FBoneVertInfo>& Infos, bool bOnlyDominant)
|
|
{
|
|
SkeletalMeshTools::CalcBoneVertInfos(SkeletalMesh, Infos, bOnlyDominant);
|
|
}
|
|
|
|
// Helper function for ConvertMeshesToStaticMesh
|
|
static void AddOrDuplicateMaterial(UMaterialInterface* InMaterialInterface, const FString& InPackageName, TArray<UMaterialInterface*>& OutMaterials)
|
|
{
|
|
if (InMaterialInterface && !InMaterialInterface->GetOuter()->IsA<UPackage>())
|
|
{
|
|
// Convert runtime material instances to new concrete material instances
|
|
// Create new package
|
|
FString OriginalMaterialName = InMaterialInterface->GetName();
|
|
FString MaterialPath = FPackageName::GetLongPackagePath(InPackageName) / OriginalMaterialName;
|
|
FString MaterialName;
|
|
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
|
AssetToolsModule.Get().CreateUniqueAssetName(MaterialPath, TEXT(""), MaterialPath, MaterialName);
|
|
UPackage* MaterialPackage = CreatePackage(NULL, *MaterialPath);
|
|
|
|
// Duplicate the object into the new package
|
|
UMaterialInterface* NewMaterialInterface = DuplicateObject<UMaterialInterface>(InMaterialInterface, MaterialPackage, *MaterialName);
|
|
NewMaterialInterface->SetFlags(RF_Public | RF_Standalone);
|
|
|
|
if (UMaterialInstanceDynamic* MaterialInstanceDynamic = Cast<UMaterialInstanceDynamic>(NewMaterialInterface))
|
|
{
|
|
UMaterialInstanceDynamic* OldMaterialInstanceDynamic = CastChecked<UMaterialInstanceDynamic>(InMaterialInterface);
|
|
MaterialInstanceDynamic->K2_CopyMaterialInstanceParameters(OldMaterialInstanceDynamic);
|
|
}
|
|
|
|
NewMaterialInterface->MarkPackageDirty();
|
|
|
|
FAssetRegistryModule::AssetCreated(NewMaterialInterface);
|
|
|
|
InMaterialInterface = NewMaterialInterface;
|
|
}
|
|
|
|
OutMaterials.Add(InMaterialInterface);
|
|
}
|
|
|
|
// Helper function for ConvertMeshesToStaticMesh
|
|
template <typename ComponentType>
|
|
static void ProcessMaterials(ComponentType* InComponent, const FString& InPackageName, TArray<UMaterialInterface*>& OutMaterials)
|
|
{
|
|
const int32 NumMaterials = InComponent->GetNumMaterials();
|
|
for (int32 MaterialIndex = 0; MaterialIndex < NumMaterials; MaterialIndex++)
|
|
{
|
|
UMaterialInterface* MaterialInterface = InComponent->GetMaterial(MaterialIndex);
|
|
AddOrDuplicateMaterial(MaterialInterface, InPackageName, OutMaterials);
|
|
}
|
|
}
|
|
|
|
// Helper function for ConvertMeshesToStaticMesh
|
|
static bool IsValidSkinnedMeshComponent(USkinnedMeshComponent* InComponent)
|
|
{
|
|
return InComponent && InComponent->MeshObject && InComponent->IsVisible();
|
|
}
|
|
|
|
/** Helper struct for tracking validity of optional buffers */
|
|
struct FRawMeshTracker
|
|
{
|
|
FRawMeshTracker()
|
|
: bValidColors(false)
|
|
{
|
|
FMemory::Memset(bValidTexCoords, 0);
|
|
}
|
|
|
|
bool bValidTexCoords[MAX_MESH_TEXTURE_COORDS];
|
|
bool bValidColors;
|
|
};
|
|
|
|
// Helper function for ConvertMeshesToStaticMesh
|
|
static void SkinnedMeshToRawMeshes(USkinnedMeshComponent* InSkinnedMeshComponent, int32 InOverallMaxLODs, const FMatrix& InComponentToWorld, const FString& InPackageName, TArray<FRawMeshTracker>& OutRawMeshTrackers, TArray<FRawMesh>& OutRawMeshes, TArray<UMaterialInterface*>& OutMaterials)
|
|
{
|
|
const int32 BaseMaterialIndex = OutMaterials.Num();
|
|
|
|
// Export all LODs to raw meshes
|
|
const int32 NumLODs = InSkinnedMeshComponent->MeshObject->GetSkeletalMeshResource().LODModels.Num();
|
|
|
|
for (int32 OverallLODIndex = 0; OverallLODIndex < InOverallMaxLODs; OverallLODIndex++)
|
|
{
|
|
int32 LODIndexRead = FMath::Min(OverallLODIndex, NumLODs - 1);
|
|
|
|
FRawMesh& RawMesh = OutRawMeshes[OverallLODIndex];
|
|
FRawMeshTracker& RawMeshTracker = OutRawMeshTrackers[OverallLODIndex];
|
|
const int32 BaseVertexIndex = RawMesh.VertexPositions.Num();
|
|
|
|
// Get the CPU skinned verts for this LOD
|
|
TArray<FFinalSkinVertex> FinalVertices;
|
|
InSkinnedMeshComponent->GetCPUSkinnedVertices(FinalVertices, LODIndexRead);
|
|
|
|
FSkeletalMeshResource& SkeletalMeshResource = InSkinnedMeshComponent->MeshObject->GetSkeletalMeshResource();
|
|
FStaticLODModel& StaticLODModel = SkeletalMeshResource.LODModels[LODIndexRead];
|
|
|
|
// Copy skinned vertex positions
|
|
for (int32 VertIndex = 0; VertIndex < FinalVertices.Num(); ++VertIndex)
|
|
{
|
|
RawMesh.VertexPositions.Add(InComponentToWorld.TransformPosition(FinalVertices[VertIndex].Position));
|
|
}
|
|
|
|
const uint32 NumTexCoords = FMath::Min(StaticLODModel.VertexBufferGPUSkin.GetNumTexCoords(), (uint32)MAX_MESH_TEXTURE_COORDS);
|
|
const int32 NumSections = StaticLODModel.Sections.Num();
|
|
FRawStaticIndexBuffer16or32Interface& IndexBuffer = *StaticLODModel.MultiSizeIndexContainer.GetIndexBuffer();
|
|
|
|
for (int32 SectionIndex = 0; SectionIndex < NumSections; SectionIndex++)
|
|
{
|
|
const FSkelMeshSection& SkelMeshSection = StaticLODModel.Sections[SectionIndex];
|
|
if (!SkelMeshSection.bDisabled)
|
|
{
|
|
// Build 'wedge' info
|
|
const int32 NumIndices = SkelMeshSection.NumTriangles * 3;
|
|
for (int32 IndexIndex = 0; IndexIndex < NumIndices; IndexIndex++)
|
|
{
|
|
int32 Index = IndexBuffer.Get(SkelMeshSection.BaseIndex + IndexIndex);
|
|
|
|
RawMesh.WedgeIndices.Add(BaseVertexIndex + Index);
|
|
|
|
const FFinalSkinVertex& SkinnedVertex = FinalVertices[Index];
|
|
const FVector TangentX = InComponentToWorld.TransformVector(SkinnedVertex.TangentX);
|
|
const FVector4 TangentZ = InComponentToWorld.TransformVector(SkinnedVertex.TangentZ);
|
|
const FVector TangentY = (TangentX ^ TangentZ).GetSafeNormal() * TangentZ.W;
|
|
|
|
RawMesh.WedgeTangentX.Add(TangentX);
|
|
RawMesh.WedgeTangentY.Add(TangentY);
|
|
RawMesh.WedgeTangentZ.Add(TangentZ);
|
|
|
|
for (uint32 TexCoordIndex = 0; TexCoordIndex < MAX_MESH_TEXTURE_COORDS; TexCoordIndex++)
|
|
{
|
|
if (TexCoordIndex >= NumTexCoords)
|
|
{
|
|
RawMesh.WedgeTexCoords[TexCoordIndex].AddDefaulted();
|
|
}
|
|
else
|
|
{
|
|
RawMesh.WedgeTexCoords[TexCoordIndex].Add(StaticLODModel.VertexBufferGPUSkin.GetVertexUV(Index, TexCoordIndex));
|
|
RawMeshTracker.bValidTexCoords[TexCoordIndex] = true;
|
|
}
|
|
}
|
|
|
|
if (StaticLODModel.ColorVertexBuffer.IsInitialized())
|
|
{
|
|
RawMesh.WedgeColors.Add(StaticLODModel.ColorVertexBuffer.VertexColor(Index));
|
|
RawMeshTracker.bValidColors = true;
|
|
}
|
|
else
|
|
{
|
|
RawMesh.WedgeColors.Add(FColor::White);
|
|
}
|
|
}
|
|
|
|
// copy face info
|
|
for (uint32 TriIndex = 0; TriIndex < SkelMeshSection.NumTriangles; TriIndex++)
|
|
{
|
|
RawMesh.FaceMaterialIndices.Add(BaseMaterialIndex + SkelMeshSection.MaterialIndex);
|
|
RawMesh.FaceSmoothingMasks.Add(0); // Assume this is ignored as bRecomputeNormals is false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ProcessMaterials<USkinnedMeshComponent>(InSkinnedMeshComponent, InPackageName, OutMaterials);
|
|
}
|
|
|
|
// Helper function for ConvertMeshesToStaticMesh
|
|
static bool IsValidStaticMeshComponent(UStaticMeshComponent* InComponent)
|
|
{
|
|
return InComponent && InComponent->GetStaticMesh() && InComponent->GetStaticMesh()->RenderData && InComponent->IsVisible();
|
|
}
|
|
|
|
// Helper function for ConvertMeshesToStaticMesh
|
|
static void StaticMeshToRawMeshes(UStaticMeshComponent* InStaticMeshComponent, int32 InOverallMaxLODs, const FMatrix& InComponentToWorld, const FString& InPackageName, TArray<FRawMeshTracker>& OutRawMeshTrackers, TArray<FRawMesh>& OutRawMeshes, TArray<UMaterialInterface*>& OutMaterials)
|
|
{
|
|
const int32 BaseMaterialIndex = OutMaterials.Num();
|
|
|
|
const int32 NumLODs = InStaticMeshComponent->GetStaticMesh()->RenderData->LODResources.Num();
|
|
|
|
for (int32 OverallLODIndex = 0; OverallLODIndex < InOverallMaxLODs; OverallLODIndex++)
|
|
{
|
|
int32 LODIndexRead = FMath::Min(OverallLODIndex, NumLODs - 1);
|
|
|
|
FRawMesh& RawMesh = OutRawMeshes[OverallLODIndex];
|
|
FRawMeshTracker& RawMeshTracker = OutRawMeshTrackers[OverallLODIndex];
|
|
const FStaticMeshLODResources& LODResource = InStaticMeshComponent->GetStaticMesh()->RenderData->LODResources[LODIndexRead];
|
|
const int32 BaseVertexIndex = RawMesh.VertexPositions.Num();
|
|
|
|
for (int32 VertIndex = 0; VertIndex < LODResource.GetNumVertices(); ++VertIndex)
|
|
{
|
|
RawMesh.VertexPositions.Add(InComponentToWorld.TransformPosition(LODResource.PositionVertexBuffer.VertexPosition((uint32)VertIndex)));
|
|
}
|
|
|
|
const FIndexArrayView IndexArrayView = LODResource.IndexBuffer.GetArrayView();
|
|
const FStaticMeshVertexBuffer& StaticMeshVertexBuffer = LODResource.VertexBuffer;
|
|
const int32 NumTexCoords = FMath::Min(StaticMeshVertexBuffer.GetNumTexCoords(), (uint32)MAX_MESH_TEXTURE_COORDS);
|
|
const int32 NumSections = LODResource.Sections.Num();
|
|
|
|
for (int32 SectionIndex = 0; SectionIndex < NumSections; SectionIndex++)
|
|
{
|
|
const FStaticMeshSection& StaticMeshSection = LODResource.Sections[SectionIndex];
|
|
|
|
const int32 NumIndices = StaticMeshSection.NumTriangles * 3;
|
|
for (int32 IndexIndex = 0; IndexIndex < NumIndices; IndexIndex++)
|
|
{
|
|
int32 Index = IndexArrayView[StaticMeshSection.FirstIndex + IndexIndex];
|
|
RawMesh.WedgeIndices.Add(BaseVertexIndex + Index);
|
|
|
|
RawMesh.WedgeTangentX.Add(InComponentToWorld.TransformVector(StaticMeshVertexBuffer.VertexTangentX(Index)));
|
|
RawMesh.WedgeTangentY.Add(InComponentToWorld.TransformVector(StaticMeshVertexBuffer.VertexTangentY(Index)));
|
|
RawMesh.WedgeTangentZ.Add(InComponentToWorld.TransformVector(StaticMeshVertexBuffer.VertexTangentZ(Index)));
|
|
|
|
for (int32 TexCoordIndex = 0; TexCoordIndex < MAX_MESH_TEXTURE_COORDS; TexCoordIndex++)
|
|
{
|
|
if (TexCoordIndex >= NumTexCoords)
|
|
{
|
|
RawMesh.WedgeTexCoords[TexCoordIndex].AddDefaulted();
|
|
}
|
|
else
|
|
{
|
|
RawMesh.WedgeTexCoords[TexCoordIndex].Add(StaticMeshVertexBuffer.GetVertexUV(Index, TexCoordIndex));
|
|
RawMeshTracker.bValidTexCoords[TexCoordIndex] = true;
|
|
}
|
|
}
|
|
|
|
if (LODResource.ColorVertexBuffer.IsInitialized())
|
|
{
|
|
RawMesh.WedgeColors.Add(LODResource.ColorVertexBuffer.VertexColor(Index));
|
|
RawMeshTracker.bValidColors = true;
|
|
}
|
|
else
|
|
{
|
|
RawMesh.WedgeColors.Add(FColor::White);
|
|
}
|
|
}
|
|
|
|
// copy face info
|
|
for (uint32 TriIndex = 0; TriIndex < StaticMeshSection.NumTriangles; TriIndex++)
|
|
{
|
|
RawMesh.FaceMaterialIndices.Add(BaseMaterialIndex + StaticMeshSection.MaterialIndex);
|
|
RawMesh.FaceSmoothingMasks.Add(0); // Assume this is ignored as bRecomputeNormals is false
|
|
}
|
|
}
|
|
}
|
|
|
|
ProcessMaterials<UStaticMeshComponent>(InStaticMeshComponent, InPackageName, OutMaterials);
|
|
}
|
|
|
|
UStaticMesh* FMeshUtilities::ConvertMeshesToStaticMesh(const TArray<UMeshComponent*>& InMeshComponents, const FTransform& InRootTransform, const FString& InPackageName)
|
|
{
|
|
// Build a package name to use
|
|
FString MeshName;
|
|
FString PackageName;
|
|
if (InPackageName.IsEmpty())
|
|
{
|
|
FString NewNameSuggestion = FString(TEXT("StaticMesh"));
|
|
FString PackageNameSuggestion = FString(TEXT("/Game/Meshes/")) + NewNameSuggestion;
|
|
FString Name;
|
|
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
|
AssetToolsModule.Get().CreateUniqueAssetName(PackageNameSuggestion, TEXT(""), PackageNameSuggestion, Name);
|
|
|
|
TSharedPtr<SDlgPickAssetPath> PickAssetPathWidget =
|
|
SNew(SDlgPickAssetPath)
|
|
.Title(LOCTEXT("ConvertToStaticMeshPickName", "Choose New StaticMesh Location"))
|
|
.DefaultAssetPath(FText::FromString(PackageNameSuggestion));
|
|
|
|
if (PickAssetPathWidget->ShowModal() == EAppReturnType::Ok)
|
|
{
|
|
// Get the full name of where we want to create the mesh asset.
|
|
PackageName = PickAssetPathWidget->GetFullAssetPath().ToString();
|
|
MeshName = FPackageName::GetLongPackageAssetName(PackageName);
|
|
|
|
// Check if the user inputed a valid asset name, if they did not, give it the generated default name
|
|
if (MeshName.IsEmpty())
|
|
{
|
|
// Use the defaults that were already generated.
|
|
PackageName = PackageNameSuggestion;
|
|
MeshName = *Name;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PackageName = InPackageName;
|
|
MeshName = *FPackageName::GetLongPackageAssetName(PackageName);
|
|
}
|
|
|
|
if(!PackageName.IsEmpty() && !MeshName.IsEmpty())
|
|
{
|
|
TArray<FRawMesh> RawMeshes;
|
|
TArray<UMaterialInterface*> Materials;
|
|
|
|
TArray<FRawMeshTracker> RawMeshTrackers;
|
|
|
|
FMatrix WorldToRoot = InRootTransform.ToMatrixWithScale().Inverse();
|
|
|
|
// first do a pass to determine the max LOD level we will be combining meshes into
|
|
int32 OverallMaxLODs = 0;
|
|
for (UMeshComponent* MeshComponent : InMeshComponents)
|
|
{
|
|
USkinnedMeshComponent* SkinnedMeshComponent = Cast<USkinnedMeshComponent>(MeshComponent);
|
|
UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(MeshComponent);
|
|
|
|
if (IsValidSkinnedMeshComponent(SkinnedMeshComponent))
|
|
{
|
|
OverallMaxLODs = FMath::Max(SkinnedMeshComponent->MeshObject->GetSkeletalMeshResource().LODModels.Num(), OverallMaxLODs);
|
|
}
|
|
else if(IsValidStaticMeshComponent(StaticMeshComponent))
|
|
{
|
|
OverallMaxLODs = FMath::Max(StaticMeshComponent->GetStaticMesh()->RenderData->LODResources.Num(), OverallMaxLODs);
|
|
}
|
|
}
|
|
|
|
// Resize raw meshes to accommodate the number of LODs we will need
|
|
RawMeshes.SetNum(OverallMaxLODs);
|
|
RawMeshTrackers.SetNum(OverallMaxLODs);
|
|
|
|
// Export all visible components
|
|
for (UMeshComponent* MeshComponent : InMeshComponents)
|
|
{
|
|
FMatrix ComponentToWorld = MeshComponent->GetComponentTransform().ToMatrixWithScale() * WorldToRoot;
|
|
|
|
USkinnedMeshComponent* SkinnedMeshComponent = Cast<USkinnedMeshComponent>(MeshComponent);
|
|
UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(MeshComponent);
|
|
|
|
if (IsValidSkinnedMeshComponent(SkinnedMeshComponent))
|
|
{
|
|
SkinnedMeshToRawMeshes(SkinnedMeshComponent, OverallMaxLODs, ComponentToWorld, PackageName, RawMeshTrackers, RawMeshes, Materials);
|
|
}
|
|
else if (IsValidStaticMeshComponent(StaticMeshComponent))
|
|
{
|
|
StaticMeshToRawMeshes(StaticMeshComponent, OverallMaxLODs, ComponentToWorld, PackageName, RawMeshTrackers, RawMeshes, Materials);
|
|
}
|
|
}
|
|
|
|
// scrub invalid vert color & tex coord data
|
|
check(RawMeshes.Num() == RawMeshTrackers.Num());
|
|
for (int32 RawMeshIndex = 0; RawMeshIndex < RawMeshes.Num(); RawMeshIndex++)
|
|
{
|
|
if (!RawMeshTrackers[RawMeshIndex].bValidColors)
|
|
{
|
|
RawMeshes[RawMeshIndex].WedgeColors.Empty();
|
|
}
|
|
|
|
for (uint32 TexCoordIndex = 0; TexCoordIndex < MAX_MESH_TEXTURE_COORDS; TexCoordIndex++)
|
|
{
|
|
if (!RawMeshTrackers[RawMeshIndex].bValidTexCoords[TexCoordIndex])
|
|
{
|
|
RawMeshes[RawMeshIndex].WedgeTexCoords[TexCoordIndex].Empty();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we got some valid data.
|
|
bool bValidData = false;
|
|
for (FRawMesh& RawMesh : RawMeshes)
|
|
{
|
|
if (RawMesh.IsValidOrFixable())
|
|
{
|
|
bValidData = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bValidData)
|
|
{
|
|
// Then find/create it.
|
|
UPackage* Package = CreatePackage(NULL, *PackageName);
|
|
check(Package);
|
|
|
|
// Create StaticMesh object
|
|
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(Package, *MeshName, RF_Public | RF_Standalone);
|
|
StaticMesh->InitResources();
|
|
|
|
StaticMesh->LightingGuid = FGuid::NewGuid();
|
|
|
|
// Add source to new StaticMesh
|
|
for (FRawMesh& RawMesh : RawMeshes)
|
|
{
|
|
if (RawMesh.IsValidOrFixable())
|
|
{
|
|
FStaticMeshSourceModel* SrcModel = new (StaticMesh->SourceModels) FStaticMeshSourceModel();
|
|
SrcModel->BuildSettings.bRecomputeNormals = false;
|
|
SrcModel->BuildSettings.bRecomputeTangents = false;
|
|
SrcModel->BuildSettings.bRemoveDegenerates = false;
|
|
SrcModel->BuildSettings.bUseHighPrecisionTangentBasis = false;
|
|
SrcModel->BuildSettings.bUseFullPrecisionUVs = false;
|
|
SrcModel->BuildSettings.bGenerateLightmapUVs = true;
|
|
SrcModel->BuildSettings.SrcLightmapIndex = 0;
|
|
SrcModel->BuildSettings.DstLightmapIndex = 1;
|
|
SrcModel->RawMeshBulkData->SaveRawMesh(RawMesh);
|
|
}
|
|
}
|
|
|
|
// Copy materials to new mesh
|
|
for(UMaterialInterface* Material : Materials)
|
|
{
|
|
StaticMesh->StaticMaterials.Add(FStaticMaterial(Material));
|
|
}
|
|
|
|
//Set the Imported version before calling the build
|
|
StaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion;
|
|
|
|
// Build mesh from source
|
|
StaticMesh->Build(false);
|
|
StaticMesh->PostEditChange();
|
|
|
|
StaticMesh->MarkPackageDirty();
|
|
|
|
// Notify asset registry of new asset
|
|
FAssetRegistryModule::AssetCreated(StaticMesh);
|
|
|
|
// Display notification so users can quickly access the mesh
|
|
if (GIsEditor)
|
|
{
|
|
FNotificationInfo Info(FText::Format(LOCTEXT("SkeletalMeshConverted", "Successfully Converted Mesh"), FText::FromString(StaticMesh->GetName())));
|
|
Info.ExpireDuration = 8.0f;
|
|
Info.bUseLargeFont = false;
|
|
Info.Hyperlink = FSimpleDelegate::CreateLambda([=]() { FAssetEditorManager::Get().OpenEditorForAssets(TArray<UObject*>({ StaticMesh })); });
|
|
Info.HyperlinkText = FText::Format(LOCTEXT("OpenNewAnimationHyperlink", "Open {0}"), FText::FromString(StaticMesh->GetName()));
|
|
TSharedPtr<SNotificationItem> Notification = FSlateNotificationManager::Get().AddNotification(Info);
|
|
if ( Notification.IsValid() )
|
|
{
|
|
Notification->SetCompletionState( SNotificationItem::CS_Success );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* Builds a renderable skeletal mesh LOD model. Note that the array of chunks
|
|
* will be destroyed during this process!
|
|
* @param LODModel Upon return contains a renderable skeletal mesh LOD model.
|
|
* @param RefSkeleton The reference skeleton associated with the model.
|
|
* @param Chunks Skinned mesh chunks from which to build the renderable model.
|
|
* @param PointToOriginalMap Maps a vertex's RawPointIdx to its index at import time.
|
|
*/
|
|
void FMeshUtilities::BuildSkeletalModelFromChunks(FStaticLODModel& LODModel, const FReferenceSkeleton& RefSkeleton, TArray<FSkinnedMeshChunk*>& Chunks, const TArray<int32>& PointToOriginalMap)
|
|
{
|
|
#if WITH_EDITORONLY_DATA
|
|
// Clear out any data currently held in the LOD model.
|
|
LODModel.Sections.Empty();
|
|
LODModel.NumVertices = 0;
|
|
if (LODModel.MultiSizeIndexContainer.IsIndexBufferValid())
|
|
{
|
|
LODModel.MultiSizeIndexContainer.GetIndexBuffer()->Empty();
|
|
}
|
|
|
|
// Setup the section and chunk arrays on the model.
|
|
for (int32 ChunkIndex = 0; ChunkIndex < Chunks.Num(); ++ChunkIndex)
|
|
{
|
|
FSkinnedMeshChunk* SrcChunk = Chunks[ChunkIndex];
|
|
|
|
FSkelMeshSection& Section = *new(LODModel.Sections) FSkelMeshSection();
|
|
Section.MaterialIndex = SrcChunk->MaterialIndex;
|
|
Exchange(Section.BoneMap, SrcChunk->BoneMap);
|
|
|
|
// Update the active bone indices on the LOD model.
|
|
for (int32 BoneIndex = 0; BoneIndex < Section.BoneMap.Num(); ++BoneIndex)
|
|
{
|
|
LODModel.ActiveBoneIndices.AddUnique(Section.BoneMap[BoneIndex]);
|
|
}
|
|
}
|
|
|
|
LODModel.ActiveBoneIndices.Sort();
|
|
|
|
// Reset 'final vertex to import vertex' map info
|
|
LODModel.MeshToImportVertexMap.Empty();
|
|
LODModel.MaxImportVertex = 0;
|
|
|
|
// Keep track of index mapping to chunk vertex offsets
|
|
TArray< TArray<uint32> > VertexIndexRemap;
|
|
VertexIndexRemap.Empty(LODModel.Sections.Num());
|
|
// Pack the chunk vertices into a single vertex buffer.
|
|
TArray<uint32> RawPointIndices;
|
|
LODModel.NumVertices = 0;
|
|
|
|
int32 PrevMaterialIndex = -1;
|
|
int32 CurrentChunkBaseVertexIndex = -1; // base vertex index for all chunks of the same material
|
|
int32 CurrentChunkVertexCount = -1; // total vertex count for all chunks of the same material
|
|
int32 CurrentVertexIndex = 0; // current vertex index added to the index buffer for all chunks of the same material
|
|
|
|
// rearrange the vert order to minimize the data fetched by the GPU
|
|
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
if (IsInGameThread())
|
|
{
|
|
GWarn->StatusUpdate(SectionIndex, LODModel.Sections.Num(), NSLOCTEXT("UnrealEd", "ProcessingSections", "Processing Sections"));
|
|
}
|
|
|
|
FSkinnedMeshChunk* SrcChunk = Chunks[SectionIndex];
|
|
FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
TArray<FSoftSkinBuildVertex>& ChunkVertices = SrcChunk->Vertices;
|
|
TArray<uint32>& ChunkIndices = SrcChunk->Indices;
|
|
|
|
// Reorder the section index buffer for better vertex cache efficiency.
|
|
CacheOptimizeIndexBuffer(ChunkIndices);
|
|
|
|
// Calculate the number of triangles in the section. Note that CacheOptimize may change the number of triangles in the index buffer!
|
|
Section.NumTriangles = ChunkIndices.Num() / 3;
|
|
TArray<FSoftSkinBuildVertex> OriginalVertices;
|
|
Exchange(ChunkVertices, OriginalVertices);
|
|
ChunkVertices.AddUninitialized(OriginalVertices.Num());
|
|
|
|
TArray<int32> IndexCache;
|
|
IndexCache.AddUninitialized(ChunkVertices.Num());
|
|
FMemory::Memset(IndexCache.GetData(), INDEX_NONE, IndexCache.Num() * IndexCache.GetTypeSize());
|
|
int32 NextAvailableIndex = 0;
|
|
// Go through the indices and assign them new values that are coherent where possible
|
|
for (int32 Index = 0; Index < ChunkIndices.Num(); Index++)
|
|
{
|
|
const int32 OriginalIndex = ChunkIndices[Index];
|
|
const int32 CachedIndex = IndexCache[OriginalIndex];
|
|
|
|
if (CachedIndex == INDEX_NONE)
|
|
{
|
|
// No new index has been allocated for this existing index, assign a new one
|
|
ChunkIndices[Index] = NextAvailableIndex;
|
|
// Mark what this index has been assigned to
|
|
IndexCache[OriginalIndex] = NextAvailableIndex;
|
|
NextAvailableIndex++;
|
|
}
|
|
else
|
|
{
|
|
// Reuse an existing index assignment
|
|
ChunkIndices[Index] = CachedIndex;
|
|
}
|
|
// Reorder the vertices based on the new index assignment
|
|
ChunkVertices[ChunkIndices[Index]] = OriginalVertices[OriginalIndex];
|
|
}
|
|
}
|
|
|
|
// Build the arrays of rigid and soft vertices on the model's chunks.
|
|
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
TArray<FSoftSkinBuildVertex>& ChunkVertices = Chunks[SectionIndex]->Vertices;
|
|
|
|
if (IsInGameThread())
|
|
{
|
|
// Only update status if in the game thread. When importing morph targets, this function can run in another thread
|
|
GWarn->StatusUpdate(SectionIndex, LODModel.Sections.Num(), NSLOCTEXT("UnrealEd", "ProcessingChunks", "Processing Chunks"));
|
|
}
|
|
|
|
CurrentVertexIndex = 0;
|
|
CurrentChunkVertexCount = 0;
|
|
PrevMaterialIndex = Section.MaterialIndex;
|
|
|
|
// Calculate the offset to this chunk's vertices in the vertex buffer.
|
|
Section.BaseVertexIndex = CurrentChunkBaseVertexIndex = LODModel.NumVertices;
|
|
|
|
// Update the size of the vertex buffer.
|
|
LODModel.NumVertices += ChunkVertices.Num();
|
|
|
|
// Separate the section's vertices into rigid and soft vertices.
|
|
TArray<uint32>& ChunkVertexIndexRemap = *new(VertexIndexRemap)TArray<uint32>();
|
|
ChunkVertexIndexRemap.AddUninitialized(ChunkVertices.Num());
|
|
|
|
for (int32 VertexIndex = 0; VertexIndex < ChunkVertices.Num(); VertexIndex++)
|
|
{
|
|
const FSoftSkinBuildVertex& SoftVertex = ChunkVertices[VertexIndex];
|
|
|
|
FSoftSkinVertex NewVertex;
|
|
NewVertex.Position = SoftVertex.Position;
|
|
NewVertex.TangentX = SoftVertex.TangentX;
|
|
NewVertex.TangentY = SoftVertex.TangentY;
|
|
NewVertex.TangentZ = SoftVertex.TangentZ;
|
|
FMemory::Memcpy(NewVertex.UVs, SoftVertex.UVs, sizeof(FVector2D)*MAX_TEXCOORDS);
|
|
NewVertex.Color = SoftVertex.Color;
|
|
for (int32 i = 0; i < MAX_TOTAL_INFLUENCES; ++i)
|
|
{
|
|
// it only adds to the bone map if it has weight on it
|
|
// BoneMap contains only the bones that has influence with weight of >0.f
|
|
// so here, just make sure it is included before setting the data
|
|
if (Section.BoneMap.IsValidIndex(SoftVertex.InfluenceBones[i]))
|
|
{
|
|
NewVertex.InfluenceBones[i] = SoftVertex.InfluenceBones[i];
|
|
NewVertex.InfluenceWeights[i] = SoftVertex.InfluenceWeights[i];
|
|
}
|
|
}
|
|
Section.SoftVertices.Add(NewVertex);
|
|
ChunkVertexIndexRemap[VertexIndex] = (uint32)(Section.BaseVertexIndex + CurrentVertexIndex);
|
|
CurrentVertexIndex++;
|
|
// add the index to the original wedge point source of this vertex
|
|
RawPointIndices.Add(SoftVertex.PointWedgeIdx);
|
|
// Also remember import index
|
|
const int32 RawVertIndex = PointToOriginalMap[SoftVertex.PointWedgeIdx];
|
|
LODModel.MeshToImportVertexMap.Add(RawVertIndex);
|
|
LODModel.MaxImportVertex = FMath::Max<float>(LODModel.MaxImportVertex, RawVertIndex);
|
|
}
|
|
|
|
// update NumVertices
|
|
Section.NumVertices = Section.SoftVertices.Num();
|
|
|
|
// update max bone influences
|
|
Section.CalcMaxBoneInfluences();
|
|
|
|
// Log info about the chunk.
|
|
UE_LOG(LogSkeletalMesh, Log, TEXT("Section %u: %u vertices, %u active bones"),
|
|
SectionIndex,
|
|
Section.GetNumVertices(),
|
|
Section.BoneMap.Num()
|
|
);
|
|
}
|
|
|
|
// Copy raw point indices to LOD model.
|
|
LODModel.RawPointIndices.RemoveBulkData();
|
|
if (RawPointIndices.Num())
|
|
{
|
|
LODModel.RawPointIndices.Lock(LOCK_READ_WRITE);
|
|
void* Dest = LODModel.RawPointIndices.Realloc(RawPointIndices.Num());
|
|
FMemory::Memcpy(Dest, RawPointIndices.GetData(), LODModel.RawPointIndices.GetBulkDataSize());
|
|
LODModel.RawPointIndices.Unlock();
|
|
}
|
|
|
|
#if DISALLOW_32BIT_INDICES
|
|
LODModel.MultiSizeIndexContainer.CreateIndexBuffer(sizeof(uint16));
|
|
#else
|
|
LODModel.MultiSizeIndexContainer.CreateIndexBuffer((LODModel.NumVertices < MAX_uint16) ? sizeof(uint16) : sizeof(uint32));
|
|
#endif
|
|
|
|
// Finish building the sections.
|
|
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
|
|
const TArray<uint32>& SectionIndices = Chunks[SectionIndex]->Indices;
|
|
FRawStaticIndexBuffer16or32Interface* IndexBuffer = LODModel.MultiSizeIndexContainer.GetIndexBuffer();
|
|
Section.BaseIndex = IndexBuffer->Num();
|
|
const int32 NumIndices = SectionIndices.Num();
|
|
const TArray<uint32>& SectionVertexIndexRemap = VertexIndexRemap[SectionIndex];
|
|
for (int32 Index = 0; Index < NumIndices; Index++)
|
|
{
|
|
uint32 VertexIndex = SectionVertexIndexRemap[SectionIndices[Index]];
|
|
IndexBuffer->AddItem(VertexIndex);
|
|
}
|
|
}
|
|
|
|
// Free the skinned mesh chunks which are no longer needed.
|
|
for (int32 i = 0; i < Chunks.Num(); ++i)
|
|
{
|
|
delete Chunks[i];
|
|
Chunks[i] = NULL;
|
|
}
|
|
Chunks.Empty();
|
|
|
|
// Build the adjacency index buffer used for tessellation.
|
|
{
|
|
TArray<FSoftSkinVertex> Vertices;
|
|
LODModel.GetVertices(Vertices);
|
|
|
|
FMultiSizeIndexContainerData IndexData;
|
|
LODModel.MultiSizeIndexContainer.GetIndexBufferData(IndexData);
|
|
|
|
FMultiSizeIndexContainerData AdjacencyIndexData;
|
|
AdjacencyIndexData.DataTypeSize = IndexData.DataTypeSize;
|
|
|
|
BuildSkeletalAdjacencyIndexBuffer(Vertices, LODModel.NumTexCoords, IndexData.Indices, AdjacencyIndexData.Indices);
|
|
LODModel.AdjacencyMultiSizeIndexContainer.RebuildIndexBuffer(AdjacencyIndexData);
|
|
}
|
|
|
|
// Compute the required bones for this model.
|
|
USkeletalMesh::CalculateRequiredBones(LODModel, RefSkeleton, NULL);
|
|
#endif // #if WITH_EDITORONLY_DATA
|
|
}
|
|
|
|
/*------------------------------------------------------------------------------
|
|
Common functionality.
|
|
------------------------------------------------------------------------------*/
|
|
|
|
/** Helper struct for building acceleration structures. */
|
|
struct FIndexAndZ
|
|
{
|
|
float Z;
|
|
int32 Index;
|
|
|
|
/** Default constructor. */
|
|
FIndexAndZ() {}
|
|
|
|
/** Initialization constructor. */
|
|
FIndexAndZ(int32 InIndex, FVector V)
|
|
{
|
|
Z = 0.30f * V.X + 0.33f * V.Y + 0.37f * V.Z;
|
|
Index = InIndex;
|
|
}
|
|
};
|
|
|
|
/** Sorting function for vertex Z/index pairs. */
|
|
struct FCompareIndexAndZ
|
|
{
|
|
FORCEINLINE bool operator()(FIndexAndZ const& A, FIndexAndZ const& B) const { return A.Z < B.Z; }
|
|
};
|
|
|
|
static int32 ComputeNumTexCoords(FRawMesh const& RawMesh, int32 MaxSupportedTexCoords)
|
|
{
|
|
int32 NumWedges = RawMesh.WedgeIndices.Num();
|
|
int32 NumTexCoords = 0;
|
|
for (int32 TexCoordIndex = 0; TexCoordIndex < MAX_MESH_TEXTURE_COORDS; ++TexCoordIndex)
|
|
{
|
|
if (RawMesh.WedgeTexCoords[TexCoordIndex].Num() != NumWedges)
|
|
{
|
|
break;
|
|
}
|
|
NumTexCoords++;
|
|
}
|
|
return FMath::Min(NumTexCoords, MaxSupportedTexCoords);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the specified points are about equal
|
|
*/
|
|
inline bool PointsEqual(const FVector& V1, const FVector& V2, float ComparisonThreshold)
|
|
{
|
|
if (FMath::Abs(V1.X - V2.X) > ComparisonThreshold
|
|
|| FMath::Abs(V1.Y - V2.Y) > ComparisonThreshold
|
|
|| FMath::Abs(V1.Z - V2.Z) > ComparisonThreshold)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static inline FVector GetPositionForWedge(FRawMesh const& Mesh, int32 WedgeIndex)
|
|
{
|
|
int32 VertexIndex = Mesh.WedgeIndices[WedgeIndex];
|
|
return Mesh.VertexPositions[VertexIndex];
|
|
}
|
|
|
|
struct FMeshEdge
|
|
{
|
|
int32 Vertices[2];
|
|
int32 Faces[2];
|
|
};
|
|
|
|
/**
|
|
* This helper class builds the edge list for a mesh. It uses a hash of vertex
|
|
* positions to edges sharing that vertex to remove the n^2 searching of all
|
|
* previously added edges. This class is templatized so it can be used with
|
|
* either static mesh or skeletal mesh vertices
|
|
*/
|
|
template <class VertexClass> class TEdgeBuilder
|
|
{
|
|
protected:
|
|
/**
|
|
* The list of indices to build the edge data from
|
|
*/
|
|
const TArray<uint32>& Indices;
|
|
/**
|
|
* The array of verts for vertex position comparison
|
|
*/
|
|
const TArray<VertexClass>& Vertices;
|
|
/**
|
|
* The array of edges to create
|
|
*/
|
|
TArray<FMeshEdge>& Edges;
|
|
/**
|
|
* List of edges that start with a given vertex
|
|
*/
|
|
TMultiMap<FVector, FMeshEdge*> VertexToEdgeList;
|
|
|
|
/**
|
|
* This function determines whether a given edge matches or not. It must be
|
|
* provided by derived classes since they have the specific information that
|
|
* this class doesn't know about (vertex info, influences, etc)
|
|
*
|
|
* @param Index1 The first index of the edge being checked
|
|
* @param Index2 The second index of the edge
|
|
* @param OtherEdge The edge to compare. Was found via the map
|
|
*
|
|
* @return true if the edge is a match, false otherwise
|
|
*/
|
|
virtual bool DoesEdgeMatch(int32 Index1, int32 Index2, FMeshEdge* OtherEdge) = 0;
|
|
|
|
/**
|
|
* Searches the list of edges to see if this one matches an existing and
|
|
* returns a pointer to it if it does
|
|
*
|
|
* @param Index1 the first index to check for
|
|
* @param Index2 the second index to check for
|
|
*
|
|
* @return NULL if no edge was found, otherwise the edge that was found
|
|
*/
|
|
inline FMeshEdge* FindOppositeEdge(int32 Index1, int32 Index2)
|
|
{
|
|
FMeshEdge* Edge = NULL;
|
|
TArray<FMeshEdge*> EdgeList;
|
|
// Search the hash for a corresponding vertex
|
|
VertexToEdgeList.MultiFind(Vertices[Index2].Position, EdgeList);
|
|
// Now search through the array for a match or not
|
|
for (int32 EdgeIndex = 0; EdgeIndex < EdgeList.Num() && Edge == NULL;
|
|
EdgeIndex++)
|
|
{
|
|
FMeshEdge* OtherEdge = EdgeList[EdgeIndex];
|
|
// See if this edge matches the passed in edge
|
|
if (OtherEdge != NULL && DoesEdgeMatch(Index1, Index2, OtherEdge))
|
|
{
|
|
// We have a match
|
|
Edge = OtherEdge;
|
|
}
|
|
}
|
|
return Edge;
|
|
}
|
|
|
|
/**
|
|
* Updates an existing edge if found or adds the new edge to the list
|
|
*
|
|
* @param Index1 the first index in the edge
|
|
* @param Index2 the second index in the edge
|
|
* @param Triangle the triangle that this edge was found in
|
|
*/
|
|
inline void AddEdge(int32 Index1, int32 Index2, int32 Triangle)
|
|
{
|
|
// If this edge matches another then just fill the other triangle
|
|
// otherwise add it
|
|
FMeshEdge* OtherEdge = FindOppositeEdge(Index1, Index2);
|
|
if (OtherEdge == NULL)
|
|
{
|
|
// Add a new edge to the array
|
|
int32 EdgeIndex = Edges.AddZeroed();
|
|
Edges[EdgeIndex].Vertices[0] = Index1;
|
|
Edges[EdgeIndex].Vertices[1] = Index2;
|
|
Edges[EdgeIndex].Faces[0] = Triangle;
|
|
Edges[EdgeIndex].Faces[1] = -1;
|
|
// Also add this edge to the hash for faster searches
|
|
// NOTE: This relies on the array never being realloced!
|
|
VertexToEdgeList.Add(Vertices[Index1].Position, &Edges[EdgeIndex]);
|
|
}
|
|
else
|
|
{
|
|
OtherEdge->Faces[1] = Triangle;
|
|
}
|
|
}
|
|
|
|
public:
|
|
/**
|
|
* Initializes the values for the code that will build the mesh edge list
|
|
*/
|
|
TEdgeBuilder(const TArray<uint32>& InIndices,
|
|
const TArray<VertexClass>& InVertices,
|
|
TArray<FMeshEdge>& OutEdges) :
|
|
Indices(InIndices), Vertices(InVertices), Edges(OutEdges)
|
|
{
|
|
// Presize the array so that there are no extra copies being done
|
|
// when adding edges to it
|
|
Edges.Empty(Indices.Num());
|
|
}
|
|
|
|
/**
|
|
* Virtual dtor
|
|
*/
|
|
virtual ~TEdgeBuilder(){}
|
|
|
|
|
|
/**
|
|
* Uses a hash of indices to edge lists so that it can avoid the n^2 search
|
|
* through the full edge list
|
|
*/
|
|
void FindEdges(void)
|
|
{
|
|
// @todo Handle something other than trilists when building edges
|
|
int32 TriangleCount = Indices.Num() / 3;
|
|
int32 EdgeCount = 0;
|
|
// Work through all triangles building the edges
|
|
for (int32 Triangle = 0; Triangle < TriangleCount; Triangle++)
|
|
{
|
|
// Determine the starting index
|
|
int32 TriangleIndex = Triangle * 3;
|
|
// Get the indices for the triangle
|
|
int32 Index1 = Indices[TriangleIndex];
|
|
int32 Index2 = Indices[TriangleIndex + 1];
|
|
int32 Index3 = Indices[TriangleIndex + 2];
|
|
// Add the first to second edge
|
|
AddEdge(Index1, Index2, Triangle);
|
|
// Now add the second to third
|
|
AddEdge(Index2, Index3, Triangle);
|
|
// Add the third to first edge
|
|
AddEdge(Index3, Index1, Triangle);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This is the static mesh specific version for finding edges
|
|
*/
|
|
class FStaticMeshEdgeBuilder : public TEdgeBuilder<FStaticMeshBuildVertex>
|
|
{
|
|
public:
|
|
/**
|
|
* Constructor that passes all work to the parent class
|
|
*/
|
|
FStaticMeshEdgeBuilder(const TArray<uint32>& InIndices,
|
|
const TArray<FStaticMeshBuildVertex>& InVertices,
|
|
TArray<FMeshEdge>& OutEdges) :
|
|
TEdgeBuilder<FStaticMeshBuildVertex>(InIndices, InVertices, OutEdges)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* This function determines whether a given edge matches or not for a static mesh
|
|
*
|
|
* @param Index1 The first index of the edge being checked
|
|
* @param Index2 The second index of the edge
|
|
* @param OtherEdge The edge to compare. Was found via the map
|
|
*
|
|
* @return true if the edge is a match, false otherwise
|
|
*/
|
|
bool DoesEdgeMatch(int32 Index1, int32 Index2, FMeshEdge* OtherEdge)
|
|
{
|
|
return Vertices[OtherEdge->Vertices[1]].Position == Vertices[Index1].Position &&
|
|
OtherEdge->Faces[1] == -1;
|
|
}
|
|
};
|
|
|
|
static void ComputeTriangleTangents(
|
|
const TArray<FVector>& InVertices,
|
|
const TArray<uint32>& InIndices,
|
|
const TArray<FVector2D>& InUVs,
|
|
TArray<FVector>& OutTangentX,
|
|
TArray<FVector>& OutTangentY,
|
|
TArray<FVector>& OutTangentZ,
|
|
float ComparisonThreshold
|
|
)
|
|
{
|
|
const int32 NumTriangles = InIndices.Num() / 3;
|
|
OutTangentX.Empty(NumTriangles);
|
|
OutTangentY.Empty(NumTriangles);
|
|
OutTangentZ.Empty(NumTriangles);
|
|
|
|
for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; TriangleIndex++)
|
|
{
|
|
int32 UVIndex = 0;
|
|
|
|
FVector P[3];
|
|
for (int32 i = 0; i < 3; ++i)
|
|
{
|
|
P[i] = InVertices[InIndices[TriangleIndex * 3 + i]];
|
|
}
|
|
|
|
const FVector Normal = ((P[1] - P[2]) ^ (P[0] - P[2])).GetSafeNormal(ComparisonThreshold);
|
|
FMatrix ParameterToLocal(
|
|
FPlane(P[1].X - P[0].X, P[1].Y - P[0].Y, P[1].Z - P[0].Z, 0),
|
|
FPlane(P[2].X - P[0].X, P[2].Y - P[0].Y, P[2].Z - P[0].Z, 0),
|
|
FPlane(P[0].X, P[0].Y, P[0].Z, 0),
|
|
FPlane(0, 0, 0, 1)
|
|
);
|
|
|
|
const FVector2D T1 = InUVs[TriangleIndex * 3 + 0];
|
|
const FVector2D T2 = InUVs[TriangleIndex * 3 + 1];
|
|
const FVector2D T3 = InUVs[TriangleIndex * 3 + 2];
|
|
|
|
FMatrix ParameterToTexture(
|
|
FPlane(T2.X - T1.X, T2.Y - T1.Y, 0, 0),
|
|
FPlane(T3.X - T1.X, T3.Y - T1.Y, 0, 0),
|
|
FPlane(T1.X, T1.Y, 1, 0),
|
|
FPlane(0, 0, 0, 1)
|
|
);
|
|
|
|
// Use InverseSlow to catch singular matrices. Inverse can miss this sometimes.
|
|
const FMatrix TextureToLocal = ParameterToTexture.Inverse() * ParameterToLocal;
|
|
|
|
OutTangentX.Add(TextureToLocal.TransformVector(FVector(1, 0, 0)).GetSafeNormal());
|
|
OutTangentY.Add(TextureToLocal.TransformVector(FVector(0, 1, 0)).GetSafeNormal());
|
|
OutTangentZ.Add(Normal);
|
|
|
|
FVector::CreateOrthonormalBasis(
|
|
OutTangentX[TriangleIndex],
|
|
OutTangentY[TriangleIndex],
|
|
OutTangentZ[TriangleIndex]
|
|
);
|
|
}
|
|
|
|
check(OutTangentX.Num() == NumTriangles);
|
|
check(OutTangentY.Num() == NumTriangles);
|
|
check(OutTangentZ.Num() == NumTriangles);
|
|
}
|
|
|
|
static void ComputeTriangleTangents(
|
|
TArray<FVector>& OutTangentX,
|
|
TArray<FVector>& OutTangentY,
|
|
TArray<FVector>& OutTangentZ,
|
|
FRawMesh const& RawMesh,
|
|
float ComparisonThreshold
|
|
)
|
|
{
|
|
ComputeTriangleTangents(RawMesh.VertexPositions, RawMesh.WedgeIndices, RawMesh.WedgeTexCoords[0], OutTangentX, OutTangentY, OutTangentZ, ComparisonThreshold);
|
|
|
|
/*int32 NumTriangles = RawMesh.WedgeIndices.Num() / 3;
|
|
TriangleTangentX.Empty(NumTriangles);
|
|
TriangleTangentY.Empty(NumTriangles);
|
|
TriangleTangentZ.Empty(NumTriangles);
|
|
|
|
for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; TriangleIndex++)
|
|
{
|
|
int32 UVIndex = 0;
|
|
|
|
FVector P[3];
|
|
for (int32 i = 0; i < 3; ++i)
|
|
{
|
|
P[i] = GetPositionForWedge(RawMesh, TriangleIndex * 3 + i);
|
|
}
|
|
|
|
const FVector Normal = ((P[1] - P[2]) ^ (P[0] - P[2])).GetSafeNormal(ComparisonThreshold);
|
|
FMatrix ParameterToLocal(
|
|
FPlane(P[1].X - P[0].X, P[1].Y - P[0].Y, P[1].Z - P[0].Z, 0),
|
|
FPlane(P[2].X - P[0].X, P[2].Y - P[0].Y, P[2].Z - P[0].Z, 0),
|
|
FPlane(P[0].X, P[0].Y, P[0].Z, 0),
|
|
FPlane(0, 0, 0, 1)
|
|
);
|
|
|
|
FVector2D T1 = RawMesh.WedgeTexCoords[UVIndex][TriangleIndex * 3 + 0];
|
|
FVector2D T2 = RawMesh.WedgeTexCoords[UVIndex][TriangleIndex * 3 + 1];
|
|
FVector2D T3 = RawMesh.WedgeTexCoords[UVIndex][TriangleIndex * 3 + 2];
|
|
FMatrix ParameterToTexture(
|
|
FPlane(T2.X - T1.X, T2.Y - T1.Y, 0, 0),
|
|
FPlane(T3.X - T1.X, T3.Y - T1.Y, 0, 0),
|
|
FPlane(T1.X, T1.Y, 1, 0),
|
|
FPlane(0, 0, 0, 1)
|
|
);
|
|
|
|
// Use InverseSlow to catch singular matrices. Inverse can miss this sometimes.
|
|
const FMatrix TextureToLocal = ParameterToTexture.Inverse() * ParameterToLocal;
|
|
|
|
TriangleTangentX.Add(TextureToLocal.TransformVector(FVector(1, 0, 0)).GetSafeNormal());
|
|
TriangleTangentY.Add(TextureToLocal.TransformVector(FVector(0, 1, 0)).GetSafeNormal());
|
|
TriangleTangentZ.Add(Normal);
|
|
|
|
FVector::CreateOrthonormalBasis(
|
|
TriangleTangentX[TriangleIndex],
|
|
TriangleTangentY[TriangleIndex],
|
|
TriangleTangentZ[TriangleIndex]
|
|
);
|
|
}
|
|
|
|
check(TriangleTangentX.Num() == NumTriangles);
|
|
check(TriangleTangentY.Num() == NumTriangles);
|
|
check(TriangleTangentZ.Num() == NumTriangles);*/
|
|
}
|
|
|
|
/**
|
|
* Create a table that maps the corner of each face to its overlapping corners.
|
|
* @param OutOverlappingCorners - Maps a corner index to the indices of all overlapping corners.
|
|
* @param RawMesh - The mesh for which to compute overlapping corners.
|
|
*/
|
|
static void FindOverlappingCorners(
|
|
TMultiMap<int32, int32>& OutOverlappingCorners,
|
|
const TArray<FVector>& InVertices,
|
|
const TArray<uint32>& InIndices,
|
|
float ComparisonThreshold
|
|
)
|
|
{
|
|
const int32 NumWedges = InIndices.Num();
|
|
|
|
// Create a list of vertex Z/index pairs
|
|
TArray<FIndexAndZ> VertIndexAndZ;
|
|
VertIndexAndZ.Reserve(NumWedges);
|
|
for (int32 WedgeIndex = 0; WedgeIndex < NumWedges; WedgeIndex++)
|
|
{
|
|
new(VertIndexAndZ)FIndexAndZ(WedgeIndex, InVertices[InIndices[WedgeIndex]]);
|
|
}
|
|
|
|
// Sort the vertices by z value
|
|
VertIndexAndZ.Sort(FCompareIndexAndZ());
|
|
|
|
// Search for duplicates, quickly!
|
|
for (int32 i = 0; i < VertIndexAndZ.Num(); i++)
|
|
{
|
|
// only need to search forward, since we add pairs both ways
|
|
for (int32 j = i + 1; j < VertIndexAndZ.Num(); j++)
|
|
{
|
|
if (FMath::Abs(VertIndexAndZ[j].Z - VertIndexAndZ[i].Z) > ComparisonThreshold)
|
|
break; // can't be any more dups
|
|
|
|
const FVector& PositionA = InVertices[InIndices[VertIndexAndZ[i].Index]];
|
|
const FVector& PositionB = InVertices[InIndices[VertIndexAndZ[j].Index]];
|
|
|
|
if (PointsEqual(PositionA, PositionB, ComparisonThreshold))
|
|
{
|
|
OutOverlappingCorners.Add(VertIndexAndZ[i].Index, VertIndexAndZ[j].Index);
|
|
OutOverlappingCorners.Add(VertIndexAndZ[j].Index, VertIndexAndZ[i].Index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a table that maps the corner of each face to its overlapping corners.
|
|
* @param OutOverlappingCorners - Maps a corner index to the indices of all overlapping corners.
|
|
* @param RawMesh - The mesh for which to compute overlapping corners.
|
|
*/
|
|
static void FindOverlappingCorners(
|
|
TMultiMap<int32, int32>& OutOverlappingCorners,
|
|
FRawMesh const& RawMesh,
|
|
float ComparisonThreshold
|
|
)
|
|
{
|
|
FindOverlappingCorners(OutOverlappingCorners, RawMesh.VertexPositions, RawMesh.WedgeIndices, ComparisonThreshold);
|
|
}
|
|
|
|
/**
|
|
* Smoothing group interpretation helper structure.
|
|
*/
|
|
struct FFanFace
|
|
{
|
|
int32 FaceIndex;
|
|
int32 LinkedVertexIndex;
|
|
bool bFilled;
|
|
bool bBlendTangents;
|
|
bool bBlendNormals;
|
|
};
|
|
|
|
static void ComputeTangents(
|
|
const TArray<FVector>& InVertices,
|
|
const TArray<uint32>& InIndices,
|
|
const TArray<FVector2D>& InUVs,
|
|
const TArray<uint32>& SmoothingGroupIndices,
|
|
TMultiMap<int32, int32> const& OverlappingCorners,
|
|
TArray<FVector>& OutTangentX,
|
|
TArray<FVector>& OutTangentY,
|
|
TArray<FVector>& OutTangentZ,
|
|
const uint32 TangentOptions
|
|
)
|
|
{
|
|
bool bBlendOverlappingNormals = (TangentOptions & ETangentOptions::BlendOverlappingNormals) != 0;
|
|
bool bIgnoreDegenerateTriangles = (TangentOptions & ETangentOptions::IgnoreDegenerateTriangles) != 0;
|
|
float ComparisonThreshold = bIgnoreDegenerateTriangles ? THRESH_POINTS_ARE_SAME : 0.0f;
|
|
|
|
// Compute per-triangle tangents.
|
|
TArray<FVector> TriangleTangentX;
|
|
TArray<FVector> TriangleTangentY;
|
|
TArray<FVector> TriangleTangentZ;
|
|
|
|
ComputeTriangleTangents(
|
|
InVertices,
|
|
InIndices,
|
|
InUVs,
|
|
TriangleTangentX,
|
|
TriangleTangentY,
|
|
TriangleTangentZ,
|
|
bIgnoreDegenerateTriangles ? SMALL_NUMBER : 0.0f
|
|
);
|
|
|
|
// Declare these out here to avoid reallocations.
|
|
TArray<FFanFace> RelevantFacesForCorner[3];
|
|
TArray<int32> AdjacentFaces;
|
|
TArray<int32> DupVerts;
|
|
|
|
int32 NumWedges = InIndices.Num();
|
|
int32 NumFaces = NumWedges / 3;
|
|
|
|
// Allocate storage for tangents if none were provided.
|
|
if (OutTangentX.Num() != NumWedges)
|
|
{
|
|
OutTangentX.Empty(NumWedges);
|
|
OutTangentX.AddZeroed(NumWedges);
|
|
}
|
|
if (OutTangentY.Num() != NumWedges)
|
|
{
|
|
OutTangentY.Empty(NumWedges);
|
|
OutTangentY.AddZeroed(NumWedges);
|
|
}
|
|
if (OutTangentZ.Num() != NumWedges)
|
|
{
|
|
OutTangentZ.Empty(NumWedges);
|
|
OutTangentZ.AddZeroed(NumWedges);
|
|
}
|
|
|
|
for (int32 FaceIndex = 0; FaceIndex < NumFaces; FaceIndex++)
|
|
{
|
|
int32 WedgeOffset = FaceIndex * 3;
|
|
FVector CornerPositions[3];
|
|
FVector CornerTangentX[3];
|
|
FVector CornerTangentY[3];
|
|
FVector CornerTangentZ[3];
|
|
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
CornerTangentX[CornerIndex] = FVector::ZeroVector;
|
|
CornerTangentY[CornerIndex] = FVector::ZeroVector;
|
|
CornerTangentZ[CornerIndex] = FVector::ZeroVector;
|
|
CornerPositions[CornerIndex] = InVertices[InIndices[WedgeOffset + CornerIndex]];
|
|
RelevantFacesForCorner[CornerIndex].Reset();
|
|
}
|
|
|
|
// Don't process degenerate triangles.
|
|
if (PointsEqual(CornerPositions[0], CornerPositions[1], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[0], CornerPositions[2], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[1], CornerPositions[2], ComparisonThreshold))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// No need to process triangles if tangents already exist.
|
|
bool bCornerHasTangents[3] = { 0 };
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
bCornerHasTangents[CornerIndex] = !OutTangentX[WedgeOffset + CornerIndex].IsZero()
|
|
&& !OutTangentY[WedgeOffset + CornerIndex].IsZero()
|
|
&& !OutTangentZ[WedgeOffset + CornerIndex].IsZero();
|
|
}
|
|
if (bCornerHasTangents[0] && bCornerHasTangents[1] && bCornerHasTangents[2])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Calculate smooth vertex normals.
|
|
float Determinant = FVector::Triple(
|
|
TriangleTangentX[FaceIndex],
|
|
TriangleTangentY[FaceIndex],
|
|
TriangleTangentZ[FaceIndex]
|
|
);
|
|
|
|
// Start building a list of faces adjacent to this face.
|
|
AdjacentFaces.Reset();
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
int32 ThisCornerIndex = WedgeOffset + CornerIndex;
|
|
DupVerts.Reset();
|
|
OverlappingCorners.MultiFind(ThisCornerIndex, DupVerts);
|
|
DupVerts.Add(ThisCornerIndex); // I am a "dup" of myself
|
|
for (int32 k = 0; k < DupVerts.Num(); k++)
|
|
{
|
|
AdjacentFaces.AddUnique(DupVerts[k] / 3);
|
|
}
|
|
}
|
|
|
|
// We need to sort these here because the criteria for point equality is
|
|
// exact, so we must ensure the exact same order for all dups.
|
|
AdjacentFaces.Sort();
|
|
|
|
// Process adjacent faces
|
|
for (int32 AdjacentFaceIndex = 0; AdjacentFaceIndex < AdjacentFaces.Num(); AdjacentFaceIndex++)
|
|
{
|
|
int32 OtherFaceIndex = AdjacentFaces[AdjacentFaceIndex];
|
|
for (int32 OurCornerIndex = 0; OurCornerIndex < 3; OurCornerIndex++)
|
|
{
|
|
if (bCornerHasTangents[OurCornerIndex])
|
|
continue;
|
|
|
|
FFanFace NewFanFace;
|
|
int32 CommonIndexCount = 0;
|
|
|
|
// Check for vertices in common.
|
|
if (FaceIndex == OtherFaceIndex)
|
|
{
|
|
CommonIndexCount = 3;
|
|
NewFanFace.LinkedVertexIndex = OurCornerIndex;
|
|
}
|
|
else
|
|
{
|
|
// Check matching vertices against main vertex .
|
|
for (int32 OtherCornerIndex = 0; OtherCornerIndex < 3; OtherCornerIndex++)
|
|
{
|
|
if (PointsEqual(
|
|
CornerPositions[OurCornerIndex],
|
|
InVertices[InIndices[OtherFaceIndex * 3 + OtherCornerIndex]],
|
|
ComparisonThreshold
|
|
))
|
|
{
|
|
CommonIndexCount++;
|
|
NewFanFace.LinkedVertexIndex = OtherCornerIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add if connected by at least one point. Smoothing matches are considered later.
|
|
if (CommonIndexCount > 0)
|
|
{
|
|
NewFanFace.FaceIndex = OtherFaceIndex;
|
|
NewFanFace.bFilled = (OtherFaceIndex == FaceIndex); // Starter face for smoothing floodfill.
|
|
NewFanFace.bBlendTangents = NewFanFace.bFilled;
|
|
NewFanFace.bBlendNormals = NewFanFace.bFilled;
|
|
RelevantFacesForCorner[OurCornerIndex].Add(NewFanFace);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find true relevance of faces for a vertex normal by traversing
|
|
// smoothing-group-compatible connected triangle fans around common vertices.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
if (bCornerHasTangents[CornerIndex])
|
|
continue;
|
|
|
|
int32 NewConnections;
|
|
do
|
|
{
|
|
NewConnections = 0;
|
|
for (int32 OtherFaceIdx = 0; OtherFaceIdx < RelevantFacesForCorner[CornerIndex].Num(); OtherFaceIdx++)
|
|
{
|
|
FFanFace& OtherFace = RelevantFacesForCorner[CornerIndex][OtherFaceIdx];
|
|
// The vertex' own face is initially the only face with bFilled == true.
|
|
if (OtherFace.bFilled)
|
|
{
|
|
for (int32 NextFaceIndex = 0; NextFaceIndex < RelevantFacesForCorner[CornerIndex].Num(); NextFaceIndex++)
|
|
{
|
|
FFanFace& NextFace = RelevantFacesForCorner[CornerIndex][NextFaceIndex];
|
|
if (!NextFace.bFilled) // && !NextFace.bBlendTangents)
|
|
{
|
|
if ((NextFaceIndex != OtherFaceIdx)
|
|
&& (SmoothingGroupIndices[NextFace.FaceIndex] & SmoothingGroupIndices[OtherFace.FaceIndex]))
|
|
{
|
|
int32 CommonVertices = 0;
|
|
int32 CommonTangentVertices = 0;
|
|
int32 CommonNormalVertices = 0;
|
|
for (int32 OtherCornerIndex = 0; OtherCornerIndex < 3; OtherCornerIndex++)
|
|
{
|
|
for (int32 NextCornerIndex = 0; NextCornerIndex < 3; NextCornerIndex++)
|
|
{
|
|
int32 NextVertexIndex = InIndices[NextFace.FaceIndex * 3 + NextCornerIndex];
|
|
int32 OtherVertexIndex = InIndices[OtherFace.FaceIndex * 3 + OtherCornerIndex];
|
|
if (PointsEqual(
|
|
InVertices[NextVertexIndex],
|
|
InVertices[OtherVertexIndex],
|
|
ComparisonThreshold))
|
|
{
|
|
CommonVertices++;
|
|
|
|
|
|
const FVector2D& UVOne = InUVs[NextFace.FaceIndex * 3 + NextCornerIndex];
|
|
const FVector2D& UVTwo = InUVs[OtherFace.FaceIndex * 3 + OtherCornerIndex];
|
|
|
|
if (UVsEqual(UVOne, UVTwo))
|
|
{
|
|
CommonTangentVertices++;
|
|
}
|
|
if (bBlendOverlappingNormals
|
|
|| NextVertexIndex == OtherVertexIndex)
|
|
{
|
|
CommonNormalVertices++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Flood fill faces with more than one common vertices which must be touching edges.
|
|
if (CommonVertices > 1)
|
|
{
|
|
NextFace.bFilled = true;
|
|
NextFace.bBlendNormals = (CommonNormalVertices > 1);
|
|
NewConnections++;
|
|
|
|
// Only blend tangents if there is no UV seam along the edge with this face.
|
|
if (OtherFace.bBlendTangents && CommonTangentVertices > 1)
|
|
{
|
|
float OtherDeterminant = FVector::Triple(
|
|
TriangleTangentX[NextFace.FaceIndex],
|
|
TriangleTangentY[NextFace.FaceIndex],
|
|
TriangleTangentZ[NextFace.FaceIndex]
|
|
);
|
|
if ((Determinant * OtherDeterminant) > 0.0f)
|
|
{
|
|
NextFace.bBlendTangents = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} while (NewConnections > 0);
|
|
}
|
|
|
|
// Vertex normal construction.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
if (bCornerHasTangents[CornerIndex])
|
|
{
|
|
CornerTangentX[CornerIndex] = OutTangentX[WedgeOffset + CornerIndex];
|
|
CornerTangentY[CornerIndex] = OutTangentY[WedgeOffset + CornerIndex];
|
|
CornerTangentZ[CornerIndex] = OutTangentZ[WedgeOffset + CornerIndex];
|
|
}
|
|
else
|
|
{
|
|
for (int32 RelevantFaceIdx = 0; RelevantFaceIdx < RelevantFacesForCorner[CornerIndex].Num(); RelevantFaceIdx++)
|
|
{
|
|
FFanFace const& RelevantFace = RelevantFacesForCorner[CornerIndex][RelevantFaceIdx];
|
|
if (RelevantFace.bFilled)
|
|
{
|
|
int32 OtherFaceIndex = RelevantFace.FaceIndex;
|
|
if (RelevantFace.bBlendTangents)
|
|
{
|
|
CornerTangentX[CornerIndex] += TriangleTangentX[OtherFaceIndex];
|
|
CornerTangentY[CornerIndex] += TriangleTangentY[OtherFaceIndex];
|
|
}
|
|
if (RelevantFace.bBlendNormals)
|
|
{
|
|
CornerTangentZ[CornerIndex] += TriangleTangentZ[OtherFaceIndex];
|
|
}
|
|
}
|
|
}
|
|
if (!OutTangentX[WedgeOffset + CornerIndex].IsZero())
|
|
{
|
|
CornerTangentX[CornerIndex] = OutTangentX[WedgeOffset + CornerIndex];
|
|
}
|
|
if (!OutTangentY[WedgeOffset + CornerIndex].IsZero())
|
|
{
|
|
CornerTangentY[CornerIndex] = OutTangentY[WedgeOffset + CornerIndex];
|
|
}
|
|
if (!OutTangentZ[WedgeOffset + CornerIndex].IsZero())
|
|
{
|
|
CornerTangentZ[CornerIndex] = OutTangentZ[WedgeOffset + CornerIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalization.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
CornerTangentX[CornerIndex].Normalize();
|
|
CornerTangentY[CornerIndex].Normalize();
|
|
CornerTangentZ[CornerIndex].Normalize();
|
|
|
|
// Gram-Schmidt orthogonalization
|
|
CornerTangentY[CornerIndex] -= CornerTangentX[CornerIndex] * (CornerTangentX[CornerIndex] | CornerTangentY[CornerIndex]);
|
|
CornerTangentY[CornerIndex].Normalize();
|
|
|
|
CornerTangentX[CornerIndex] -= CornerTangentZ[CornerIndex] * (CornerTangentZ[CornerIndex] | CornerTangentX[CornerIndex]);
|
|
CornerTangentX[CornerIndex].Normalize();
|
|
CornerTangentY[CornerIndex] -= CornerTangentZ[CornerIndex] * (CornerTangentZ[CornerIndex] | CornerTangentY[CornerIndex]);
|
|
CornerTangentY[CornerIndex].Normalize();
|
|
}
|
|
|
|
// Copy back to the mesh.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
OutTangentX[WedgeOffset + CornerIndex] = CornerTangentX[CornerIndex];
|
|
OutTangentY[WedgeOffset + CornerIndex] = CornerTangentY[CornerIndex];
|
|
OutTangentZ[WedgeOffset + CornerIndex] = CornerTangentZ[CornerIndex];
|
|
}
|
|
}
|
|
|
|
check(OutTangentX.Num() == NumWedges);
|
|
check(OutTangentY.Num() == NumWedges);
|
|
check(OutTangentZ.Num() == NumWedges);
|
|
}
|
|
|
|
|
|
static void ComputeTangents(
|
|
FRawMesh& RawMesh,
|
|
TMultiMap<int32, int32> const& OverlappingCorners,
|
|
uint32 TangentOptions
|
|
)
|
|
{
|
|
const float ComparisonThreshold = (TangentOptions & ETangentOptions::IgnoreDegenerateTriangles) ? THRESH_POINTS_ARE_SAME : 0.0f;
|
|
ComputeTangents(RawMesh.VertexPositions, RawMesh.WedgeIndices, RawMesh.WedgeTexCoords[0], RawMesh.FaceSmoothingMasks, OverlappingCorners, RawMesh.WedgeTangentX, RawMesh.WedgeTangentY, RawMesh.WedgeTangentZ, TangentOptions);
|
|
}
|
|
|
|
/*------------------------------------------------------------------------------
|
|
MikkTSpace for computing tangents.
|
|
------------------------------------------------------------------------------*/
|
|
|
|
static int MikkGetNumFaces(const SMikkTSpaceContext* Context)
|
|
{
|
|
FRawMesh *UserData = (FRawMesh*)(Context->m_pUserData);
|
|
return UserData->WedgeIndices.Num() / 3;
|
|
}
|
|
|
|
static int MikkGetNumVertsOfFace(const SMikkTSpaceContext* Context, const int FaceIdx)
|
|
{
|
|
// All of our meshes are triangles.
|
|
return 3;
|
|
}
|
|
|
|
static void MikkGetPosition(const SMikkTSpaceContext* Context, float Position[3], const int FaceIdx, const int VertIdx)
|
|
{
|
|
FRawMesh *UserData = (FRawMesh*)(Context->m_pUserData);
|
|
FVector VertexPosition = UserData->GetWedgePosition(FaceIdx * 3 + VertIdx);
|
|
Position[0] = VertexPosition.X;
|
|
Position[1] = VertexPosition.Y;
|
|
Position[2] = VertexPosition.Z;
|
|
}
|
|
|
|
static void MikkGetNormal(const SMikkTSpaceContext* Context, float Normal[3], const int FaceIdx, const int VertIdx)
|
|
{
|
|
FRawMesh *UserData = (FRawMesh*)(Context->m_pUserData);
|
|
FVector &VertexNormal = UserData->WedgeTangentZ[FaceIdx * 3 + VertIdx];
|
|
for (int32 i = 0; i < 3; ++i)
|
|
{
|
|
Normal[i] = VertexNormal[i];
|
|
}
|
|
}
|
|
|
|
static void MikkSetTSpaceBasic(const SMikkTSpaceContext* Context, const float Tangent[3], const float BitangentSign, const int FaceIdx, const int VertIdx)
|
|
{
|
|
FRawMesh *UserData = (FRawMesh*)(Context->m_pUserData);
|
|
FVector &VertexTangent = UserData->WedgeTangentX[FaceIdx * 3 + VertIdx];
|
|
for (int32 i = 0; i < 3; ++i)
|
|
{
|
|
VertexTangent[i] = Tangent[i];
|
|
}
|
|
FVector Bitangent = BitangentSign * FVector::CrossProduct(UserData->WedgeTangentZ[FaceIdx * 3 + VertIdx], VertexTangent);
|
|
FVector &VertexBitangent = UserData->WedgeTangentY[FaceIdx * 3 + VertIdx];
|
|
for (int32 i = 0; i < 3; ++i)
|
|
{
|
|
VertexBitangent[i] = -Bitangent[i];
|
|
}
|
|
}
|
|
|
|
static void MikkGetTexCoord(const SMikkTSpaceContext* Context, float UV[2], const int FaceIdx, const int VertIdx)
|
|
{
|
|
FRawMesh *UserData = (FRawMesh*)(Context->m_pUserData);
|
|
FVector2D &TexCoord = UserData->WedgeTexCoords[0][FaceIdx * 3 + VertIdx];
|
|
UV[0] = TexCoord.X;
|
|
UV[1] = TexCoord.Y;
|
|
}
|
|
|
|
// MikkTSpace implementations for skeletal meshes, where tangents/bitangents are ultimately derived from lists of attributes.
|
|
|
|
// Holder for skeletal data to be passed to MikkTSpace.
|
|
// Holds references to the wedge, face and points vectors that BuildSkeletalMesh is given.
|
|
// Holds reference to the calculated normals array, which will be fleshed out if they've been calculated.
|
|
// Holds reference to the newly created tangent and bitangent arrays, which MikkTSpace will fleshed out if required.
|
|
class MikkTSpace_Skeletal_Mesh
|
|
{
|
|
public:
|
|
const TArray<FMeshWedge> &wedges; //Reference to wedge list.
|
|
const TArray<FMeshFace> &faces; //Reference to face list. Also contains normal/tangent/bitanget/UV coords for each vertex of the face.
|
|
const TArray<FVector> &points; //Reference to position list.
|
|
bool bComputeNormals; //Copy of bComputeNormals.
|
|
TArray<FVector> &TangentsX; //Reference to newly created tangents list.
|
|
TArray<FVector> &TangentsY; //Reference to newly created bitangents list.
|
|
TArray<FVector> &TangentsZ; //Reference to computed normals, will be empty otherwise.
|
|
|
|
MikkTSpace_Skeletal_Mesh(
|
|
const TArray<FMeshWedge> &Wedges,
|
|
const TArray<FMeshFace> &Faces,
|
|
const TArray<FVector> &Points,
|
|
bool bInComputeNormals,
|
|
TArray<FVector> &VertexTangentsX,
|
|
TArray<FVector> &VertexTangentsY,
|
|
TArray<FVector> &VertexTangentsZ
|
|
)
|
|
:
|
|
wedges(Wedges),
|
|
faces(Faces),
|
|
points(Points),
|
|
bComputeNormals(bInComputeNormals),
|
|
TangentsX(VertexTangentsX),
|
|
TangentsY(VertexTangentsY),
|
|
TangentsZ(VertexTangentsZ)
|
|
{
|
|
}
|
|
};
|
|
|
|
static int MikkGetNumFaces_Skeletal(const SMikkTSpaceContext* Context)
|
|
{
|
|
MikkTSpace_Skeletal_Mesh *UserData = (MikkTSpace_Skeletal_Mesh*)(Context->m_pUserData);
|
|
return UserData->faces.Num();
|
|
}
|
|
|
|
static int MikkGetNumVertsOfFace_Skeletal(const SMikkTSpaceContext* Context, const int FaceIdx)
|
|
{
|
|
// Confirmed?
|
|
return 3;
|
|
}
|
|
|
|
static void MikkGetPosition_Skeletal(const SMikkTSpaceContext* Context, float Position[3], const int FaceIdx, const int VertIdx)
|
|
{
|
|
MikkTSpace_Skeletal_Mesh *UserData = (MikkTSpace_Skeletal_Mesh*)(Context->m_pUserData);
|
|
const FVector &VertexPosition = UserData->points[UserData->wedges[UserData->faces[FaceIdx].iWedge[VertIdx]].iVertex];
|
|
Position[0] = VertexPosition.X;
|
|
Position[1] = VertexPosition.Y;
|
|
Position[2] = VertexPosition.Z;
|
|
}
|
|
|
|
static void MikkGetNormal_Skeletal(const SMikkTSpaceContext* Context, float Normal[3], const int FaceIdx, const int VertIdx)
|
|
{
|
|
MikkTSpace_Skeletal_Mesh *UserData = (MikkTSpace_Skeletal_Mesh*)(Context->m_pUserData);
|
|
// Get different normals depending on whether they've been calculated or not.
|
|
if (UserData->bComputeNormals) {
|
|
FVector &VertexNormal = UserData->TangentsZ[FaceIdx * 3 + VertIdx];
|
|
Normal[0] = VertexNormal.X;
|
|
Normal[1] = VertexNormal.Y;
|
|
Normal[2] = VertexNormal.Z;
|
|
}
|
|
else
|
|
{
|
|
const FVector &VertexNormal = UserData->faces[FaceIdx].TangentZ[VertIdx];
|
|
Normal[0] = VertexNormal.X;
|
|
Normal[1] = VertexNormal.Y;
|
|
Normal[2] = VertexNormal.Z;
|
|
}
|
|
}
|
|
|
|
static void MikkSetTSpaceBasic_Skeletal(const SMikkTSpaceContext* Context, const float Tangent[3], const float BitangentSign, const int FaceIdx, const int VertIdx)
|
|
{
|
|
MikkTSpace_Skeletal_Mesh *UserData = (MikkTSpace_Skeletal_Mesh*)(Context->m_pUserData);
|
|
FVector &VertexTangent = UserData->TangentsX[FaceIdx * 3 + VertIdx];
|
|
VertexTangent.X = Tangent[0];
|
|
VertexTangent.Y = Tangent[1];
|
|
VertexTangent.Z = Tangent[2];
|
|
|
|
FVector Bitangent;
|
|
// Get different normals depending on whether they've been calculated or not.
|
|
if (UserData->bComputeNormals) {
|
|
Bitangent = BitangentSign * FVector::CrossProduct(UserData->TangentsZ[FaceIdx * 3 + VertIdx], VertexTangent);
|
|
}
|
|
else
|
|
{
|
|
Bitangent = BitangentSign * FVector::CrossProduct(UserData->faces[FaceIdx].TangentZ[VertIdx], VertexTangent);
|
|
}
|
|
FVector &VertexBitangent = UserData->TangentsY[FaceIdx * 3 + VertIdx];
|
|
// Switch the tangent space swizzle to X+Y-Z+ for legacy reasons.
|
|
VertexBitangent.X = -Bitangent[0];
|
|
VertexBitangent.Y = -Bitangent[1];
|
|
VertexBitangent.Z = -Bitangent[2];
|
|
}
|
|
|
|
static void MikkGetTexCoord_Skeletal(const SMikkTSpaceContext* Context, float UV[2], const int FaceIdx, const int VertIdx)
|
|
{
|
|
MikkTSpace_Skeletal_Mesh *UserData = (MikkTSpace_Skeletal_Mesh*)(Context->m_pUserData);
|
|
const FVector2D &TexCoord = UserData->wedges[UserData->faces[FaceIdx].iWedge[VertIdx]].UVs[0];
|
|
UV[0] = TexCoord.X;
|
|
UV[1] = TexCoord.Y;
|
|
}
|
|
|
|
static void ComputeTangents_MikkTSpace(
|
|
FRawMesh& RawMesh,
|
|
TMultiMap<int32, int32> const& OverlappingCorners,
|
|
uint32 TangentOptions
|
|
)
|
|
{
|
|
bool bBlendOverlappingNormals = (TangentOptions & ETangentOptions::BlendOverlappingNormals) != 0;
|
|
bool bIgnoreDegenerateTriangles = (TangentOptions & ETangentOptions::IgnoreDegenerateTriangles) != 0;
|
|
float ComparisonThreshold = bIgnoreDegenerateTriangles ? THRESH_POINTS_ARE_SAME : 0.0f;
|
|
|
|
// Compute per-triangle tangents.
|
|
TArray<FVector> TriangleTangentX;
|
|
TArray<FVector> TriangleTangentY;
|
|
TArray<FVector> TriangleTangentZ;
|
|
|
|
ComputeTriangleTangents(
|
|
TriangleTangentX,
|
|
TriangleTangentY,
|
|
TriangleTangentZ,
|
|
RawMesh,
|
|
bIgnoreDegenerateTriangles ? SMALL_NUMBER : 0.0f
|
|
);
|
|
|
|
// Declare these out here to avoid reallocations.
|
|
TArray<FFanFace> RelevantFacesForCorner[3];
|
|
TArray<int32> AdjacentFaces;
|
|
TArray<int32> DupVerts;
|
|
|
|
int32 NumWedges = RawMesh.WedgeIndices.Num();
|
|
int32 NumFaces = NumWedges / 3;
|
|
|
|
bool bWedgeNormals = true;
|
|
bool bWedgeTSpace = false;
|
|
for (int32 WedgeIdx = 0; WedgeIdx < RawMesh.WedgeTangentZ.Num(); ++WedgeIdx)
|
|
{
|
|
bWedgeNormals = bWedgeNormals && (!RawMesh.WedgeTangentZ[WedgeIdx].IsNearlyZero());
|
|
}
|
|
|
|
if (RawMesh.WedgeTangentX.Num() > 0 && RawMesh.WedgeTangentY.Num() > 0)
|
|
{
|
|
bWedgeTSpace = true;
|
|
for (int32 WedgeIdx = 0; WedgeIdx < RawMesh.WedgeTangentX.Num()
|
|
&& WedgeIdx < RawMesh.WedgeTangentY.Num(); ++WedgeIdx)
|
|
{
|
|
bWedgeTSpace = bWedgeTSpace && (!RawMesh.WedgeTangentX[WedgeIdx].IsNearlyZero()) && (!RawMesh.WedgeTangentY[WedgeIdx].IsNearlyZero());
|
|
}
|
|
}
|
|
|
|
// Allocate storage for tangents if none were provided, and calculate normals for MikkTSpace.
|
|
if (RawMesh.WedgeTangentZ.Num() != NumWedges || !bWedgeNormals)
|
|
{
|
|
// normals are not included, so we should calculate them
|
|
RawMesh.WedgeTangentZ.Empty(NumWedges);
|
|
RawMesh.WedgeTangentZ.AddZeroed(NumWedges);
|
|
|
|
// we need to calculate normals for MikkTSpace
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Invalid vertex normals found for mesh. Forcing recomputation of vertex normals for MikkTSpace. Fix mesh or disable \"Use MikkTSpace Tangent Space\" to avoid forced recomputation of normals."));
|
|
|
|
for (int32 FaceIndex = 0; FaceIndex < NumFaces; FaceIndex++)
|
|
{
|
|
int32 WedgeOffset = FaceIndex * 3;
|
|
FVector CornerPositions[3];
|
|
FVector CornerNormal[3];
|
|
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
CornerNormal[CornerIndex] = FVector::ZeroVector;
|
|
CornerPositions[CornerIndex] = GetPositionForWedge(RawMesh, WedgeOffset + CornerIndex);
|
|
RelevantFacesForCorner[CornerIndex].Reset();
|
|
}
|
|
|
|
// Don't process degenerate triangles.
|
|
if (PointsEqual(CornerPositions[0], CornerPositions[1], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[0], CornerPositions[2], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[1], CornerPositions[2], ComparisonThreshold))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// No need to process triangles if tangents already exist.
|
|
bool bCornerHasNormal[3] = { 0 };
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
bCornerHasNormal[CornerIndex] = !RawMesh.WedgeTangentZ[WedgeOffset + CornerIndex].IsZero();
|
|
}
|
|
if (bCornerHasNormal[0] && bCornerHasNormal[1] && bCornerHasNormal[2])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Start building a list of faces adjacent to this face.
|
|
AdjacentFaces.Reset();
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
int32 ThisCornerIndex = WedgeOffset + CornerIndex;
|
|
DupVerts.Reset();
|
|
OverlappingCorners.MultiFind(ThisCornerIndex, DupVerts);
|
|
DupVerts.Add(ThisCornerIndex); // I am a "dup" of myself
|
|
for (int32 k = 0; k < DupVerts.Num(); k++)
|
|
{
|
|
AdjacentFaces.AddUnique(DupVerts[k] / 3);
|
|
}
|
|
}
|
|
|
|
// We need to sort these here because the criteria for point equality is
|
|
// exact, so we must ensure the exact same order for all dups.
|
|
AdjacentFaces.Sort();
|
|
|
|
// Process adjacent faces
|
|
for (int32 AdjacentFaceIndex = 0; AdjacentFaceIndex < AdjacentFaces.Num(); AdjacentFaceIndex++)
|
|
{
|
|
int32 OtherFaceIndex = AdjacentFaces[AdjacentFaceIndex];
|
|
for (int32 OurCornerIndex = 0; OurCornerIndex < 3; OurCornerIndex++)
|
|
{
|
|
if (bCornerHasNormal[OurCornerIndex])
|
|
continue;
|
|
|
|
FFanFace NewFanFace;
|
|
int32 CommonIndexCount = 0;
|
|
|
|
// Check for vertices in common.
|
|
if (FaceIndex == OtherFaceIndex)
|
|
{
|
|
CommonIndexCount = 3;
|
|
NewFanFace.LinkedVertexIndex = OurCornerIndex;
|
|
}
|
|
else
|
|
{
|
|
// Check matching vertices against main vertex .
|
|
for (int32 OtherCornerIndex = 0; OtherCornerIndex < 3; OtherCornerIndex++)
|
|
{
|
|
if (PointsEqual(
|
|
CornerPositions[OurCornerIndex],
|
|
GetPositionForWedge(RawMesh, OtherFaceIndex * 3 + OtherCornerIndex),
|
|
ComparisonThreshold
|
|
))
|
|
{
|
|
CommonIndexCount++;
|
|
NewFanFace.LinkedVertexIndex = OtherCornerIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add if connected by at least one point. Smoothing matches are considered later.
|
|
if (CommonIndexCount > 0)
|
|
{
|
|
NewFanFace.FaceIndex = OtherFaceIndex;
|
|
NewFanFace.bFilled = (OtherFaceIndex == FaceIndex); // Starter face for smoothing floodfill.
|
|
NewFanFace.bBlendTangents = NewFanFace.bFilled;
|
|
NewFanFace.bBlendNormals = NewFanFace.bFilled;
|
|
RelevantFacesForCorner[OurCornerIndex].Add(NewFanFace);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find true relevance of faces for a vertex normal by traversing
|
|
// smoothing-group-compatible connected triangle fans around common vertices.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
if (bCornerHasNormal[CornerIndex])
|
|
continue;
|
|
|
|
int32 NewConnections;
|
|
do
|
|
{
|
|
NewConnections = 0;
|
|
for (int32 OtherFaceIdx = 0; OtherFaceIdx < RelevantFacesForCorner[CornerIndex].Num(); OtherFaceIdx++)
|
|
{
|
|
FFanFace& OtherFace = RelevantFacesForCorner[CornerIndex][OtherFaceIdx];
|
|
// The vertex' own face is initially the only face with bFilled == true.
|
|
if (OtherFace.bFilled)
|
|
{
|
|
for (int32 NextFaceIndex = 0; NextFaceIndex < RelevantFacesForCorner[CornerIndex].Num(); NextFaceIndex++)
|
|
{
|
|
FFanFace& NextFace = RelevantFacesForCorner[CornerIndex][NextFaceIndex];
|
|
if (!NextFace.bFilled) // && !NextFace.bBlendTangents)
|
|
{
|
|
if ((NextFaceIndex != OtherFaceIdx)
|
|
&& (RawMesh.FaceSmoothingMasks[NextFace.FaceIndex] & RawMesh.FaceSmoothingMasks[OtherFace.FaceIndex]))
|
|
{
|
|
int32 CommonVertices = 0;
|
|
int32 CommonNormalVertices = 0;
|
|
for (int32 OtherCornerIndex = 0; OtherCornerIndex < 3; OtherCornerIndex++)
|
|
{
|
|
for (int32 NextCornerIndex = 0; NextCornerIndex < 3; NextCornerIndex++)
|
|
{
|
|
int32 NextVertexIndex = RawMesh.WedgeIndices[NextFace.FaceIndex * 3 + NextCornerIndex];
|
|
int32 OtherVertexIndex = RawMesh.WedgeIndices[OtherFace.FaceIndex * 3 + OtherCornerIndex];
|
|
if (PointsEqual(
|
|
RawMesh.VertexPositions[NextVertexIndex],
|
|
RawMesh.VertexPositions[OtherVertexIndex],
|
|
ComparisonThreshold))
|
|
{
|
|
CommonVertices++;
|
|
if (bBlendOverlappingNormals
|
|
|| NextVertexIndex == OtherVertexIndex)
|
|
{
|
|
CommonNormalVertices++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Flood fill faces with more than one common vertices which must be touching edges.
|
|
if (CommonVertices > 1)
|
|
{
|
|
NextFace.bFilled = true;
|
|
NextFace.bBlendNormals = (CommonNormalVertices > 1);
|
|
NewConnections++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
while (NewConnections > 0);
|
|
}
|
|
|
|
|
|
// Vertex normal construction.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
if (bCornerHasNormal[CornerIndex])
|
|
{
|
|
CornerNormal[CornerIndex] = RawMesh.WedgeTangentZ[WedgeOffset + CornerIndex];
|
|
}
|
|
else
|
|
{
|
|
for (int32 RelevantFaceIdx = 0; RelevantFaceIdx < RelevantFacesForCorner[CornerIndex].Num(); RelevantFaceIdx++)
|
|
{
|
|
FFanFace const& RelevantFace = RelevantFacesForCorner[CornerIndex][RelevantFaceIdx];
|
|
if (RelevantFace.bFilled)
|
|
{
|
|
int32 OtherFaceIndex = RelevantFace.FaceIndex;
|
|
if (RelevantFace.bBlendNormals)
|
|
{
|
|
CornerNormal[CornerIndex] += TriangleTangentZ[OtherFaceIndex];
|
|
}
|
|
}
|
|
}
|
|
if (!RawMesh.WedgeTangentZ[WedgeOffset + CornerIndex].IsZero())
|
|
{
|
|
CornerNormal[CornerIndex] = RawMesh.WedgeTangentZ[WedgeOffset + CornerIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalization.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
CornerNormal[CornerIndex].Normalize();
|
|
}
|
|
|
|
// Copy back to the mesh.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
RawMesh.WedgeTangentZ[WedgeOffset + CornerIndex] = CornerNormal[CornerIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (RawMesh.WedgeTangentX.Num() != NumWedges)
|
|
{
|
|
RawMesh.WedgeTangentX.Empty(NumWedges);
|
|
RawMesh.WedgeTangentX.AddZeroed(NumWedges);
|
|
}
|
|
if (RawMesh.WedgeTangentY.Num() != NumWedges)
|
|
{
|
|
RawMesh.WedgeTangentY.Empty(NumWedges);
|
|
RawMesh.WedgeTangentY.AddZeroed(NumWedges);
|
|
}
|
|
|
|
if (!bWedgeTSpace)
|
|
{
|
|
// we can use mikktspace to calculate the tangents
|
|
SMikkTSpaceInterface MikkTInterface;
|
|
MikkTInterface.m_getNormal = MikkGetNormal;
|
|
MikkTInterface.m_getNumFaces = MikkGetNumFaces;
|
|
MikkTInterface.m_getNumVerticesOfFace = MikkGetNumVertsOfFace;
|
|
MikkTInterface.m_getPosition = MikkGetPosition;
|
|
MikkTInterface.m_getTexCoord = MikkGetTexCoord;
|
|
MikkTInterface.m_setTSpaceBasic = MikkSetTSpaceBasic;
|
|
MikkTInterface.m_setTSpace = nullptr;
|
|
|
|
SMikkTSpaceContext MikkTContext;
|
|
MikkTContext.m_pInterface = &MikkTInterface;
|
|
MikkTContext.m_pUserData = (void*)(&RawMesh);
|
|
MikkTContext.m_bIgnoreDegenerates = bIgnoreDegenerateTriangles;
|
|
genTangSpaceDefault(&MikkTContext);
|
|
}
|
|
|
|
check(RawMesh.WedgeTangentX.Num() == NumWedges);
|
|
check(RawMesh.WedgeTangentY.Num() == NumWedges);
|
|
check(RawMesh.WedgeTangentZ.Num() == NumWedges);
|
|
}
|
|
|
|
static void BuildDepthOnlyIndexBuffer(
|
|
TArray<uint32>& OutDepthIndices,
|
|
const TArray<FStaticMeshBuildVertex>& InVertices,
|
|
const TArray<uint32>& InIndices,
|
|
const TArray<FStaticMeshSection>& InSections
|
|
)
|
|
{
|
|
int32 NumVertices = InVertices.Num();
|
|
if (InIndices.Num() <= 0 || NumVertices <= 0)
|
|
{
|
|
OutDepthIndices.Empty();
|
|
return;
|
|
}
|
|
|
|
// Create a mapping of index -> first overlapping index to accelerate the construction of the shadow index buffer.
|
|
TArray<FIndexAndZ> VertIndexAndZ;
|
|
VertIndexAndZ.Empty(NumVertices);
|
|
for (int32 VertIndex = 0; VertIndex < NumVertices; VertIndex++)
|
|
{
|
|
new(VertIndexAndZ)FIndexAndZ(VertIndex, InVertices[VertIndex].Position);
|
|
}
|
|
VertIndexAndZ.Sort(FCompareIndexAndZ());
|
|
|
|
// Setup the index map. 0xFFFFFFFF == not set.
|
|
TArray<uint32> IndexMap;
|
|
IndexMap.AddUninitialized(NumVertices);
|
|
FMemory::Memset(IndexMap.GetData(), 0xFF, NumVertices * sizeof(uint32));
|
|
|
|
// Search for duplicates, quickly!
|
|
for (int32 i = 0; i < VertIndexAndZ.Num(); i++)
|
|
{
|
|
uint32 SrcIndex = VertIndexAndZ[i].Index;
|
|
float Z = VertIndexAndZ[i].Z;
|
|
IndexMap[SrcIndex] = FMath::Min(IndexMap[SrcIndex], SrcIndex);
|
|
|
|
// Search forward since we add pairs both ways.
|
|
for (int32 j = i + 1; j < VertIndexAndZ.Num(); j++)
|
|
{
|
|
if (FMath::Abs(VertIndexAndZ[j].Z - Z) > THRESH_POINTS_ARE_SAME * 4.01f)
|
|
break; // can't be any more dups
|
|
|
|
uint32 OtherIndex = VertIndexAndZ[j].Index;
|
|
if (PointsEqual(InVertices[SrcIndex].Position, InVertices[OtherIndex].Position,/*bUseEpsilonCompare=*/ true))
|
|
{
|
|
IndexMap[SrcIndex] = FMath::Min(IndexMap[SrcIndex], OtherIndex);
|
|
IndexMap[OtherIndex] = FMath::Min(IndexMap[OtherIndex], SrcIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build the depth-only index buffer by remapping all indices to the first overlapping
|
|
// vertex in the vertex buffer.
|
|
OutDepthIndices.Empty();
|
|
for (int32 SectionIndex = 0; SectionIndex < InSections.Num(); ++SectionIndex)
|
|
{
|
|
const FStaticMeshSection& Section = InSections[SectionIndex];
|
|
int32 FirstIndex = Section.FirstIndex;
|
|
int32 LastIndex = FirstIndex + Section.NumTriangles * 3;
|
|
for (int32 SrcIndex = FirstIndex; SrcIndex < LastIndex; ++SrcIndex)
|
|
{
|
|
uint32 VertIndex = InIndices[SrcIndex];
|
|
OutDepthIndices.Add(IndexMap[VertIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
static float GetComparisonThreshold(FMeshBuildSettings const& BuildSettings)
|
|
{
|
|
return BuildSettings.bRemoveDegenerates ? THRESH_POINTS_ARE_SAME : 0.0f;
|
|
}
|
|
|
|
/*------------------------------------------------------------------------------
|
|
Static mesh building.
|
|
------------------------------------------------------------------------------*/
|
|
|
|
static FStaticMeshBuildVertex BuildStaticMeshVertex(FRawMesh const& RawMesh, int32 WedgeIndex, FVector BuildScale)
|
|
{
|
|
FStaticMeshBuildVertex Vertex;
|
|
Vertex.Position = GetPositionForWedge(RawMesh, WedgeIndex) * BuildScale;
|
|
|
|
const FMatrix ScaleMatrix = FScaleMatrix(BuildScale).Inverse().GetTransposed();
|
|
Vertex.TangentX = ScaleMatrix.TransformVector(RawMesh.WedgeTangentX[WedgeIndex]).GetSafeNormal();
|
|
Vertex.TangentY = ScaleMatrix.TransformVector(RawMesh.WedgeTangentY[WedgeIndex]).GetSafeNormal();
|
|
Vertex.TangentZ = ScaleMatrix.TransformVector(RawMesh.WedgeTangentZ[WedgeIndex]).GetSafeNormal();
|
|
|
|
if (RawMesh.WedgeColors.IsValidIndex(WedgeIndex))
|
|
{
|
|
Vertex.Color = RawMesh.WedgeColors[WedgeIndex];
|
|
}
|
|
else
|
|
{
|
|
Vertex.Color = FColor::White;
|
|
}
|
|
|
|
int32 NumTexCoords = FMath::Min<int32>(MAX_MESH_TEXTURE_COORDS, MAX_STATIC_TEXCOORDS);
|
|
for (int32 i = 0; i < NumTexCoords; ++i)
|
|
{
|
|
if (RawMesh.WedgeTexCoords[i].IsValidIndex(WedgeIndex))
|
|
{
|
|
Vertex.UVs[i] = RawMesh.WedgeTexCoords[i][WedgeIndex];
|
|
}
|
|
else
|
|
{
|
|
Vertex.UVs[i] = FVector2D(0.0f, 0.0f);
|
|
}
|
|
}
|
|
return Vertex;
|
|
}
|
|
|
|
static bool AreVerticesEqual(
|
|
FStaticMeshBuildVertex const& A,
|
|
FStaticMeshBuildVertex const& B,
|
|
float ComparisonThreshold
|
|
)
|
|
{
|
|
if (!PointsEqual(A.Position, B.Position, ComparisonThreshold)
|
|
|| !NormalsEqual(A.TangentX, B.TangentX)
|
|
|| !NormalsEqual(A.TangentY, B.TangentY)
|
|
|| !NormalsEqual(A.TangentZ, B.TangentZ)
|
|
|| A.Color != B.Color)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// UVs
|
|
for (int32 UVIndex = 0; UVIndex < MAX_STATIC_TEXCOORDS; UVIndex++)
|
|
{
|
|
if (!UVsEqual(A.UVs[UVIndex], B.UVs[UVIndex]))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FMeshUtilities::BuildStaticMeshVertexAndIndexBuffers(
|
|
TArray<FStaticMeshBuildVertex>& OutVertices,
|
|
TArray<TArray<uint32> >& OutPerSectionIndices,
|
|
TArray<int32>& OutWedgeMap,
|
|
const FRawMesh& RawMesh,
|
|
const TMultiMap<int32, int32>& OverlappingCorners,
|
|
const TMap<uint32, uint32>& MaterialToSectionMapping,
|
|
float ComparisonThreshold,
|
|
FVector BuildScale,
|
|
int32 ImportVersion
|
|
)
|
|
{
|
|
TMap<int32, int32> FinalVerts;
|
|
TArray<int32> DupVerts;
|
|
int32 NumFaces = RawMesh.WedgeIndices.Num() / 3;
|
|
|
|
// Process each face, build vertex buffer and per-section index buffers.
|
|
for (int32 FaceIndex = 0; FaceIndex < NumFaces; FaceIndex++)
|
|
{
|
|
int32 VertexIndices[3];
|
|
FVector CornerPositions[3];
|
|
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
CornerPositions[CornerIndex] = GetPositionForWedge(RawMesh, FaceIndex * 3 + CornerIndex);
|
|
}
|
|
|
|
// Don't process degenerate triangles.
|
|
if (PointsEqual(CornerPositions[0], CornerPositions[1], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[0], CornerPositions[2], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[1], CornerPositions[2], ComparisonThreshold))
|
|
{
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
OutWedgeMap.Add(INDEX_NONE);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
int32 WedgeIndex = FaceIndex * 3 + CornerIndex;
|
|
FStaticMeshBuildVertex ThisVertex = BuildStaticMeshVertex(RawMesh, WedgeIndex, BuildScale);
|
|
|
|
DupVerts.Reset();
|
|
OverlappingCorners.MultiFind(WedgeIndex, DupVerts);
|
|
DupVerts.Sort();
|
|
|
|
int32 Index = INDEX_NONE;
|
|
for (int32 k = 0; k < DupVerts.Num(); k++)
|
|
{
|
|
if (DupVerts[k] >= WedgeIndex)
|
|
{
|
|
// the verts beyond me haven't been placed yet, so these duplicates are not relevant
|
|
break;
|
|
}
|
|
|
|
int32 *Location = FinalVerts.Find(DupVerts[k]);
|
|
if (Location != NULL
|
|
&& AreVerticesEqual(ThisVertex, OutVertices[*Location], ComparisonThreshold))
|
|
{
|
|
Index = *Location;
|
|
break;
|
|
}
|
|
}
|
|
if (Index == INDEX_NONE)
|
|
{
|
|
Index = OutVertices.Add(ThisVertex);
|
|
FinalVerts.Add(WedgeIndex, Index);
|
|
}
|
|
VertexIndices[CornerIndex] = Index;
|
|
}
|
|
|
|
// Reject degenerate triangles.
|
|
if (VertexIndices[0] == VertexIndices[1]
|
|
|| VertexIndices[1] == VertexIndices[2]
|
|
|| VertexIndices[0] == VertexIndices[2])
|
|
{
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
OutWedgeMap.Add(INDEX_NONE);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Put the indices in the material index buffer.
|
|
uint32 SectionIndex = 0;
|
|
if (ImportVersion < RemoveStaticMeshSkinxxWorkflow)
|
|
{
|
|
SectionIndex = FMath::Clamp(RawMesh.FaceMaterialIndices[FaceIndex], 0, OutPerSectionIndices.Num() - 1);
|
|
}
|
|
else
|
|
{
|
|
SectionIndex = MaterialToSectionMapping.FindChecked(RawMesh.FaceMaterialIndices[FaceIndex]);
|
|
}
|
|
TArray<uint32>& SectionIndices = OutPerSectionIndices[SectionIndex];
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
SectionIndices.Add(VertexIndices[CornerIndex]);
|
|
OutWedgeMap.Add(VertexIndices[CornerIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::CacheOptimizeVertexAndIndexBuffer(
|
|
TArray<FStaticMeshBuildVertex>& Vertices,
|
|
TArray<TArray<uint32> >& PerSectionIndices,
|
|
TArray<int32>& WedgeMap
|
|
)
|
|
{
|
|
// Copy the vertices since we will be reordering them
|
|
TArray<FStaticMeshBuildVertex> OriginalVertices = Vertices;
|
|
|
|
// Initialize a cache that stores which indices have been assigned
|
|
TArray<int32> IndexCache;
|
|
IndexCache.AddUninitialized(Vertices.Num());
|
|
FMemory::Memset(IndexCache.GetData(), INDEX_NONE, IndexCache.Num() * IndexCache.GetTypeSize());
|
|
int32 NextAvailableIndex = 0;
|
|
|
|
// Iterate through the section index buffers,
|
|
// Optimizing index order for the post transform cache (minimizes the number of vertices transformed),
|
|
// And vertex order for the pre transform cache (minimizes the amount of vertex data fetched by the GPU).
|
|
for (int32 SectionIndex = 0; SectionIndex < PerSectionIndices.Num(); SectionIndex++)
|
|
{
|
|
TArray<uint32>& Indices = PerSectionIndices[SectionIndex];
|
|
|
|
if (Indices.Num())
|
|
{
|
|
// Optimize the index buffer for the post transform cache with.
|
|
CacheOptimizeIndexBuffer(Indices);
|
|
|
|
// Copy the index buffer since we will be reordering it
|
|
TArray<uint32> OriginalIndices = Indices;
|
|
|
|
// Go through the indices and assign them new values that are coherent where possible
|
|
for (int32 Index = 0; Index < Indices.Num(); Index++)
|
|
{
|
|
const int32 CachedIndex = IndexCache[OriginalIndices[Index]];
|
|
|
|
if (CachedIndex == INDEX_NONE)
|
|
{
|
|
// No new index has been allocated for this existing index, assign a new one
|
|
Indices[Index] = NextAvailableIndex;
|
|
// Mark what this index has been assigned to
|
|
IndexCache[OriginalIndices[Index]] = NextAvailableIndex;
|
|
NextAvailableIndex++;
|
|
}
|
|
else
|
|
{
|
|
// Reuse an existing index assignment
|
|
Indices[Index] = CachedIndex;
|
|
}
|
|
// Reorder the vertices based on the new index assignment
|
|
Vertices[Indices[Index]] = OriginalVertices[OriginalIndices[Index]];
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 i = 0; i < WedgeMap.Num(); i++)
|
|
{
|
|
int32 MappedIndex = WedgeMap[i];
|
|
if (MappedIndex != INDEX_NONE)
|
|
{
|
|
WedgeMap[i] = IndexCache[MappedIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
class FStaticMeshUtilityBuilder
|
|
{
|
|
public:
|
|
FStaticMeshUtilityBuilder() : Stage(EStage::Uninit), NumValidLODs(0) {}
|
|
|
|
bool GatherSourceMeshesPerLOD(TArray<FStaticMeshSourceModel>& SourceModels, IMeshReduction* MeshReduction)
|
|
{
|
|
check(Stage == EStage::Uninit);
|
|
|
|
// Gather source meshes for each LOD.
|
|
for (int32 LODIndex = 0; LODIndex < SourceModels.Num(); ++LODIndex)
|
|
{
|
|
FStaticMeshSourceModel& SrcModel = SourceModels[LODIndex];
|
|
FRawMesh& RawMesh = *new(LODMeshes)FRawMesh;
|
|
TMultiMap<int32, int32>& OverlappingCorners = *new(LODOverlappingCorners)TMultiMap<int32, int32>;
|
|
|
|
if (!SrcModel.RawMeshBulkData->IsEmpty())
|
|
{
|
|
SrcModel.RawMeshBulkData->LoadRawMesh(RawMesh);
|
|
// Make sure the raw mesh is not irreparably malformed.
|
|
if (!RawMesh.IsValidOrFixable())
|
|
{
|
|
UE_LOG(LogMeshUtilities, Error, TEXT("Raw mesh is corrupt for LOD%d."), LODIndex);
|
|
return false;
|
|
}
|
|
LODBuildSettings[LODIndex] = SrcModel.BuildSettings;
|
|
|
|
float ComparisonThreshold = GetComparisonThreshold(LODBuildSettings[LODIndex]);
|
|
int32 NumWedges = RawMesh.WedgeIndices.Num();
|
|
|
|
// Find overlapping corners to accelerate adjacency.
|
|
FindOverlappingCorners(OverlappingCorners, RawMesh, ComparisonThreshold);
|
|
|
|
// Figure out if we should recompute normals and tangents.
|
|
bool bRecomputeNormals = SrcModel.BuildSettings.bRecomputeNormals || RawMesh.WedgeTangentZ.Num() != NumWedges;
|
|
bool bRecomputeTangents = SrcModel.BuildSettings.bRecomputeTangents || RawMesh.WedgeTangentX.Num() != NumWedges || RawMesh.WedgeTangentY.Num() != NumWedges;
|
|
|
|
// Dump normals and tangents if we are recomputing them.
|
|
if (bRecomputeTangents)
|
|
{
|
|
RawMesh.WedgeTangentX.Empty(NumWedges);
|
|
RawMesh.WedgeTangentX.AddZeroed(NumWedges);
|
|
RawMesh.WedgeTangentY.Empty(NumWedges);
|
|
RawMesh.WedgeTangentY.AddZeroed(NumWedges);
|
|
}
|
|
if (bRecomputeNormals)
|
|
{
|
|
RawMesh.WedgeTangentZ.Empty(NumWedges);
|
|
RawMesh.WedgeTangentZ.AddZeroed(NumWedges);
|
|
}
|
|
|
|
// Compute any missing tangents.
|
|
{
|
|
// Static meshes always blend normals of overlapping corners.
|
|
uint32 TangentOptions = ETangentOptions::BlendOverlappingNormals;
|
|
if (SrcModel.BuildSettings.bRemoveDegenerates)
|
|
{
|
|
// If removing degenerate triangles, ignore them when computing tangents.
|
|
TangentOptions |= ETangentOptions::IgnoreDegenerateTriangles;
|
|
}
|
|
|
|
//MikkTSpace should be use only when the user want to recompute the normals or tangents otherwise should always fallback on builtin
|
|
if (SrcModel.BuildSettings.bUseMikkTSpace && (SrcModel.BuildSettings.bRecomputeNormals || SrcModel.BuildSettings.bRecomputeTangents))
|
|
{
|
|
ComputeTangents_MikkTSpace(RawMesh, OverlappingCorners, TangentOptions);
|
|
}
|
|
else
|
|
{
|
|
ComputeTangents(RawMesh, OverlappingCorners, TangentOptions);
|
|
}
|
|
}
|
|
|
|
// At this point the mesh will have valid tangents.
|
|
check(RawMesh.WedgeTangentX.Num() == NumWedges);
|
|
check(RawMesh.WedgeTangentY.Num() == NumWedges);
|
|
check(RawMesh.WedgeTangentZ.Num() == NumWedges);
|
|
|
|
// Generate lightmap UVs
|
|
if (SrcModel.BuildSettings.bGenerateLightmapUVs)
|
|
{
|
|
if (RawMesh.WedgeTexCoords[SrcModel.BuildSettings.SrcLightmapIndex].Num() == 0)
|
|
{
|
|
SrcModel.BuildSettings.SrcLightmapIndex = 0;
|
|
}
|
|
|
|
FLayoutUV Packer(&RawMesh, SrcModel.BuildSettings.SrcLightmapIndex, SrcModel.BuildSettings.DstLightmapIndex, SrcModel.BuildSettings.MinLightmapResolution);
|
|
|
|
Packer.FindCharts(OverlappingCorners);
|
|
bool bPackSuccess = Packer.FindBestPacking();
|
|
if (bPackSuccess)
|
|
{
|
|
Packer.CommitPackedUVs();
|
|
}
|
|
}
|
|
HasRawMesh[LODIndex] = true;
|
|
}
|
|
else if (LODIndex > 0 && MeshReduction)
|
|
{
|
|
// If a raw mesh is not explicitly provided, use the raw mesh of the
|
|
// next highest LOD.
|
|
RawMesh = LODMeshes[LODIndex - 1];
|
|
OverlappingCorners = LODOverlappingCorners[LODIndex - 1];
|
|
LODBuildSettings[LODIndex] = LODBuildSettings[LODIndex - 1];
|
|
HasRawMesh[LODIndex] = false;
|
|
}
|
|
}
|
|
check(LODMeshes.Num() == SourceModels.Num());
|
|
check(LODOverlappingCorners.Num() == SourceModels.Num());
|
|
|
|
// Bail if there is no raw mesh data from which to build a renderable mesh.
|
|
if (LODMeshes.Num() == 0 || LODMeshes[0].WedgeIndices.Num() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Stage = EStage::Gathered;
|
|
return true;
|
|
}
|
|
|
|
bool ReduceLODs(TArray<FStaticMeshSourceModel>& SourceModels, const FStaticMeshLODGroup& LODGroup, IMeshReduction* MeshReduction, bool& bOutWasReduced)
|
|
{
|
|
check(Stage == EStage::Gathered);
|
|
|
|
// Reduce each LOD mesh according to its reduction settings.
|
|
for (int32 LODIndex = 0; LODIndex < SourceModels.Num(); ++LODIndex)
|
|
{
|
|
const FStaticMeshSourceModel& SrcModel = SourceModels[LODIndex];
|
|
FMeshReductionSettings ReductionSettings = LODGroup.GetSettings(SrcModel.ReductionSettings, LODIndex);
|
|
LODMaxDeviation[NumValidLODs] = 0.0f;
|
|
if (LODIndex != NumValidLODs)
|
|
{
|
|
LODBuildSettings[NumValidLODs] = LODBuildSettings[LODIndex];
|
|
LODOverlappingCorners[NumValidLODs] = LODOverlappingCorners[LODIndex];
|
|
}
|
|
|
|
if (MeshReduction && (ReductionSettings.PercentTriangles < 1.0f || ReductionSettings.MaxDeviation > 0.0f))
|
|
{
|
|
FRawMesh& InMesh = LODMeshes[ReductionSettings.BaseLODModel];
|
|
FRawMesh& DestMesh = LODMeshes[NumValidLODs];
|
|
TMultiMap<int32, int32>& InOverlappingCorners = LODOverlappingCorners[ReductionSettings.BaseLODModel];
|
|
TMultiMap<int32, int32>& DestOverlappingCorners = LODOverlappingCorners[NumValidLODs];
|
|
|
|
MeshReduction->Reduce(DestMesh, LODMaxDeviation[NumValidLODs], InMesh, InOverlappingCorners, ReductionSettings);
|
|
if (DestMesh.WedgeIndices.Num() > 0 && !DestMesh.IsValid())
|
|
{
|
|
UE_LOG(LogMeshUtilities, Error, TEXT("Mesh reduction produced a corrupt mesh for LOD%d"), LODIndex);
|
|
return false;
|
|
}
|
|
bOutWasReduced = true;
|
|
|
|
// Recompute adjacency information.
|
|
DestOverlappingCorners.Reset();
|
|
float ComparisonThreshold = GetComparisonThreshold(LODBuildSettings[NumValidLODs]);
|
|
FindOverlappingCorners(DestOverlappingCorners, DestMesh, ComparisonThreshold);
|
|
}
|
|
|
|
if (LODMeshes[NumValidLODs].WedgeIndices.Num() > 0)
|
|
{
|
|
NumValidLODs++;
|
|
}
|
|
}
|
|
|
|
if (NumValidLODs < 1)
|
|
{
|
|
return false;
|
|
}
|
|
Stage = EStage::Reduce;
|
|
return true;
|
|
}
|
|
|
|
bool GenerateRenderingMeshes(FMeshUtilities& MeshUtilities, FStaticMeshRenderData& OutRenderData, TArray<FStaticMeshSourceModel>& InOutModels, int32 ImportVersion)
|
|
{
|
|
check(Stage == EStage::Reduce);
|
|
// Generate per-LOD rendering data.
|
|
OutRenderData.AllocateLODResources(NumValidLODs);
|
|
for (int32 LODIndex = 0; LODIndex < NumValidLODs; ++LODIndex)
|
|
{
|
|
FStaticMeshLODResources& LODModel = OutRenderData.LODResources[LODIndex];
|
|
FRawMesh& RawMesh = LODMeshes[LODIndex];
|
|
LODModel.MaxDeviation = LODMaxDeviation[LODIndex];
|
|
|
|
TArray<FStaticMeshBuildVertex> Vertices;
|
|
TArray<TArray<uint32> > PerSectionIndices;
|
|
|
|
TMap<uint32, uint32> MaterialToSectionMapping;
|
|
|
|
// Find out how many sections are in the mesh.
|
|
TArray<int32> MaterialIndices;
|
|
for ( const int32 MaterialIndex : RawMesh.FaceMaterialIndices )
|
|
{
|
|
// Find all unique material indices
|
|
MaterialIndices.AddUnique(MaterialIndex);
|
|
}
|
|
|
|
// Need X number of sections for X number of material indices
|
|
//for (const int32 MaterialIndex : MaterialIndices)
|
|
for ( int32 Index = 0; Index < MaterialIndices.Num(); ++Index)
|
|
{
|
|
const int32 MaterialIndex = MaterialIndices[Index];
|
|
FStaticMeshSection* Section = new(LODModel.Sections) FStaticMeshSection();
|
|
Section->MaterialIndex = MaterialIndex;
|
|
if (ImportVersion < RemoveStaticMeshSkinxxWorkflow)
|
|
{
|
|
MaterialToSectionMapping.Add(MaterialIndex, MaterialIndex);
|
|
}
|
|
else
|
|
{
|
|
MaterialToSectionMapping.Add(MaterialIndex, Index);
|
|
}
|
|
new(PerSectionIndices)TArray<uint32>;
|
|
}
|
|
|
|
// Build and cache optimize vertex and index buffers.
|
|
{
|
|
// TODO_STATICMESH: The wedge map is only valid for LODIndex 0 if no reduction has been performed.
|
|
// We can compute an approximate one instead for other LODs.
|
|
TArray<int32> TempWedgeMap;
|
|
TArray<int32>& WedgeMap = (LODIndex == 0 && InOutModels[0].ReductionSettings.PercentTriangles >= 1.0f) ? OutRenderData.WedgeMap : TempWedgeMap;
|
|
float ComparisonThreshold = GetComparisonThreshold(LODBuildSettings[LODIndex]);
|
|
MeshUtilities.BuildStaticMeshVertexAndIndexBuffers(Vertices, PerSectionIndices, WedgeMap, RawMesh, LODOverlappingCorners[LODIndex], MaterialToSectionMapping, ComparisonThreshold, LODBuildSettings[LODIndex].BuildScale3D, ImportVersion);
|
|
check(WedgeMap.Num() == RawMesh.WedgeIndices.Num());
|
|
|
|
if (RawMesh.WedgeIndices.Num() < 100000 * 3)
|
|
{
|
|
MeshUtilities.CacheOptimizeVertexAndIndexBuffer(Vertices, PerSectionIndices, WedgeMap);
|
|
check(WedgeMap.Num() == RawMesh.WedgeIndices.Num());
|
|
}
|
|
}
|
|
|
|
verifyf(Vertices.Num() != 0, TEXT("No valid vertices found for the mesh."));
|
|
|
|
// Initialize the vertex buffer.
|
|
int32 NumTexCoords = ComputeNumTexCoords(RawMesh, MAX_STATIC_TEXCOORDS);
|
|
LODModel.VertexBuffer.SetUseHighPrecisionTangentBasis(LODBuildSettings[LODIndex].bUseHighPrecisionTangentBasis);
|
|
LODModel.VertexBuffer.SetUseFullPrecisionUVs(LODBuildSettings[LODIndex].bUseFullPrecisionUVs);
|
|
LODModel.VertexBuffer.Init(Vertices, NumTexCoords);
|
|
LODModel.PositionVertexBuffer.Init(Vertices);
|
|
LODModel.ColorVertexBuffer.Init(Vertices);
|
|
|
|
// Concatenate the per-section index buffers.
|
|
TArray<uint32> CombinedIndices;
|
|
bool bNeeds32BitIndices = false;
|
|
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
FStaticMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
TArray<uint32> const& SectionIndices = PerSectionIndices[SectionIndex];
|
|
Section.FirstIndex = 0;
|
|
Section.NumTriangles = 0;
|
|
Section.MinVertexIndex = 0;
|
|
Section.MaxVertexIndex = 0;
|
|
|
|
if (SectionIndices.Num())
|
|
{
|
|
Section.FirstIndex = CombinedIndices.Num();
|
|
Section.NumTriangles = SectionIndices.Num() / 3;
|
|
|
|
CombinedIndices.AddUninitialized(SectionIndices.Num());
|
|
uint32* DestPtr = &CombinedIndices[Section.FirstIndex];
|
|
uint32 const* SrcPtr = SectionIndices.GetData();
|
|
|
|
Section.MinVertexIndex = *SrcPtr;
|
|
Section.MaxVertexIndex = *SrcPtr;
|
|
|
|
for (int32 Index = 0; Index < SectionIndices.Num(); Index++)
|
|
{
|
|
uint32 VertIndex = *SrcPtr++;
|
|
|
|
bNeeds32BitIndices |= (VertIndex > MAX_uint16);
|
|
Section.MinVertexIndex = FMath::Min<uint32>(VertIndex, Section.MinVertexIndex);
|
|
Section.MaxVertexIndex = FMath::Max<uint32>(VertIndex, Section.MaxVertexIndex);
|
|
*DestPtr++ = VertIndex;
|
|
}
|
|
}
|
|
}
|
|
LODModel.IndexBuffer.SetIndices(CombinedIndices, bNeeds32BitIndices ? EIndexBufferStride::Force32Bit : EIndexBufferStride::Force16Bit);
|
|
|
|
// Build the reversed index buffer.
|
|
if (InOutModels[0].BuildSettings.bBuildReversedIndexBuffer)
|
|
{
|
|
TArray<uint32> InversedIndices;
|
|
const int32 IndexCount = CombinedIndices.Num();
|
|
InversedIndices.AddUninitialized(IndexCount);
|
|
|
|
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); ++SectionIndex)
|
|
{
|
|
const FStaticMeshSection& SectionInfo = LODModel.Sections[SectionIndex];
|
|
const int32 SectionIndexCount = SectionInfo.NumTriangles * 3;
|
|
|
|
for (int32 i = 0; i < SectionIndexCount; ++i)
|
|
{
|
|
InversedIndices[SectionInfo.FirstIndex + i] = CombinedIndices[SectionInfo.FirstIndex + SectionIndexCount - 1 - i];
|
|
}
|
|
}
|
|
LODModel.ReversedIndexBuffer.SetIndices(InversedIndices, bNeeds32BitIndices ? EIndexBufferStride::Force32Bit : EIndexBufferStride::Force16Bit);
|
|
}
|
|
|
|
// Build the depth-only index buffer.
|
|
TArray<uint32> DepthOnlyIndices;
|
|
{
|
|
BuildDepthOnlyIndexBuffer(
|
|
DepthOnlyIndices,
|
|
Vertices,
|
|
CombinedIndices,
|
|
LODModel.Sections
|
|
);
|
|
|
|
if (DepthOnlyIndices.Num() < 50000 * 3)
|
|
{
|
|
MeshUtilities.CacheOptimizeIndexBuffer(DepthOnlyIndices);
|
|
}
|
|
|
|
LODModel.DepthOnlyIndexBuffer.SetIndices(DepthOnlyIndices, bNeeds32BitIndices ? EIndexBufferStride::Force32Bit : EIndexBufferStride::Force16Bit);
|
|
}
|
|
|
|
// Build the inversed depth only index buffer.
|
|
if (InOutModels[0].BuildSettings.bBuildReversedIndexBuffer)
|
|
{
|
|
TArray<uint32> ReversedDepthOnlyIndices;
|
|
const int32 IndexCount = DepthOnlyIndices.Num();
|
|
ReversedDepthOnlyIndices.AddUninitialized(IndexCount);
|
|
for (int32 i = 0; i < IndexCount; ++i)
|
|
{
|
|
ReversedDepthOnlyIndices[i] = DepthOnlyIndices[IndexCount - 1 - i];
|
|
}
|
|
LODModel.ReversedDepthOnlyIndexBuffer.SetIndices(ReversedDepthOnlyIndices, bNeeds32BitIndices ? EIndexBufferStride::Force32Bit : EIndexBufferStride::Force16Bit);
|
|
}
|
|
|
|
// Build a list of wireframe edges in the static mesh.
|
|
{
|
|
TArray<FMeshEdge> Edges;
|
|
TArray<uint32> WireframeIndices;
|
|
|
|
FStaticMeshEdgeBuilder(CombinedIndices, Vertices, Edges).FindEdges();
|
|
WireframeIndices.Empty(2 * Edges.Num());
|
|
for (int32 EdgeIndex = 0; EdgeIndex < Edges.Num(); EdgeIndex++)
|
|
{
|
|
FMeshEdge& Edge = Edges[EdgeIndex];
|
|
WireframeIndices.Add(Edge.Vertices[0]);
|
|
WireframeIndices.Add(Edge.Vertices[1]);
|
|
}
|
|
LODModel.WireframeIndexBuffer.SetIndices(WireframeIndices, bNeeds32BitIndices ? EIndexBufferStride::Force32Bit : EIndexBufferStride::Force16Bit);
|
|
}
|
|
|
|
// Build the adjacency index buffer used for tessellation.
|
|
if (InOutModels[0].BuildSettings.bBuildAdjacencyBuffer)
|
|
{
|
|
TArray<uint32> AdjacencyIndices;
|
|
|
|
BuildStaticAdjacencyIndexBuffer(
|
|
LODModel.PositionVertexBuffer,
|
|
LODModel.VertexBuffer,
|
|
CombinedIndices,
|
|
AdjacencyIndices
|
|
);
|
|
LODModel.AdjacencyIndexBuffer.SetIndices(AdjacencyIndices, bNeeds32BitIndices ? EIndexBufferStride::Force32Bit : EIndexBufferStride::Force16Bit);
|
|
}
|
|
}
|
|
|
|
// Copy the original material indices to fixup meshes before compacting of materials was done.
|
|
if (NumValidLODs > 0)
|
|
{
|
|
OutRenderData.MaterialIndexToImportIndex = LODMeshes[0].MaterialIndexToImportIndex;
|
|
}
|
|
|
|
// Calculate the bounding box.
|
|
FBox BoundingBox(0);
|
|
FPositionVertexBuffer& BasePositionVertexBuffer = OutRenderData.LODResources[0].PositionVertexBuffer;
|
|
for (uint32 VertexIndex = 0; VertexIndex < BasePositionVertexBuffer.GetNumVertices(); VertexIndex++)
|
|
{
|
|
BoundingBox += BasePositionVertexBuffer.VertexPosition(VertexIndex);
|
|
}
|
|
BoundingBox.GetCenterAndExtents(OutRenderData.Bounds.Origin, OutRenderData.Bounds.BoxExtent);
|
|
|
|
// Calculate the bounding sphere, using the center of the bounding box as the origin.
|
|
OutRenderData.Bounds.SphereRadius = 0.0f;
|
|
for (uint32 VertexIndex = 0; VertexIndex < BasePositionVertexBuffer.GetNumVertices(); VertexIndex++)
|
|
{
|
|
OutRenderData.Bounds.SphereRadius = FMath::Max(
|
|
(BasePositionVertexBuffer.VertexPosition(VertexIndex) - OutRenderData.Bounds.Origin).Size(),
|
|
OutRenderData.Bounds.SphereRadius
|
|
);
|
|
}
|
|
|
|
Stage = EStage::GenerateRendering;
|
|
return true;
|
|
}
|
|
|
|
bool ReplaceRawMeshModels(TArray<FStaticMeshSourceModel>& SourceModels)
|
|
{
|
|
check(Stage == EStage::Reduce);
|
|
|
|
check(HasRawMesh[0]);
|
|
check(SourceModels.Num() >= NumValidLODs);
|
|
bool bDirty = false;
|
|
for (int32 Index = 1; Index < NumValidLODs; ++Index)
|
|
{
|
|
if (!HasRawMesh[Index])
|
|
{
|
|
SourceModels[Index].RawMeshBulkData->SaveRawMesh(LODMeshes[Index]);
|
|
bDirty = true;
|
|
}
|
|
}
|
|
|
|
Stage = EStage::ReplaceRaw;
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
enum class EStage
|
|
{
|
|
Uninit,
|
|
Gathered,
|
|
Reduce,
|
|
GenerateRendering,
|
|
ReplaceRaw,
|
|
};
|
|
|
|
EStage Stage;
|
|
|
|
int32 NumValidLODs;
|
|
|
|
TIndirectArray<FRawMesh> LODMeshes;
|
|
TIndirectArray<TMultiMap<int32, int32> > LODOverlappingCorners;
|
|
float LODMaxDeviation[MAX_STATIC_MESH_LODS];
|
|
FMeshBuildSettings LODBuildSettings[MAX_STATIC_MESH_LODS];
|
|
bool HasRawMesh[MAX_STATIC_MESH_LODS];
|
|
};
|
|
|
|
bool FMeshUtilities::BuildStaticMesh(FStaticMeshRenderData& OutRenderData, TArray<FStaticMeshSourceModel>& SourceModels, const FStaticMeshLODGroup& LODGroup, int32 ImportVersion)
|
|
{
|
|
FStaticMeshUtilityBuilder Builder;
|
|
if (!Builder.GatherSourceMeshesPerLOD(SourceModels, StaticMeshReduction))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool bWasReduced = false;
|
|
if (!Builder.ReduceLODs(SourceModels, LODGroup, StaticMeshReduction, bWasReduced))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Builder.GenerateRenderingMeshes(*this, OutRenderData, SourceModels, ImportVersion);
|
|
}
|
|
|
|
bool FMeshUtilities::GenerateStaticMeshLODs(TArray<FStaticMeshSourceModel>& Models, const FStaticMeshLODGroup& LODGroup)
|
|
{
|
|
FStaticMeshUtilityBuilder Builder;
|
|
if (!Builder.GatherSourceMeshesPerLOD(Models, StaticMeshReduction))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool bWasReduced = false;
|
|
if (!Builder.ReduceLODs(Models, LODGroup, StaticMeshReduction, bWasReduced))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (bWasReduced)
|
|
{
|
|
return Builder.ReplaceRawMeshModels(Models);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
class IMeshBuildData
|
|
{
|
|
public:
|
|
virtual uint32 GetWedgeIndex(uint32 FaceIndex, uint32 TriIndex) = 0;
|
|
virtual uint32 GetVertexIndex(uint32 WedgeIndex) = 0;
|
|
virtual uint32 GetVertexIndex(uint32 FaceIndex, uint32 TriIndex) = 0;
|
|
virtual FVector GetVertexPosition(uint32 WedgeIndex) = 0;
|
|
virtual FVector GetVertexPosition(uint32 FaceIndex, uint32 TriIndex) = 0;
|
|
virtual FVector2D GetVertexUV(uint32 FaceIndex, uint32 TriIndex, uint32 UVIndex) = 0;
|
|
virtual uint32 GetFaceSmoothingGroups(uint32 FaceIndex) = 0;
|
|
|
|
virtual uint32 GetNumFaces() = 0;
|
|
virtual uint32 GetNumWedges() = 0;
|
|
|
|
virtual TArray<FVector>& GetTangentArray(uint32 Axis) = 0;
|
|
virtual void ValidateTangentArraySize() = 0;
|
|
|
|
virtual SMikkTSpaceInterface* GetMikkTInterface() = 0;
|
|
virtual void* GetMikkTUserData() = 0;
|
|
|
|
const IMeshUtilities::MeshBuildOptions& BuildOptions;
|
|
TArray<FText>* OutWarningMessages;
|
|
TArray<FName>* OutWarningNames;
|
|
bool bTooManyVerts;
|
|
|
|
protected:
|
|
IMeshBuildData(
|
|
const IMeshUtilities::MeshBuildOptions& InBuildOptions,
|
|
TArray<FText>* InWarningMessages,
|
|
TArray<FName>* InWarningNames)
|
|
: BuildOptions(InBuildOptions)
|
|
, OutWarningMessages(InWarningMessages)
|
|
, OutWarningNames(InWarningNames)
|
|
, bTooManyVerts(false)
|
|
{
|
|
}
|
|
};
|
|
|
|
class SkeletalMeshBuildData : public IMeshBuildData
|
|
{
|
|
public:
|
|
SkeletalMeshBuildData(
|
|
FStaticLODModel& InLODModel,
|
|
const FReferenceSkeleton& InRefSkeleton,
|
|
const TArray<FVertInfluence>& InInfluences,
|
|
const TArray<FMeshWedge>& InWedges,
|
|
const TArray<FMeshFace>& InFaces,
|
|
const TArray<FVector>& InPoints,
|
|
const TArray<int32>& InPointToOriginalMap,
|
|
const IMeshUtilities::MeshBuildOptions& InBuildOptions,
|
|
TArray<FText>* InWarningMessages,
|
|
TArray<FName>* InWarningNames)
|
|
: IMeshBuildData(InBuildOptions, InWarningMessages, InWarningNames)
|
|
, MikkTUserData(InWedges, InFaces, InPoints, InBuildOptions.bComputeNormals, TangentX, TangentY, TangentZ)
|
|
, LODModel(InLODModel)
|
|
, RefSkeleton(InRefSkeleton)
|
|
, Influences(InInfluences)
|
|
, Wedges(InWedges)
|
|
, Faces(InFaces)
|
|
, Points(InPoints)
|
|
, PointToOriginalMap(InPointToOriginalMap)
|
|
{
|
|
MikkTInterface.m_getNormal = MikkGetNormal_Skeletal;
|
|
MikkTInterface.m_getNumFaces = MikkGetNumFaces_Skeletal;
|
|
MikkTInterface.m_getNumVerticesOfFace = MikkGetNumVertsOfFace_Skeletal;
|
|
MikkTInterface.m_getPosition = MikkGetPosition_Skeletal;
|
|
MikkTInterface.m_getTexCoord = MikkGetTexCoord_Skeletal;
|
|
MikkTInterface.m_setTSpaceBasic = MikkSetTSpaceBasic_Skeletal;
|
|
MikkTInterface.m_setTSpace = nullptr;
|
|
}
|
|
|
|
virtual uint32 GetWedgeIndex(uint32 FaceIndex, uint32 TriIndex) override
|
|
{
|
|
return Faces[FaceIndex].iWedge[TriIndex];
|
|
}
|
|
|
|
virtual uint32 GetVertexIndex(uint32 WedgeIndex) override
|
|
{
|
|
return Wedges[WedgeIndex].iVertex;
|
|
}
|
|
|
|
virtual uint32 GetVertexIndex(uint32 FaceIndex, uint32 TriIndex) override
|
|
{
|
|
return Wedges[Faces[FaceIndex].iWedge[TriIndex]].iVertex;
|
|
}
|
|
|
|
virtual FVector GetVertexPosition(uint32 WedgeIndex) override
|
|
{
|
|
return Points[Wedges[WedgeIndex].iVertex];
|
|
}
|
|
|
|
virtual FVector GetVertexPosition(uint32 FaceIndex, uint32 TriIndex) override
|
|
{
|
|
return Points[Wedges[Faces[FaceIndex].iWedge[TriIndex]].iVertex];
|
|
}
|
|
|
|
virtual FVector2D GetVertexUV(uint32 FaceIndex, uint32 TriIndex, uint32 UVIndex) override
|
|
{
|
|
return Wedges[Faces[FaceIndex].iWedge[TriIndex]].UVs[UVIndex];
|
|
}
|
|
|
|
virtual uint32 GetFaceSmoothingGroups(uint32 FaceIndex)
|
|
{
|
|
return Faces[FaceIndex].SmoothingGroups;
|
|
}
|
|
|
|
virtual uint32 GetNumFaces() override
|
|
{
|
|
return Faces.Num();
|
|
}
|
|
|
|
virtual uint32 GetNumWedges() override
|
|
{
|
|
return Wedges.Num();
|
|
}
|
|
|
|
virtual TArray<FVector>& GetTangentArray(uint32 Axis) override
|
|
{
|
|
if (Axis == 0)
|
|
{
|
|
return TangentX;
|
|
}
|
|
else if (Axis == 1)
|
|
{
|
|
return TangentY;
|
|
}
|
|
|
|
return TangentZ;
|
|
}
|
|
|
|
virtual void ValidateTangentArraySize() override
|
|
{
|
|
check(TangentX.Num() == Wedges.Num());
|
|
check(TangentY.Num() == Wedges.Num());
|
|
check(TangentZ.Num() == Wedges.Num());
|
|
}
|
|
|
|
virtual SMikkTSpaceInterface* GetMikkTInterface() override
|
|
{
|
|
return &MikkTInterface;
|
|
}
|
|
|
|
virtual void* GetMikkTUserData() override
|
|
{
|
|
return (void*)&MikkTUserData;
|
|
}
|
|
|
|
TArray<FVector> TangentX;
|
|
TArray<FVector> TangentY;
|
|
TArray<FVector> TangentZ;
|
|
TArray<FSkinnedMeshChunk*> Chunks;
|
|
|
|
SMikkTSpaceInterface MikkTInterface;
|
|
MikkTSpace_Skeletal_Mesh MikkTUserData;
|
|
|
|
FStaticLODModel& LODModel;
|
|
const FReferenceSkeleton& RefSkeleton;
|
|
const TArray<FVertInfluence>& Influences;
|
|
const TArray<FMeshWedge>& Wedges;
|
|
const TArray<FMeshFace>& Faces;
|
|
const TArray<FVector>& Points;
|
|
const TArray<int32>& PointToOriginalMap;
|
|
};
|
|
|
|
class FSkeletalMeshUtilityBuilder
|
|
{
|
|
public:
|
|
FSkeletalMeshUtilityBuilder()
|
|
: Stage(EStage::Uninit)
|
|
{
|
|
}
|
|
|
|
public:
|
|
void Skeletal_FindOverlappingCorners(
|
|
TMultiMap<int32, int32>& OutOverlappingCorners,
|
|
IMeshBuildData* BuildData,
|
|
float ComparisonThreshold
|
|
)
|
|
{
|
|
int32 NumFaces = BuildData->GetNumFaces();
|
|
int32 NumWedges = BuildData->GetNumWedges();
|
|
check(NumFaces * 3 <= NumWedges);
|
|
|
|
// Create a list of vertex Z/index pairs
|
|
TArray<FIndexAndZ> VertIndexAndZ;
|
|
VertIndexAndZ.Empty(NumWedges);
|
|
for (int32 FaceIndex = 0; FaceIndex < NumFaces; FaceIndex++)
|
|
{
|
|
for (int32 TriIndex = 0; TriIndex < 3; ++TriIndex)
|
|
{
|
|
uint32 Index = BuildData->GetWedgeIndex(FaceIndex, TriIndex);
|
|
new(VertIndexAndZ)FIndexAndZ(Index, BuildData->GetVertexPosition(Index));
|
|
}
|
|
}
|
|
|
|
// Sort the vertices by z value
|
|
VertIndexAndZ.Sort(FCompareIndexAndZ());
|
|
|
|
// Search for duplicates, quickly!
|
|
for (int32 i = 0; i < VertIndexAndZ.Num(); i++)
|
|
{
|
|
// only need to search forward, since we add pairs both ways
|
|
for (int32 j = i + 1; j < VertIndexAndZ.Num(); j++)
|
|
{
|
|
if (FMath::Abs(VertIndexAndZ[j].Z - VertIndexAndZ[i].Z) > ComparisonThreshold)
|
|
break; // can't be any more dups
|
|
|
|
FVector PositionA = BuildData->GetVertexPosition(VertIndexAndZ[i].Index);
|
|
FVector PositionB = BuildData->GetVertexPosition(VertIndexAndZ[j].Index);
|
|
|
|
if (PointsEqual(PositionA, PositionB, ComparisonThreshold))
|
|
{
|
|
OutOverlappingCorners.Add(VertIndexAndZ[i].Index, VertIndexAndZ[j].Index);
|
|
OutOverlappingCorners.Add(VertIndexAndZ[j].Index, VertIndexAndZ[i].Index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Skeletal_ComputeTriangleTangents(
|
|
TArray<FVector>& TriangleTangentX,
|
|
TArray<FVector>& TriangleTangentY,
|
|
TArray<FVector>& TriangleTangentZ,
|
|
IMeshBuildData* BuildData,
|
|
float ComparisonThreshold
|
|
)
|
|
{
|
|
int32 NumTriangles = BuildData->GetNumFaces();
|
|
TriangleTangentX.Empty(NumTriangles);
|
|
TriangleTangentY.Empty(NumTriangles);
|
|
TriangleTangentZ.Empty(NumTriangles);
|
|
|
|
for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; TriangleIndex++)
|
|
{
|
|
const int32 UVIndex = 0;
|
|
FVector P[3];
|
|
|
|
for (int32 i = 0; i < 3; ++i)
|
|
{
|
|
P[i] = BuildData->GetVertexPosition(TriangleIndex, i);
|
|
}
|
|
|
|
const FVector Normal = ((P[1] - P[2]) ^ (P[0] - P[2])).GetSafeNormal(ComparisonThreshold);
|
|
FMatrix ParameterToLocal(
|
|
FPlane(P[1].X - P[0].X, P[1].Y - P[0].Y, P[1].Z - P[0].Z, 0),
|
|
FPlane(P[2].X - P[0].X, P[2].Y - P[0].Y, P[2].Z - P[0].Z, 0),
|
|
FPlane(P[0].X, P[0].Y, P[0].Z, 0),
|
|
FPlane(0, 0, 0, 1)
|
|
);
|
|
|
|
FVector2D T1 = BuildData->GetVertexUV(TriangleIndex, 0, UVIndex);
|
|
FVector2D T2 = BuildData->GetVertexUV(TriangleIndex, 1, UVIndex);
|
|
FVector2D T3 = BuildData->GetVertexUV(TriangleIndex, 2, UVIndex);
|
|
FMatrix ParameterToTexture(
|
|
FPlane(T2.X - T1.X, T2.Y - T1.Y, 0, 0),
|
|
FPlane(T3.X - T1.X, T3.Y - T1.Y, 0, 0),
|
|
FPlane(T1.X, T1.Y, 1, 0),
|
|
FPlane(0, 0, 0, 1)
|
|
);
|
|
|
|
// Use InverseSlow to catch singular matrices. Inverse can miss this sometimes.
|
|
const FMatrix TextureToLocal = ParameterToTexture.Inverse() * ParameterToLocal;
|
|
|
|
TriangleTangentX.Add(TextureToLocal.TransformVector(FVector(1, 0, 0)).GetSafeNormal());
|
|
TriangleTangentY.Add(TextureToLocal.TransformVector(FVector(0, 1, 0)).GetSafeNormal());
|
|
TriangleTangentZ.Add(Normal);
|
|
|
|
FVector::CreateOrthonormalBasis(
|
|
TriangleTangentX[TriangleIndex],
|
|
TriangleTangentY[TriangleIndex],
|
|
TriangleTangentZ[TriangleIndex]
|
|
);
|
|
}
|
|
}
|
|
|
|
void Skeletal_ComputeTangents(
|
|
IMeshBuildData* BuildData,
|
|
TMultiMap<int32, int32> const& OverlappingCorners
|
|
)
|
|
{
|
|
bool bBlendOverlappingNormals = true;
|
|
bool bIgnoreDegenerateTriangles = BuildData->BuildOptions.bRemoveDegenerateTriangles;
|
|
float ComparisonThreshold = bIgnoreDegenerateTriangles ? THRESH_POINTS_ARE_SAME : 0.0f;
|
|
|
|
// Compute per-triangle tangents.
|
|
TArray<FVector> TriangleTangentX;
|
|
TArray<FVector> TriangleTangentY;
|
|
TArray<FVector> TriangleTangentZ;
|
|
|
|
Skeletal_ComputeTriangleTangents(
|
|
TriangleTangentX,
|
|
TriangleTangentY,
|
|
TriangleTangentZ,
|
|
BuildData,
|
|
bIgnoreDegenerateTriangles ? SMALL_NUMBER : 0.0f
|
|
);
|
|
|
|
TArray<FVector>& WedgeTangentX = BuildData->GetTangentArray(0);
|
|
TArray<FVector>& WedgeTangentY = BuildData->GetTangentArray(1);
|
|
TArray<FVector>& WedgeTangentZ = BuildData->GetTangentArray(2);
|
|
|
|
// Declare these out here to avoid reallocations.
|
|
TArray<FFanFace> RelevantFacesForCorner[3];
|
|
TArray<int32> AdjacentFaces;
|
|
TArray<int32> DupVerts;
|
|
|
|
int32 NumFaces = BuildData->GetNumFaces();
|
|
int32 NumWedges = BuildData->GetNumWedges();
|
|
check(NumFaces * 3 <= NumWedges);
|
|
|
|
// Allocate storage for tangents if none were provided.
|
|
if (WedgeTangentX.Num() != NumWedges)
|
|
{
|
|
WedgeTangentX.Empty(NumWedges);
|
|
WedgeTangentX.AddZeroed(NumWedges);
|
|
}
|
|
if (WedgeTangentY.Num() != NumWedges)
|
|
{
|
|
WedgeTangentY.Empty(NumWedges);
|
|
WedgeTangentY.AddZeroed(NumWedges);
|
|
}
|
|
if (WedgeTangentZ.Num() != NumWedges)
|
|
{
|
|
WedgeTangentZ.Empty(NumWedges);
|
|
WedgeTangentZ.AddZeroed(NumWedges);
|
|
}
|
|
|
|
for (int32 FaceIndex = 0; FaceIndex < NumFaces; FaceIndex++)
|
|
{
|
|
int32 WedgeOffset = FaceIndex * 3;
|
|
FVector CornerPositions[3];
|
|
FVector CornerTangentX[3];
|
|
FVector CornerTangentY[3];
|
|
FVector CornerTangentZ[3];
|
|
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
CornerTangentX[CornerIndex] = FVector::ZeroVector;
|
|
CornerTangentY[CornerIndex] = FVector::ZeroVector;
|
|
CornerTangentZ[CornerIndex] = FVector::ZeroVector;
|
|
CornerPositions[CornerIndex] = BuildData->GetVertexPosition(FaceIndex, CornerIndex);
|
|
RelevantFacesForCorner[CornerIndex].Reset();
|
|
}
|
|
|
|
// Don't process degenerate triangles.
|
|
if (PointsEqual(CornerPositions[0], CornerPositions[1], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[0], CornerPositions[2], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[1], CornerPositions[2], ComparisonThreshold))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// No need to process triangles if tangents already exist.
|
|
bool bCornerHasTangents[3] = { 0 };
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
bCornerHasTangents[CornerIndex] = !WedgeTangentX[WedgeOffset + CornerIndex].IsZero()
|
|
&& !WedgeTangentY[WedgeOffset + CornerIndex].IsZero()
|
|
&& !WedgeTangentZ[WedgeOffset + CornerIndex].IsZero();
|
|
}
|
|
if (bCornerHasTangents[0] && bCornerHasTangents[1] && bCornerHasTangents[2])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Calculate smooth vertex normals.
|
|
float Determinant = FVector::Triple(
|
|
TriangleTangentX[FaceIndex],
|
|
TriangleTangentY[FaceIndex],
|
|
TriangleTangentZ[FaceIndex]
|
|
);
|
|
|
|
// Start building a list of faces adjacent to this face.
|
|
AdjacentFaces.Reset();
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
int32 ThisCornerIndex = WedgeOffset + CornerIndex;
|
|
DupVerts.Reset();
|
|
OverlappingCorners.MultiFind(ThisCornerIndex, DupVerts);
|
|
DupVerts.Add(ThisCornerIndex); // I am a "dup" of myself
|
|
for (int32 k = 0; k < DupVerts.Num(); k++)
|
|
{
|
|
AdjacentFaces.AddUnique(DupVerts[k] / 3);
|
|
}
|
|
}
|
|
|
|
// We need to sort these here because the criteria for point equality is
|
|
// exact, so we must ensure the exact same order for all dups.
|
|
AdjacentFaces.Sort();
|
|
|
|
// Process adjacent faces
|
|
for (int32 AdjacentFaceIndex = 0; AdjacentFaceIndex < AdjacentFaces.Num(); AdjacentFaceIndex++)
|
|
{
|
|
int32 OtherFaceIndex = AdjacentFaces[AdjacentFaceIndex];
|
|
for (int32 OurCornerIndex = 0; OurCornerIndex < 3; OurCornerIndex++)
|
|
{
|
|
if (bCornerHasTangents[OurCornerIndex])
|
|
continue;
|
|
|
|
FFanFace NewFanFace;
|
|
int32 CommonIndexCount = 0;
|
|
|
|
// Check for vertices in common.
|
|
if (FaceIndex == OtherFaceIndex)
|
|
{
|
|
CommonIndexCount = 3;
|
|
NewFanFace.LinkedVertexIndex = OurCornerIndex;
|
|
}
|
|
else
|
|
{
|
|
// Check matching vertices against main vertex .
|
|
for (int32 OtherCornerIndex = 0; OtherCornerIndex < 3; OtherCornerIndex++)
|
|
{
|
|
if (PointsEqual(
|
|
CornerPositions[OurCornerIndex],
|
|
BuildData->GetVertexPosition(OtherFaceIndex, OtherCornerIndex),
|
|
ComparisonThreshold
|
|
))
|
|
{
|
|
CommonIndexCount++;
|
|
NewFanFace.LinkedVertexIndex = OtherCornerIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add if connected by at least one point. Smoothing matches are considered later.
|
|
if (CommonIndexCount > 0)
|
|
{
|
|
NewFanFace.FaceIndex = OtherFaceIndex;
|
|
NewFanFace.bFilled = (OtherFaceIndex == FaceIndex); // Starter face for smoothing floodfill.
|
|
NewFanFace.bBlendTangents = NewFanFace.bFilled;
|
|
NewFanFace.bBlendNormals = NewFanFace.bFilled;
|
|
RelevantFacesForCorner[OurCornerIndex].Add(NewFanFace);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find true relevance of faces for a vertex normal by traversing
|
|
// smoothing-group-compatible connected triangle fans around common vertices.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
if (bCornerHasTangents[CornerIndex])
|
|
continue;
|
|
|
|
int32 NewConnections;
|
|
do
|
|
{
|
|
NewConnections = 0;
|
|
for (int32 OtherFaceIdx = 0; OtherFaceIdx < RelevantFacesForCorner[CornerIndex].Num(); OtherFaceIdx++)
|
|
{
|
|
FFanFace& OtherFace = RelevantFacesForCorner[CornerIndex][OtherFaceIdx];
|
|
// The vertex' own face is initially the only face with bFilled == true.
|
|
if (OtherFace.bFilled)
|
|
{
|
|
for (int32 NextFaceIndex = 0; NextFaceIndex < RelevantFacesForCorner[CornerIndex].Num(); NextFaceIndex++)
|
|
{
|
|
FFanFace& NextFace = RelevantFacesForCorner[CornerIndex][NextFaceIndex];
|
|
if (!NextFace.bFilled) // && !NextFace.bBlendTangents)
|
|
{
|
|
if (NextFaceIndex != OtherFaceIdx)
|
|
//&& (RawMesh.FaceSmoothingMasks[NextFace.FaceIndex] & RawMesh.FaceSmoothingMasks[OtherFace.FaceIndex]))
|
|
{
|
|
int32 CommonVertices = 0;
|
|
int32 CommonTangentVertices = 0;
|
|
int32 CommonNormalVertices = 0;
|
|
for (int32 OtherCornerIndex = 0; OtherCornerIndex < 3; OtherCornerIndex++)
|
|
{
|
|
for (int32 NextCornerIndex = 0; NextCornerIndex < 3; NextCornerIndex++)
|
|
{
|
|
int32 NextVertexIndex = BuildData->GetVertexIndex(NextFace.FaceIndex, NextCornerIndex);
|
|
int32 OtherVertexIndex = BuildData->GetVertexIndex(OtherFace.FaceIndex, OtherCornerIndex);
|
|
if (PointsEqual(
|
|
BuildData->GetVertexPosition(NextFace.FaceIndex, NextCornerIndex),
|
|
BuildData->GetVertexPosition(OtherFace.FaceIndex, OtherCornerIndex),
|
|
ComparisonThreshold))
|
|
{
|
|
CommonVertices++;
|
|
|
|
|
|
if (UVsEqual(
|
|
BuildData->GetVertexUV(NextFace.FaceIndex, NextCornerIndex, 0),
|
|
BuildData->GetVertexUV(OtherFace.FaceIndex, OtherCornerIndex, 0)))
|
|
{
|
|
CommonTangentVertices++;
|
|
}
|
|
if (bBlendOverlappingNormals
|
|
|| NextVertexIndex == OtherVertexIndex)
|
|
{
|
|
CommonNormalVertices++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Flood fill faces with more than one common vertices which must be touching edges.
|
|
if (CommonVertices > 1)
|
|
{
|
|
NextFace.bFilled = true;
|
|
NextFace.bBlendNormals = (CommonNormalVertices > 1);
|
|
NewConnections++;
|
|
|
|
// Only blend tangents if there is no UV seam along the edge with this face.
|
|
if (OtherFace.bBlendTangents && CommonTangentVertices > 1)
|
|
{
|
|
float OtherDeterminant = FVector::Triple(
|
|
TriangleTangentX[NextFace.FaceIndex],
|
|
TriangleTangentY[NextFace.FaceIndex],
|
|
TriangleTangentZ[NextFace.FaceIndex]
|
|
);
|
|
if ((Determinant * OtherDeterminant) > 0.0f)
|
|
{
|
|
NextFace.bBlendTangents = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
while (NewConnections > 0);
|
|
}
|
|
|
|
// Vertex normal construction.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
if (bCornerHasTangents[CornerIndex])
|
|
{
|
|
CornerTangentX[CornerIndex] = WedgeTangentX[WedgeOffset + CornerIndex];
|
|
CornerTangentY[CornerIndex] = WedgeTangentY[WedgeOffset + CornerIndex];
|
|
CornerTangentZ[CornerIndex] = WedgeTangentZ[WedgeOffset + CornerIndex];
|
|
}
|
|
else
|
|
{
|
|
for (int32 RelevantFaceIdx = 0; RelevantFaceIdx < RelevantFacesForCorner[CornerIndex].Num(); RelevantFaceIdx++)
|
|
{
|
|
FFanFace const& RelevantFace = RelevantFacesForCorner[CornerIndex][RelevantFaceIdx];
|
|
if (RelevantFace.bFilled)
|
|
{
|
|
int32 OtherFaceIndex = RelevantFace.FaceIndex;
|
|
if (RelevantFace.bBlendTangents)
|
|
{
|
|
CornerTangentX[CornerIndex] += TriangleTangentX[OtherFaceIndex];
|
|
CornerTangentY[CornerIndex] += TriangleTangentY[OtherFaceIndex];
|
|
}
|
|
if (RelevantFace.bBlendNormals)
|
|
{
|
|
CornerTangentZ[CornerIndex] += TriangleTangentZ[OtherFaceIndex];
|
|
}
|
|
}
|
|
}
|
|
if (!WedgeTangentX[WedgeOffset + CornerIndex].IsZero())
|
|
{
|
|
CornerTangentX[CornerIndex] = WedgeTangentX[WedgeOffset + CornerIndex];
|
|
}
|
|
if (!WedgeTangentY[WedgeOffset + CornerIndex].IsZero())
|
|
{
|
|
CornerTangentY[CornerIndex] = WedgeTangentY[WedgeOffset + CornerIndex];
|
|
}
|
|
if (!WedgeTangentZ[WedgeOffset + CornerIndex].IsZero())
|
|
{
|
|
CornerTangentZ[CornerIndex] = WedgeTangentZ[WedgeOffset + CornerIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalization.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
CornerTangentX[CornerIndex].Normalize();
|
|
CornerTangentY[CornerIndex].Normalize();
|
|
CornerTangentZ[CornerIndex].Normalize();
|
|
|
|
// Gram-Schmidt orthogonalization
|
|
CornerTangentY[CornerIndex] -= CornerTangentX[CornerIndex] * (CornerTangentX[CornerIndex] | CornerTangentY[CornerIndex]);
|
|
CornerTangentY[CornerIndex].Normalize();
|
|
|
|
CornerTangentX[CornerIndex] -= CornerTangentZ[CornerIndex] * (CornerTangentZ[CornerIndex] | CornerTangentX[CornerIndex]);
|
|
CornerTangentX[CornerIndex].Normalize();
|
|
CornerTangentY[CornerIndex] -= CornerTangentZ[CornerIndex] * (CornerTangentZ[CornerIndex] | CornerTangentY[CornerIndex]);
|
|
CornerTangentY[CornerIndex].Normalize();
|
|
}
|
|
|
|
// Copy back to the mesh.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
WedgeTangentX[WedgeOffset + CornerIndex] = CornerTangentX[CornerIndex];
|
|
WedgeTangentY[WedgeOffset + CornerIndex] = CornerTangentY[CornerIndex];
|
|
WedgeTangentZ[WedgeOffset + CornerIndex] = CornerTangentZ[CornerIndex];
|
|
}
|
|
}
|
|
|
|
check(WedgeTangentX.Num() == NumWedges);
|
|
check(WedgeTangentY.Num() == NumWedges);
|
|
check(WedgeTangentZ.Num() == NumWedges);
|
|
}
|
|
|
|
void Skeletal_ComputeTangents_MikkTSpace(
|
|
IMeshBuildData* BuildData,
|
|
TMultiMap<int32, int32> const& OverlappingCorners
|
|
)
|
|
{
|
|
bool bBlendOverlappingNormals = true;
|
|
bool bIgnoreDegenerateTriangles = BuildData->BuildOptions.bRemoveDegenerateTriangles;
|
|
float ComparisonThreshold = bIgnoreDegenerateTriangles ? THRESH_POINTS_ARE_SAME : 0.0f;
|
|
|
|
// Compute per-triangle tangents.
|
|
TArray<FVector> TriangleTangentX;
|
|
TArray<FVector> TriangleTangentY;
|
|
TArray<FVector> TriangleTangentZ;
|
|
|
|
Skeletal_ComputeTriangleTangents(
|
|
TriangleTangentX,
|
|
TriangleTangentY,
|
|
TriangleTangentZ,
|
|
BuildData,
|
|
bIgnoreDegenerateTriangles ? SMALL_NUMBER : 0.0f
|
|
);
|
|
|
|
TArray<FVector>& WedgeTangentX = BuildData->GetTangentArray(0);
|
|
TArray<FVector>& WedgeTangentY = BuildData->GetTangentArray(1);
|
|
TArray<FVector>& WedgeTangentZ = BuildData->GetTangentArray(2);
|
|
|
|
// Declare these out here to avoid reallocations.
|
|
TArray<FFanFace> RelevantFacesForCorner[3];
|
|
TArray<int32> AdjacentFaces;
|
|
TArray<int32> DupVerts;
|
|
|
|
int32 NumFaces = BuildData->GetNumFaces();
|
|
int32 NumWedges = BuildData->GetNumWedges();
|
|
check(NumFaces * 3 == NumWedges);
|
|
|
|
bool bWedgeNormals = true;
|
|
bool bWedgeTSpace = false;
|
|
for (int32 WedgeIdx = 0; WedgeIdx < WedgeTangentZ.Num(); ++WedgeIdx)
|
|
{
|
|
for (int32 VertexIndex = 0; VertexIndex < 3; VertexIndex++)
|
|
bWedgeNormals = bWedgeNormals && (!WedgeTangentZ[WedgeIdx].IsNearlyZero());
|
|
}
|
|
|
|
if (WedgeTangentX.Num() > 0 && WedgeTangentY.Num() > 0)
|
|
{
|
|
bWedgeTSpace = true;
|
|
for (int32 WedgeIdx = 0; WedgeIdx < WedgeTangentX.Num()
|
|
&& WedgeIdx < WedgeTangentY.Num(); ++WedgeIdx)
|
|
{
|
|
bWedgeTSpace = bWedgeTSpace && (!WedgeTangentX[WedgeIdx].IsNearlyZero()) && (!WedgeTangentY[WedgeIdx].IsNearlyZero());
|
|
}
|
|
}
|
|
|
|
// Allocate storage for tangents if none were provided, and calculate normals for MikkTSpace.
|
|
if (WedgeTangentZ.Num() != NumWedges || !bWedgeNormals)
|
|
{
|
|
// normals are not included, so we should calculate them
|
|
WedgeTangentZ.Empty(NumWedges);
|
|
WedgeTangentZ.AddZeroed(NumWedges);
|
|
// we need to calculate normals for MikkTSpace
|
|
|
|
for (int32 FaceIndex = 0; FaceIndex < NumFaces; FaceIndex++)
|
|
{
|
|
int32 WedgeOffset = FaceIndex * 3;
|
|
FVector CornerPositions[3];
|
|
FVector CornerNormal[3];
|
|
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
CornerNormal[CornerIndex] = FVector::ZeroVector;
|
|
CornerPositions[CornerIndex] = BuildData->GetVertexPosition(FaceIndex, CornerIndex);
|
|
RelevantFacesForCorner[CornerIndex].Reset();
|
|
}
|
|
|
|
// Don't process degenerate triangles.
|
|
if (PointsEqual(CornerPositions[0], CornerPositions[1], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[0], CornerPositions[2], ComparisonThreshold)
|
|
|| PointsEqual(CornerPositions[1], CornerPositions[2], ComparisonThreshold))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// No need to process triangles if tangents already exist.
|
|
bool bCornerHasNormal[3] = { 0 };
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
bCornerHasNormal[CornerIndex] = !WedgeTangentZ[WedgeOffset + CornerIndex].IsZero();
|
|
}
|
|
if (bCornerHasNormal[0] && bCornerHasNormal[1] && bCornerHasNormal[2])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Start building a list of faces adjacent to this face.
|
|
AdjacentFaces.Reset();
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
int32 ThisCornerIndex = WedgeOffset + CornerIndex;
|
|
DupVerts.Reset();
|
|
OverlappingCorners.MultiFind(ThisCornerIndex, DupVerts);
|
|
DupVerts.Add(ThisCornerIndex); // I am a "dup" of myself
|
|
for (int32 k = 0; k < DupVerts.Num(); k++)
|
|
{
|
|
AdjacentFaces.AddUnique(DupVerts[k] / 3);
|
|
}
|
|
}
|
|
|
|
// We need to sort these here because the criteria for point equality is
|
|
// exact, so we must ensure the exact same order for all dups.
|
|
AdjacentFaces.Sort();
|
|
|
|
// Process adjacent faces
|
|
for (int32 AdjacentFaceIndex = 0; AdjacentFaceIndex < AdjacentFaces.Num(); AdjacentFaceIndex++)
|
|
{
|
|
int32 OtherFaceIndex = AdjacentFaces[AdjacentFaceIndex];
|
|
for (int32 OurCornerIndex = 0; OurCornerIndex < 3; OurCornerIndex++)
|
|
{
|
|
if (bCornerHasNormal[OurCornerIndex])
|
|
continue;
|
|
|
|
FFanFace NewFanFace;
|
|
int32 CommonIndexCount = 0;
|
|
|
|
// Check for vertices in common.
|
|
if (FaceIndex == OtherFaceIndex)
|
|
{
|
|
CommonIndexCount = 3;
|
|
NewFanFace.LinkedVertexIndex = OurCornerIndex;
|
|
}
|
|
else
|
|
{
|
|
// Check matching vertices against main vertex .
|
|
for (int32 OtherCornerIndex = 0; OtherCornerIndex < 3; OtherCornerIndex++)
|
|
{
|
|
if (PointsEqual(
|
|
CornerPositions[OurCornerIndex],
|
|
BuildData->GetVertexPosition(OtherFaceIndex, OtherCornerIndex),
|
|
ComparisonThreshold
|
|
))
|
|
{
|
|
CommonIndexCount++;
|
|
NewFanFace.LinkedVertexIndex = OtherCornerIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add if connected by at least one point. Smoothing matches are considered later.
|
|
if (CommonIndexCount > 0)
|
|
{
|
|
NewFanFace.FaceIndex = OtherFaceIndex;
|
|
NewFanFace.bFilled = (OtherFaceIndex == FaceIndex); // Starter face for smoothing floodfill.
|
|
NewFanFace.bBlendTangents = NewFanFace.bFilled;
|
|
NewFanFace.bBlendNormals = NewFanFace.bFilled;
|
|
RelevantFacesForCorner[OurCornerIndex].Add(NewFanFace);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find true relevance of faces for a vertex normal by traversing
|
|
// smoothing-group-compatible connected triangle fans around common vertices.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
if (bCornerHasNormal[CornerIndex])
|
|
continue;
|
|
|
|
int32 NewConnections;
|
|
do
|
|
{
|
|
NewConnections = 0;
|
|
for (int32 OtherFaceIdx = 0; OtherFaceIdx < RelevantFacesForCorner[CornerIndex].Num(); OtherFaceIdx++)
|
|
{
|
|
FFanFace& OtherFace = RelevantFacesForCorner[CornerIndex][OtherFaceIdx];
|
|
// The vertex' own face is initially the only face with bFilled == true.
|
|
if (OtherFace.bFilled)
|
|
{
|
|
for (int32 NextFaceIndex = 0; NextFaceIndex < RelevantFacesForCorner[CornerIndex].Num(); NextFaceIndex++)
|
|
{
|
|
FFanFace& NextFace = RelevantFacesForCorner[CornerIndex][NextFaceIndex];
|
|
if (!NextFace.bFilled) // && !NextFace.bBlendTangents)
|
|
{
|
|
if ((NextFaceIndex != OtherFaceIdx)
|
|
&& (BuildData->GetFaceSmoothingGroups(NextFace.FaceIndex) & BuildData->GetFaceSmoothingGroups(OtherFace.FaceIndex)))
|
|
{
|
|
int32 CommonVertices = 0;
|
|
int32 CommonNormalVertices = 0;
|
|
for (int32 OtherCornerIndex = 0; OtherCornerIndex < 3; OtherCornerIndex++)
|
|
{
|
|
for (int32 NextCornerIndex = 0; NextCornerIndex < 3; NextCornerIndex++)
|
|
{
|
|
int32 NextVertexIndex = BuildData->GetVertexIndex(NextFace.FaceIndex, NextCornerIndex);
|
|
int32 OtherVertexIndex = BuildData->GetVertexIndex(OtherFace.FaceIndex, OtherCornerIndex);
|
|
if (PointsEqual(
|
|
BuildData->GetVertexPosition(NextFace.FaceIndex, NextCornerIndex),
|
|
BuildData->GetVertexPosition(OtherFace.FaceIndex, OtherCornerIndex),
|
|
ComparisonThreshold))
|
|
{
|
|
CommonVertices++;
|
|
if (bBlendOverlappingNormals
|
|
|| NextVertexIndex == OtherVertexIndex)
|
|
{
|
|
CommonNormalVertices++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Flood fill faces with more than one common vertices which must be touching edges.
|
|
if (CommonVertices > 1)
|
|
{
|
|
NextFace.bFilled = true;
|
|
NextFace.bBlendNormals = (CommonNormalVertices > 1);
|
|
NewConnections++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
while (NewConnections > 0);
|
|
}
|
|
|
|
// Vertex normal construction.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
if (bCornerHasNormal[CornerIndex])
|
|
{
|
|
CornerNormal[CornerIndex] = WedgeTangentZ[WedgeOffset + CornerIndex];
|
|
}
|
|
else
|
|
{
|
|
for (int32 RelevantFaceIdx = 0; RelevantFaceIdx < RelevantFacesForCorner[CornerIndex].Num(); RelevantFaceIdx++)
|
|
{
|
|
FFanFace const& RelevantFace = RelevantFacesForCorner[CornerIndex][RelevantFaceIdx];
|
|
if (RelevantFace.bFilled)
|
|
{
|
|
int32 OtherFaceIndex = RelevantFace.FaceIndex;
|
|
if (RelevantFace.bBlendNormals)
|
|
{
|
|
CornerNormal[CornerIndex] += TriangleTangentZ[OtherFaceIndex];
|
|
}
|
|
}
|
|
}
|
|
if (!WedgeTangentZ[WedgeOffset + CornerIndex].IsZero())
|
|
{
|
|
CornerNormal[CornerIndex] = WedgeTangentZ[WedgeOffset + CornerIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalization.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
CornerNormal[CornerIndex].Normalize();
|
|
}
|
|
|
|
// Copy back to the mesh.
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++)
|
|
{
|
|
WedgeTangentZ[WedgeOffset + CornerIndex] = CornerNormal[CornerIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (WedgeTangentX.Num() != NumWedges)
|
|
{
|
|
WedgeTangentX.Empty(NumWedges);
|
|
WedgeTangentX.AddZeroed(NumWedges);
|
|
}
|
|
if (WedgeTangentY.Num() != NumWedges)
|
|
{
|
|
WedgeTangentY.Empty(NumWedges);
|
|
WedgeTangentY.AddZeroed(NumWedges);
|
|
}
|
|
|
|
//if (!bWedgeTSpace)
|
|
{
|
|
// we can use mikktspace to calculate the tangents
|
|
SMikkTSpaceContext MikkTContext;
|
|
MikkTContext.m_pInterface = BuildData->GetMikkTInterface();
|
|
MikkTContext.m_pUserData = BuildData->GetMikkTUserData();
|
|
//MikkTContext.m_bIgnoreDegenerates = bIgnoreDegenerateTriangles;
|
|
|
|
genTangSpaceDefault(&MikkTContext);
|
|
}
|
|
|
|
check(WedgeTangentX.Num() == NumWedges);
|
|
check(WedgeTangentY.Num() == NumWedges);
|
|
check(WedgeTangentZ.Num() == NumWedges);
|
|
}
|
|
|
|
bool PrepareSourceMesh(IMeshBuildData* BuildData)
|
|
{
|
|
check(Stage == EStage::Uninit);
|
|
|
|
BeginSlowTask();
|
|
|
|
TMultiMap<int32, int32>& OverlappingCorners = *new(LODOverlappingCorners)TMultiMap<int32, int32>;
|
|
|
|
float ComparisonThreshold = THRESH_POINTS_ARE_SAME;//GetComparisonThreshold(LODBuildSettings[LODIndex]);
|
|
int32 NumWedges = BuildData->GetNumWedges();
|
|
|
|
// Find overlapping corners to accelerate adjacency.
|
|
Skeletal_FindOverlappingCorners(OverlappingCorners, BuildData, ComparisonThreshold);
|
|
|
|
// Figure out if we should recompute normals and tangents.
|
|
bool bRecomputeNormals = BuildData->BuildOptions.bComputeNormals;
|
|
bool bRecomputeTangents = BuildData->BuildOptions.bComputeTangents;
|
|
|
|
// Dump normals and tangents if we are recomputing them.
|
|
if (bRecomputeTangents)
|
|
{
|
|
TArray<FVector>& TangentX = BuildData->GetTangentArray(0);
|
|
TArray<FVector>& TangentY = BuildData->GetTangentArray(1);
|
|
|
|
TangentX.Empty(NumWedges);
|
|
TangentX.AddZeroed(NumWedges);
|
|
TangentY.Empty(NumWedges);
|
|
TangentY.AddZeroed(NumWedges);
|
|
}
|
|
if (bRecomputeNormals)
|
|
{
|
|
TArray<FVector>& TangentZ = BuildData->GetTangentArray(2);
|
|
TangentZ.Empty(NumWedges);
|
|
TangentZ.AddZeroed(NumWedges);
|
|
}
|
|
|
|
// Compute any missing tangents. MikkTSpace should be use only when the user want to recompute the normals or tangents otherwise should always fallback on builtin
|
|
if (BuildData->BuildOptions.bUseMikkTSpace && (BuildData->BuildOptions.bComputeNormals || BuildData->BuildOptions.bComputeTangents))
|
|
{
|
|
Skeletal_ComputeTangents_MikkTSpace(BuildData, OverlappingCorners);
|
|
}
|
|
else
|
|
{
|
|
Skeletal_ComputeTangents(BuildData, OverlappingCorners);
|
|
}
|
|
|
|
// At this point the mesh will have valid tangents.
|
|
BuildData->ValidateTangentArraySize();
|
|
check(LODOverlappingCorners.Num() == 1);
|
|
|
|
EndSlowTask();
|
|
|
|
Stage = EStage::Prepared;
|
|
return true;
|
|
}
|
|
|
|
bool GenerateSkeletalRenderMesh(IMeshBuildData* InBuildData)
|
|
{
|
|
check(Stage == EStage::Prepared);
|
|
|
|
SkeletalMeshBuildData& BuildData = *(SkeletalMeshBuildData*)InBuildData;
|
|
|
|
BeginSlowTask();
|
|
|
|
// Find wedge influences.
|
|
TArray<int32> WedgeInfluenceIndices;
|
|
TMap<uint32, uint32> VertexIndexToInfluenceIndexMap;
|
|
|
|
for (uint32 LookIdx = 0; LookIdx < (uint32)BuildData.Influences.Num(); LookIdx++)
|
|
{
|
|
// Order matters do not allow the map to overwrite an existing value.
|
|
if (!VertexIndexToInfluenceIndexMap.Find(BuildData.Influences[LookIdx].VertIndex))
|
|
{
|
|
VertexIndexToInfluenceIndexMap.Add(BuildData.Influences[LookIdx].VertIndex, LookIdx);
|
|
}
|
|
}
|
|
|
|
for (int32 WedgeIndex = 0; WedgeIndex < BuildData.Wedges.Num(); WedgeIndex++)
|
|
{
|
|
uint32* InfluenceIndex = VertexIndexToInfluenceIndexMap.Find(BuildData.Wedges[WedgeIndex].iVertex);
|
|
|
|
if (InfluenceIndex)
|
|
{
|
|
WedgeInfluenceIndices.Add(*InfluenceIndex);
|
|
}
|
|
else
|
|
{
|
|
// we have missing influence vert, we weight to root
|
|
WedgeInfluenceIndices.Add(0);
|
|
|
|
// add warning message
|
|
if (BuildData.OutWarningMessages)
|
|
{
|
|
BuildData.OutWarningMessages->Add(FText::Format(FText::FromString("Missing influence on vert {0}. Weighting it to root."), FText::FromString(FString::FromInt(BuildData.Wedges[WedgeIndex].iVertex))));
|
|
if (BuildData.OutWarningNames)
|
|
{
|
|
BuildData.OutWarningNames->Add(FFbxErrors::SkeletalMesh_VertMissingInfluences);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
check(BuildData.Wedges.Num() == WedgeInfluenceIndices.Num());
|
|
|
|
TArray<FSkeletalMeshVertIndexAndZ> VertIndexAndZ;
|
|
TArray<FSoftSkinBuildVertex> RawVertices;
|
|
|
|
VertIndexAndZ.Empty(BuildData.Points.Num());
|
|
RawVertices.Reserve(BuildData.Points.Num());
|
|
|
|
for (int32 FaceIndex = 0; FaceIndex < BuildData.Faces.Num(); FaceIndex++)
|
|
{
|
|
// Only update the status progress bar if we are in the game thread and every thousand faces.
|
|
// Updating status is extremely slow
|
|
if (FaceIndex % 5000 == 0)
|
|
{
|
|
UpdateSlowTask(FaceIndex, BuildData.Faces.Num());
|
|
}
|
|
|
|
const FMeshFace& Face = BuildData.Faces[FaceIndex];
|
|
|
|
for (int32 VertexIndex = 0; VertexIndex < 3; VertexIndex++)
|
|
{
|
|
FSoftSkinBuildVertex Vertex;
|
|
const uint32 WedgeIndex = BuildData.GetWedgeIndex(FaceIndex, VertexIndex);
|
|
const FMeshWedge& Wedge = BuildData.Wedges[WedgeIndex];
|
|
|
|
Vertex.Position = BuildData.GetVertexPosition(FaceIndex, VertexIndex);
|
|
|
|
FVector TangentX, TangentY, TangentZ;
|
|
TangentX = BuildData.TangentX[WedgeIndex].GetSafeNormal();
|
|
TangentY = BuildData.TangentY[WedgeIndex].GetSafeNormal();
|
|
TangentZ = BuildData.TangentZ[WedgeIndex].GetSafeNormal();
|
|
|
|
/*if (BuildData.BuildOptions.bComputeNormals || BuildData.BuildOptions.bComputeTangents)
|
|
{
|
|
TangentX = BuildData.TangentX[VertexIndex].GetSafeNormal();
|
|
TangentY = BuildData.TangentY[VertexIndex].GetSafeNormal();
|
|
|
|
if( BuildData.BuildOptions.bComputeNormals )
|
|
{
|
|
TangentZ = BuildData.TangentZ[VertexIndex].GetSafeNormal();
|
|
}
|
|
else
|
|
{
|
|
//TangentZ = Face.TangentZ[VertexIndex];
|
|
}
|
|
|
|
TangentY -= TangentX * (TangentX | TangentY);
|
|
TangentY.Normalize();
|
|
|
|
TangentX -= TangentZ * (TangentZ | TangentX);
|
|
TangentY -= TangentZ * (TangentZ | TangentY);
|
|
|
|
TangentX.Normalize();
|
|
TangentY.Normalize();
|
|
}
|
|
else*/
|
|
{
|
|
//TangentX = Face.TangentX[VertexIndex];
|
|
//TangentY = Face.TangentY[VertexIndex];
|
|
//TangentZ = Face.TangentZ[VertexIndex];
|
|
|
|
// Normalize overridden tangents. Its possible for them to import un-normalized.
|
|
TangentX.Normalize();
|
|
TangentY.Normalize();
|
|
TangentZ.Normalize();
|
|
}
|
|
|
|
Vertex.TangentX = TangentX;
|
|
Vertex.TangentY = TangentY;
|
|
Vertex.TangentZ = TangentZ;
|
|
|
|
FMemory::Memcpy(Vertex.UVs, Wedge.UVs, sizeof(FVector2D)*MAX_TEXCOORDS);
|
|
Vertex.Color = Wedge.Color;
|
|
|
|
{
|
|
// Count the influences.
|
|
int32 InfIdx = WedgeInfluenceIndices[Face.iWedge[VertexIndex]];
|
|
int32 LookIdx = InfIdx;
|
|
|
|
uint32 InfluenceCount = 0;
|
|
while (BuildData.Influences.IsValidIndex(LookIdx) && (BuildData.Influences[LookIdx].VertIndex == Wedge.iVertex))
|
|
{
|
|
InfluenceCount++;
|
|
LookIdx++;
|
|
}
|
|
InfluenceCount = FMath::Min<uint32>(InfluenceCount, MAX_TOTAL_INFLUENCES);
|
|
|
|
// Setup the vertex influences.
|
|
Vertex.InfluenceBones[0] = 0;
|
|
Vertex.InfluenceWeights[0] = 255;
|
|
for (uint32 i = 1; i < MAX_TOTAL_INFLUENCES; i++)
|
|
{
|
|
Vertex.InfluenceBones[i] = 0;
|
|
Vertex.InfluenceWeights[i] = 0;
|
|
}
|
|
|
|
uint32 TotalInfluenceWeight = 0;
|
|
for (uint32 i = 0; i < InfluenceCount; i++)
|
|
{
|
|
FBoneIndexType BoneIndex = (FBoneIndexType)BuildData.Influences[InfIdx + i].BoneIndex;
|
|
if (BoneIndex >= BuildData.RefSkeleton.GetRawBoneNum())
|
|
continue;
|
|
|
|
Vertex.InfluenceBones[i] = BoneIndex;
|
|
Vertex.InfluenceWeights[i] = (uint8)(BuildData.Influences[InfIdx + i].Weight * 255.0f);
|
|
TotalInfluenceWeight += Vertex.InfluenceWeights[i];
|
|
}
|
|
Vertex.InfluenceWeights[0] += 255 - TotalInfluenceWeight;
|
|
}
|
|
|
|
// Add the vertex as well as its original index in the points array
|
|
Vertex.PointWedgeIdx = Wedge.iVertex;
|
|
|
|
int32 RawIndex = RawVertices.Add(Vertex);
|
|
|
|
// Add an efficient way to find dupes of this vertex later for fast combining of vertices
|
|
FSkeletalMeshVertIndexAndZ IAndZ;
|
|
IAndZ.Index = RawIndex;
|
|
IAndZ.Z = Vertex.Position.Z;
|
|
|
|
VertIndexAndZ.Add(IAndZ);
|
|
}
|
|
}
|
|
|
|
// Generate chunks and their vertices and indices
|
|
SkeletalMeshTools::BuildSkeletalMeshChunks(BuildData.Faces, RawVertices, VertIndexAndZ, BuildData.BuildOptions.bKeepOverlappingVertices, BuildData.Chunks, BuildData.bTooManyVerts);
|
|
|
|
// Chunk vertices to satisfy the requested limit.
|
|
const uint32 MaxGPUSkinBones = FGPUBaseSkinVertexFactory::GetMaxGPUSkinBones();
|
|
check(MaxGPUSkinBones <= FGPUBaseSkinVertexFactory::GHardwareMaxGPUSkinBones);
|
|
SkeletalMeshTools::ChunkSkinnedVertices(BuildData.Chunks, MaxGPUSkinBones);
|
|
|
|
EndSlowTask();
|
|
|
|
Stage = EStage::GenerateRendering;
|
|
return true;
|
|
}
|
|
|
|
void BeginSlowTask()
|
|
{
|
|
if (IsInGameThread())
|
|
{
|
|
GWarn->BeginSlowTask(NSLOCTEXT("UnrealEd", "ProcessingSkeletalTriangles", "Processing Mesh Triangles"), true);
|
|
}
|
|
}
|
|
|
|
void UpdateSlowTask(int32 Numerator, int32 Denominator)
|
|
{
|
|
if (IsInGameThread())
|
|
{
|
|
GWarn->StatusUpdate(Numerator, Denominator, NSLOCTEXT("UnrealEd", "ProcessingSkeletalTriangles", "Processing Mesh Triangles"));
|
|
}
|
|
}
|
|
|
|
void EndSlowTask()
|
|
{
|
|
if (IsInGameThread())
|
|
{
|
|
GWarn->EndSlowTask();
|
|
}
|
|
}
|
|
|
|
private:
|
|
enum class EStage
|
|
{
|
|
Uninit,
|
|
Prepared,
|
|
GenerateRendering,
|
|
};
|
|
|
|
TIndirectArray<TMultiMap<int32, int32> > LODOverlappingCorners;
|
|
EStage Stage;
|
|
};
|
|
|
|
bool FMeshUtilities::BuildSkeletalMesh(FStaticLODModel& LODModel, const FReferenceSkeleton& RefSkeleton, const TArray<FVertInfluence>& Influences, const TArray<FMeshWedge>& Wedges, const TArray<FMeshFace>& Faces, const TArray<FVector>& Points, const TArray<int32>& PointToOriginalMap, const MeshBuildOptions& BuildOptions, TArray<FText> * OutWarningMessages, TArray<FName> * OutWarningNames)
|
|
{
|
|
#if WITH_EDITORONLY_DATA
|
|
// Temporarily supporting both import paths
|
|
if (!BuildOptions.bUseMikkTSpace)
|
|
{
|
|
return BuildSkeletalMesh_Legacy(LODModel, RefSkeleton, Influences, Wedges, Faces, Points, PointToOriginalMap, BuildOptions.bKeepOverlappingVertices, BuildOptions.bComputeNormals, BuildOptions.bComputeTangents, OutWarningMessages, OutWarningNames);
|
|
}
|
|
|
|
SkeletalMeshBuildData BuildData(
|
|
LODModel,
|
|
RefSkeleton,
|
|
Influences,
|
|
Wedges,
|
|
Faces,
|
|
Points,
|
|
PointToOriginalMap,
|
|
BuildOptions,
|
|
OutWarningMessages,
|
|
OutWarningNames);
|
|
|
|
FSkeletalMeshUtilityBuilder Builder;
|
|
if (!Builder.PrepareSourceMesh(&BuildData))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Builder.GenerateSkeletalRenderMesh(&BuildData))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Build the skeletal model from chunks.
|
|
Builder.BeginSlowTask();
|
|
BuildSkeletalModelFromChunks(BuildData.LODModel, BuildData.RefSkeleton, BuildData.Chunks, BuildData.PointToOriginalMap);
|
|
Builder.EndSlowTask();
|
|
|
|
// Only show these warnings if in the game thread. When importing morph targets, this function can run in another thread and these warnings dont prevent the mesh from importing
|
|
if (IsInGameThread())
|
|
{
|
|
bool bHasBadSections = false;
|
|
for (int32 SectionIndex = 0; SectionIndex < BuildData.LODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
FSkelMeshSection& Section = BuildData.LODModel.Sections[SectionIndex];
|
|
bHasBadSections |= (Section.NumTriangles == 0);
|
|
|
|
// Log info about the section.
|
|
UE_LOG(LogSkeletalMesh, Log, TEXT("Section %u: Material=%u, %u triangles"),
|
|
SectionIndex,
|
|
Section.MaterialIndex,
|
|
Section.NumTriangles
|
|
);
|
|
}
|
|
if (bHasBadSections)
|
|
{
|
|
FText BadSectionMessage(NSLOCTEXT("UnrealEd", "Error_SkeletalMeshHasBadSections", "Input mesh has a section with no triangles. This mesh may not render properly."));
|
|
if (BuildData.OutWarningMessages)
|
|
{
|
|
BuildData.OutWarningMessages->Add(BadSectionMessage);
|
|
if (BuildData.OutWarningNames)
|
|
{
|
|
BuildData.OutWarningNames->Add(FFbxErrors::SkeletalMesh_SectionWithNoTriangle);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FMessageDialog::Open(EAppMsgType::Ok, BadSectionMessage);
|
|
}
|
|
}
|
|
|
|
if (BuildData.bTooManyVerts)
|
|
{
|
|
FText TooManyVertsMessage(NSLOCTEXT("UnrealEd", "Error_SkeletalMeshTooManyVertices", "Input mesh has too many vertices. The generated mesh will be corrupt! Consider adding extra materials to split up the source mesh into smaller chunks."));
|
|
|
|
if (BuildData.OutWarningMessages)
|
|
{
|
|
BuildData.OutWarningMessages->Add(TooManyVertsMessage);
|
|
if (BuildData.OutWarningNames)
|
|
{
|
|
BuildData.OutWarningNames->Add(FFbxErrors::SkeletalMesh_TooManyVertices);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FMessageDialog::Open(EAppMsgType::Ok, TooManyVertsMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
#else
|
|
if (OutWarningMessages)
|
|
{
|
|
OutWarningMessages->Add(FText::FromString(TEXT("Cannot call FMeshUtilities::BuildSkeletalMesh on a console!")));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSkeletalMesh, Fatal, TEXT("Cannot call FMeshUtilities::BuildSkeletalMesh on a console!"));
|
|
}
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
//@TODO: The OutMessages has to be a struct that contains FText/FName, or make it Token and add that as error. Needs re-work. Temporary workaround for now.
|
|
bool FMeshUtilities::BuildSkeletalMesh_Legacy(FStaticLODModel& LODModel, const FReferenceSkeleton& RefSkeleton, const TArray<FVertInfluence>& Influences, const TArray<FMeshWedge>& Wedges, const TArray<FMeshFace>& Faces, const TArray<FVector>& Points, const TArray<int32>& PointToOriginalMap, bool bKeepOverlappingVertices, bool bComputeNormals, bool bComputeTangents, TArray<FText> * OutWarningMessages, TArray<FName> * OutWarningNames)
|
|
{
|
|
bool bTooManyVerts = false;
|
|
|
|
check(PointToOriginalMap.Num() == Points.Num());
|
|
|
|
// Calculate face tangent vectors.
|
|
TArray<FVector> FaceTangentX;
|
|
TArray<FVector> FaceTangentY;
|
|
FaceTangentX.AddUninitialized(Faces.Num());
|
|
FaceTangentY.AddUninitialized(Faces.Num());
|
|
|
|
if (bComputeNormals || bComputeTangents)
|
|
{
|
|
for (int32 FaceIndex = 0; FaceIndex < Faces.Num(); FaceIndex++)
|
|
{
|
|
FVector P1 = Points[Wedges[Faces[FaceIndex].iWedge[0]].iVertex],
|
|
P2 = Points[Wedges[Faces[FaceIndex].iWedge[1]].iVertex],
|
|
P3 = Points[Wedges[Faces[FaceIndex].iWedge[2]].iVertex];
|
|
FVector TriangleNormal = FPlane(P3, P2, P1);
|
|
FMatrix ParameterToLocal(
|
|
FPlane(P2.X - P1.X, P2.Y - P1.Y, P2.Z - P1.Z, 0),
|
|
FPlane(P3.X - P1.X, P3.Y - P1.Y, P3.Z - P1.Z, 0),
|
|
FPlane(P1.X, P1.Y, P1.Z, 0),
|
|
FPlane(0, 0, 0, 1)
|
|
);
|
|
|
|
float U1 = Wedges[Faces[FaceIndex].iWedge[0]].UVs[0].X,
|
|
U2 = Wedges[Faces[FaceIndex].iWedge[1]].UVs[0].X,
|
|
U3 = Wedges[Faces[FaceIndex].iWedge[2]].UVs[0].X,
|
|
V1 = Wedges[Faces[FaceIndex].iWedge[0]].UVs[0].Y,
|
|
V2 = Wedges[Faces[FaceIndex].iWedge[1]].UVs[0].Y,
|
|
V3 = Wedges[Faces[FaceIndex].iWedge[2]].UVs[0].Y;
|
|
|
|
FMatrix ParameterToTexture(
|
|
FPlane(U2 - U1, V2 - V1, 0, 0),
|
|
FPlane(U3 - U1, V3 - V1, 0, 0),
|
|
FPlane(U1, V1, 1, 0),
|
|
FPlane(0, 0, 0, 1)
|
|
);
|
|
|
|
FMatrix TextureToLocal = ParameterToTexture.Inverse() * ParameterToLocal;
|
|
FVector TangentX = TextureToLocal.TransformVector(FVector(1, 0, 0)).GetSafeNormal(),
|
|
TangentY = TextureToLocal.TransformVector(FVector(0, 1, 0)).GetSafeNormal(),
|
|
TangentZ;
|
|
|
|
TangentX = TangentX - TriangleNormal * (TangentX | TriangleNormal);
|
|
TangentY = TangentY - TriangleNormal * (TangentY | TriangleNormal);
|
|
|
|
FaceTangentX[FaceIndex] = TangentX.GetSafeNormal();
|
|
FaceTangentY[FaceIndex] = TangentY.GetSafeNormal();
|
|
}
|
|
}
|
|
|
|
TArray<int32> WedgeInfluenceIndices;
|
|
|
|
// Find wedge influences.
|
|
TMap<uint32, uint32> VertexIndexToInfluenceIndexMap;
|
|
|
|
for (uint32 LookIdx = 0; LookIdx < (uint32)Influences.Num(); LookIdx++)
|
|
{
|
|
// Order matters do not allow the map to overwrite an existing value.
|
|
if (!VertexIndexToInfluenceIndexMap.Find(Influences[LookIdx].VertIndex))
|
|
{
|
|
VertexIndexToInfluenceIndexMap.Add(Influences[LookIdx].VertIndex, LookIdx);
|
|
}
|
|
}
|
|
|
|
for (int32 WedgeIndex = 0; WedgeIndex < Wedges.Num(); WedgeIndex++)
|
|
{
|
|
uint32* InfluenceIndex = VertexIndexToInfluenceIndexMap.Find(Wedges[WedgeIndex].iVertex);
|
|
|
|
if (InfluenceIndex)
|
|
{
|
|
WedgeInfluenceIndices.Add(*InfluenceIndex);
|
|
}
|
|
else
|
|
{
|
|
// we have missing influence vert, we weight to root
|
|
WedgeInfluenceIndices.Add(0);
|
|
|
|
// add warning message
|
|
if (OutWarningMessages)
|
|
{
|
|
OutWarningMessages->Add(FText::Format(FText::FromString("Missing influence on vert {0}. Weighting it to root."), FText::FromString(FString::FromInt(Wedges[WedgeIndex].iVertex))));
|
|
if (OutWarningNames)
|
|
{
|
|
OutWarningNames->Add(FFbxErrors::SkeletalMesh_VertMissingInfluences);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
check(Wedges.Num() == WedgeInfluenceIndices.Num());
|
|
|
|
// Calculate smooth wedge tangent vectors.
|
|
|
|
if (IsInGameThread())
|
|
{
|
|
// Only update status if in the game thread. When importing morph targets, this function can run in another thread
|
|
GWarn->BeginSlowTask(NSLOCTEXT("UnrealEd", "ProcessingSkeletalTriangles", "Processing Mesh Triangles"), true);
|
|
}
|
|
|
|
|
|
// To accelerate generation of adjacency, we'll create a table that maps each vertex index
|
|
// to its overlapping vertices, and a table that maps a vertex to the its influenced faces
|
|
TMultiMap<int32, int32> Vert2Duplicates;
|
|
TMultiMap<int32, int32> Vert2Faces;
|
|
TArray<FSkeletalMeshVertIndexAndZ> VertIndexAndZ;
|
|
{
|
|
// Create a list of vertex Z/index pairs
|
|
VertIndexAndZ.Empty(Points.Num());
|
|
for (int32 i = 0; i < Points.Num(); i++)
|
|
{
|
|
FSkeletalMeshVertIndexAndZ iandz;
|
|
iandz.Index = i;
|
|
iandz.Z = Points[i].Z;
|
|
VertIndexAndZ.Add(iandz);
|
|
}
|
|
|
|
// Sorting function for vertex Z/index pairs
|
|
struct FCompareFSkeletalMeshVertIndexAndZ
|
|
{
|
|
FORCEINLINE bool operator()(const FSkeletalMeshVertIndexAndZ& A, const FSkeletalMeshVertIndexAndZ& B) const
|
|
{
|
|
return A.Z < B.Z;
|
|
}
|
|
};
|
|
|
|
// Sort the vertices by z value
|
|
VertIndexAndZ.Sort(FCompareFSkeletalMeshVertIndexAndZ());
|
|
|
|
// Search for duplicates, quickly!
|
|
for (int32 i = 0; i < VertIndexAndZ.Num(); i++)
|
|
{
|
|
// only need to search forward, since we add pairs both ways
|
|
for (int32 j = i + 1; j < VertIndexAndZ.Num(); j++)
|
|
{
|
|
if (FMath::Abs(VertIndexAndZ[j].Z - VertIndexAndZ[i].Z) > THRESH_POINTS_ARE_SAME)
|
|
{
|
|
// our list is sorted, so there can't be any more dupes
|
|
break;
|
|
}
|
|
|
|
// check to see if the points are really overlapping
|
|
if (PointsEqual(
|
|
Points[VertIndexAndZ[i].Index],
|
|
Points[VertIndexAndZ[j].Index]))
|
|
{
|
|
Vert2Duplicates.Add(VertIndexAndZ[i].Index, VertIndexAndZ[j].Index);
|
|
Vert2Duplicates.Add(VertIndexAndZ[j].Index, VertIndexAndZ[i].Index);
|
|
}
|
|
}
|
|
}
|
|
|
|
// we are done with this
|
|
VertIndexAndZ.Reset();
|
|
|
|
// now create a map from vert indices to faces
|
|
for (int32 FaceIndex = 0; FaceIndex < Faces.Num(); FaceIndex++)
|
|
{
|
|
const FMeshFace& Face = Faces[FaceIndex];
|
|
for (int32 VertexIndex = 0; VertexIndex < 3; VertexIndex++)
|
|
{
|
|
Vert2Faces.AddUnique(Wedges[Face.iWedge[VertexIndex]].iVertex, FaceIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
TArray<FSkinnedMeshChunk*> Chunks;
|
|
TArray<int32> AdjacentFaces;
|
|
TArray<int32> DupVerts;
|
|
TArray<int32> DupFaces;
|
|
|
|
// List of raw calculated vertices that will be merged later
|
|
TArray<FSoftSkinBuildVertex> RawVertices;
|
|
RawVertices.Reserve(Points.Num());
|
|
|
|
// Create a list of vertex Z/index pairs
|
|
|
|
for (int32 FaceIndex = 0; FaceIndex < Faces.Num(); FaceIndex++)
|
|
{
|
|
// Only update the status progress bar if we are in the gamethread and every thousand faces.
|
|
// Updating status is extremely slow
|
|
if (FaceIndex % 5000 == 0 && IsInGameThread())
|
|
{
|
|
// Only update status if in the game thread. When importing morph targets, this function can run in another thread
|
|
GWarn->StatusUpdate(FaceIndex, Faces.Num(), NSLOCTEXT("UnrealEd", "ProcessingSkeletalTriangles", "Processing Mesh Triangles"));
|
|
}
|
|
|
|
const FMeshFace& Face = Faces[FaceIndex];
|
|
|
|
FVector VertexTangentX[3],
|
|
VertexTangentY[3],
|
|
VertexTangentZ[3];
|
|
|
|
if (bComputeNormals || bComputeTangents)
|
|
{
|
|
for (int32 VertexIndex = 0; VertexIndex < 3; VertexIndex++)
|
|
{
|
|
VertexTangentX[VertexIndex] = FVector::ZeroVector;
|
|
VertexTangentY[VertexIndex] = FVector::ZeroVector;
|
|
VertexTangentZ[VertexIndex] = FVector::ZeroVector;
|
|
}
|
|
|
|
FVector TriangleNormal = FPlane(
|
|
Points[Wedges[Face.iWedge[2]].iVertex],
|
|
Points[Wedges[Face.iWedge[1]].iVertex],
|
|
Points[Wedges[Face.iWedge[0]].iVertex]
|
|
);
|
|
float Determinant = FVector::Triple(FaceTangentX[FaceIndex], FaceTangentY[FaceIndex], TriangleNormal);
|
|
|
|
// Start building a list of faces adjacent to this triangle
|
|
AdjacentFaces.Reset();
|
|
for (int32 VertexIndex = 0; VertexIndex < 3; VertexIndex++)
|
|
{
|
|
int32 vert = Wedges[Face.iWedge[VertexIndex]].iVertex;
|
|
DupVerts.Reset();
|
|
Vert2Duplicates.MultiFind(vert, DupVerts);
|
|
DupVerts.Add(vert); // I am a "dupe" of myself
|
|
for (int32 k = 0; k < DupVerts.Num(); k++)
|
|
{
|
|
DupFaces.Reset();
|
|
Vert2Faces.MultiFind(DupVerts[k], DupFaces);
|
|
for (int32 l = 0; l < DupFaces.Num(); l++)
|
|
{
|
|
AdjacentFaces.AddUnique(DupFaces[l]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process adjacent faces
|
|
for (int32 AdjacentFaceIndex = 0; AdjacentFaceIndex < AdjacentFaces.Num(); AdjacentFaceIndex++)
|
|
{
|
|
int32 OtherFaceIndex = AdjacentFaces[AdjacentFaceIndex];
|
|
const FMeshFace& OtherFace = Faces[OtherFaceIndex];
|
|
FVector OtherTriangleNormal = FPlane(
|
|
Points[Wedges[OtherFace.iWedge[2]].iVertex],
|
|
Points[Wedges[OtherFace.iWedge[1]].iVertex],
|
|
Points[Wedges[OtherFace.iWedge[0]].iVertex]
|
|
);
|
|
float OtherFaceDeterminant = FVector::Triple(FaceTangentX[OtherFaceIndex], FaceTangentY[OtherFaceIndex], OtherTriangleNormal);
|
|
|
|
for (int32 VertexIndex = 0; VertexIndex < 3; VertexIndex++)
|
|
{
|
|
for (int32 OtherVertexIndex = 0; OtherVertexIndex < 3; OtherVertexIndex++)
|
|
{
|
|
if (PointsEqual(
|
|
Points[Wedges[OtherFace.iWedge[OtherVertexIndex]].iVertex],
|
|
Points[Wedges[Face.iWedge[VertexIndex]].iVertex]
|
|
))
|
|
{
|
|
if (Determinant * OtherFaceDeterminant > 0.0f && SkeletalMeshTools::SkeletalMesh_UVsEqual(Wedges[OtherFace.iWedge[OtherVertexIndex]], Wedges[Face.iWedge[VertexIndex]]))
|
|
{
|
|
VertexTangentX[VertexIndex] += FaceTangentX[OtherFaceIndex];
|
|
VertexTangentY[VertexIndex] += FaceTangentY[OtherFaceIndex];
|
|
}
|
|
|
|
// Only contribute 'normal' if the vertices are truly one and the same to obey hard "smoothing" edges baked into
|
|
// the mesh by vertex duplication
|
|
if (Wedges[OtherFace.iWedge[OtherVertexIndex]].iVertex == Wedges[Face.iWedge[VertexIndex]].iVertex)
|
|
{
|
|
VertexTangentZ[VertexIndex] += OtherTriangleNormal;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 VertexIndex = 0; VertexIndex < 3; VertexIndex++)
|
|
{
|
|
FSoftSkinBuildVertex Vertex;
|
|
|
|
Vertex.Position = Points[Wedges[Face.iWedge[VertexIndex]].iVertex];
|
|
|
|
FVector TangentX, TangentY, TangentZ;
|
|
|
|
if (bComputeNormals || bComputeTangents)
|
|
{
|
|
TangentX = VertexTangentX[VertexIndex].GetSafeNormal();
|
|
TangentY = VertexTangentY[VertexIndex].GetSafeNormal();
|
|
|
|
if (bComputeNormals)
|
|
{
|
|
TangentZ = VertexTangentZ[VertexIndex].GetSafeNormal();
|
|
}
|
|
else
|
|
{
|
|
TangentZ = Face.TangentZ[VertexIndex];
|
|
}
|
|
|
|
TangentY -= TangentX * (TangentX | TangentY);
|
|
TangentY.Normalize();
|
|
|
|
TangentX -= TangentZ * (TangentZ | TangentX);
|
|
TangentY -= TangentZ * (TangentZ | TangentY);
|
|
|
|
TangentX.Normalize();
|
|
TangentY.Normalize();
|
|
}
|
|
else
|
|
{
|
|
TangentX = Face.TangentX[VertexIndex];
|
|
TangentY = Face.TangentY[VertexIndex];
|
|
TangentZ = Face.TangentZ[VertexIndex];
|
|
|
|
// Normalize overridden tangents. Its possible for them to import un-normalized.
|
|
TangentX.Normalize();
|
|
TangentY.Normalize();
|
|
TangentZ.Normalize();
|
|
}
|
|
|
|
Vertex.TangentX = TangentX;
|
|
Vertex.TangentY = TangentY;
|
|
Vertex.TangentZ = TangentZ;
|
|
|
|
FMemory::Memcpy(Vertex.UVs, Wedges[Face.iWedge[VertexIndex]].UVs, sizeof(FVector2D)*MAX_TEXCOORDS);
|
|
Vertex.Color = Wedges[Face.iWedge[VertexIndex]].Color;
|
|
|
|
{
|
|
// Count the influences.
|
|
|
|
int32 InfIdx = WedgeInfluenceIndices[Face.iWedge[VertexIndex]];
|
|
int32 LookIdx = InfIdx;
|
|
|
|
uint32 InfluenceCount = 0;
|
|
while (Influences.IsValidIndex(LookIdx) && (Influences[LookIdx].VertIndex == Wedges[Face.iWedge[VertexIndex]].iVertex))
|
|
{
|
|
InfluenceCount++;
|
|
LookIdx++;
|
|
}
|
|
InfluenceCount = FMath::Min<uint32>(InfluenceCount, MAX_TOTAL_INFLUENCES);
|
|
|
|
// Setup the vertex influences.
|
|
|
|
Vertex.InfluenceBones[0] = 0;
|
|
Vertex.InfluenceWeights[0] = 255;
|
|
for (uint32 i = 1; i < MAX_TOTAL_INFLUENCES; i++)
|
|
{
|
|
Vertex.InfluenceBones[i] = 0;
|
|
Vertex.InfluenceWeights[i] = 0;
|
|
}
|
|
|
|
uint32 TotalInfluenceWeight = 0;
|
|
for (uint32 i = 0; i < InfluenceCount; i++)
|
|
{
|
|
FBoneIndexType BoneIndex = (FBoneIndexType)Influences[InfIdx + i].BoneIndex;
|
|
if (BoneIndex >= RefSkeleton.GetRawBoneNum())
|
|
continue;
|
|
|
|
Vertex.InfluenceBones[i] = BoneIndex;
|
|
Vertex.InfluenceWeights[i] = (uint8)(Influences[InfIdx + i].Weight * 255.0f);
|
|
TotalInfluenceWeight += Vertex.InfluenceWeights[i];
|
|
}
|
|
Vertex.InfluenceWeights[0] += 255 - TotalInfluenceWeight;
|
|
}
|
|
|
|
// Add the vertex as well as its original index in the points array
|
|
Vertex.PointWedgeIdx = Wedges[Face.iWedge[VertexIndex]].iVertex;
|
|
|
|
int32 RawIndex = RawVertices.Add(Vertex);
|
|
|
|
// Add an efficient way to find dupes of this vertex later for fast combining of vertices
|
|
FSkeletalMeshVertIndexAndZ IAndZ;
|
|
IAndZ.Index = RawIndex;
|
|
IAndZ.Z = Vertex.Position.Z;
|
|
|
|
VertIndexAndZ.Add(IAndZ);
|
|
}
|
|
}
|
|
|
|
// Generate chunks and their vertices and indices
|
|
SkeletalMeshTools::BuildSkeletalMeshChunks(Faces, RawVertices, VertIndexAndZ, bKeepOverlappingVertices, Chunks, bTooManyVerts);
|
|
|
|
// Chunk vertices to satisfy the requested limit.
|
|
const uint32 MaxGPUSkinBones = FGPUBaseSkinVertexFactory::GetMaxGPUSkinBones();
|
|
check(MaxGPUSkinBones <= FGPUBaseSkinVertexFactory::GHardwareMaxGPUSkinBones);
|
|
SkeletalMeshTools::ChunkSkinnedVertices(Chunks, MaxGPUSkinBones);
|
|
|
|
// Build the skeletal model from chunks.
|
|
BuildSkeletalModelFromChunks(LODModel, RefSkeleton, Chunks, PointToOriginalMap);
|
|
|
|
if (IsInGameThread())
|
|
{
|
|
// Only update status if in the game thread. When importing morph targets, this function can run in another thread
|
|
GWarn->EndSlowTask();
|
|
}
|
|
|
|
// Only show these warnings if in the game thread. When importing morph targets, this function can run in another thread and these warnings dont prevent the mesh from importing
|
|
if (IsInGameThread())
|
|
{
|
|
bool bHasBadSections = false;
|
|
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
bHasBadSections |= (Section.NumTriangles == 0);
|
|
|
|
// Log info about the section.
|
|
UE_LOG(LogSkeletalMesh, Log, TEXT("Section %u: Material=%u, %u triangles"),
|
|
SectionIndex,
|
|
Section.MaterialIndex,
|
|
Section.NumTriangles
|
|
);
|
|
}
|
|
if (bHasBadSections)
|
|
{
|
|
FText BadSectionMessage(NSLOCTEXT("UnrealEd", "Error_SkeletalMeshHasBadSections", "Input mesh has a section with no triangles. This mesh may not render properly."));
|
|
if (OutWarningMessages)
|
|
{
|
|
OutWarningMessages->Add(BadSectionMessage);
|
|
if (OutWarningNames)
|
|
{
|
|
OutWarningNames->Add(FFbxErrors::SkeletalMesh_SectionWithNoTriangle);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FMessageDialog::Open(EAppMsgType::Ok, BadSectionMessage);
|
|
}
|
|
}
|
|
|
|
if (bTooManyVerts)
|
|
{
|
|
FText TooManyVertsMessage(NSLOCTEXT("UnrealEd", "Error_SkeletalMeshTooManyVertices", "Input mesh has too many vertices. The generated mesh will be corrupt! Consider adding extra materials to split up the source mesh into smaller chunks."));
|
|
|
|
if (OutWarningMessages)
|
|
{
|
|
OutWarningMessages->Add(TooManyVertsMessage);
|
|
if (OutWarningNames)
|
|
{
|
|
OutWarningNames->Add(FFbxErrors::SkeletalMesh_TooManyVertices);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FMessageDialog::Open(EAppMsgType::Ok, TooManyVertsMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool NonOpaqueMaterialPredicate(UStaticMeshComponent* InMesh)
|
|
{
|
|
TArray<UMaterialInterface*> OutMaterials;
|
|
InMesh->GetUsedMaterials(OutMaterials);
|
|
for (auto Material : OutMaterials)
|
|
{
|
|
if (Material == nullptr || Material->GetBlendMode() != BLEND_Opaque)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static FIntPoint ConditionalImageResize(const FIntPoint& SrcSize, const FIntPoint& DesiredSize, TArray<FColor>& InOutImage, bool bLinearSpace)
|
|
{
|
|
const int32 NumDesiredSamples = DesiredSize.X*DesiredSize.Y;
|
|
if (InOutImage.Num() && InOutImage.Num() != NumDesiredSamples)
|
|
{
|
|
check(InOutImage.Num() == SrcSize.X*SrcSize.Y);
|
|
TArray<FColor> OutImage;
|
|
if (NumDesiredSamples > 0)
|
|
{
|
|
FImageUtils::ImageResize(SrcSize.X, SrcSize.Y, InOutImage, DesiredSize.X, DesiredSize.Y, OutImage, bLinearSpace);
|
|
}
|
|
Exchange(InOutImage, OutImage);
|
|
return DesiredSize;
|
|
}
|
|
|
|
return SrcSize;
|
|
}
|
|
|
|
static void RetrieveValidStaticMeshComponentsForMerging(AActor* InActor, TArray<UStaticMeshComponent*>& OutComponents)
|
|
{
|
|
TInlineComponentArray<UStaticMeshComponent*> Components;
|
|
InActor->GetComponents<UStaticMeshComponent>(Components);
|
|
// TODO: support derived classes from static component
|
|
Components.RemoveAll([](UStaticMeshComponent* Val){ return !(Val->GetClass() == UStaticMeshComponent::StaticClass() || Val->IsA(USplineMeshComponent::StaticClass())); });
|
|
|
|
// TODO: support non-opaque materials
|
|
//Components.RemoveAll(&NonOpaqueMaterialPredicate);
|
|
OutComponents.Append(Components);
|
|
}
|
|
|
|
static void CheckWrappingUVs(TArray<FRawMeshExt>& SourceMeshes, TArray<bool>& MeshShouldBakeVertexData)
|
|
{
|
|
const uint32 MeshCount = SourceMeshes.Num();
|
|
for (uint32 MeshIndex = 0; MeshIndex < MeshCount; ++MeshIndex)
|
|
{
|
|
FRawMeshExt& SourceMesh = SourceMeshes[MeshIndex];
|
|
const int32 LODIndex = SourceMeshes[MeshIndex].ExportLODIndex;
|
|
if (SourceMesh.bShouldExportLOD[LODIndex])
|
|
{
|
|
FRawMesh* RawMesh = SourceMesh.MeshLODData[LODIndex].RawMesh;
|
|
check(RawMesh);
|
|
|
|
for (uint32 ChannelIndex = 0; ChannelIndex < MAX_MESH_TEXTURE_COORDS; ++ChannelIndex)
|
|
{
|
|
bool bProcessed = false;
|
|
bool bHasCoordinates = (RawMesh->WedgeTexCoords[ChannelIndex].Num() != 0);
|
|
|
|
if (bHasCoordinates)
|
|
{
|
|
FVector2D Min(FLT_MAX, FLT_MAX);
|
|
FVector2D Max(-FLT_MAX, -FLT_MAX);
|
|
for (const FVector2D& Coordinate : RawMesh->WedgeTexCoords[ChannelIndex])
|
|
{
|
|
if ((FMath::IsNegativeFloat(Coordinate.X) || FMath::IsNegativeFloat(Coordinate.Y)) || (Coordinate.X > (1.0f + KINDA_SMALL_NUMBER) || Coordinate.Y > (1.0f + KINDA_SMALL_NUMBER)))
|
|
{
|
|
MeshShouldBakeVertexData[MeshIndex] = true;
|
|
bProcessed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bProcessed)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::CreateProxyMesh(const TArray<AActor*>& InActors, const struct FMeshProxySettings& InMeshProxySettings, UPackage* InOuter, const FString& InProxyBasePackageName, const FGuid InGuid, FCreateProxyDelegate InProxyCreatedDelegate, const bool bAllowAsync, const float ScreenAreaSize)
|
|
{
|
|
// Error/warning checking for input
|
|
if (MeshMerging == NULL)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("No automatic mesh merging module available"));
|
|
return;
|
|
}
|
|
|
|
// Check that the delegate has a func-ptr bound to it
|
|
if (!InProxyCreatedDelegate.IsBound())
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Invalid (unbound) delegate for returning generated proxy mesh"));
|
|
return;
|
|
}
|
|
|
|
// No actors given as input
|
|
if (InActors.Num() == 0)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("No actors specified to generate a proxy mesh for"));
|
|
return;
|
|
}
|
|
|
|
// Base asset name for a new assets
|
|
// In case outer is null ProxyBasePackageName has to be long package name
|
|
if (InOuter == nullptr && FPackageName::IsShortPackageName(InProxyBasePackageName))
|
|
{
|
|
UE_LOG(LogMeshUtilities, Warning, TEXT("Invalid long package name: '%s'."), *InProxyBasePackageName);
|
|
return;
|
|
}
|
|
|
|
FScopedSlowTask SlowTask(100.f, (LOCTEXT("CreateProxyMesh_CreateMesh", "Creating Mesh Proxy")));
|
|
SlowTask.MakeDialog();
|
|
|
|
// Retrieve static mesh components valid for merging from the given set of actors
|
|
TArray<UStaticMeshComponent*> ComponentsToMerge;
|
|
{
|
|
// Collect components to merge
|
|
for (AActor* Actor : InActors)
|
|
{
|
|
RetrieveValidStaticMeshComponentsForMerging(Actor, ComponentsToMerge);
|
|
}
|
|
}
|
|
|
|
// Check if there are actually any static mesh components to merge
|
|
if (ComponentsToMerge.Num() == 0)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("No valid static mesh components found in given set of Actors"));
|
|
return;
|
|
}
|
|
|
|
typedef FIntPoint FMeshIdAndLOD;
|
|
TArray<FRawMeshExt> SourceMeshes;
|
|
TArray<FSectionInfo> UniqueSections;
|
|
TMap<FMeshIdAndLOD, TArray<int32>> GlobalMaterialMap;
|
|
static const int32 ProxyMeshTargetLODLevel = 0;
|
|
|
|
FBoxSphereBounds EstimatedBounds(ForceInitToZero);
|
|
for (const UStaticMeshComponent* StaticMeshComponent : ComponentsToMerge)
|
|
{
|
|
EstimatedBounds = EstimatedBounds + StaticMeshComponent->Bounds;
|
|
}
|
|
|
|
static const float FOVRad = 90.0f * (float)PI / 360.0f;
|
|
static const FMatrix ProjectionMatrix = FPerspectiveMatrix(FOVRad, 1920, 1080, 0.01f);
|
|
FHierarchicalLODUtilitiesModule& Module = FModuleManager::LoadModuleChecked<FHierarchicalLODUtilitiesModule>("HierarchicalLODUtilities");
|
|
IHierarchicalLODUtilities* Utilities = Module.GetUtilities();
|
|
float EstimatedDistance = Utilities->CalculateDrawDistanceFromScreenSize(EstimatedBounds.SphereRadius, ScreenAreaSize, ProjectionMatrix);
|
|
|
|
SlowTask.EnterProgressFrame(5.0f, LOCTEXT("CreateProxyMesh_CollectingMeshes", "Collecting Input Static Meshes"));
|
|
|
|
// Retrieve mesh / material data
|
|
for (const UStaticMeshComponent* StaticMeshComponent : ComponentsToMerge)
|
|
{
|
|
TArray<int32> StaticMeshGlobalMaterialMap;
|
|
FRawMesh* RawMesh = new FRawMesh();
|
|
FMemory::Memzero(RawMesh, sizeof(FRawMesh));
|
|
|
|
const int32 ProxyMeshSourceLODLevel = InMeshProxySettings.bCalculateCorrectLODModel ? Utilities->GetLODLevelForScreenSize(StaticMeshComponent, Utilities->CalculateScreenSizeFromDrawDistance(StaticMeshComponent->Bounds.SphereRadius, ProjectionMatrix, EstimatedDistance)) : 0;
|
|
// Proxy meshes should always propagate vertex colours for material baking
|
|
static const bool bPropagateVertexColours = true;
|
|
|
|
const bool bValidRawMesh = ConstructRawMesh(StaticMeshComponent, ProxyMeshSourceLODLevel, bPropagateVertexColours, *RawMesh, UniqueSections, StaticMeshGlobalMaterialMap);
|
|
|
|
if ( bValidRawMesh )
|
|
{
|
|
// Add constructed raw mesh to source mesh array
|
|
const int32 SourceMeshIndex = SourceMeshes.AddZeroed();
|
|
SourceMeshes[SourceMeshIndex].MeshLODData[ProxyMeshTargetLODLevel].RawMesh = RawMesh;
|
|
SourceMeshes[SourceMeshIndex].bShouldExportLOD[ProxyMeshTargetLODLevel] = true;
|
|
SourceMeshes[SourceMeshIndex].ExportLODIndex = ProxyMeshTargetLODLevel;
|
|
|
|
// Make sure we do now the bounds of our UVs
|
|
//CalculateTextureCoordinateBoundsForRawMesh(*SourceMeshes[SourceMeshIndex].MeshLODData[ProxyMeshTargetLODLevel].RawMesh, SourceMeshes[SourceMeshIndex].MeshLODData[ProxyMeshTargetLODLevel].TexCoordBounds);
|
|
|
|
// Append retrieved materials for this static mesh component to the global material map
|
|
GlobalMaterialMap.Add(FMeshIdAndLOD(SourceMeshIndex, ProxyMeshTargetLODLevel), StaticMeshGlobalMaterialMap);
|
|
}
|
|
}
|
|
|
|
if (SourceMeshes.Num() == 0)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("No valid (or completely culled) raw meshes constructed from static mesh components"));
|
|
return;
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame(InMeshProxySettings.bUseLandscapeCulling ? 40.0f : 45.0f, LOCTEXT("CreateProxyMesh_RemapAndFlatten", "Remapping and Flattening Materials"));
|
|
TArray<bool> MeshShouldBakeVertexData;
|
|
MeshShouldBakeVertexData.AddZeroed(SourceMeshes.Num());
|
|
CheckWrappingUVs(SourceMeshes, MeshShouldBakeVertexData);
|
|
|
|
TMap<FMeshIdAndLOD, TArray<int32> > NewGlobalMaterialMap;
|
|
TArray<FSectionInfo> NewUniqueSections;
|
|
FMaterialUtilities::RemapUniqueMaterialIndices(
|
|
UniqueSections,
|
|
SourceMeshes,
|
|
GlobalMaterialMap,
|
|
InMeshProxySettings.MaterialSettings,
|
|
true, // Always need vertex data for baking materials
|
|
true, // Always want to merge materials
|
|
MeshShouldBakeVertexData,
|
|
NewGlobalMaterialMap,
|
|
NewUniqueSections);
|
|
// Use shared material data.
|
|
Exchange(GlobalMaterialMap, NewGlobalMaterialMap);
|
|
Exchange(UniqueSections, NewUniqueSections);
|
|
|
|
// Flatten Materials
|
|
TArray<FFlattenMaterial> FlattenedMaterials;
|
|
|
|
TArray<UMaterialInterface*> Materials;
|
|
for (const FSectionInfo& Section : UniqueSections)
|
|
{
|
|
Materials.Push(Section.Material);
|
|
}
|
|
|
|
|
|
FlattenMaterialsWithMeshData(Materials, SourceMeshes, GlobalMaterialMap, MeshShouldBakeVertexData, InMeshProxySettings.MaterialSettings, FlattenedMaterials);
|
|
|
|
for (FRawMeshExt& MeshData : SourceMeshes)
|
|
{
|
|
if (MeshData.MeshLODData[MeshData.ExportLODIndex].NewUVs.Num() == 0)
|
|
{
|
|
MeshData.MeshLODData[MeshData.ExportLODIndex].TexCoordBounds.Empty();
|
|
}
|
|
}
|
|
|
|
for (FFlattenMaterial& InMaterial : FlattenedMaterials)
|
|
{
|
|
FMaterialUtilities::OptimizeFlattenMaterial(InMaterial);
|
|
}
|
|
|
|
//For each raw mesh, re-map the material indices from Local to Global material indices space
|
|
for (int32 RawMeshIndex = 0; RawMeshIndex < SourceMeshes.Num(); ++RawMeshIndex)
|
|
{
|
|
const TArray<int32>& GlobalMaterialIndices = *GlobalMaterialMap.Find(FMeshIdAndLOD(RawMeshIndex, ProxyMeshTargetLODLevel));
|
|
TArray<int32>& MaterialIndices = SourceMeshes[RawMeshIndex].MeshLODData[ProxyMeshTargetLODLevel].RawMesh->FaceMaterialIndices;
|
|
int32 MaterialIndicesCount = MaterialIndices.Num();
|
|
|
|
for (int32 TriangleIndex = 0; TriangleIndex < MaterialIndicesCount; ++TriangleIndex)
|
|
{
|
|
int32 LocalMaterialIndex = MaterialIndices[TriangleIndex];
|
|
int32 GlobalMaterialIndex = GlobalMaterialIndices[LocalMaterialIndex];
|
|
|
|
//Assign the new material index to the raw mesh
|
|
MaterialIndices[TriangleIndex] = GlobalMaterialIndex;
|
|
}
|
|
}
|
|
|
|
// Build proxy mesh
|
|
|
|
// Landscape culling
|
|
TArray<FRawMesh*> CullingRawMeshes;
|
|
if (InMeshProxySettings.bUseLandscapeCulling)
|
|
{
|
|
SlowTask.EnterProgressFrame(5.0f, LOCTEXT("CreateProxyMesh_LandscapeCulling", "Applying Landscape Culling"));
|
|
|
|
// Extract landscape proxies and cull volumes from the world
|
|
TArray<ALandscapeProxy*> LandscapeActors;
|
|
TArray<AMeshMergeCullingVolume*> CullVolumes;
|
|
|
|
UWorld* InWorld = InActors[0]->GetWorld();
|
|
|
|
uint32 MaxLandscapeExportLOD = 0;
|
|
if (InWorld->IsValidLowLevel())
|
|
{
|
|
for (FConstLevelIterator Iterator = InWorld->GetLevelIterator(); Iterator; ++Iterator)
|
|
{
|
|
for (AActor* Actor : (*Iterator)->Actors)
|
|
{
|
|
if (Actor)
|
|
{
|
|
ALandscapeProxy* LandscapeProxy = Cast<ALandscapeProxy>(Actor);
|
|
if (LandscapeProxy && LandscapeProxy->bUseLandscapeForCullingInvisibleHLODVertices)
|
|
{
|
|
// Retrieve highest landscape LOD level possible
|
|
MaxLandscapeExportLOD = FMath::Max(MaxLandscapeExportLOD, FMath::CeilLogTwo(LandscapeProxy->SubsectionSizeQuads + 1) - 1);
|
|
LandscapeActors.Add(LandscapeProxy);
|
|
}
|
|
// Check for culling volumes
|
|
AMeshMergeCullingVolume* Volume = Cast<AMeshMergeCullingVolume>(Actor);
|
|
if (Volume)
|
|
{
|
|
// If the mesh's bounds intersect with the volume there is a possibility of culling
|
|
const bool bIntersecting = Volume->EncompassesPoint(EstimatedBounds.Origin, EstimatedBounds.SphereRadius, nullptr);
|
|
if (bIntersecting)
|
|
{
|
|
CullVolumes.Add(Volume);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setting determines the precision at which we should export the landscape for culling (highest, half or lowest)
|
|
const uint32 LandscapeExportLOD = ((float)MaxLandscapeExportLOD * (0.5f * (float)InMeshProxySettings.LandscapeCullingPrecision));
|
|
for (ALandscapeProxy* Landscape : LandscapeActors)
|
|
{
|
|
// Export the landscape to raw mesh format
|
|
FRawMesh* LandscapeRawMesh = new FRawMesh();
|
|
FBoxSphereBounds LandscapeBounds = EstimatedBounds;
|
|
Landscape->ExportToRawMesh(LandscapeExportLOD, *LandscapeRawMesh, LandscapeBounds);
|
|
if (LandscapeRawMesh->VertexPositions.Num())
|
|
{
|
|
CullingRawMeshes.Add(LandscapeRawMesh);
|
|
}
|
|
}
|
|
|
|
// Also add volume mesh data as culling meshes
|
|
for (AMeshMergeCullingVolume* Volume : CullVolumes)
|
|
{
|
|
// Export the landscape to raw mesh format
|
|
FRawMesh* VolumeMesh = new FRawMesh();
|
|
|
|
TArray<FStaticMaterial> VolumeMaterials;
|
|
GetBrushMesh(Volume, Volume->Brush, *VolumeMesh, VolumeMaterials);
|
|
|
|
// Offset vertices to correct world position;
|
|
FVector VolumeLocation = Volume->GetActorLocation();
|
|
for (FVector& Position : VolumeMesh->VertexPositions)
|
|
{
|
|
Position += VolumeLocation;
|
|
}
|
|
|
|
CullingRawMeshes.Add(VolumeMesh);
|
|
}
|
|
}
|
|
|
|
// Allocate merge complete data
|
|
FMergeCompleteData* Data = new FMergeCompleteData();
|
|
Data->InOuter = InOuter;
|
|
Data->InProxySettings = InMeshProxySettings;
|
|
Data->ProxyBasePackageName = InProxyBasePackageName;
|
|
Data->CallbackDelegate = InProxyCreatedDelegate;
|
|
|
|
// Add this proxy job to map
|
|
Processor->AddProxyJob(InGuid, Data);
|
|
|
|
// We are only using LOD level 0 (ProxyMeshTargetLODLevel)
|
|
TArray<FMeshMergeData> MergeData;
|
|
for (FRawMeshExt& SourceMesh : SourceMeshes)
|
|
{
|
|
MergeData.Add(SourceMesh.MeshLODData[ProxyMeshTargetLODLevel]);
|
|
}
|
|
|
|
// Populate landscape clipping geometry
|
|
for (FRawMesh* RawMesh : CullingRawMeshes)
|
|
{
|
|
FMeshMergeData ClipData;
|
|
ClipData.bIsClippingMesh = true;
|
|
ClipData.RawMesh = RawMesh;
|
|
MergeData.Add(ClipData);
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame(50.0f, LOCTEXT("CreateProxyMesh_GenerateProxy", "Generating Proxy Mesh"));
|
|
// Choose Simplygon Swarm (if available) or local proxy lod method
|
|
if (DistributedMeshMerging != nullptr && GetDefault<UEditorPerProjectUserSettings>()->bUseSimplygonSwarm && bAllowAsync)
|
|
{
|
|
DistributedMeshMerging->ProxyLOD(MergeData, Data->InProxySettings, FlattenedMaterials, InGuid);
|
|
}
|
|
else
|
|
{
|
|
MeshMerging->ProxyLOD(MergeData, Data->InProxySettings, FlattenedMaterials, InGuid);
|
|
Processor->Tick(0); // make sure caller gets merging results
|
|
}
|
|
|
|
for (FMeshMergeData& DataToRelease : MergeData)
|
|
{
|
|
DataToRelease.ReleaseData();
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::CreateProxyMesh(const TArray<AActor*>& Actors, const struct FMeshProxySettings& InProxySettings, UPackage* InOuter, const FString& ProxyBasePackageName, TArray<UObject*>& OutAssetsToSync, FVector& OutProxyLocation)
|
|
{
|
|
CreateProxyMesh(Actors, InProxySettings, InOuter, ProxyBasePackageName, OutAssetsToSync);
|
|
}
|
|
|
|
void FMeshUtilities::CreateProxyMesh(const TArray<AActor*>& Actors, const struct FMeshProxySettings& InProxySettings, UPackage* InOuter, const FString& ProxyBasePackageName, TArray<UObject*>& OutAssetsToSync, const float ScreenAreaSize)
|
|
{
|
|
FCreateProxyDelegate Delegate;
|
|
|
|
FGuid JobGuid = FGuid::NewGuid();
|
|
Delegate.BindLambda(
|
|
[&](const FGuid Guid, TArray<UObject*>& InAssetsToSync)
|
|
{
|
|
if (JobGuid == Guid)
|
|
{
|
|
OutAssetsToSync.Append(InAssetsToSync);
|
|
}
|
|
}
|
|
);
|
|
|
|
CreateProxyMesh(Actors, InProxySettings, InOuter, ProxyBasePackageName, JobGuid, Delegate, false, ScreenAreaSize);
|
|
}
|
|
|
|
void FMeshUtilities::FlattenMaterialsWithMeshData(TArray<UMaterialInterface*>& InMaterials, TArray<FRawMeshExt>& InSourceMeshes, TMap<FMeshIdAndLOD, TArray<int32>>& InMaterialIndexMap, TArray<bool>& InMeshShouldBakeVertexData, const FMaterialProxySettings &InMaterialProxySettings, TArray<FFlattenMaterial> &OutFlattenedMaterials) const
|
|
{
|
|
FScopedSlowTask SlowTask(InMaterials.Num(), (LOCTEXT("FlattenMaterialsWithMeshData", "Flattening Materials With Mesh Data")));
|
|
SlowTask.MakeDialog();
|
|
|
|
// Prepare container for cached shaders.
|
|
TMap<UMaterialInterface*, FExportMaterialProxyCache> CachedShaders;
|
|
CachedShaders.Empty(InMaterials.Num());
|
|
|
|
bool bDitheredLODTransition = false;
|
|
|
|
for (int32 MaterialIndex = 0; MaterialIndex < InMaterials.Num(); MaterialIndex++)
|
|
{
|
|
UMaterialInterface* CurrentMaterial = InMaterials[MaterialIndex];
|
|
SlowTask.EnterProgressFrame(1.0f, FText::FromString(FString::Printf(TEXT("Flattening out %s"), *CurrentMaterial->GetName())));
|
|
|
|
// Store if any material uses dithered transitions
|
|
bDitheredLODTransition |= CurrentMaterial->IsDitheredLODTransition();
|
|
|
|
// Check if we already have cached compiled shader for this material.
|
|
FExportMaterialProxyCache* CachedShader = CachedShaders.Find(CurrentMaterial);
|
|
if (CachedShader == nullptr)
|
|
{
|
|
CachedShader = &CachedShaders.Add(CurrentMaterial);
|
|
}
|
|
|
|
FFlattenMaterial FlattenMaterial = FMaterialUtilities::CreateFlattenMaterialWithSettings(InMaterialProxySettings);
|
|
|
|
/* Find a mesh which uses the current material. Materials using vertex data are added for each individual mesh using it,
|
|
which is why baking down the materials like this works. :) */
|
|
int32 UsedMeshIndex = 0;
|
|
int32 LocalMaterialIndex = 0;
|
|
int32 LocalTextureBoundIndex = 0;
|
|
FMeshMergeData* MergeData = nullptr;
|
|
for (int32 MeshIndex = 0; MeshIndex < InSourceMeshes.Num() && MergeData == nullptr; MeshIndex++)
|
|
{
|
|
const int32 LODIndex = InSourceMeshes[MeshIndex].ExportLODIndex;
|
|
if (InSourceMeshes[MeshIndex].MeshLODData[LODIndex].RawMesh->VertexPositions.Num())
|
|
{
|
|
const TArray<int32>& GlobalMaterialIndices = *InMaterialIndexMap.Find(FMeshIdAndLOD(MeshIndex, LODIndex));
|
|
for (LocalMaterialIndex = 0; LocalMaterialIndex < GlobalMaterialIndices.Num(); LocalMaterialIndex++)
|
|
{
|
|
// Only need to set merge data if we need to bake out using vertex data for this specific mesh
|
|
if (InMeshShouldBakeVertexData[MeshIndex] && GlobalMaterialIndices[LocalMaterialIndex] == MaterialIndex)
|
|
{
|
|
UsedMeshIndex = MeshIndex;
|
|
MergeData = &InSourceMeshes[MeshIndex].MeshLODData[LODIndex];
|
|
LocalTextureBoundIndex = LocalMaterialIndex;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If there is specific vertex data available and used in the material we should generate non-overlapping UVs
|
|
if (MergeData && InMeshShouldBakeVertexData[UsedMeshIndex])
|
|
{
|
|
// Generate new non-overlapping texture coordinates for mesh if needed
|
|
if (MergeData->TexCoordBounds.Num() == 0)
|
|
{
|
|
// Calculate the max bounds for this raw mesh
|
|
CalculateTextureCoordinateBoundsForRawMesh(*MergeData->RawMesh, MergeData->TexCoordBounds);
|
|
}
|
|
|
|
if (MergeData->NewUVs.Num() == 0)
|
|
{
|
|
// Generate unique UVs
|
|
GenerateUniqueUVsForStaticMesh(*MergeData->RawMesh, InMaterialProxySettings.TextureSize.GetMax(), MergeData->NewUVs);
|
|
}
|
|
|
|
// Export the material using mesh data to support vertex based material properties
|
|
FMaterialUtilities::ExportMaterial(
|
|
CurrentMaterial,
|
|
MergeData->RawMesh,
|
|
LocalMaterialIndex,
|
|
MergeData->TexCoordBounds[LocalTextureBoundIndex],
|
|
MergeData->NewUVs,
|
|
FlattenMaterial,
|
|
CachedShader);
|
|
}
|
|
else
|
|
{
|
|
// Export the material without vertex data
|
|
FMaterialUtilities::ExportMaterial(
|
|
CurrentMaterial,
|
|
FlattenMaterial,
|
|
CachedShader);
|
|
}
|
|
|
|
// Fill flatten material samples alpha values with 255 (for saving out textures correctly for Simplygon Swarm)
|
|
FlattenMaterial.FillAlphaValues(255);
|
|
|
|
// Add flattened material to outgoing array
|
|
OutFlattenedMaterials.Add(FlattenMaterial);
|
|
|
|
// Check if this material will be used later. If not - release shader.
|
|
bool bMaterialStillUsed = false;
|
|
for (int32 Index = MaterialIndex + 1; Index < InMaterials.Num(); Index++)
|
|
{
|
|
if (InMaterials[Index] == CurrentMaterial)
|
|
{
|
|
bMaterialStillUsed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!bMaterialStillUsed)
|
|
{
|
|
CachedShader->Release();
|
|
}
|
|
}
|
|
|
|
if (OutFlattenedMaterials.Num() > 1)
|
|
{
|
|
// Dither transition fix-up
|
|
for (FFlattenMaterial& FlatMaterial : OutFlattenedMaterials)
|
|
{
|
|
FlatMaterial.bDitheredLODTransition = bDitheredLODTransition;
|
|
}
|
|
|
|
// Start with determining maximum emissive scale
|
|
float MaxEmissiveScale = 0.0f;
|
|
for (FFlattenMaterial& FlatMaterial : OutFlattenedMaterials)
|
|
{
|
|
if (FlatMaterial.DoesPropertyContainData(EFlattenMaterialProperties::Emissive))
|
|
{
|
|
if (FlatMaterial.EmissiveScale > MaxEmissiveScale)
|
|
{
|
|
MaxEmissiveScale = FlatMaterial.EmissiveScale;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (MaxEmissiveScale > 0.001f)
|
|
{
|
|
// Rescale all materials.
|
|
for (FFlattenMaterial& FlatMaterial : OutFlattenedMaterials)
|
|
{
|
|
const float Scale = FlatMaterial.EmissiveScale / MaxEmissiveScale;
|
|
if (FMath::Abs(Scale - 1.0f) < 0.01f)
|
|
{
|
|
// Difference is not noticeable for this material, or this material has maximal emissive level.
|
|
continue;
|
|
}
|
|
// Rescale emissive data.
|
|
TArray<FColor>& EmissiveSamples = FlatMaterial.GetPropertySamples(EFlattenMaterialProperties::Emissive);
|
|
for (int32 PixelIndex = 0; PixelIndex < EmissiveSamples.Num(); PixelIndex++)
|
|
{
|
|
FColor& C = EmissiveSamples[PixelIndex];
|
|
C.R = FMath::RoundToInt(C.R * Scale);
|
|
C.G = FMath::RoundToInt(C.G * Scale);
|
|
C.B = FMath::RoundToInt(C.B * Scale);
|
|
}
|
|
|
|
// Update emissive scale to maximum
|
|
FlatMaterial.EmissiveScale = MaxEmissiveScale;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Exports static mesh LOD render data to a RawMesh
|
|
static void ExportStaticMeshLOD(const FStaticMeshLODResources& StaticMeshLOD, FRawMesh& OutRawMesh)
|
|
{
|
|
const int32 NumWedges = StaticMeshLOD.IndexBuffer.GetNumIndices();
|
|
const int32 NumVertexPositions = StaticMeshLOD.PositionVertexBuffer.GetNumVertices();
|
|
const int32 NumFaces = NumWedges / 3;
|
|
|
|
// Indices
|
|
StaticMeshLOD.IndexBuffer.GetCopy(OutRawMesh.WedgeIndices);
|
|
|
|
// Vertex positions
|
|
if (NumVertexPositions > 0)
|
|
{
|
|
OutRawMesh.VertexPositions.Empty(NumVertexPositions);
|
|
for (int32 PosIdx = 0; PosIdx < NumVertexPositions; ++PosIdx)
|
|
{
|
|
FVector Pos = StaticMeshLOD.PositionVertexBuffer.VertexPosition(PosIdx);
|
|
OutRawMesh.VertexPositions.Add(Pos);
|
|
}
|
|
}
|
|
|
|
// Vertex data
|
|
if (StaticMeshLOD.VertexBuffer.GetNumVertices() > 0)
|
|
{
|
|
OutRawMesh.WedgeTangentX.Empty(NumWedges);
|
|
OutRawMesh.WedgeTangentY.Empty(NumWedges);
|
|
OutRawMesh.WedgeTangentZ.Empty(NumWedges);
|
|
|
|
const int32 NumTexCoords = StaticMeshLOD.VertexBuffer.GetNumTexCoords();
|
|
for (int32 TexCoodIdx = 0; TexCoodIdx < NumTexCoords; ++TexCoodIdx)
|
|
{
|
|
OutRawMesh.WedgeTexCoords[TexCoodIdx].Empty(NumWedges);
|
|
}
|
|
|
|
for (int32 WedgeIndex : OutRawMesh.WedgeIndices)
|
|
{
|
|
FVector WedgeTangentX = StaticMeshLOD.VertexBuffer.VertexTangentX(WedgeIndex);
|
|
FVector WedgeTangentY = StaticMeshLOD.VertexBuffer.VertexTangentY(WedgeIndex);
|
|
FVector WedgeTangentZ = StaticMeshLOD.VertexBuffer.VertexTangentZ(WedgeIndex);
|
|
OutRawMesh.WedgeTangentX.Add(WedgeTangentX);
|
|
OutRawMesh.WedgeTangentY.Add(WedgeTangentY);
|
|
OutRawMesh.WedgeTangentZ.Add(WedgeTangentZ);
|
|
|
|
for (int32 TexCoodIdx = 0; TexCoodIdx < NumTexCoords; ++TexCoodIdx)
|
|
{
|
|
FVector2D WedgeTexCoord = StaticMeshLOD.VertexBuffer.GetVertexUV(WedgeIndex, TexCoodIdx);
|
|
OutRawMesh.WedgeTexCoords[TexCoodIdx].Add(WedgeTexCoord);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Vertex colors
|
|
if (StaticMeshLOD.ColorVertexBuffer.GetNumVertices() > 0)
|
|
{
|
|
OutRawMesh.WedgeColors.Empty(NumWedges);
|
|
for (int32 WedgeIndex : OutRawMesh.WedgeIndices)
|
|
{
|
|
FColor VertexColor = StaticMeshLOD.ColorVertexBuffer.VertexColor(WedgeIndex);
|
|
OutRawMesh.WedgeColors.Add(VertexColor);
|
|
}
|
|
}
|
|
|
|
// Materials
|
|
{
|
|
OutRawMesh.FaceMaterialIndices.Empty(NumFaces);
|
|
OutRawMesh.FaceMaterialIndices.SetNumZeroed(NumFaces);
|
|
|
|
for (const FStaticMeshSection& Section : StaticMeshLOD.Sections)
|
|
{
|
|
uint32 FirstTriangle = Section.FirstIndex / 3;
|
|
for (uint32 TriangleIndex = 0; TriangleIndex < Section.NumTriangles; ++TriangleIndex)
|
|
{
|
|
OutRawMesh.FaceMaterialIndices[FirstTriangle + TriangleIndex] = Section.MaterialIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Smoothing masks
|
|
{
|
|
OutRawMesh.FaceSmoothingMasks.Empty(NumFaces);
|
|
OutRawMesh.FaceSmoothingMasks.SetNumUninitialized(NumFaces);
|
|
|
|
for (auto& SmoothingMask : OutRawMesh.FaceSmoothingMasks)
|
|
{
|
|
SmoothingMask = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const bool IsLandscapeHit(const FVector& RayOrigin, const FVector& RayEndPoint, const UWorld* World, const TArray<ALandscapeProxy*>& LandscapeProxies, FVector& OutHitLocation)
|
|
{
|
|
static FName TraceTag = FName(TEXT("LandscapeTrace"));
|
|
TArray<FHitResult> Results;
|
|
// Each landscape component has 2 collision shapes, 1 of them is specific to landscape editor
|
|
// Trace only ECC_Visibility channel, so we do hit only Editor specific shape
|
|
World->LineTraceMultiByObjectType(Results, RayOrigin, RayEndPoint, FCollisionObjectQueryParams(ECollisionChannel::ECC_Visibility), FCollisionQueryParams(TraceTag, true));
|
|
|
|
bool bHitLandscape = false;
|
|
|
|
for (const FHitResult& HitResult : Results)
|
|
{
|
|
ULandscapeHeightfieldCollisionComponent* CollisionComponent = Cast<ULandscapeHeightfieldCollisionComponent>(HitResult.Component.Get());
|
|
if (CollisionComponent)
|
|
{
|
|
ALandscapeProxy* HitLandscape = CollisionComponent->GetLandscapeProxy();
|
|
if (HitLandscape && LandscapeProxies.Contains(HitLandscape))
|
|
{
|
|
// Could write a correct clipping algorithm, that clips the triangle to hit location
|
|
OutHitLocation = HitLandscape->LandscapeActorToWorld().InverseTransformPosition(HitResult.Location);
|
|
// Above landscape so visible
|
|
bHitLandscape = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bHitLandscape;
|
|
}
|
|
|
|
|
|
void CullTrianglesFromVolumesAndUnderLandscapes(const UStaticMeshComponent* InMeshComponent, FRawMesh &OutRawMesh)
|
|
{
|
|
UWorld* World = InMeshComponent->GetWorld();
|
|
TArray<ALandscapeProxy*> Landscapes;
|
|
TArray<AMeshMergeCullingVolume*> CullVolumes;
|
|
|
|
FBox ComponentBox(InMeshComponent->Bounds.Origin - InMeshComponent->Bounds.BoxExtent, InMeshComponent->Bounds.Origin + InMeshComponent->Bounds.BoxExtent);
|
|
|
|
for (ULevel* Level : World->GetLevels())
|
|
{
|
|
for (AActor* Actor : Level->Actors)
|
|
{
|
|
ALandscape* Proxy = Cast<ALandscape>(Actor);
|
|
if (Proxy && Proxy->bUseLandscapeForCullingInvisibleHLODVertices)
|
|
{
|
|
FVector Origin, Extent;
|
|
Proxy->GetActorBounds(false, Origin, Extent);
|
|
FBox LandscapeBox(Origin - Extent, Origin + Extent);
|
|
|
|
// Ignore Z axis for 2d bounds check
|
|
if (LandscapeBox.IntersectXY(ComponentBox))
|
|
{
|
|
Landscapes.Add(Proxy->GetLandscapeActor());
|
|
}
|
|
}
|
|
|
|
// Check for culling volumes
|
|
AMeshMergeCullingVolume* Volume = Cast<AMeshMergeCullingVolume>(Actor);
|
|
if (Volume)
|
|
{
|
|
// If the mesh's bounds intersect with the volume there is a possibility of culling
|
|
const bool bIntersecting = Volume->EncompassesPoint(InMeshComponent->Bounds.Origin, InMeshComponent->Bounds.SphereRadius, nullptr);
|
|
if (bIntersecting)
|
|
{
|
|
CullVolumes.Add(Volume);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TArray<bool> VertexVisible;
|
|
VertexVisible.AddZeroed(OutRawMesh.VertexPositions.Num());
|
|
int32 Index = 0;
|
|
|
|
for (const FVector& Position : OutRawMesh.VertexPositions)
|
|
{
|
|
// Start with setting visibility to true on all vertices
|
|
VertexVisible[Index] = true;
|
|
|
|
// Check if this vertex is culled due to being underneath a landscape
|
|
if (Landscapes.Num() > 0)
|
|
{
|
|
bool bVertexWithinLandscapeBounds = false;
|
|
|
|
for (ALandscapeProxy* Proxy : Landscapes)
|
|
{
|
|
FVector Origin, Extent;
|
|
Proxy->GetActorBounds(false, Origin, Extent);
|
|
FBox LandscapeBox(Origin - Extent, Origin + Extent);
|
|
bVertexWithinLandscapeBounds |= LandscapeBox.IsInsideXY(Position);
|
|
}
|
|
|
|
if (bVertexWithinLandscapeBounds)
|
|
{
|
|
const FVector Start = Position;
|
|
FVector End = Position - (WORLD_MAX * FVector::UpVector);
|
|
FVector OutHit;
|
|
const bool IsAboveLandscape = IsLandscapeHit(Start, End, World, Landscapes, OutHit);
|
|
|
|
End = Position + (WORLD_MAX * FVector::UpVector);
|
|
const bool IsUnderneathLandscape = IsLandscapeHit(Start, End, World, Landscapes, OutHit);
|
|
|
|
// Vertex is visible when above landscape (with actual landscape underneath) or if there is no landscape beneath or above the vertex (falls outside of landscape bounds)
|
|
VertexVisible[Index] = (IsAboveLandscape && !IsUnderneathLandscape);// || (!IsAboveLandscape && !IsUnderneathLandscape);
|
|
}
|
|
}
|
|
|
|
// Volume culling
|
|
for (AMeshMergeCullingVolume* Volume : CullVolumes)
|
|
{
|
|
const bool bVertexIsInsideVolume = Volume->EncompassesPoint(Position, 0.0f, nullptr);
|
|
if (bVertexIsInsideVolume)
|
|
{
|
|
// Inside a culling volume so invisible
|
|
VertexVisible[Index] = false;
|
|
}
|
|
}
|
|
|
|
Index++;
|
|
}
|
|
|
|
|
|
// We now know which vertices are below the landscape
|
|
TArray<bool> TriangleVisible;
|
|
int32 NumTriangles = OutRawMesh.WedgeIndices.Num() / 3;
|
|
TriangleVisible.AddZeroed(NumTriangles);
|
|
|
|
bool bCreateNewMesh = false;
|
|
|
|
// Determine which triangles of the mesh are visible
|
|
for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; TriangleIndex++)
|
|
{
|
|
bool AboveLandscape = false;
|
|
|
|
for (int32 WedgeIndex = 0; WedgeIndex < 3; ++WedgeIndex)
|
|
{
|
|
AboveLandscape |= VertexVisible[OutRawMesh.WedgeIndices[(TriangleIndex * 3) + WedgeIndex]];
|
|
}
|
|
TriangleVisible[TriangleIndex] = AboveLandscape;
|
|
bCreateNewMesh |= !AboveLandscape;
|
|
|
|
}
|
|
|
|
// Check whether or not we have to create a new mesh
|
|
if (bCreateNewMesh)
|
|
{
|
|
FRawMesh NewRawMesh;
|
|
TMap<int32, int32> VertexRemapping;
|
|
|
|
// Fill new mesh with data only from visible triangles
|
|
for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; ++TriangleIndex)
|
|
{
|
|
if (!TriangleVisible[TriangleIndex])
|
|
continue;
|
|
|
|
for (int32 WedgeIndex = 0; WedgeIndex < 3; ++WedgeIndex)
|
|
{
|
|
int32 OldIndex = OutRawMesh.WedgeIndices[(TriangleIndex * 3) + WedgeIndex];
|
|
|
|
int32 NewIndex;
|
|
|
|
int32* RemappedIndex = VertexRemapping.Find(Index);
|
|
if (RemappedIndex)
|
|
{
|
|
NewIndex = *RemappedIndex;
|
|
}
|
|
else
|
|
{
|
|
NewIndex = NewRawMesh.VertexPositions.Add(OutRawMesh.VertexPositions[OldIndex]);
|
|
VertexRemapping.Add(OldIndex, NewIndex);
|
|
}
|
|
|
|
NewRawMesh.WedgeIndices.Add(NewIndex);
|
|
if (OutRawMesh.WedgeColors.Num()) NewRawMesh.WedgeColors.Add(OutRawMesh.WedgeColors[(TriangleIndex * 3) + WedgeIndex]);
|
|
if (OutRawMesh.WedgeTangentX.Num()) NewRawMesh.WedgeTangentX.Add(OutRawMesh.WedgeTangentX[(TriangleIndex * 3) + WedgeIndex]);
|
|
if (OutRawMesh.WedgeTangentY.Num()) NewRawMesh.WedgeTangentY.Add(OutRawMesh.WedgeTangentY[(TriangleIndex * 3) + WedgeIndex]);
|
|
if (OutRawMesh.WedgeTangentZ.Num()) NewRawMesh.WedgeTangentZ.Add(OutRawMesh.WedgeTangentZ[(TriangleIndex * 3) + WedgeIndex]);
|
|
|
|
for (int32 UVIndex = 0; UVIndex < MAX_MESH_TEXTURE_COORDS; ++UVIndex)
|
|
{
|
|
if (OutRawMesh.WedgeTexCoords[UVIndex].Num())
|
|
{
|
|
NewRawMesh.WedgeTexCoords[UVIndex].Add(OutRawMesh.WedgeTexCoords[UVIndex][(TriangleIndex * 3) + WedgeIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
NewRawMesh.FaceMaterialIndices.Add(OutRawMesh.FaceMaterialIndices[TriangleIndex]);
|
|
NewRawMesh.FaceSmoothingMasks.Add(OutRawMesh.FaceSmoothingMasks[TriangleIndex]);
|
|
}
|
|
|
|
OutRawMesh = NewRawMesh;
|
|
}
|
|
}
|
|
|
|
void PropagateSplineDeformationToRawMesh(const USplineMeshComponent* InSplineMeshComponent, FRawMesh &OutRawMesh)
|
|
{
|
|
// Apply spline deformation for each vertex's tangents
|
|
for (int32 iVert = 0; iVert < OutRawMesh.WedgeIndices.Num(); ++iVert)
|
|
{
|
|
uint32 Index = OutRawMesh.WedgeIndices[iVert];
|
|
float& AxisValue = USplineMeshComponent::GetAxisValue(OutRawMesh.VertexPositions[Index], InSplineMeshComponent->ForwardAxis);
|
|
FTransform SliceTransform = InSplineMeshComponent->CalcSliceTransform(AxisValue);
|
|
|
|
// Transform tangents first
|
|
if (OutRawMesh.WedgeTangentX.Num())
|
|
{
|
|
OutRawMesh.WedgeTangentX[iVert] = SliceTransform.TransformVector(OutRawMesh.WedgeTangentX[iVert]);
|
|
}
|
|
|
|
if (OutRawMesh.WedgeTangentY.Num())
|
|
{
|
|
OutRawMesh.WedgeTangentY[iVert] = SliceTransform.TransformVector(OutRawMesh.WedgeTangentY[iVert]);
|
|
}
|
|
|
|
if (OutRawMesh.WedgeTangentZ.Num())
|
|
{
|
|
OutRawMesh.WedgeTangentZ[iVert] = SliceTransform.TransformVector(OutRawMesh.WedgeTangentZ[iVert]);
|
|
}
|
|
}
|
|
|
|
// Apply spline deformation for each vertex position
|
|
for (int32 iVert = 0; iVert < OutRawMesh.VertexPositions.Num(); ++iVert)
|
|
{
|
|
float& AxisValue = USplineMeshComponent::GetAxisValue(OutRawMesh.VertexPositions[iVert], InSplineMeshComponent->ForwardAxis);
|
|
FTransform SliceTransform = InSplineMeshComponent->CalcSliceTransform(AxisValue);
|
|
AxisValue = 0.0f;
|
|
OutRawMesh.VertexPositions[iVert] = SliceTransform.TransformPosition(OutRawMesh.VertexPositions[iVert]);
|
|
}
|
|
}
|
|
|
|
|
|
void TransformRawMeshVertexData(const FTransform& InTransform, FRawMesh &OutRawMesh )
|
|
{
|
|
for (FVector& Vertex : OutRawMesh.VertexPositions)
|
|
{
|
|
Vertex = InTransform.TransformPosition(Vertex);
|
|
}
|
|
|
|
for (FVector& TangentX : OutRawMesh.WedgeTangentX)
|
|
{
|
|
TangentX = InTransform.TransformVectorNoScale(TangentX);
|
|
}
|
|
|
|
for (FVector& TangentY : OutRawMesh.WedgeTangentY)
|
|
{
|
|
TangentY = InTransform.TransformVectorNoScale(TangentY);
|
|
}
|
|
|
|
for (FVector& TangentZ : OutRawMesh.WedgeTangentZ)
|
|
{
|
|
TangentZ = InTransform.TransformVectorNoScale(TangentZ);
|
|
}
|
|
|
|
const bool bIsMirrored = InTransform.GetDeterminant() < 0.f;
|
|
if (bIsMirrored)
|
|
{
|
|
// Flip faces
|
|
for (int32 FaceIdx = 0; FaceIdx < OutRawMesh.WedgeIndices.Num() / 3; FaceIdx++)
|
|
{
|
|
int32 I0 = FaceIdx * 3 + 0;
|
|
int32 I2 = FaceIdx * 3 + 2;
|
|
Swap(OutRawMesh.WedgeIndices[I0], OutRawMesh.WedgeIndices[I2]);
|
|
|
|
// seems like vertex colors and UVs are not indexed, so swap values instead
|
|
if (OutRawMesh.WedgeColors.Num())
|
|
{
|
|
Swap(OutRawMesh.WedgeColors[I0], OutRawMesh.WedgeColors[I2]);
|
|
}
|
|
|
|
for (int32 i = 0; i < MAX_MESH_TEXTURE_COORDS; ++i)
|
|
{
|
|
if (OutRawMesh.WedgeTexCoords[i].Num())
|
|
{
|
|
Swap(OutRawMesh.WedgeTexCoords[i][I0], OutRawMesh.WedgeTexCoords[i][I2]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void RecomputeTangentsAndNormalsForRawMesh(bool bRecomputeTangents, bool bRecomputeNormals, const FMeshBuildSettings& InBuildSettings, FRawMesh &OutRawMesh )
|
|
{
|
|
const int32 NumWedges = OutRawMesh.WedgeIndices.Num();
|
|
|
|
// Dump normals and tangents if we are recomputing them.
|
|
if (bRecomputeTangents)
|
|
{
|
|
OutRawMesh.WedgeTangentX.Empty(NumWedges);
|
|
OutRawMesh.WedgeTangentX.AddZeroed(NumWedges);
|
|
OutRawMesh.WedgeTangentY.Empty(NumWedges);
|
|
OutRawMesh.WedgeTangentY.AddZeroed(NumWedges);
|
|
}
|
|
|
|
if (bRecomputeNormals)
|
|
{
|
|
OutRawMesh.WedgeTangentZ.Empty(NumWedges);
|
|
OutRawMesh.WedgeTangentZ.AddZeroed(NumWedges);
|
|
}
|
|
|
|
// Compute any missing tangents.
|
|
if (bRecomputeNormals || bRecomputeTangents)
|
|
{
|
|
float ComparisonThreshold = GetComparisonThreshold(InBuildSettings);
|
|
TMultiMap<int32, int32> OverlappingCorners;
|
|
FindOverlappingCorners(OverlappingCorners, OutRawMesh, ComparisonThreshold);
|
|
|
|
// Static meshes always blend normals of overlapping corners.
|
|
uint32 TangentOptions = ETangentOptions::BlendOverlappingNormals;
|
|
if (InBuildSettings.bRemoveDegenerates)
|
|
{
|
|
// If removing degenerate triangles, ignore them when computing tangents.
|
|
TangentOptions |= ETangentOptions::IgnoreDegenerateTriangles;
|
|
}
|
|
if (InBuildSettings.bUseMikkTSpace)
|
|
{
|
|
ComputeTangents_MikkTSpace(OutRawMesh, OverlappingCorners, TangentOptions);
|
|
}
|
|
else
|
|
{
|
|
ComputeTangents(OutRawMesh, OverlappingCorners, TangentOptions);
|
|
}
|
|
}
|
|
|
|
// At this point the mesh will have valid tangents.
|
|
check(OutRawMesh.WedgeTangentX.Num() == NumWedges);
|
|
check(OutRawMesh.WedgeTangentY.Num() == NumWedges);
|
|
check(OutRawMesh.WedgeTangentZ.Num() == NumWedges);
|
|
}
|
|
|
|
bool FMeshUtilities::ConstructRawMesh(
|
|
const UStaticMeshComponent* InMeshComponent,
|
|
int32 InLODIndex,
|
|
const bool bPropagateVertexColours,
|
|
FRawMesh& OutRawMesh,
|
|
TArray<FSectionInfo>& OutUniqueSections,
|
|
TArray<int32>& OutGlobalMaterialIndices) const
|
|
{
|
|
// Retrieve source static mesh
|
|
const UStaticMesh* SourceStaticMesh = InMeshComponent->GetStaticMesh();
|
|
|
|
if (SourceStaticMesh == NULL)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Warning, TEXT("No static mesh actor found in component %s."), *InMeshComponent->GetName());
|
|
return false;
|
|
}
|
|
|
|
if (!SourceStaticMesh->SourceModels.IsValidIndex(InLODIndex))
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("No mesh data found for LOD%d %s."), InLODIndex, *SourceStaticMesh->GetName());
|
|
return false;
|
|
}
|
|
|
|
if (!SourceStaticMesh->RenderData->LODResources.IsValidIndex(InLODIndex))
|
|
{
|
|
UE_LOG(LogMeshUtilities, Warning, TEXT("No mesh render data found for LOD%d %s."), InLODIndex, *SourceStaticMesh->GetName());
|
|
return false;
|
|
}
|
|
|
|
const FStaticMeshSourceModel& SourceStaticMeshModel = SourceStaticMesh->SourceModels[InLODIndex];
|
|
|
|
// Imported meshes will have a filled RawMeshBulkData set
|
|
const bool bImportedMesh = !SourceStaticMeshModel.RawMeshBulkData->IsEmpty();
|
|
// Check whether or not this mesh has been reduced in-engine
|
|
const bool bReducedMesh = (SourceStaticMeshModel.ReductionSettings.PercentTriangles < 1.0f);
|
|
// rying to retrieve rawmesh from SourceStaticMeshModel was giving issues, which causes a mismatch
|
|
const bool bRenderDataMismatch = (InLODIndex > 0);
|
|
|
|
// Determine whether we load the raw mesh data from (original) import data or from the generated render data resources
|
|
if (bImportedMesh && !InMeshComponent->IsA<USplineMeshComponent>() && !bReducedMesh && !bRenderDataMismatch)
|
|
{
|
|
SourceStaticMeshModel.RawMeshBulkData->LoadRawMesh(OutRawMesh);
|
|
}
|
|
else
|
|
{
|
|
ExportStaticMeshLOD(SourceStaticMesh->RenderData->LODResources[InLODIndex], OutRawMesh);
|
|
}
|
|
|
|
// Make sure the raw mesh is not irreparably malformed.
|
|
if (!OutRawMesh.IsValidOrFixable())
|
|
{
|
|
UE_LOG(LogMeshUtilities, Error, TEXT("Raw mesh (%s) is corrupt for LOD%d."), *SourceStaticMesh->GetName(), InLODIndex);
|
|
return false;
|
|
}
|
|
|
|
// Handle spline mesh deformation
|
|
if (InMeshComponent->IsA<USplineMeshComponent>())
|
|
{
|
|
const USplineMeshComponent* SplineMeshComponent = Cast<USplineMeshComponent>(InMeshComponent);
|
|
// Deform raw mesh data according to the Spline Mesh Component's data
|
|
PropagateSplineDeformationToRawMesh(SplineMeshComponent, OutRawMesh);
|
|
}
|
|
|
|
// Use build settings from base mesh for LOD entries that was generated inside Editor.
|
|
const FMeshBuildSettings& BuildSettings = bImportedMesh ? SourceStaticMeshModel.BuildSettings : SourceStaticMesh->SourceModels[0].BuildSettings;
|
|
|
|
// Transform raw mesh to world space
|
|
FTransform ComponentToWorldTransform = InMeshComponent->ComponentToWorld;
|
|
// Take into account build scale settings only for meshes imported from raw data
|
|
// meshes reconstructed from render data already have build scale applied
|
|
if (bImportedMesh)
|
|
{
|
|
ComponentToWorldTransform.SetScale3D(ComponentToWorldTransform.GetScale3D()*BuildSettings.BuildScale3D);
|
|
}
|
|
|
|
// If specified propagate painted vertex colors into our raw mesh
|
|
if (bPropagateVertexColours)
|
|
{
|
|
PropagatePaintedColorsToRawMesh(InMeshComponent, InLODIndex, OutRawMesh);
|
|
}
|
|
|
|
// Transform raw mesh vertex data by the Static Mesh Component's component to world transformation
|
|
TransformRawMeshVertexData(ComponentToWorldTransform, OutRawMesh);
|
|
|
|
// Culling triangles could lead to an entirely empty RawMesh (all vertices culled)
|
|
if (!OutRawMesh.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Figure out if we should recompute normals and tangents. By default generated LODs should not recompute normals
|
|
const bool bIsMirrored = ComponentToWorldTransform.GetDeterminant() < 0.f;
|
|
bool bRecomputeNormals = (bImportedMesh && BuildSettings.bRecomputeNormals) || OutRawMesh.WedgeTangentZ.Num() == 0 || bIsMirrored;
|
|
bool bRecomputeTangents = (bImportedMesh && BuildSettings.bRecomputeTangents) || OutRawMesh.WedgeTangentX.Num() == 0 || OutRawMesh.WedgeTangentY.Num() == 0 || bIsMirrored;
|
|
|
|
if (bRecomputeNormals || bRecomputeTangents)
|
|
{
|
|
RecomputeTangentsAndNormalsForRawMesh(bRecomputeTangents, bRecomputeNormals, BuildSettings, OutRawMesh);
|
|
}
|
|
|
|
// Retrieving materials
|
|
UMaterialInterface* DefaultMaterial = Cast<UMaterialInterface>(UMaterial::GetDefaultMaterial(MD_Surface));
|
|
|
|
//Need to store the unique material indices in order to re-map the material indices in each rawmesh
|
|
for (const FStaticMeshSection& Section : SourceStaticMesh->RenderData->LODResources[InLODIndex].Sections)
|
|
{
|
|
// Add material and store the material ID
|
|
UMaterialInterface* MaterialToAdd = InMeshComponent->GetMaterial(Section.MaterialIndex);
|
|
|
|
if (MaterialToAdd)
|
|
{
|
|
//Need to check if the resource exists
|
|
FMaterialResource* Resource = MaterialToAdd->GetMaterialResource(GMaxRHIFeatureLevel);
|
|
if (!Resource)
|
|
{
|
|
MaterialToAdd = DefaultMaterial;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MaterialToAdd = DefaultMaterial;
|
|
}
|
|
|
|
FSectionInfo SectionInfo;
|
|
SectionInfo.Material = MaterialToAdd;
|
|
SectionInfo.bCollisionEnabled = Section.bEnableCollision;
|
|
SectionInfo.bShadowCastingEnabled = Section.bCastShadow;
|
|
const int32 MaterialIdx = OutUniqueSections.Add(SectionInfo);
|
|
|
|
const int32 MaterialMapIdx = OutGlobalMaterialIndices.Add(MaterialIdx);
|
|
|
|
// Update face material indices?
|
|
if (OutRawMesh.FaceMaterialIndices.Num())
|
|
{
|
|
for (int32& MaterialIndex : OutRawMesh.FaceMaterialIndices)
|
|
{
|
|
if (MaterialIndex == Section.MaterialIndex)
|
|
{
|
|
MaterialIndex = MaterialMapIdx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FMeshUtilities::ExtractMeshDataForGeometryCache(FRawMesh& RawMesh, const FMeshBuildSettings& BuildSettings, TArray<FStaticMeshBuildVertex>& OutVertices, TArray<TArray<uint32> >& OutPerSectionIndices, int32 ImportVersion)
|
|
{
|
|
int32 NumWedges = RawMesh.WedgeIndices.Num();
|
|
|
|
// Figure out if we should recompute normals and tangents. By default generated LODs should not recompute normals
|
|
bool bRecomputeNormals = (BuildSettings.bRecomputeNormals) || RawMesh.WedgeTangentZ.Num() == 0;
|
|
bool bRecomputeTangents = (BuildSettings.bRecomputeTangents) || RawMesh.WedgeTangentX.Num() == 0 || RawMesh.WedgeTangentY.Num() == 0;
|
|
|
|
// Dump normals and tangents if we are recomputing them.
|
|
if (bRecomputeTangents)
|
|
{
|
|
RawMesh.WedgeTangentX.Empty(NumWedges);
|
|
RawMesh.WedgeTangentX.AddZeroed(NumWedges);
|
|
RawMesh.WedgeTangentY.Empty(NumWedges);
|
|
RawMesh.WedgeTangentY.AddZeroed(NumWedges);
|
|
}
|
|
|
|
if (bRecomputeNormals)
|
|
{
|
|
RawMesh.WedgeTangentZ.Empty(NumWedges);
|
|
RawMesh.WedgeTangentZ.AddZeroed(NumWedges);
|
|
}
|
|
|
|
// Compute any missing tangents.
|
|
TMultiMap<int32, int32> OverlappingCorners;
|
|
if (bRecomputeNormals || bRecomputeTangents)
|
|
{
|
|
float ComparisonThreshold = GetComparisonThreshold(BuildSettings);
|
|
FindOverlappingCorners(OverlappingCorners, RawMesh, ComparisonThreshold);
|
|
|
|
// Static meshes always blend normals of overlapping corners.
|
|
uint32 TangentOptions = ETangentOptions::BlendOverlappingNormals;
|
|
if (BuildSettings.bRemoveDegenerates)
|
|
{
|
|
// If removing degenerate triangles, ignore them when computing tangents.
|
|
TangentOptions |= ETangentOptions::IgnoreDegenerateTriangles;
|
|
}
|
|
if (BuildSettings.bUseMikkTSpace)
|
|
{
|
|
ComputeTangents_MikkTSpace(RawMesh, OverlappingCorners, TangentOptions);
|
|
}
|
|
else
|
|
{
|
|
ComputeTangents(RawMesh, OverlappingCorners, TangentOptions);
|
|
}
|
|
}
|
|
|
|
// At this point the mesh will have valid tangents.
|
|
check(RawMesh.WedgeTangentX.Num() == NumWedges);
|
|
check(RawMesh.WedgeTangentY.Num() == NumWedges);
|
|
check(RawMesh.WedgeTangentZ.Num() == NumWedges);
|
|
|
|
TArray<int32> OutWedgeMap;
|
|
|
|
int32 MaxMaterialIndex = 1;
|
|
for (int32 FaceIndex = 0; FaceIndex < RawMesh.FaceMaterialIndices.Num(); FaceIndex++)
|
|
{
|
|
MaxMaterialIndex = FMath::Max<int32>(RawMesh.FaceMaterialIndices[FaceIndex], MaxMaterialIndex);
|
|
}
|
|
|
|
TMap<uint32, uint32> MaterialToSectionMapping;
|
|
for (int32 i = 0; i <= MaxMaterialIndex; ++i)
|
|
{
|
|
OutPerSectionIndices.Push(TArray<uint32>());
|
|
MaterialToSectionMapping.Add(i, i);
|
|
}
|
|
|
|
BuildStaticMeshVertexAndIndexBuffers(OutVertices, OutPerSectionIndices, OutWedgeMap, RawMesh, OverlappingCorners, MaterialToSectionMapping, KINDA_SMALL_NUMBER, BuildSettings.BuildScale3D, ImportVersion);
|
|
|
|
if (RawMesh.WedgeIndices.Num() < 100000 * 3)
|
|
{
|
|
CacheOptimizeVertexAndIndexBuffer(OutVertices, OutPerSectionIndices, OutWedgeMap);
|
|
check(OutWedgeMap.Num() == RawMesh.WedgeIndices.Num());
|
|
}
|
|
}
|
|
|
|
/*------------------------------------------------------------------------------
|
|
Mesh merging
|
|
------------------------------------------------------------------------------*/
|
|
bool FMeshUtilities::PropagatePaintedColorsToRawMesh(const UStaticMeshComponent* StaticMeshComponent, int32 LODIndex, FRawMesh& RawMesh) const
|
|
{
|
|
UStaticMesh* StaticMesh = StaticMeshComponent->GetStaticMesh();
|
|
|
|
if (StaticMesh->SourceModels.IsValidIndex(LODIndex) &&
|
|
StaticMeshComponent->LODData.IsValidIndex(LODIndex) &&
|
|
StaticMeshComponent->LODData[LODIndex].OverrideVertexColors != nullptr)
|
|
{
|
|
FColorVertexBuffer& ColorVertexBuffer = *StaticMeshComponent->LODData[LODIndex].OverrideVertexColors;
|
|
FStaticMeshSourceModel& SrcModel = StaticMesh->SourceModels[LODIndex];
|
|
FStaticMeshRenderData& RenderData = *StaticMesh->RenderData;
|
|
FStaticMeshLODResources& RenderModel = RenderData.LODResources[LODIndex];
|
|
|
|
if (ColorVertexBuffer.GetNumVertices() == RenderModel.GetNumVertices())
|
|
{
|
|
int32 NumWedges = RawMesh.WedgeIndices.Num();
|
|
const bool bUseWedgeMap = RenderData.WedgeMap.Num() > 0 && RenderData.WedgeMap.Num() == NumWedges;
|
|
// If we have a wedge map
|
|
if (bUseWedgeMap)
|
|
{
|
|
if (RenderData.WedgeMap.Num() == NumWedges)
|
|
{
|
|
int32 NumExistingColors = RawMesh.WedgeColors.Num();
|
|
if (NumExistingColors < NumWedges)
|
|
{
|
|
RawMesh.WedgeColors.AddUninitialized(NumWedges - NumExistingColors);
|
|
}
|
|
|
|
for (int32 i = 0; i < NumWedges; ++i)
|
|
{
|
|
FColor WedgeColor = FColor::White;
|
|
int32 Index = RenderData.WedgeMap[i];
|
|
if (Index != INDEX_NONE)
|
|
{
|
|
WedgeColor = ColorVertexBuffer.VertexColor(Index);
|
|
}
|
|
|
|
RawMesh.WedgeColors[i] = WedgeColor;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
// No wedge map (this can happen when we poly reduce the LOD for example)
|
|
// Use index buffer directly
|
|
else
|
|
{
|
|
UE_LOG(LogMeshUtilities, Warning, TEXT("{%s} Wedge map size %d is wrong or empty. Expected %d. Falling back on using index buffer for propagating vertex painting"), *StaticMesh->GetName(), RenderData.WedgeMap.Num(), RawMesh.WedgeIndices.Num());
|
|
|
|
RawMesh.WedgeColors.SetNumUninitialized(NumWedges);
|
|
|
|
if (RawMesh.VertexPositions.Num() == ColorVertexBuffer.GetNumVertices())
|
|
{
|
|
for (int32 i = 0; i < NumWedges; ++i)
|
|
{
|
|
FColor WedgeColor = FColor::White;
|
|
uint32 VertIndex = RawMesh.WedgeIndices[i];
|
|
|
|
if (VertIndex < ColorVertexBuffer.GetNumVertices())
|
|
{
|
|
WedgeColor = ColorVertexBuffer.VertexColor(VertIndex);
|
|
}
|
|
RawMesh.WedgeColors[i] = WedgeColor;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void TransformPhysicsGeometry(const FTransform& InTransform, FKAggregateGeom& AggGeom)
|
|
{
|
|
FTransform NoScaleInTransform = InTransform;
|
|
NoScaleInTransform.SetScale3D(FVector(1, 1, 1));
|
|
|
|
for (FKSphereElem& Elem : AggGeom.SphereElems)
|
|
{
|
|
FTransform ElemTM = Elem.GetTransform();
|
|
Elem.SetTransform(ElemTM*NoScaleInTransform);
|
|
}
|
|
|
|
for (FKBoxElem& Elem : AggGeom.BoxElems)
|
|
{
|
|
FTransform ElemTM = Elem.GetTransform();
|
|
Elem.SetTransform(ElemTM*NoScaleInTransform);
|
|
}
|
|
|
|
for (FKSphylElem& Elem : AggGeom.SphylElems)
|
|
{
|
|
FTransform ElemTM = Elem.GetTransform();
|
|
Elem.SetTransform(ElemTM*NoScaleInTransform);
|
|
}
|
|
|
|
for (FKConvexElem& Elem : AggGeom.ConvexElems)
|
|
{
|
|
FTransform ElemTM = Elem.GetTransform();
|
|
Elem.SetTransform(ElemTM*InTransform);
|
|
}
|
|
|
|
// seems like all primitives except Convex need separate scaling pass
|
|
const FVector Scale3D = InTransform.GetScale3D();
|
|
if (!Scale3D.Equals(FVector(1.f)))
|
|
{
|
|
const float MinPrimSize = KINDA_SMALL_NUMBER;
|
|
|
|
for (FKSphereElem& Elem : AggGeom.SphereElems)
|
|
{
|
|
Elem.ScaleElem(Scale3D, MinPrimSize);
|
|
}
|
|
|
|
for (FKBoxElem& Elem : AggGeom.BoxElems)
|
|
{
|
|
Elem.ScaleElem(Scale3D, MinPrimSize);
|
|
}
|
|
|
|
for (FKSphylElem& Elem : AggGeom.SphylElems)
|
|
{
|
|
Elem.ScaleElem(Scale3D, MinPrimSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ExtractPhysicsGeometry(UStaticMeshComponent* InMeshComponent, FKAggregateGeom& OutAggGeom)
|
|
{
|
|
UStaticMesh* SrcMesh = InMeshComponent->GetStaticMesh();
|
|
if (SrcMesh == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!SrcMesh->BodySetup)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OutAggGeom = SrcMesh->BodySetup->AggGeom;
|
|
|
|
// Convert boxes to convex, so they can be sheared
|
|
for (int32 BoxIdx = 0; BoxIdx < OutAggGeom.BoxElems.Num(); BoxIdx++)
|
|
{
|
|
FKConvexElem* NewConvexColl = new(OutAggGeom.ConvexElems) FKConvexElem();
|
|
NewConvexColl->ConvexFromBoxElem(OutAggGeom.BoxElems[BoxIdx]);
|
|
}
|
|
OutAggGeom.BoxElems.Empty();
|
|
|
|
// we are not owner of this stuff
|
|
OutAggGeom.RenderInfo = nullptr;
|
|
for (FKConvexElem& Elem : OutAggGeom.ConvexElems)
|
|
{
|
|
Elem.ConvexMesh = nullptr;
|
|
Elem.ConvexMeshNegX = nullptr;
|
|
}
|
|
|
|
// Transform geometry to world space
|
|
FTransform CtoM = InMeshComponent->ComponentToWorld;
|
|
TransformPhysicsGeometry(CtoM, OutAggGeom);
|
|
}
|
|
|
|
void FMeshUtilities::CalculateTextureCoordinateBoundsForRawMesh(const FRawMesh& InRawMesh, TArray<FBox2D>& OutBounds) const
|
|
{
|
|
const int32 NumWedges = InRawMesh.WedgeIndices.Num();
|
|
const int32 NumTris = NumWedges / 3;
|
|
|
|
OutBounds.Empty();
|
|
int32 WedgeIndex = 0;
|
|
for (int32 TriIndex = 0; TriIndex < NumTris; TriIndex++)
|
|
{
|
|
int MaterialIndex = InRawMesh.FaceMaterialIndices[TriIndex];
|
|
if (OutBounds.Num() <= MaterialIndex)
|
|
OutBounds.SetNumZeroed(MaterialIndex + 1);
|
|
{
|
|
int32 CachedWedgeIndex = WedgeIndex;
|
|
for (int32 UVIndex = 0; UVIndex < MAX_MESH_TEXTURE_COORDS; ++UVIndex)
|
|
{
|
|
WedgeIndex = CachedWedgeIndex;
|
|
if (InRawMesh.WedgeTexCoords[UVIndex].Num())
|
|
{
|
|
for (int32 CornerIndex = 0; CornerIndex < 3; CornerIndex++, WedgeIndex++)
|
|
{
|
|
OutBounds[MaterialIndex] += InRawMesh.WedgeTexCoords[UVIndex][WedgeIndex];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::CalculateTextureCoordinateBoundsForSkeletalMesh(const FStaticLODModel& LODModel, TArray<FBox2D>& OutBounds) const
|
|
{
|
|
TArray<FSoftSkinVertex> Vertices;
|
|
FMultiSizeIndexContainerData IndexData;
|
|
LODModel.GetVertices(Vertices);
|
|
LODModel.MultiSizeIndexContainer.GetIndexBufferData(IndexData);
|
|
|
|
#if WITH_APEX_CLOTHING
|
|
const uint32 SectionCount = (uint32)LODModel.NumNonClothingSections();
|
|
#else
|
|
const uint32 SectionCount = LODModel.Sections.Num();
|
|
#endif // #if WITH_APEX_CLOTHING
|
|
|
|
check(OutBounds.Num() != 0);
|
|
|
|
for (uint32 SectionIndex = 0; SectionIndex < SectionCount; ++SectionIndex)
|
|
{
|
|
const FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
const uint32 FirstIndex = Section.BaseIndex;
|
|
const uint32 LastIndex = FirstIndex + Section.NumTriangles * 3;
|
|
const int32 MaterialIndex = Section.MaterialIndex;
|
|
|
|
if (OutBounds.Num() <= MaterialIndex)
|
|
{
|
|
OutBounds.SetNumZeroed(MaterialIndex + 1);
|
|
}
|
|
|
|
for (uint32 Index = FirstIndex; Index < LastIndex; ++Index)
|
|
{
|
|
uint32 VertexIndex = IndexData.Indices[Index];
|
|
FSoftSkinVertex& Vertex = Vertices[VertexIndex];
|
|
|
|
FVector2D TexCoord = Vertex.UVs[0];
|
|
OutBounds[MaterialIndex] += TexCoord;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CopyTextureRect(const FColor* Src, const FIntPoint& SrcSize, FColor* Dst, const FIntPoint& DstSize, const FIntPoint& DstPos)
|
|
{
|
|
int32 RowLength = SrcSize.X*sizeof(FColor);
|
|
FColor* RowDst = Dst + DstSize.X*DstPos.Y;
|
|
const FColor* RowSrc = Src;
|
|
|
|
for (int32 RowIdx = 0; RowIdx < SrcSize.Y; ++RowIdx)
|
|
{
|
|
FMemory::Memcpy(RowDst + DstPos.X, RowSrc, RowLength);
|
|
|
|
RowDst += DstSize.X;
|
|
RowSrc += SrcSize.X;
|
|
}
|
|
}
|
|
|
|
static void SetTextureRect(const FColor& ColorValue, const FIntPoint& SrcSize, FColor* Dst, const FIntPoint& DstSize, const FIntPoint& DstPos)
|
|
{
|
|
FColor* RowDst = Dst + DstSize.X*DstPos.Y;
|
|
|
|
for (int32 RowIdx = 0; RowIdx < SrcSize.Y; ++RowIdx)
|
|
{
|
|
for (int32 ColIdx = 0; ColIdx < SrcSize.X; ++ColIdx)
|
|
{
|
|
RowDst[DstPos.X + ColIdx] = ColorValue;
|
|
}
|
|
|
|
RowDst += DstSize.X;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
struct FRawMeshUVTransform
|
|
{
|
|
FVector2D Offset;
|
|
FVector2D Scale;
|
|
|
|
bool IsValid() const
|
|
{
|
|
return (Scale != FVector2D::ZeroVector);
|
|
}
|
|
};
|
|
|
|
static FVector2D GetValidUV(const FVector2D& UV)
|
|
{
|
|
FVector2D NewUV = UV;
|
|
// first make sure they're positive
|
|
if (UV.X < 0.0f)
|
|
{
|
|
NewUV.X = UV.X + FMath::CeilToInt(FMath::Abs(UV.X));
|
|
}
|
|
|
|
if (UV.Y < 0.0f)
|
|
{
|
|
NewUV.Y = UV.Y + FMath::CeilToInt(FMath::Abs(UV.Y));
|
|
}
|
|
|
|
// now make sure they're within [0, 1]
|
|
if (UV.X > 1.0f)
|
|
{
|
|
NewUV.X = FMath::Fmod(NewUV.X, 1.0f);
|
|
}
|
|
|
|
if (UV.Y > 1.0f)
|
|
{
|
|
NewUV.Y = FMath::Fmod(NewUV.Y, 1.0f);
|
|
}
|
|
|
|
return NewUV;
|
|
}
|
|
|
|
static void MergeFlattenedMaterials(TArray<struct FFlattenMaterial>& InMaterialList, FFlattenMaterial& OutMergedMaterial, TArray<FRawMeshUVTransform>& OutUVTransforms)
|
|
{
|
|
OutUVTransforms.Reserve(InMaterialList.Num());
|
|
|
|
// Fill output UV transforms with invalid values
|
|
for (auto Material : InMaterialList)
|
|
{
|
|
|
|
// Invalid UV transform
|
|
FRawMeshUVTransform UVTransform;
|
|
UVTransform.Offset = FVector2D::ZeroVector;
|
|
UVTransform.Scale = FVector2D::ZeroVector;
|
|
OutUVTransforms.Add(UVTransform);
|
|
}
|
|
|
|
int32 AtlasGridSize = FMath::CeilToInt(FMath::Sqrt(InMaterialList.Num()));
|
|
FIntPoint AtlasTextureSize = OutMergedMaterial.GetPropertySize(EFlattenMaterialProperties::Diffuse);
|
|
FIntPoint ExportTextureSize = AtlasTextureSize / AtlasGridSize;
|
|
int32 AtlasNumSamples = AtlasTextureSize.X*AtlasTextureSize.Y;
|
|
|
|
|
|
for (int32 PropertyIndex = 0; PropertyIndex < (int32)EFlattenMaterialProperties::NumFlattenMaterialProperties; ++PropertyIndex)
|
|
{
|
|
EFlattenMaterialProperties Property = (EFlattenMaterialProperties)PropertyIndex;
|
|
if (OutMergedMaterial.ShouldGenerateDataForProperty(Property))
|
|
{
|
|
check(OutMergedMaterial.GetPropertySize(Property) == AtlasTextureSize);
|
|
TArray<FColor>& Samples = OutMergedMaterial.GetPropertySamples(Property);
|
|
Samples.SetNumZeroed(AtlasNumSamples);
|
|
}
|
|
}
|
|
|
|
int32 AtlasRowIdx = 0;
|
|
int32 AtlasColIdx = 0;
|
|
FIntPoint AtlasTargetPos = FIntPoint(0, 0);
|
|
|
|
bool bSamplesWritten[(uint32)EFlattenMaterialProperties::NumFlattenMaterialProperties];
|
|
FMemory::Memset(bSamplesWritten, 0);
|
|
|
|
// Flatten all materials and merge them into one material using texture atlases
|
|
for (int32 MatIdx = 0; MatIdx < InMaterialList.Num(); ++MatIdx)
|
|
{
|
|
FFlattenMaterial& FlatMaterial = InMaterialList[MatIdx];
|
|
for (int32 PropertyIndex = 0; PropertyIndex < (int32)EFlattenMaterialProperties::NumFlattenMaterialProperties; ++PropertyIndex)
|
|
{
|
|
EFlattenMaterialProperties Property = (EFlattenMaterialProperties)PropertyIndex;
|
|
if (OutMergedMaterial.ShouldGenerateDataForProperty(Property) && FlatMaterial.DoesPropertyContainData(Property))
|
|
{
|
|
TArray<FColor>& SourceSamples = FlatMaterial.GetPropertySamples(Property);
|
|
TArray<FColor>& TargetSamples = OutMergedMaterial.GetPropertySamples(Property);
|
|
if (FlatMaterial.IsPropertyConstant(Property))
|
|
{
|
|
SetTextureRect(SourceSamples[0], ExportTextureSize, TargetSamples.GetData(), AtlasTextureSize, AtlasTargetPos);
|
|
}
|
|
else
|
|
{
|
|
FIntPoint PropertySize = FlatMaterial.GetPropertySize(Property);
|
|
PropertySize = ConditionalImageResize(PropertySize, ExportTextureSize, SourceSamples, false);
|
|
CopyTextureRect(SourceSamples.GetData(), ExportTextureSize, TargetSamples.GetData(), AtlasTextureSize, AtlasTargetPos);
|
|
FlatMaterial.SetPropertySize(Property, PropertySize);
|
|
}
|
|
|
|
bSamplesWritten[PropertyIndex] |= true;
|
|
}
|
|
}
|
|
|
|
check(OutUVTransforms.IsValidIndex(MatIdx));
|
|
|
|
OutUVTransforms[MatIdx].Offset = FVector2D(
|
|
(float)AtlasTargetPos.X / AtlasTextureSize.X,
|
|
(float)AtlasTargetPos.Y / AtlasTextureSize.Y);
|
|
|
|
OutUVTransforms[MatIdx].Scale = FVector2D(
|
|
(float)ExportTextureSize.X / AtlasTextureSize.X,
|
|
(float)ExportTextureSize.Y / AtlasTextureSize.Y);
|
|
|
|
AtlasColIdx++;
|
|
if (AtlasColIdx >= AtlasGridSize)
|
|
{
|
|
AtlasColIdx = 0;
|
|
AtlasRowIdx++;
|
|
}
|
|
|
|
AtlasTargetPos = FIntPoint(AtlasColIdx*ExportTextureSize.X, AtlasRowIdx*ExportTextureSize.Y);
|
|
}
|
|
|
|
// Check if some properties weren't populated with data (which means we can empty them out)
|
|
for (int32 PropertyIndex = 0; PropertyIndex < (int32)EFlattenMaterialProperties::NumFlattenMaterialProperties; ++PropertyIndex)
|
|
{
|
|
if (!bSamplesWritten[PropertyIndex])
|
|
{
|
|
EFlattenMaterialProperties Property = (EFlattenMaterialProperties)PropertyIndex;
|
|
OutMergedMaterial.GetPropertySamples(Property).Empty();
|
|
OutMergedMaterial.SetPropertySize(Property, FIntPoint(0, 0));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
static void FlattenBinnedMaterials(TArray<struct FFlattenMaterial>& InMaterialList, const TArray<FBox2D>& InMaterialBoxes, FFlattenMaterial& OutMergedMaterial, TArray<FRawMeshUVTransform>& OutUVTransforms)
|
|
{
|
|
OutUVTransforms.Reserve(InMaterialList.Num());
|
|
|
|
// We support merging only for opaque materials
|
|
// Fill output UV transforms with invalid values
|
|
for (auto Material : InMaterialList)
|
|
{
|
|
// Invalid UV transform
|
|
FRawMeshUVTransform UVTransform;
|
|
UVTransform.Offset = FVector2D::ZeroVector;
|
|
UVTransform.Scale = FVector2D::ZeroVector;
|
|
OutUVTransforms.Add(UVTransform);
|
|
}
|
|
|
|
// Merge all material properties
|
|
for (int32 Index = 0; Index < (int32)EFlattenMaterialProperties::NumFlattenMaterialProperties; ++Index)
|
|
{
|
|
const EFlattenMaterialProperties Property = (EFlattenMaterialProperties)Index;
|
|
const FIntPoint& OutTextureSize = OutMergedMaterial.GetPropertySize(Property);
|
|
if (OutTextureSize != FIntPoint::ZeroValue)
|
|
{
|
|
TArray<FColor>& OutSamples = OutMergedMaterial.GetPropertySamples(Property);
|
|
OutSamples.Reserve(OutTextureSize.X * OutTextureSize.Y);
|
|
OutSamples.SetNumZeroed(OutTextureSize.X * OutTextureSize.Y);
|
|
|
|
bool bMaterialsWritten = false;
|
|
for (int32 MaterialIndex = 0; MaterialIndex < InMaterialList.Num(); ++MaterialIndex)
|
|
{
|
|
// Determine output size and offset
|
|
FFlattenMaterial& FlatMaterial = InMaterialList[MaterialIndex];
|
|
|
|
if (FlatMaterial.DoesPropertyContainData(Property))
|
|
{
|
|
FBox2D MaterialBox = InMaterialBoxes[MaterialIndex];
|
|
const FIntPoint& InputSize = FlatMaterial.GetPropertySize(Property);
|
|
TArray<FColor>& InputSamples = FlatMaterial.GetPropertySamples(Property);
|
|
|
|
// Resize material to match output (area) size
|
|
FIntPoint OutputSize = FIntPoint(OutTextureSize.X * MaterialBox.GetSize().X, OutTextureSize.Y * MaterialBox.GetSize().Y);
|
|
ConditionalImageResize(InputSize, OutputSize, InputSamples, false);
|
|
|
|
// Copy material data to the merged 'atlas' texture
|
|
FIntPoint OutputPosition = FIntPoint(OutTextureSize.X * MaterialBox.Min.X, OutTextureSize.Y * MaterialBox.Min.Y);
|
|
CopyTextureRect(InputSamples.GetData(), OutputSize, OutSamples.GetData(), OutTextureSize, OutputPosition);
|
|
|
|
// Set the UV tranforms only once
|
|
if (Index == 0)
|
|
{
|
|
FRawMeshUVTransform& UVTransform = OutUVTransforms[MaterialIndex];
|
|
UVTransform.Offset = MaterialBox.Min;
|
|
UVTransform.Scale = MaterialBox.GetSize();
|
|
}
|
|
|
|
bMaterialsWritten = true;
|
|
}
|
|
}
|
|
|
|
if (!bMaterialsWritten)
|
|
{
|
|
OutSamples.Empty();
|
|
OutMergedMaterial.SetPropertySize(Property, FIntPoint(0, 0));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::MergeActors(
|
|
const TArray<AActor*>& SourceActors,
|
|
const FMeshMergingSettings& InSettings,
|
|
UPackage* InOuter,
|
|
const FString& InBasePackageName,
|
|
int32 UseLOD, // does not build all LODs but only use this LOD to create base mesh
|
|
TArray<UObject*>& OutAssetsToSync,
|
|
FVector& OutMergedActorLocation,
|
|
bool bSilent) const
|
|
{
|
|
MergeActors(SourceActors, InSettings, InOuter, InBasePackageName, OutAssetsToSync, OutMergedActorLocation, bSilent);
|
|
}
|
|
|
|
void FMeshUtilities::MergeActors(
|
|
const TArray<AActor*>& SourceActors,
|
|
const FMeshMergingSettings& InSettings,
|
|
UPackage* InOuter,
|
|
const FString& InBasePackageName,
|
|
TArray<UObject*>& OutAssetsToSync,
|
|
FVector& OutMergedActorLocation,
|
|
bool bSilent) const
|
|
{
|
|
checkf(SourceActors.Num(), TEXT("No actors supplied for merging"));
|
|
|
|
TArray<UStaticMeshComponent*> ComponentsToMerge;
|
|
ComponentsToMerge.Reserve(SourceActors.Num());
|
|
// Collect static mesh components
|
|
for (AActor* Actor : SourceActors)
|
|
{
|
|
TInlineComponentArray<UStaticMeshComponent*> Components;
|
|
Actor->GetComponents<UStaticMeshComponent>(Components);
|
|
|
|
// Filter out bad components
|
|
for (UStaticMeshComponent* MeshComponent : Components)
|
|
{
|
|
if (MeshComponent->GetStaticMesh() != nullptr &&
|
|
MeshComponent->GetStaticMesh()->SourceModels.Num() > 0)
|
|
{
|
|
ComponentsToMerge.Add(MeshComponent);
|
|
}
|
|
}
|
|
}
|
|
|
|
checkf(SourceActors.Num(), TEXT("No valid components found in actors supplied for merging"));
|
|
|
|
UWorld* World = SourceActors[0]->GetWorld();
|
|
checkf(World != nullptr, TEXT("Invalid world retrieved from Actor"));
|
|
const float ScreenSize = TNumericLimits<float>::Max();
|
|
MergeStaticMeshComponents(ComponentsToMerge, World, InSettings, InOuter, InBasePackageName, OutAssetsToSync, OutMergedActorLocation, ScreenSize, bSilent);
|
|
}
|
|
|
|
void FMeshUtilities::MergeStaticMeshComponents(const TArray<UStaticMeshComponent*>& ComponentsToMerge, UWorld* World, const FMeshMergingSettings& InSettings, UPackage* InOuter, const FString& InBasePackageName, TArray<UObject*>& OutAssetsToSync, FVector& OutMergedActorLocation, const float ScreenSize, bool bSilent /*= false*/) const
|
|
{
|
|
FScopedSlowTask SlowTask(100.f, (LOCTEXT("MergeStaticMeshComponents_BuildingMesh", "Merging Static Mesh Components")));
|
|
SlowTask.MakeDialog();
|
|
|
|
TArray<FSectionInfo> UniqueSections;
|
|
TMap<FMeshIdAndLOD, TArray<int32>> MaterialMap;
|
|
TArray<FRawMeshExt> SourceMeshes;
|
|
bool bWithVertexColors[MAX_STATIC_MESH_LODS] = {};
|
|
bool bOcuppiedUVChannels[MAX_STATIC_MESH_LODS][MAX_MESH_TEXTURE_COORDS] = {};
|
|
UBodySetup* BodySetupSource = nullptr;
|
|
|
|
checkf(ComponentsToMerge.Num(), TEXT("No valid components supplied for merging"));
|
|
|
|
SourceMeshes.AddZeroed(ComponentsToMerge.Num());
|
|
|
|
// Use first mesh for naming and pivot
|
|
FString MergedAssetPackageName;
|
|
FVector MergedAssetPivot;
|
|
|
|
int32 NumMaxLOD = 0;
|
|
for (int32 MeshId = 0; MeshId < ComponentsToMerge.Num(); ++MeshId)
|
|
{
|
|
UStaticMeshComponent* MeshComponent = ComponentsToMerge[MeshId];
|
|
|
|
// Determine the maximum number of LOD levels found in the source meshes
|
|
NumMaxLOD = FMath::Max(NumMaxLOD, MeshComponent->GetStaticMesh()->SourceModels.Num());
|
|
|
|
// Save the pivot and asset package name of the first mesh, will later be used for creating merged mesh asset
|
|
if (MeshId == 0)
|
|
{
|
|
// Mesh component pivot point
|
|
MergedAssetPivot = InSettings.bPivotPointAtZero ? FVector::ZeroVector : MeshComponent->ComponentToWorld.GetLocation();
|
|
// Source mesh asset package name
|
|
MergedAssetPackageName = MeshComponent->GetStaticMesh()->GetOutermost()->GetName();
|
|
}
|
|
}
|
|
|
|
// Cap the number of LOD levels to the max
|
|
NumMaxLOD = FMath::Min(NumMaxLOD, MAX_STATIC_MESH_LODS);
|
|
|
|
int32 BaseLODIndex = 0;
|
|
// Are we going to export a single LOD or not
|
|
if (InSettings.LODSelectionType == EMeshLODSelectionType::SpecificLOD && InSettings.SpecificLOD >= 0)
|
|
{
|
|
// Will export only one specified LOD as LOD0 for the merged mesh
|
|
BaseLODIndex = FMath::Max(0, FMath::Min(InSettings.SpecificLOD, MAX_STATIC_MESH_LODS));
|
|
}
|
|
|
|
const bool bMergeAllAvailableLODs = InSettings.LODSelectionType == EMeshLODSelectionType::AllLODs;
|
|
|
|
SlowTask.EnterProgressFrame(10.0f, LOCTEXT("MergeStaticMeshComponents_RetrievingStaticMeshes", "Collecting Source Static Meshes"));
|
|
for (int32 MeshId = 0; MeshId < ComponentsToMerge.Num(); ++MeshId)
|
|
{
|
|
UStaticMeshComponent* StaticMeshComponent = ComponentsToMerge[MeshId];
|
|
|
|
// LOD index will be overridden if the user has chosen to pick it according to the viewing distance
|
|
int32 CalculatedLODIndex = -1;
|
|
if (InSettings.LODSelectionType == EMeshLODSelectionType::CalculateLOD && ScreenSize > 0.0f && ScreenSize < 1.0f)
|
|
{
|
|
FHierarchicalLODUtilitiesModule& Module = FModuleManager::LoadModuleChecked<FHierarchicalLODUtilitiesModule>("HierarchicalLODUtilities");
|
|
IHierarchicalLODUtilities* Utilities = Module.GetUtilities();
|
|
CalculatedLODIndex = Utilities->GetLODLevelForScreenSize(StaticMeshComponent, ScreenSize);
|
|
}
|
|
SourceMeshes[MeshId].SourceStaticMesh = StaticMeshComponent->GetStaticMesh();
|
|
|
|
// Retrieve the lowest available LOD level from the mesh
|
|
int32 StartLODIndex = InSettings.LODSelectionType == EMeshLODSelectionType::CalculateLOD ? CalculatedLODIndex : FMath::Min(BaseLODIndex, StaticMeshComponent->GetStaticMesh()->SourceModels.Num() - 1);
|
|
int32 EndLODIndex = bMergeAllAvailableLODs ? FMath::Min(StaticMeshComponent->GetStaticMesh()->SourceModels.Num(), MAX_STATIC_MESH_LODS) : StartLODIndex + 1;
|
|
|
|
SourceMeshes[MeshId].MaxLODExport = EndLODIndex - 1;
|
|
|
|
// Set export LOD index if we are exporting one specifically
|
|
SourceMeshes[MeshId].ExportLODIndex = !bMergeAllAvailableLODs ? StartLODIndex : -1;
|
|
|
|
for (int32 LODIndex = StartLODIndex; LODIndex < EndLODIndex; ++LODIndex)
|
|
{
|
|
// Store source static mesh and set LOD export flag
|
|
SourceMeshes[MeshId].SourceStaticMesh = StaticMeshComponent->GetStaticMesh();
|
|
SourceMeshes[MeshId].bShouldExportLOD[LODIndex] = false;
|
|
|
|
TArray<int32> MeshMaterialMap;
|
|
// Retrieve and construct raw mesh from source meshes
|
|
SourceMeshes[MeshId].MeshLODData[LODIndex].RawMesh = new FRawMesh();
|
|
FRawMesh* RawMeshLOD = SourceMeshes[MeshId].MeshLODData[LODIndex].RawMesh;
|
|
if ( ConstructRawMesh(StaticMeshComponent, LODIndex, InSettings.bBakeVertexDataToMesh || InSettings.bUseVertexDataForBakingMaterial, *RawMeshLOD, UniqueSections, MeshMaterialMap))
|
|
{
|
|
// Only flag the lod to be eligible for exporting if we found valid data
|
|
SourceMeshes[MeshId].bShouldExportLOD[LODIndex] = true;
|
|
|
|
// Check if vertex colours should be propagated
|
|
if (InSettings.bBakeVertexDataToMesh)
|
|
{
|
|
// Whether at least one of the meshes has vertex colors
|
|
bWithVertexColors[LODIndex] |= (RawMeshLOD->WedgeColors.Num() != 0);
|
|
}
|
|
|
|
// Which UV channels has data at least in one mesh
|
|
for (int32 ChannelIdx = 0; ChannelIdx < MAX_MESH_TEXTURE_COORDS; ++ChannelIdx)
|
|
{
|
|
bOcuppiedUVChannels[LODIndex][ChannelIdx] |= (RawMeshLOD->WedgeTexCoords[ChannelIdx].Num() != 0) && StaticMeshComponent->GetStaticMesh()->LightMapCoordinateIndex != ChannelIdx;
|
|
}
|
|
|
|
if ( InSettings.bUseLandscapeCulling )
|
|
{
|
|
// Landscape / volume culling
|
|
CullTrianglesFromVolumesAndUnderLandscapes(StaticMeshComponent, *RawMeshLOD);
|
|
|
|
if (!RawMeshLOD->IsValid())
|
|
{
|
|
RawMeshLOD = nullptr;
|
|
SourceMeshes[MeshId].bShouldExportLOD[LODIndex] = false;
|
|
}
|
|
}
|
|
|
|
if (SourceMeshes[MeshId].bShouldExportLOD[LODIndex])
|
|
{
|
|
MaterialMap.Add(FMeshIdAndLOD(MeshId, LODIndex), MeshMaterialMap);
|
|
//CalculateTextureCoordinateBoundsForRawMesh(*SourceMeshes[MeshId].MeshLODData[LODIndex].RawMesh, SourceMeshes[MeshId].MeshLODData[LODIndex].TexCoordBounds);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Failed to retrieve static meshes/materials cannot merge anything
|
|
if (MaterialMap.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (InSettings.bMergePhysicsData)
|
|
{
|
|
for (int32 MeshId = 0; MeshId < ComponentsToMerge.Num(); ++MeshId)
|
|
{
|
|
UStaticMeshComponent* MeshComponent = ComponentsToMerge[MeshId];
|
|
ExtractPhysicsGeometry(MeshComponent, SourceMeshes[MeshId].AggGeom);
|
|
|
|
// We will use first valid BodySetup as a source of physics settings
|
|
if (BodySetupSource == nullptr)
|
|
{
|
|
BodySetupSource = MeshComponent->GetStaticMesh()->BodySetup;
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool bShouldBakeOutMaterials = InSettings.bMergeMaterials && !bMergeAllAvailableLODs;
|
|
SlowTask.EnterProgressFrame(bShouldBakeOutMaterials ? 10.0f : 40.0f, LOCTEXT("MergeStaticMeshComponents_RemapMaterials", "Remapping Duplicate Materials"));
|
|
|
|
// Remap material indices regardless of baking out materials or not (could give a draw call decrease)
|
|
TArray<bool> MeshShouldBakeVertexData;
|
|
MeshShouldBakeVertexData.AddZeroed(SourceMeshes.Num());
|
|
|
|
if (bShouldBakeOutMaterials)
|
|
{
|
|
// If we have UVs outside of the UV boundaries we should use unique UVs to render out the materials
|
|
CheckWrappingUVs(SourceMeshes, MeshShouldBakeVertexData);
|
|
}
|
|
|
|
TMap<FMeshIdAndLOD, TArray<int32> > NewMaterialMap;
|
|
TArray<FSectionInfo> NewSections;
|
|
FMaterialUtilities::RemapUniqueMaterialIndices(
|
|
UniqueSections,
|
|
SourceMeshes,
|
|
MaterialMap,
|
|
InSettings.MaterialSettings,
|
|
InSettings.bUseVertexDataForBakingMaterial,
|
|
InSettings.bMergeMaterials,
|
|
MeshShouldBakeVertexData,
|
|
NewMaterialMap,
|
|
NewSections);
|
|
// Use shared material data.
|
|
Exchange(MaterialMap, NewMaterialMap);
|
|
Exchange(UniqueSections, NewSections);
|
|
|
|
if (bShouldBakeOutMaterials)
|
|
{
|
|
// Should merge flattened materials into one texture
|
|
SlowTask.EnterProgressFrame(30.0f, LOCTEXT("MergeStaticMeshComponents_BakingDownMaterials", "Rendering out Materials"));
|
|
|
|
// Flatten Materials
|
|
TArray<FFlattenMaterial> FlattenedMaterials;
|
|
TArray<UMaterialInterface*> Materials;
|
|
for (const FSectionInfo& Section : UniqueSections)
|
|
{
|
|
Materials.Push(Section.Material);
|
|
}
|
|
|
|
FlattenMaterialsWithMeshData(Materials, SourceMeshes, MaterialMap, MeshShouldBakeVertexData, InSettings.MaterialSettings, FlattenedMaterials);
|
|
|
|
// Try to optimize materials where possible
|
|
for (FFlattenMaterial& InMaterial : FlattenedMaterials)
|
|
{
|
|
FMaterialUtilities::OptimizeFlattenMaterial(InMaterial);
|
|
}
|
|
|
|
FIntPoint AtlasTextureSize = InSettings.MaterialSettings.TextureSize;
|
|
FFlattenMaterial MergedFlatMaterial;
|
|
MergedFlatMaterial.SetPropertySize(EFlattenMaterialProperties::Diffuse, AtlasTextureSize);
|
|
MergedFlatMaterial.SetPropertySize(EFlattenMaterialProperties::Normal, InSettings.MaterialSettings.bNormalMap ? AtlasTextureSize : FIntPoint::ZeroValue);
|
|
MergedFlatMaterial.SetPropertySize(EFlattenMaterialProperties::Metallic, InSettings.MaterialSettings.bMetallicMap ? AtlasTextureSize : FIntPoint::ZeroValue);
|
|
MergedFlatMaterial.SetPropertySize(EFlattenMaterialProperties::Roughness, InSettings.MaterialSettings.bRoughnessMap ? AtlasTextureSize : FIntPoint::ZeroValue);
|
|
MergedFlatMaterial.SetPropertySize(EFlattenMaterialProperties::Specular, InSettings.MaterialSettings.bSpecularMap ? AtlasTextureSize : FIntPoint::ZeroValue);
|
|
MergedFlatMaterial.SetPropertySize(EFlattenMaterialProperties::Emissive, InSettings.MaterialSettings.bEmissiveMap ? AtlasTextureSize : FIntPoint::ZeroValue);
|
|
MergedFlatMaterial.SetPropertySize(EFlattenMaterialProperties::Opacity, InSettings.MaterialSettings.bOpacityMap ? AtlasTextureSize : FIntPoint::ZeroValue);
|
|
|
|
TArray<FRawMeshUVTransform> UVTransforms;
|
|
|
|
if (InSettings.bUseTextureBinning)
|
|
{
|
|
TArray<float> MaterialImportance;
|
|
FMaterialUtilities::DetermineMaterialImportance(Materials, MaterialImportance);
|
|
TArray<FBox2D> MaterialBoxes;
|
|
FMaterialUtilities::GeneratedBinnedTextureSquares(FVector2D(1.0f, 1.0f), MaterialImportance, MaterialBoxes);
|
|
FlattenBinnedMaterials(FlattenedMaterials, MaterialBoxes, MergedFlatMaterial, UVTransforms);
|
|
}
|
|
else
|
|
{
|
|
MergeFlattenedMaterials(FlattenedMaterials, MergedFlatMaterial, UVTransforms);
|
|
}
|
|
|
|
FMaterialUtilities::OptimizeFlattenMaterial(MergedFlatMaterial);
|
|
|
|
// Adjust UVs and remap material indices
|
|
for (int32 MeshIndex = 0; MeshIndex < SourceMeshes.Num(); ++MeshIndex)
|
|
{
|
|
const int32 LODIndex = SourceMeshes[MeshIndex].ExportLODIndex;
|
|
FRawMesh& RawMesh = *SourceMeshes[MeshIndex].MeshLODData[LODIndex].RawMesh;
|
|
if (RawMesh.VertexPositions.Num())
|
|
{
|
|
const TArray<int32> MaterialIndices = MaterialMap[FMeshIdAndLOD(MeshIndex, LODIndex)];
|
|
|
|
// If we end up in the situation where we have two of the same meshes which require baking vertex data (thus unique UVs), the first one to be found in the array will be used to bake out the material and generate new uvs for it. The other one however will not have the new UVs and thus the baked out material does not match up with its uvs which makes the mesh be UVed incorrectly with the new baked material.
|
|
if (!SourceMeshes[MeshIndex].MeshLODData[LODIndex].NewUVs.Num() && MeshShouldBakeVertexData[MeshIndex])
|
|
{
|
|
// Calculate the max bounds for this raw mesh
|
|
CalculateTextureCoordinateBoundsForRawMesh(*SourceMeshes[MeshIndex].MeshLODData[LODIndex].RawMesh, SourceMeshes[MeshIndex].MeshLODData[LODIndex].TexCoordBounds);
|
|
|
|
// Generate unique UVs
|
|
GenerateUniqueUVsForStaticMesh(*SourceMeshes[MeshIndex].MeshLODData[LODIndex].RawMesh, InSettings.MaterialSettings.TextureSize.GetMax(), SourceMeshes[MeshIndex].MeshLODData[LODIndex].NewUVs);
|
|
}
|
|
|
|
for (int32 UVChannelIdx = 0; UVChannelIdx < MAX_MESH_TEXTURE_COORDS; ++UVChannelIdx)
|
|
{
|
|
// Determine if we should use original or non-overlapping generated UVs
|
|
TArray<FVector2D>& UVs = SourceMeshes[MeshIndex].MeshLODData[LODIndex].NewUVs.Num() ? SourceMeshes[MeshIndex].MeshLODData[LODIndex].NewUVs : RawMesh.WedgeTexCoords[UVChannelIdx];
|
|
if (RawMesh.WedgeTexCoords[UVChannelIdx].Num() > 0)
|
|
{
|
|
int32 UVIdx = 0;
|
|
for (int32 FaceMaterialIndex : RawMesh.FaceMaterialIndices)
|
|
{
|
|
const FRawMeshUVTransform& UVTransform = UVTransforms[MaterialIndices[FaceMaterialIndex]];
|
|
if (UVTransform.IsValid())
|
|
{
|
|
FVector2D UV0 = GetValidUV(UVs[UVIdx + 0]);
|
|
FVector2D UV1 = GetValidUV(UVs[UVIdx + 1]);
|
|
FVector2D UV2 = GetValidUV(UVs[UVIdx + 2]);
|
|
RawMesh.WedgeTexCoords[UVChannelIdx][UVIdx + 0] = UV0 * UVTransform.Scale + UVTransform.Offset;
|
|
RawMesh.WedgeTexCoords[UVChannelIdx][UVIdx + 1] = UV1 * UVTransform.Scale + UVTransform.Offset;
|
|
RawMesh.WedgeTexCoords[UVChannelIdx][UVIdx + 2] = UV2 * UVTransform.Scale + UVTransform.Offset;
|
|
}
|
|
|
|
UVIdx += 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset material indexes
|
|
for (int32& FaceMaterialIndex : RawMesh.FaceMaterialIndices)
|
|
{
|
|
FaceMaterialIndex = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Create merged material asset
|
|
FString MaterialAssetName;
|
|
FString MaterialPackageName;
|
|
if (InBasePackageName.IsEmpty())
|
|
{
|
|
MaterialAssetName = TEXT("M_MERGED_") + FPackageName::GetShortName(MergedAssetPackageName);
|
|
MaterialPackageName = FPackageName::GetLongPackagePath(MergedAssetPackageName) + TEXT("/") + MaterialAssetName;
|
|
}
|
|
else
|
|
{
|
|
MaterialAssetName = TEXT("M_") + FPackageName::GetShortName(InBasePackageName);
|
|
MaterialPackageName = FPackageName::GetLongPackagePath(InBasePackageName) + TEXT("/") + MaterialAssetName;
|
|
}
|
|
|
|
UPackage* MaterialPackage = InOuter;
|
|
if (MaterialPackage == nullptr)
|
|
{
|
|
MaterialPackage = CreatePackage(nullptr, *MaterialPackageName);
|
|
check(MaterialPackage);
|
|
MaterialPackage->FullyLoad();
|
|
MaterialPackage->Modify();
|
|
}
|
|
|
|
UMaterialInstanceConstant* MergedMaterial = ProxyMaterialUtilities::CreateProxyMaterialInstance(MaterialPackage, InSettings.MaterialSettings, MergedFlatMaterial, MaterialAssetName, MaterialPackageName);
|
|
// Set material static lighting usage flag if project has static lighting enabled
|
|
static const auto AllowStaticLightingVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.AllowStaticLighting"));
|
|
const bool bAllowStaticLighting = (!AllowStaticLightingVar || AllowStaticLightingVar->GetValueOnGameThread() != 0);
|
|
if (bAllowStaticLighting)
|
|
{
|
|
MergedMaterial->CheckMaterialUsage(MATUSAGE_StaticLighting);
|
|
}
|
|
|
|
// Only end up with one material so clear array first
|
|
UniqueSections.Empty();
|
|
|
|
FSectionInfo NewSection;
|
|
NewSection.Material = MergedMaterial;
|
|
NewSection.bShadowCastingEnabled = true;
|
|
NewSection.bCollisionEnabled = false;
|
|
|
|
UniqueSections.Add(NewSection);
|
|
}
|
|
|
|
FRawMeshExt MergedMesh;
|
|
FMemory::Memset(&MergedMesh, 0, sizeof(MergedMesh));
|
|
|
|
// Flatten out the occupied UV channel flags, we need this to ensure the same amount of uv sets written out for each mesh
|
|
bool bFlattenedOcuppiedUVChannels[MAX_MESH_TEXTURE_COORDS];
|
|
FMemory::Memset(bFlattenedOcuppiedUVChannels, 0, sizeof(bool) * MAX_MESH_TEXTURE_COORDS);
|
|
bFlattenedOcuppiedUVChannels[0] = true;
|
|
for (int CoordinateIndex = 0; CoordinateIndex < MAX_MESH_TEXTURE_COORDS; ++CoordinateIndex)
|
|
{
|
|
for (int32 LODIndex = 0; LODIndex < MAX_STATIC_MESH_LODS; ++LODIndex)
|
|
{
|
|
bFlattenedOcuppiedUVChannels[CoordinateIndex] |= bOcuppiedUVChannels[LODIndex][CoordinateIndex];
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame(40.0f, LOCTEXT("MergeStaticMeshComponents_MergingMeshesTogether", "Generating Output Static Mesh"));
|
|
FMeshSectionInfoMap SectionInfoMap;
|
|
int32 MaxExportLODs = bMergeAllAvailableLODs ? NumMaxLOD : 1;
|
|
// Merge meshes into single mesh
|
|
for (int32 SourceMeshIdx = 0; SourceMeshIdx < SourceMeshes.Num(); ++SourceMeshIdx)
|
|
{
|
|
for (int32 TargetLODIndex = 0; TargetLODIndex < MaxExportLODs; ++TargetLODIndex)
|
|
{
|
|
int32 SourceLODIndex = SourceMeshes[SourceMeshIdx].bShouldExportLOD[TargetLODIndex] ? TargetLODIndex : (SourceMeshes[SourceMeshIdx].MaxLODExport);
|
|
|
|
if (!bMergeAllAvailableLODs)
|
|
{
|
|
SourceLODIndex = SourceMeshes[SourceMeshIdx].ExportLODIndex;
|
|
}
|
|
|
|
// Allocate raw meshes where needed
|
|
if (MergedMesh.MeshLODData[TargetLODIndex].RawMesh == nullptr)
|
|
{
|
|
MergedMesh.MeshLODData[TargetLODIndex].RawMesh = new FRawMesh();
|
|
}
|
|
|
|
// Merge vertex data from source mesh list into single mesh
|
|
const FRawMesh& SourceRawMesh = *SourceMeshes[SourceMeshIdx].MeshLODData[SourceLODIndex].RawMesh;
|
|
|
|
if (SourceRawMesh.VertexPositions.Num() == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const TArray<int32> MaterialIndices = MaterialMap[FMeshIdAndLOD(SourceMeshIdx, SourceLODIndex)];
|
|
check(MaterialIndices.Num() > 0);
|
|
|
|
FRawMesh& TargetRawMesh = *MergedMesh.MeshLODData[TargetLODIndex].RawMesh;
|
|
TargetRawMesh.FaceSmoothingMasks.Append(SourceRawMesh.FaceSmoothingMasks);
|
|
|
|
if (InSettings.bMergeMaterials && !bMergeAllAvailableLODs)
|
|
{
|
|
TargetRawMesh.FaceMaterialIndices.AddZeroed(SourceRawMesh.FaceMaterialIndices.Num());
|
|
}
|
|
else
|
|
{
|
|
for (const int32 Index : SourceRawMesh.FaceMaterialIndices)
|
|
{
|
|
TargetRawMesh.FaceMaterialIndices.Add(MaterialIndices[Index]);
|
|
}
|
|
}
|
|
|
|
int32 IndicesOffset = TargetRawMesh.VertexPositions.Num();
|
|
|
|
for (int32 Index : SourceRawMesh.WedgeIndices)
|
|
{
|
|
TargetRawMesh.WedgeIndices.Add(Index + IndicesOffset);
|
|
}
|
|
|
|
for (FVector VertexPos : SourceRawMesh.VertexPositions)
|
|
{
|
|
TargetRawMesh.VertexPositions.Add(VertexPos - MergedAssetPivot);
|
|
}
|
|
|
|
TargetRawMesh.WedgeTangentX.Append(SourceRawMesh.WedgeTangentX);
|
|
TargetRawMesh.WedgeTangentY.Append(SourceRawMesh.WedgeTangentY);
|
|
TargetRawMesh.WedgeTangentZ.Append(SourceRawMesh.WedgeTangentZ);
|
|
|
|
// Deal with vertex colors
|
|
// Some meshes may have it, in this case merged mesh will be forced to have vertex colors as well
|
|
if (InSettings.bBakeVertexDataToMesh)
|
|
{
|
|
if (bWithVertexColors[SourceLODIndex] && SourceRawMesh.WedgeColors.Num())
|
|
{
|
|
TargetRawMesh.WedgeColors.Append(SourceRawMesh.WedgeColors);
|
|
}
|
|
else
|
|
{
|
|
// In case this source mesh does not have vertex colors, fill target with 0xFF
|
|
int32 ColorsOffset = TargetRawMesh.WedgeColors.Num();
|
|
int32 ColorsNum = SourceRawMesh.WedgeIndices.Num();
|
|
TargetRawMesh.WedgeColors.AddUninitialized(ColorsNum);
|
|
FMemory::Memset(&TargetRawMesh.WedgeColors[ColorsOffset], 0xFF, ColorsNum*TargetRawMesh.WedgeColors.GetTypeSize());
|
|
}
|
|
}
|
|
|
|
|
|
// Merge all other UV channels
|
|
for (int32 ChannelIdx = 0; ChannelIdx < MAX_MESH_TEXTURE_COORDS; ++ChannelIdx)
|
|
{
|
|
// Whether this channel has data
|
|
if (bFlattenedOcuppiedUVChannels[ChannelIdx])
|
|
{
|
|
const TArray<FVector2D>& SourceChannel = SourceRawMesh.WedgeTexCoords[ChannelIdx];
|
|
TArray<FVector2D>& TargetChannel = TargetRawMesh.WedgeTexCoords[ChannelIdx];
|
|
|
|
// Whether source mesh has data in this channel
|
|
if (SourceChannel.Num())
|
|
{
|
|
TargetChannel.Append(SourceChannel);
|
|
}
|
|
else
|
|
{
|
|
// Fill with zero coordinates if source mesh has no data for this channel
|
|
const int32 TexCoordNum = SourceRawMesh.WedgeIndices.Num();
|
|
for (int32 CoordIdx = 0; CoordIdx < TexCoordNum; ++CoordIdx)
|
|
{
|
|
TargetChannel.Add(FVector2D::ZeroVector);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Populate mesh section map
|
|
for (int32 TargetLODIndex = 0; TargetLODIndex < MaxExportLODs; ++TargetLODIndex)
|
|
{
|
|
TArray<uint32> UniqueMaterialIndices;
|
|
FRawMesh& TargetRawMesh = *MergedMesh.MeshLODData[TargetLODIndex].RawMesh;
|
|
|
|
for (uint32 MaterialIndex : TargetRawMesh.FaceMaterialIndices)
|
|
{
|
|
UniqueMaterialIndices.AddUnique(MaterialIndex);
|
|
}
|
|
|
|
for (int32 Index = 0; Index < UniqueMaterialIndices.Num(); ++Index)
|
|
{
|
|
const uint32 MaterialIndex = UniqueMaterialIndices[Index];
|
|
FSectionInfo StoredSectionInfo = UniqueSections[MaterialIndex];
|
|
|
|
FMeshSectionInfo SectionInfo;
|
|
SectionInfo.bCastShadow = StoredSectionInfo.bShadowCastingEnabled;
|
|
SectionInfo.bEnableCollision = StoredSectionInfo.bCollisionEnabled;
|
|
SectionInfo.MaterialIndex = MaterialIndex;
|
|
SectionInfoMap.Set(TargetLODIndex, Index, SectionInfo);
|
|
}
|
|
}
|
|
|
|
// Transform physics primitives to merged mesh pivot
|
|
if (InSettings.bMergePhysicsData && !MergedAssetPivot.IsZero())
|
|
{
|
|
FTransform PivotTM(-MergedAssetPivot);
|
|
for (auto& SourceMesh : SourceMeshes)
|
|
{
|
|
TransformPhysicsGeometry(PivotTM, SourceMesh.AggGeom);
|
|
}
|
|
}
|
|
|
|
// Compute target lightmap channel for each LOD, by looking at the first empty UV channel
|
|
int32 LightMapUVChannel = InSettings.bGenerateLightMapUV ? -1 : 0;
|
|
if (InSettings.bGenerateLightMapUV)
|
|
{
|
|
for (int32 ChannelIdx = 0; ChannelIdx < MAX_MESH_TEXTURE_COORDS; ++ChannelIdx)
|
|
{
|
|
bool bOccupied = false;
|
|
if (bFlattenedOcuppiedUVChannels[ChannelIdx])
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
LightMapUVChannel = ChannelIdx;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (LightMapUVChannel == -1)
|
|
{
|
|
// Output warning message
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Failed to find available lightmap uv channel"));
|
|
LightMapUVChannel = 0;
|
|
}
|
|
}
|
|
|
|
//
|
|
//Create merged mesh asset
|
|
//
|
|
SlowTask.EnterProgressFrame(10.0f, LOCTEXT("MergeStaticMeshComponents_CreatingAsset", "Creating Output Assets"));
|
|
{
|
|
FString AssetName;
|
|
FString PackageName;
|
|
if (InBasePackageName.IsEmpty())
|
|
{
|
|
AssetName = TEXT("SM_MERGED_") + FPackageName::GetShortName(MergedAssetPackageName);
|
|
PackageName = FPackageName::GetLongPackagePath(MergedAssetPackageName) + TEXT("/") + AssetName;
|
|
}
|
|
else
|
|
{
|
|
AssetName = FPackageName::GetShortName(InBasePackageName);
|
|
PackageName = InBasePackageName;
|
|
}
|
|
|
|
UPackage* Package = InOuter;
|
|
if (Package == nullptr)
|
|
{
|
|
Package = CreatePackage(NULL, *PackageName);
|
|
check(Package);
|
|
Package->FullyLoad();
|
|
Package->Modify();
|
|
}
|
|
|
|
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(Package, *AssetName, RF_Public | RF_Standalone);
|
|
StaticMesh->InitResources();
|
|
|
|
FString OutputPath = StaticMesh->GetPathName();
|
|
|
|
// make sure it has a new lighting guid
|
|
StaticMesh->LightingGuid = FGuid::NewGuid();
|
|
if (InSettings.bGenerateLightMapUV)
|
|
{
|
|
StaticMesh->LightMapResolution = InSettings.TargetLightMapResolution;
|
|
StaticMesh->LightMapCoordinateIndex = LightMapUVChannel;
|
|
}
|
|
|
|
for (int32 LODIndex = 0; LODIndex < NumMaxLOD; ++LODIndex)
|
|
{
|
|
if (MergedMesh.MeshLODData[LODIndex].RawMesh != nullptr)
|
|
{
|
|
FRawMesh& MergedMeshLOD = *MergedMesh.MeshLODData[LODIndex].RawMesh;
|
|
if (MergedMeshLOD.VertexPositions.Num() > 0)
|
|
{
|
|
FStaticMeshSourceModel* SrcModel = new (StaticMesh->SourceModels) FStaticMeshSourceModel();
|
|
/*Don't allow the engine to recalculate normals*/
|
|
SrcModel->BuildSettings.bRecomputeNormals = false;
|
|
SrcModel->BuildSettings.bRecomputeTangents = false;
|
|
SrcModel->BuildSettings.bRemoveDegenerates = false;
|
|
SrcModel->BuildSettings.bUseHighPrecisionTangentBasis = false;
|
|
SrcModel->BuildSettings.bUseFullPrecisionUVs = false;
|
|
SrcModel->BuildSettings.bGenerateLightmapUVs = InSettings.bGenerateLightMapUV;
|
|
SrcModel->BuildSettings.MinLightmapResolution = InSettings.TargetLightMapResolution;
|
|
SrcModel->BuildSettings.SrcLightmapIndex = 0;
|
|
SrcModel->BuildSettings.DstLightmapIndex = LightMapUVChannel;
|
|
|
|
SrcModel->RawMeshBulkData->SaveRawMesh(MergedMeshLOD);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Assign materials
|
|
for (const FSectionInfo& Section : UniqueSections)
|
|
{
|
|
UMaterialInterface* Material = Section.Material;
|
|
if (Material && !Material->IsAsset())
|
|
{
|
|
Material = nullptr; // do not save non-asset materials
|
|
}
|
|
|
|
StaticMesh->StaticMaterials.Add(FStaticMaterial(Material));
|
|
}
|
|
|
|
if (InSettings.bMergePhysicsData)
|
|
{
|
|
StaticMesh->CreateBodySetup();
|
|
if (BodySetupSource)
|
|
{
|
|
StaticMesh->BodySetup->CopyBodyPropertiesFrom(BodySetupSource);
|
|
}
|
|
|
|
StaticMesh->BodySetup->AggGeom = FKAggregateGeom();
|
|
// Copy collision from the source meshes
|
|
for (const FRawMeshExt& SourceMesh : SourceMeshes)
|
|
{
|
|
StaticMesh->BodySetup->AddCollisionFrom(SourceMesh.AggGeom);
|
|
}
|
|
|
|
// Bake rotation into verts of convex hulls, so they scale correctly after rotation
|
|
for (FKConvexElem& ConvexElem : StaticMesh->BodySetup->AggGeom.ConvexElems)
|
|
{
|
|
ConvexElem.BakeTransformToVerts();
|
|
}
|
|
}
|
|
|
|
StaticMesh->SectionInfoMap.CopyFrom(SectionInfoMap);
|
|
|
|
//Set the Imported version before calling the build
|
|
StaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion;
|
|
|
|
StaticMesh->Build(bSilent);
|
|
StaticMesh->PostEditChange();
|
|
|
|
OutAssetsToSync.Add(StaticMesh);
|
|
OutMergedActorLocation = MergedAssetPivot;
|
|
}
|
|
|
|
for (FRawMeshExt& SourceMesh : SourceMeshes)
|
|
{
|
|
for (FMeshMergeData& Mergedata : SourceMesh.MeshLODData)
|
|
{
|
|
Mergedata.ReleaseData();
|
|
}
|
|
}
|
|
|
|
for (FMeshMergeData& Mergedata : MergedMesh.MeshLODData)
|
|
{
|
|
Mergedata.ReleaseData();
|
|
}
|
|
}
|
|
|
|
void FMeshUtilities::MergeStaticMeshComponents(const TArray<UStaticMeshComponent*>& ComponentsToMerge, UWorld* World, const FMeshMergingSettings& InSettings, UPackage* InOuter, const FString& InBasePackageName, int32 UseLOD, /* does not build all LODs but only use this LOD to create base mesh */ TArray<UObject*>& OutAssetsToSync, FVector& OutMergedActorLocation, const float ScreenAreaSize, bool bSilent /*= false*/) const
|
|
{
|
|
MergeStaticMeshComponents(ComponentsToMerge, World, InSettings, InOuter, InBasePackageName, OutAssetsToSync, OutMergedActorLocation, ScreenAreaSize, bSilent);
|
|
}
|
|
|
|
bool FMeshUtilities::RemoveBonesFromMesh(USkeletalMesh* SkeletalMesh, int32 LODIndex, const TArray<FName>* BoneNamesToRemove) const
|
|
{
|
|
IMeshBoneReductionModule& MeshBoneReductionModule = FModuleManager::Get().LoadModuleChecked<IMeshBoneReductionModule>("MeshBoneReduction");
|
|
IMeshBoneReduction * MeshBoneReductionInterface = MeshBoneReductionModule.GetMeshBoneReductionInterface();
|
|
|
|
return MeshBoneReductionInterface->ReduceBoneCounts(SkeletalMesh, LODIndex, BoneNamesToRemove);
|
|
}
|
|
|
|
/*------------------------------------------------------------------------------
|
|
Mesh reduction .
|
|
------------------------------------------------------------------------------*/
|
|
|
|
IMeshReduction* FMeshUtilities::GetStaticMeshReductionInterface()
|
|
{
|
|
return StaticMeshReduction;
|
|
}
|
|
|
|
IMeshReduction* FMeshUtilities::GetSkeletalMeshReductionInterface()
|
|
{
|
|
return SkeletalMeshReduction;
|
|
}
|
|
|
|
/*------------------------------------------------------------------------------
|
|
Mesh merging.
|
|
------------------------------------------------------------------------------*/
|
|
IMeshMerging* FMeshUtilities::GetMeshMergingInterface()
|
|
{
|
|
return MeshMerging;
|
|
}
|
|
|
|
class FMeshSimplifcationSettingsCustomization : public IDetailCustomization
|
|
{
|
|
public:
|
|
static TSharedRef<IDetailCustomization> MakeInstance()
|
|
{
|
|
return MakeShareable( new FMeshSimplifcationSettingsCustomization );
|
|
}
|
|
|
|
virtual void CustomizeDetails( IDetailLayoutBuilder& DetailBuilder ) override
|
|
{
|
|
MeshReductionModuleProperty = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMeshSimplificationSettings, MeshReductionModuleName));
|
|
|
|
IDetailCategoryBuilder& Category = DetailBuilder.EditCategory(TEXT("General"));
|
|
|
|
IDetailPropertyRow& PropertyRow = Category.AddProperty(MeshReductionModuleProperty);
|
|
|
|
FDetailWidgetRow& WidgetRow = PropertyRow.CustomWidget();
|
|
WidgetRow.NameContent()
|
|
[
|
|
MeshReductionModuleProperty->CreatePropertyNameWidget()
|
|
];
|
|
|
|
WidgetRow.ValueContent()
|
|
.MaxDesiredWidth(0)
|
|
[
|
|
SNew(SComboButton)
|
|
.OnGetMenuContent(this, &FMeshSimplifcationSettingsCustomization::GenerateMeshSimplifierMenu)
|
|
.ContentPadding(FMargin(2.0f, 2.0f))
|
|
.ButtonContent()
|
|
[
|
|
SNew(STextBlock)
|
|
.Font(IDetailLayoutBuilder::GetDetailFont())
|
|
.Text(this, &FMeshSimplifcationSettingsCustomization::GetCurrentMeshSimplifierName)
|
|
]
|
|
];
|
|
}
|
|
|
|
private:
|
|
FText GetCurrentMeshSimplifierName() const
|
|
{
|
|
if(MeshReductionModuleProperty->IsValidHandle())
|
|
{
|
|
FText Name;
|
|
MeshReductionModuleProperty->GetValueAsDisplayText(Name);
|
|
|
|
return Name;
|
|
}
|
|
else
|
|
{
|
|
return LOCTEXT("AutomaticMeshReductionPlugin", "Automatic");
|
|
}
|
|
}
|
|
|
|
TSharedRef<SWidget> GenerateMeshSimplifierMenu() const
|
|
{
|
|
FMenuBuilder MenuBuilder(true, nullptr);
|
|
|
|
TArray<FName> ModuleNames;
|
|
FModuleManager::Get().FindModules(TEXT("*MeshReduction"), ModuleNames);
|
|
|
|
MenuBuilder.BeginSection(NAME_None, LOCTEXT("AvailableReductionPluginsMenuSection", "Available Plugins"));
|
|
if(ModuleNames.Num() > 0)
|
|
{
|
|
for(FName ModuleName : ModuleNames)
|
|
{
|
|
FUIAction UIAction;
|
|
UIAction.ExecuteAction.BindSP(this, &FMeshSimplifcationSettingsCustomization::OnMeshSimplificationModuleChosen, ModuleName);
|
|
UIAction.GetActionCheckState.BindSP(this, &FMeshSimplifcationSettingsCustomization::IsMeshSimplificationModuleChosen, ModuleName);
|
|
|
|
MenuBuilder.AddMenuEntry( FText::FromName(ModuleName), FText::GetEmpty(), FSlateIcon(), UIAction, NAME_None, EUserInterfaceActionType::RadioButton );
|
|
|
|
}
|
|
|
|
MenuBuilder.AddMenuSeparator();
|
|
}
|
|
|
|
FUIAction OpenMarketplaceAction;
|
|
OpenMarketplaceAction.ExecuteAction.BindSP(this, &FMeshSimplifcationSettingsCustomization::OnFindReductionPluginsClicked);
|
|
FSlateIcon Icon = FSlateIcon(FEditorStyle::Get().GetStyleSetName(), "LevelEditor.OpenMarketplace.Menu");
|
|
MenuBuilder.AddMenuEntry( LOCTEXT("FindMoreReductionPluginsLink", "Search the Marketplace"), LOCTEXT("FindMoreReductionPluginsLink_Tooltip", "Opens the Marketplace to find more mesh reduction plugins"), Icon, OpenMarketplaceAction);
|
|
return MenuBuilder.MakeWidget();
|
|
}
|
|
|
|
void OnMeshSimplificationModuleChosen(FName ModuleName)
|
|
{
|
|
if(MeshReductionModuleProperty->IsValidHandle())
|
|
{
|
|
MeshReductionModuleProperty->SetValue(ModuleName);
|
|
}
|
|
}
|
|
|
|
ECheckBoxState IsMeshSimplificationModuleChosen(FName ModuleName)
|
|
{
|
|
if(MeshReductionModuleProperty->IsValidHandle())
|
|
{
|
|
FName CurrentModuleName;
|
|
MeshReductionModuleProperty->GetValue(CurrentModuleName);
|
|
return CurrentModuleName == ModuleName ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
|
}
|
|
|
|
return ECheckBoxState::Unchecked;
|
|
}
|
|
|
|
void OnFindReductionPluginsClicked()
|
|
{
|
|
FString URL;
|
|
FUnrealEdMisc::Get().GetURL(TEXT("MeshSimplificationPluginsURL"), URL);
|
|
|
|
FUnrealEdMisc::Get().OpenMarketplace(URL);
|
|
}
|
|
private:
|
|
TSharedPtr<IPropertyHandle> MeshReductionModuleProperty;
|
|
};
|
|
|
|
/*------------------------------------------------------------------------------
|
|
Module initialization / teardown.
|
|
------------------------------------------------------------------------------*/
|
|
|
|
void FMeshUtilities::StartupModule()
|
|
{
|
|
check(StaticMeshReduction == NULL);
|
|
check(SkeletalMeshReduction == NULL);
|
|
check(MeshMerging == NULL);
|
|
|
|
Processor = new FProxyGenerationProcessor();
|
|
|
|
FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
|
|
|
PropertyEditorModule.RegisterCustomClassLayout("MeshSimplificationSettings", FOnGetDetailCustomizationInstance::CreateStatic(&FMeshSimplifcationSettingsCustomization::MakeInstance));
|
|
|
|
// This module could be launched very early by static meshes loading before the settings class that stores this value has had a chance to load. Have to read from the config file early in the startup process
|
|
FString MeshReductionModuleName;
|
|
GConfig->GetString(TEXT("/Script/Engine.MeshSimplificationSettings"), TEXT("r.MeshReductionModule"), MeshReductionModuleName, GEngineIni);
|
|
CVarMeshReductionModule->Set(*MeshReductionModuleName);
|
|
|
|
// Initially load the mesh reduction module that was previously saved in the settings
|
|
UpdateMeshReductionModule();
|
|
|
|
{
|
|
TArray<FName> SwarmModuleNames;
|
|
FModuleManager::Get().FindModules(TEXT("*SimplygonSwarm"), SwarmModuleNames);
|
|
|
|
// Look for MeshReduction interface
|
|
|
|
|
|
for (int32 Index = 0; Index < SwarmModuleNames.Num(); Index++)
|
|
{
|
|
IMeshReductionModule& MeshReductionModule = FModuleManager::LoadModuleChecked<IMeshReductionModule>(SwarmModuleNames[Index]);
|
|
|
|
// Look for distributed mesh merging interface
|
|
if (DistributedMeshMerging == NULL)
|
|
{
|
|
DistributedMeshMerging = MeshReductionModule.GetMeshMergingInterface();
|
|
|
|
if (DistributedMeshMerging)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("Using %s for distributed automatic mesh merging"), *SwarmModuleNames[Index].ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!StaticMeshReduction)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("No automatic static mesh reduction module available"));
|
|
}
|
|
if (!SkeletalMeshReduction)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("No automatic skeletal mesh reduction module available"));
|
|
}
|
|
|
|
if (!MeshMerging)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("No automatic mesh merging module available"));
|
|
}
|
|
else
|
|
{
|
|
MeshMerging->CompleteDelegate.BindRaw(Processor, &FProxyGenerationProcessor::ProxyGenerationComplete);
|
|
MeshMerging->FailedDelegate.BindRaw(Processor, &FProxyGenerationProcessor::ProxyGenerationFailed);
|
|
}
|
|
|
|
if (!DistributedMeshMerging)
|
|
{
|
|
UE_LOG(LogMeshUtilities, Log, TEXT("No distributed automatic mesh merging module available"));
|
|
}
|
|
else
|
|
{
|
|
DistributedMeshMerging->CompleteDelegate.BindRaw(Processor, &FProxyGenerationProcessor::ProxyGenerationComplete);
|
|
DistributedMeshMerging->FailedDelegate.BindRaw(Processor, &FProxyGenerationProcessor::ProxyGenerationFailed);
|
|
}
|
|
}
|
|
|
|
bDisableTriangleOrderOptimization = (CVarTriangleOrderOptimization.GetValueOnGameThread() == 2);
|
|
|
|
bUsingNvTriStrip = !bDisableTriangleOrderOptimization && (CVarTriangleOrderOptimization.GetValueOnGameThread() == 0);
|
|
|
|
// Construct and cache the version string for the mesh utilities module.
|
|
VersionString = FString::Printf(
|
|
TEXT("%s%s%s"),
|
|
MESH_UTILITIES_VER,
|
|
StaticMeshReduction ? *StaticMeshReduction->GetVersionString() : TEXT(""),
|
|
bUsingNvTriStrip ? TEXT("_NvTriStrip") : TEXT("")
|
|
);
|
|
bUsingSimplygon = VersionString.Contains(TEXT("Simplygon"));
|
|
|
|
// hook up level editor extension for skeletal mesh conversion
|
|
ModuleLoadedDelegateHandle = FModuleManager::Get().OnModulesChanged().AddLambda([this](FName InModuleName, EModuleChangeReason InChangeReason)
|
|
{
|
|
if (InChangeReason == EModuleChangeReason::ModuleLoaded)
|
|
{
|
|
if (InModuleName == "LevelEditor")
|
|
{
|
|
AddLevelViewportMenuExtender();
|
|
}
|
|
else if (InModuleName == "AnimationBlueprintEditor")
|
|
{
|
|
AddAnimationBlueprintEditorToolbarExtender();
|
|
}
|
|
else if (InModuleName == "AnimationEditor")
|
|
{
|
|
AddAnimationEditorToolbarExtender();
|
|
}
|
|
else if (InModuleName == "SkeletalMeshEditor")
|
|
{
|
|
AddSkeletalMeshEditorToolbarExtender();
|
|
}
|
|
else if (InModuleName == "SkeletonEditor")
|
|
{
|
|
AddSkeletonEditorToolbarExtender();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void FMeshUtilities::ShutdownModule()
|
|
{
|
|
static const FName PropertyEditorModuleName("PropertyEditor");
|
|
if(FModuleManager::Get().IsModuleLoaded(PropertyEditorModuleName))
|
|
{
|
|
FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>(PropertyEditorModuleName);
|
|
|
|
PropertyEditorModule.UnregisterCustomClassLayout("MeshSimplificationSettings");
|
|
}
|
|
|
|
RemoveLevelViewportMenuExtender();
|
|
RemoveAnimationBlueprintEditorToolbarExtender();
|
|
RemoveAnimationEditorToolbarExtender();
|
|
RemoveSkeletalMeshEditorToolbarExtender();
|
|
RemoveSkeletonEditorToolbarExtender();
|
|
FModuleManager::Get().OnModulesChanged().Remove(ModuleLoadedDelegateHandle);
|
|
StaticMeshReduction = NULL;
|
|
SkeletalMeshReduction = NULL;
|
|
MeshMerging = NULL;
|
|
VersionString.Empty();
|
|
}
|
|
|
|
bool FMeshUtilities::GenerateUniqueUVsForStaticMesh(const FRawMesh& RawMesh, int32 TextureResolution, TArray<FVector2D>& OutTexCoords) const
|
|
{
|
|
// Create a copy of original mesh
|
|
FRawMesh TempMesh = RawMesh;
|
|
|
|
// Find overlapping corners for UV generator. Allow some threshold - this should not produce any error in a case if resulting
|
|
// mesh will not merge these vertices.
|
|
TMultiMap<int32, int32> OverlappingCorners;
|
|
FindOverlappingCorners(OverlappingCorners, RawMesh, THRESH_POINTS_ARE_SAME);
|
|
|
|
// Generate new UVs
|
|
FLayoutUV Packer(&TempMesh, 0, 1, FMath::Clamp(TextureResolution / 4, 32, 512));
|
|
Packer.FindCharts(OverlappingCorners);
|
|
|
|
bool bPackSuccess = Packer.FindBestPacking();
|
|
if (bPackSuccess)
|
|
{
|
|
Packer.CommitPackedUVs();
|
|
// Save generated UVs
|
|
OutTexCoords = TempMesh.WedgeTexCoords[1];
|
|
}
|
|
return bPackSuccess;
|
|
}
|
|
|
|
bool FMeshUtilities::GenerateUniqueUVsForSkeletalMesh(const FStaticLODModel& LODModel, int32 TextureResolution, TArray<FVector2D>& OutTexCoords) const
|
|
{
|
|
// Get easy to use SkeletalMesh data
|
|
TArray<FSoftSkinVertex> Vertices;
|
|
FMultiSizeIndexContainerData IndexData;
|
|
LODModel.GetVertices(Vertices);
|
|
LODModel.MultiSizeIndexContainer.GetIndexBufferData(IndexData);
|
|
|
|
int32 NumCorners = IndexData.Indices.Num();
|
|
|
|
// Generate FRawMesh from FStaticLODModel
|
|
FRawMesh TempMesh;
|
|
TempMesh.WedgeIndices.AddUninitialized(NumCorners);
|
|
TempMesh.WedgeTexCoords[0].AddUninitialized(NumCorners);
|
|
TempMesh.VertexPositions.AddUninitialized(NumCorners);
|
|
|
|
// Prepare vertex to wedge map
|
|
// PrevCorner[i] points to previous corner which shares the same wedge
|
|
TArray<int32> LastWedgeCorner;
|
|
LastWedgeCorner.AddUninitialized(Vertices.Num());
|
|
TArray<int32> PrevCorner;
|
|
PrevCorner.AddUninitialized(NumCorners);
|
|
for (int32 Index = 0; Index < Vertices.Num(); Index++)
|
|
{
|
|
LastWedgeCorner[Index] = -1;
|
|
}
|
|
|
|
for (int32 Index = 0; Index < NumCorners; Index++)
|
|
{
|
|
// Copy static vertex data
|
|
int32 VertexIndex = IndexData.Indices[Index];
|
|
FSoftSkinVertex& Vertex = Vertices[VertexIndex];
|
|
TempMesh.WedgeIndices[Index] = Index; // rudimental data, not really used by FLayoutUV - but array size matters
|
|
TempMesh.WedgeTexCoords[0][Index] = Vertex.UVs[0];
|
|
TempMesh.VertexPositions[Index] = Vertex.Position;
|
|
// Link all corners belonging to a single wedge into list
|
|
int32 PrevCornerIndex = LastWedgeCorner[VertexIndex];
|
|
LastWedgeCorner[VertexIndex] = Index;
|
|
PrevCorner[Index] = PrevCornerIndex;
|
|
}
|
|
|
|
// return GenerateUniqueUVsForStaticMesh(TempMesh, TextureResolution, OutTexCoords);
|
|
|
|
// Build overlapping corners map
|
|
TMultiMap<int32, int32> OverlappingCorners;
|
|
for (int32 Index = 0; Index < NumCorners; Index++)
|
|
{
|
|
int VertexIndex = IndexData.Indices[Index];
|
|
for (int32 CornerIndex = LastWedgeCorner[VertexIndex]; CornerIndex >= 0; CornerIndex = PrevCorner[CornerIndex])
|
|
{
|
|
if (CornerIndex != Index)
|
|
{
|
|
OverlappingCorners.Add(Index, CornerIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate new UVs
|
|
FLayoutUV Packer(&TempMesh, 0, 1, FMath::Clamp(TextureResolution / 4, 32, 512));
|
|
Packer.FindCharts(OverlappingCorners);
|
|
|
|
bool bPackSuccess = Packer.FindBestPacking();
|
|
if (bPackSuccess)
|
|
{
|
|
Packer.CommitPackedUVs();
|
|
// Save generated UVs
|
|
OutTexCoords = TempMesh.WedgeTexCoords[1];
|
|
}
|
|
return bPackSuccess;
|
|
}
|
|
|
|
void FMeshUtilities::CalculateTangents(const TArray<FVector>& InVertices, const TArray<uint32>& InIndices, const TArray<FVector2D>& InUVs, const TArray<uint32>& InSmoothingGroupIndices, const uint32 InTangentOptions, TArray<FVector>& OutTangentX, TArray<FVector>& OutTangentY, TArray<FVector>& OutNormals) const
|
|
{
|
|
const float ComparisonThreshold = (InTangentOptions & ETangentOptions::IgnoreDegenerateTriangles ) ? THRESH_POINTS_ARE_SAME : 0.0f;
|
|
|
|
TMultiMap<int32, int32> OverlappingCorners;
|
|
FindOverlappingCorners(OverlappingCorners, InVertices, InIndices, ComparisonThreshold);
|
|
ComputeTangents(InVertices, InIndices, InUVs, InSmoothingGroupIndices, OverlappingCorners, OutTangentX, OutTangentY, OutNormals, InTangentOptions);
|
|
}
|
|
|
|
void FMeshUtilities::AddAnimationBlueprintEditorToolbarExtender()
|
|
{
|
|
IAnimationBlueprintEditorModule& AnimationBlueprintEditorModule = FModuleManager::Get().LoadModuleChecked<IAnimationBlueprintEditorModule>("AnimationBlueprintEditor");
|
|
auto& ToolbarExtenders = AnimationBlueprintEditorModule.GetAllAnimationBlueprintEditorToolbarExtenders();
|
|
|
|
ToolbarExtenders.Add(IAnimationBlueprintEditorModule::FAnimationBlueprintEditorToolbarExtender::CreateRaw(this, &FMeshUtilities::GetAnimationBlueprintEditorToolbarExtender));
|
|
AnimationBlueprintEditorExtenderHandle = ToolbarExtenders.Last().GetHandle();
|
|
}
|
|
|
|
void FMeshUtilities::RemoveAnimationBlueprintEditorToolbarExtender()
|
|
{
|
|
IAnimationBlueprintEditorModule* AnimationBlueprintEditorModule = FModuleManager::Get().GetModulePtr<IAnimationBlueprintEditorModule>("AnimationBlueprintEditor");
|
|
if (AnimationBlueprintEditorModule)
|
|
{
|
|
typedef IAnimationBlueprintEditorModule::FAnimationBlueprintEditorToolbarExtender DelegateType;
|
|
AnimationBlueprintEditorModule->GetAllAnimationBlueprintEditorToolbarExtenders().RemoveAll([=](const DelegateType& In) { return In.GetHandle() == AnimationBlueprintEditorExtenderHandle; });
|
|
}
|
|
}
|
|
|
|
TSharedRef<FExtender> FMeshUtilities::GetAnimationBlueprintEditorToolbarExtender(const TSharedRef<FUICommandList> CommandList, TSharedRef<IAnimationBlueprintEditor> InAnimationBlueprintEditor)
|
|
{
|
|
TSharedRef<FExtender> Extender = MakeShareable(new FExtender);
|
|
|
|
UMeshComponent* MeshComponent = Cast<UMeshComponent>(InAnimationBlueprintEditor->GetPersonaToolkit()->GetPreviewMeshComponent());
|
|
|
|
Extender->AddToolBarExtension(
|
|
"Asset",
|
|
EExtensionHook::After,
|
|
CommandList,
|
|
FToolBarExtensionDelegate::CreateRaw(this, &FMeshUtilities::HandleAddConvertComponentToStaticMeshToToolbar, MeshComponent)
|
|
);
|
|
|
|
return Extender;
|
|
}
|
|
|
|
void FMeshUtilities::AddAnimationEditorToolbarExtender()
|
|
{
|
|
IAnimationEditorModule& AnimationEditorModule = FModuleManager::Get().LoadModuleChecked<IAnimationEditorModule>("AnimationEditor");
|
|
auto& ToolbarExtenders = AnimationEditorModule.GetAllAnimationEditorToolbarExtenders();
|
|
|
|
ToolbarExtenders.Add(IAnimationEditorModule::FAnimationEditorToolbarExtender::CreateRaw(this, &FMeshUtilities::GetAnimationEditorToolbarExtender));
|
|
AnimationEditorExtenderHandle = ToolbarExtenders.Last().GetHandle();
|
|
}
|
|
|
|
void FMeshUtilities::RemoveAnimationEditorToolbarExtender()
|
|
{
|
|
IAnimationEditorModule* AnimationEditorModule = FModuleManager::Get().GetModulePtr<IAnimationEditorModule>("AnimationEditor");
|
|
if (AnimationEditorModule)
|
|
{
|
|
typedef IAnimationEditorModule::FAnimationEditorToolbarExtender DelegateType;
|
|
AnimationEditorModule->GetAllAnimationEditorToolbarExtenders().RemoveAll([=](const DelegateType& In) { return In.GetHandle() == AnimationEditorExtenderHandle; });
|
|
}
|
|
}
|
|
|
|
TSharedRef<FExtender> FMeshUtilities::GetAnimationEditorToolbarExtender(const TSharedRef<FUICommandList> CommandList, TSharedRef<IAnimationEditor> InAnimationEditor)
|
|
{
|
|
TSharedRef<FExtender> Extender = MakeShareable(new FExtender);
|
|
|
|
UMeshComponent* MeshComponent = Cast<UMeshComponent>(InAnimationEditor->GetPersonaToolkit()->GetPreviewMeshComponent());
|
|
|
|
Extender->AddToolBarExtension(
|
|
"Asset",
|
|
EExtensionHook::After,
|
|
CommandList,
|
|
FToolBarExtensionDelegate::CreateRaw(this, &FMeshUtilities::HandleAddConvertComponentToStaticMeshToToolbar, MeshComponent)
|
|
);
|
|
|
|
return Extender;
|
|
}
|
|
|
|
void FMeshUtilities::AddSkeletalMeshEditorToolbarExtender()
|
|
{
|
|
ISkeletalMeshEditorModule& SkeletalMeshEditorModule = FModuleManager::Get().LoadModuleChecked<ISkeletalMeshEditorModule>("SkeletalMeshEditor");
|
|
auto& ToolbarExtenders = SkeletalMeshEditorModule.GetAllSkeletalMeshEditorToolbarExtenders();
|
|
|
|
ToolbarExtenders.Add(ISkeletalMeshEditorModule::FSkeletalMeshEditorToolbarExtender::CreateRaw(this, &FMeshUtilities::GetSkeletalMeshEditorToolbarExtender));
|
|
SkeletalMeshEditorExtenderHandle = ToolbarExtenders.Last().GetHandle();
|
|
}
|
|
|
|
void FMeshUtilities::RemoveSkeletalMeshEditorToolbarExtender()
|
|
{
|
|
ISkeletalMeshEditorModule* SkeletalMeshEditorModule = FModuleManager::Get().GetModulePtr<ISkeletalMeshEditorModule>("SkeletalMeshEditor");
|
|
if (SkeletalMeshEditorModule)
|
|
{
|
|
typedef ISkeletalMeshEditorModule::FSkeletalMeshEditorToolbarExtender DelegateType;
|
|
SkeletalMeshEditorModule->GetAllSkeletalMeshEditorToolbarExtenders().RemoveAll([=](const DelegateType& In) { return In.GetHandle() == SkeletalMeshEditorExtenderHandle; });
|
|
}
|
|
}
|
|
|
|
TSharedRef<FExtender> FMeshUtilities::GetSkeletalMeshEditorToolbarExtender(const TSharedRef<FUICommandList> CommandList, TSharedRef<ISkeletalMeshEditor> InSkeletalMeshEditor)
|
|
{
|
|
TSharedRef<FExtender> Extender = MakeShareable(new FExtender);
|
|
|
|
UMeshComponent* MeshComponent = Cast<UMeshComponent>(InSkeletalMeshEditor->GetPersonaToolkit()->GetPreviewMeshComponent());
|
|
|
|
Extender->AddToolBarExtension(
|
|
"Asset",
|
|
EExtensionHook::After,
|
|
CommandList,
|
|
FToolBarExtensionDelegate::CreateRaw(this, &FMeshUtilities::HandleAddConvertComponentToStaticMeshToToolbar, MeshComponent)
|
|
);
|
|
|
|
return Extender;
|
|
}
|
|
|
|
void FMeshUtilities::AddSkeletonEditorToolbarExtender()
|
|
{
|
|
ISkeletonEditorModule& SkeletonEditorModule = FModuleManager::Get().LoadModuleChecked<ISkeletonEditorModule>("SkeletonEditor");
|
|
auto& ToolbarExtenders = SkeletonEditorModule.GetAllSkeletonEditorToolbarExtenders();
|
|
|
|
ToolbarExtenders.Add(ISkeletonEditorModule::FSkeletonEditorToolbarExtender::CreateRaw(this, &FMeshUtilities::GetSkeletonEditorToolbarExtender));
|
|
SkeletonEditorExtenderHandle = ToolbarExtenders.Last().GetHandle();
|
|
}
|
|
|
|
void FMeshUtilities::RemoveSkeletonEditorToolbarExtender()
|
|
{
|
|
ISkeletonEditorModule* SkeletonEditorModule = FModuleManager::Get().GetModulePtr<ISkeletonEditorModule>("SkeletonEditor");
|
|
if (SkeletonEditorModule)
|
|
{
|
|
typedef ISkeletonEditorModule::FSkeletonEditorToolbarExtender DelegateType;
|
|
SkeletonEditorModule->GetAllSkeletonEditorToolbarExtenders().RemoveAll([=](const DelegateType& In) { return In.GetHandle() == SkeletonEditorExtenderHandle; });
|
|
}
|
|
}
|
|
|
|
TSharedRef<FExtender> FMeshUtilities::GetSkeletonEditorToolbarExtender(const TSharedRef<FUICommandList> CommandList, TSharedRef<ISkeletonEditor> InSkeletonEditor)
|
|
{
|
|
TSharedRef<FExtender> Extender = MakeShareable(new FExtender);
|
|
|
|
UMeshComponent* MeshComponent = Cast<UMeshComponent>(InSkeletonEditor->GetPersonaToolkit()->GetPreviewMeshComponent());
|
|
|
|
Extender->AddToolBarExtension(
|
|
"Asset",
|
|
EExtensionHook::After,
|
|
CommandList,
|
|
FToolBarExtensionDelegate::CreateRaw(this, &FMeshUtilities::HandleAddConvertComponentToStaticMeshToToolbar, MeshComponent)
|
|
);
|
|
|
|
return Extender;
|
|
}
|
|
|
|
|
|
void FMeshUtilities::HandleAddConvertComponentToStaticMeshToToolbar(FToolBarBuilder& ParentToolbarBuilder, UMeshComponent* InMeshComponent)
|
|
{
|
|
ParentToolbarBuilder.AddToolBarButton(
|
|
FUIAction(FExecuteAction::CreateLambda([this, InMeshComponent]()
|
|
{
|
|
ConvertMeshesToStaticMesh(TArray<UMeshComponent*>({ InMeshComponent }), InMeshComponent->GetComponentToWorld());
|
|
})),
|
|
NAME_None,
|
|
LOCTEXT("MakeStaticMesh", "Make Static Mesh"),
|
|
LOCTEXT("MakeStaticMeshTooltip", "Make a new static mesh out of the preview's current pose."),
|
|
FSlateIcon("EditorStyle", "Persona.ConvertToStaticMesh")
|
|
);
|
|
}
|
|
|
|
void FMeshUtilities::AddLevelViewportMenuExtender()
|
|
{
|
|
FLevelEditorModule& LevelEditorModule = FModuleManager::Get().LoadModuleChecked<FLevelEditorModule>("LevelEditor");
|
|
auto& MenuExtenders = LevelEditorModule.GetAllLevelViewportContextMenuExtenders();
|
|
|
|
MenuExtenders.Add(FLevelEditorModule::FLevelViewportMenuExtender_SelectedActors::CreateRaw(this, &FMeshUtilities::GetLevelViewportContextMenuExtender));
|
|
LevelViewportExtenderHandle = MenuExtenders.Last().GetHandle();
|
|
}
|
|
|
|
void FMeshUtilities::RemoveLevelViewportMenuExtender()
|
|
{
|
|
if (LevelViewportExtenderHandle.IsValid())
|
|
{
|
|
FLevelEditorModule* LevelEditorModule = FModuleManager::Get().GetModulePtr<FLevelEditorModule>("LevelEditor");
|
|
if (LevelEditorModule)
|
|
{
|
|
typedef FLevelEditorModule::FLevelViewportMenuExtender_SelectedActors DelegateType;
|
|
LevelEditorModule->GetAllLevelViewportContextMenuExtenders().RemoveAll([=](const DelegateType& In) { return In.GetHandle() == LevelViewportExtenderHandle; });
|
|
}
|
|
}
|
|
}
|
|
|
|
TSharedRef<FExtender> FMeshUtilities::GetLevelViewportContextMenuExtender(const TSharedRef<FUICommandList> CommandList, const TArray<AActor*> InActors)
|
|
{
|
|
TSharedRef<FExtender> Extender = MakeShareable(new FExtender);
|
|
|
|
if (InActors.Num() > 0)
|
|
{
|
|
bool bHasComponent = false;
|
|
for (AActor* Actor : InActors)
|
|
{
|
|
TInlineComponentArray<UMeshComponent*> Components(Actor);
|
|
if (Components.Num() > 0)
|
|
{
|
|
bHasComponent = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bHasComponent)
|
|
{
|
|
FText ActorName = InActors.Num() == 1 ? FText::Format(LOCTEXT("ActorNameSingular", "\"{0}\""), FText::FromString(InActors[0]->GetActorLabel())) : LOCTEXT("ActorNamePlural", "Actors");
|
|
|
|
FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
|
|
TSharedRef<FUICommandList> LevelEditorCommandBindings = LevelEditor.GetGlobalLevelEditorActions();
|
|
|
|
Extender->AddMenuExtension("ActorControl", EExtensionHook::After, LevelEditorCommandBindings, FMenuExtensionDelegate::CreateLambda(
|
|
[this, ActorName, InActors](FMenuBuilder& MenuBuilder) {
|
|
|
|
MenuBuilder.AddMenuEntry(
|
|
FText::Format(LOCTEXT("ConvertSelectedActorsToStaticMeshText", "Convert {0} To Static Mesh"), ActorName),
|
|
LOCTEXT("ConvertSelectedActorsToStaticMeshTooltip", "Convert the selected actor's meshes to a new Static Mesh asset. Supports static and skeletal meshes."),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateRaw(this, &FMeshUtilities::ConvertActorMeshesToStaticMesh, InActors))
|
|
);
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
return Extender;
|
|
}
|
|
|
|
void FMeshUtilities::ConvertActorMeshesToStaticMesh(const TArray<AActor*> InActors)
|
|
{
|
|
TArray<UMeshComponent*> MeshComponents;
|
|
|
|
for (AActor* Actor : InActors)
|
|
{
|
|
// add all components from this actor
|
|
TInlineComponentArray<UMeshComponent*> ActorComponents(Actor);
|
|
for (UMeshComponent* ActorComponent : ActorComponents)
|
|
{
|
|
MeshComponents.AddUnique(ActorComponent);
|
|
}
|
|
|
|
// add all attached actors
|
|
TArray<AActor*> AttachedActors;
|
|
Actor->GetAttachedActors(AttachedActors);
|
|
for (AActor* AttachedActor : AttachedActors)
|
|
{
|
|
TInlineComponentArray<UMeshComponent*> AttachedActorComponents(AttachedActor);
|
|
for (UMeshComponent* AttachedActorComponent : AttachedActorComponents)
|
|
{
|
|
MeshComponents.AddUnique(AttachedActorComponent);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto GetActorRootTransform = [](AActor* InActor)
|
|
{
|
|
FTransform RootTransform(FTransform::Identity);
|
|
if (ACharacter* Character = Cast<ACharacter>(InActor))
|
|
{
|
|
RootTransform = Character->GetTransform();
|
|
RootTransform.SetLocation(RootTransform.GetLocation() - FVector(0.0f, 0.0f, Character->GetCapsuleComponent()->GetScaledCapsuleHalfHeight()));
|
|
}
|
|
else
|
|
{
|
|
// otherwise just use the actor's origin
|
|
RootTransform = InActor->GetTransform();
|
|
}
|
|
|
|
return RootTransform;
|
|
};
|
|
|
|
// now pick a root transform
|
|
FTransform RootTransform(FTransform::Identity);
|
|
if (InActors.Num() == 1)
|
|
{
|
|
RootTransform = GetActorRootTransform(InActors[0]);
|
|
}
|
|
else
|
|
{
|
|
// multiple actors use the average of their origins, with Z being the min of all origins. Rotation is identity for simplicity
|
|
FVector Location(FVector::ZeroVector);
|
|
float MinZ = FLT_MAX;
|
|
for (AActor* Actor : InActors)
|
|
{
|
|
FTransform ActorTransform(GetActorRootTransform(Actor));
|
|
Location += ActorTransform.GetLocation();
|
|
MinZ = FMath::Min(ActorTransform.GetLocation().Z, MinZ);
|
|
}
|
|
Location /= (float)InActors.Num();
|
|
Location.Z = MinZ;
|
|
|
|
RootTransform.SetLocation(Location);
|
|
}
|
|
|
|
ConvertMeshesToStaticMesh(MeshComponents, RootTransform);
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|
|
|
|
|