언리얼 엔진 플러그인 시스템을 통해 누구든지 엔진 전체를 리컴파일 및 배포할 필요 없이 다양한 신규 기능을 추가할 수 있습니다. 여기서는 렌즈 디스토션 글로벌 셰이더 플러그인을 다시 만들어 보면서 블루프린트를 통해 제어할 수 있는 글로벌 셰이더를 구현하는 방법을 알아보겠습니다.
이미지를 클릭하면 전체 크기로 표시됩니다.
이 예시에서는 Engine\Plugins\Compositing\LensDistortion 에서 찾을 수 있는 기존 렌즈 디스토션 플러그인을 Foo 라는 새 플러그인으로 다시 만들어 보면서 이 워크플로의 구현 방법을 직접 시연합니다.
플러그인 내 셰이더 퀵스타트에서 글로벌 셰이더를 만들어 플러그인으로 사용하는 방법을 확인하실 수 있습니다.
1 - 코드 구성
언리얼 엔진용 새 플러그인 제작을 시작하기에 앞서, 먼저 Visual Studio가 설치되었는지 확인해야 합니다. 플러그인 코드가 실행될 수 있도록 컴파일해야 하기 때문에 Visual Studio는 이 퀵스타트에 필수입니다. 그 방법을 확실히 알지 못하는 경우 언리얼 엔진용 Visual Studio 구성 문서에서 도움을 받으세요.
-
먼저, 새 게임(Games) 프로젝트를 만들고 기본(Blank) 템플릿을 선택합니다. 최대 퀄리티(Maximum Quality) 및 시작용 콘텐츠 없음(No Starter Content) 을 활성화해야 합니다.
-
프로젝트가 생성되었다면 Visual Studio가 열립니다. 그런 다음 ShadersInPlugins 프로젝트를 우클릭하고 빌드(Build) 옵션을 선택하여 프로젝트를 컴파일합니다.
-
프로젝트 컴파일이 완료되면 Visual Studio 내에서 F5 를 눌러 언리얼 엔진 에디터에서 ShadersInPlugins 프로젝트를 실행합니다.
-
언리얼 엔진 에디터 로딩이 완료되면 편집(Edit) > 플러그인(Plugins) 으로 이동하여 플러그인(Plugins) 매니저를 연 후 플러그인 창의 우측 하단에 있는 새 플러그인(New Plugin) 옵션을 클릭하여 새 플러그인 생성 창을 엽니다.
이미지를 클릭하면 전체 크기로 표시됩니다.
-
새 플러그인(New Plugin) 창에서 빈(Blank) 플러그인을 선택한 후 이름으로 Foo 를 입력합니다. 모든 세팅은 디폴트로 유지합니다. 모두 완료되면 플러그인 생성(Create Plugin) 버튼을 눌러 플러그인의 초기 필수 콘텐츠를 모두 생성합니다.
이미지를 클릭하면 전체 크기로 표시됩니다.
-
완료되면 언리얼 엔진과 Visual Studio를 닫은 후 프로젝트 폴더 내부에 생성된 Plugins > Foo 플러그인 폴더로 이동합니다.
-
Foo 플러그인 폴더 안에 Shaders 라는 새 폴더를 추가한 후, 해당 폴더 안에 Private 이라는 새 폴더를 생성합니다.
이미지를 클릭하면 전체 크기로 표시됩니다.
-
Private 폴더 안에 새 텍스트 파일을 생성하고 MyShader.USF 로 명명합니다. 그리고 다음 HLSL 코드를 복사하여 이 파일에 붙여 넣은 후 파일을 저장합니다.
파일 확장자를 .USF 로 변경해야 합니다. 아니면 언리얼 엔진이 이 파일이 셰이더 파일이라는 것을 이해하지 못합니다.
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. /*============================================================================= LensDistortionUVGeneration.usf: Generate lens distortion and undistortion UV displacement map into a render target. The pixel shader directly compute the distort viewport UV to undistort viewport UV displacement using Sv_Position and the reference equations and store them into the red and green channels. However to avoid resolving with a ferrari method, or doing a newton method on the GPU to compute the undistort viewport UV to distort viewport UV displacement, this couple of shaders works as follow: The vertex shader undistort the grid's vertices, and pass down to the pixel shader the viewport UV of where they should have been on screen without undistortion. The pixel shader can then generate the undistort viewport UV to distort viewport UV displacement by just subtracting the pixel's viewport UV. =============================================================================*/ #include "/Engine/Public/Platform.ush" // 뷰포트 UV 좌표 내 픽셀의 크기입니다. float2 PixelUVSize; // K1, K2, K3 float3 RadialDistortionCoefs; // P1, P2 float2 TangentialDistortionCoefs; // 왜곡되지 않은 뷰포트의 카메라 매트릭스입니다. float4 UndistortedCameraMatrix; // 왜곡된 뷰포트의 카메라 매트릭스입니다. float4 DistortedCameraMatrix; // 곱셈을 출력하고 렌더 타깃에 더합니다. float2 OutputMultiplyAndAdd; // V.z=1에서 뷰 위치의 왜곡을 해제합니다. float2 UndistortNormalizedViewPosition(float2 V) { float2 V2 = V * V; float R2 = V2.x + V2.y; // 방사상 디스토션입니다(MF_Undistortion.uasset와 일치시키기 위해 괄호를 추가로 사용). float2 UndistortedV = V * (1.0 + R2 * (RadialDistortionCoefs.x + R2 * (RadialDistortionCoefs.y + R2 * RadialDistortionCoefs.z))); // 탄젠트 디스토션입니다. UndistortedV.x += TangentialDistortionCoefs.y * (R2 + 2 * V2.x) + 2 * TangentialDistortionCoefs.x * V.x * V.y; UndistortedV.y += TangentialDistortionCoefs.x * (R2 + 2 * V2.y) + 2 * TangentialDistortionCoefs.y * V.x * V.y; return UndistortedV; } // 왜곡된 뷰포트 UV의 왜곡되지 않은 뷰포트 UV를 반환합니다. // // 참고: // UV의 최초 위치는 좌측 하단입니다. float2 UndistortViewportUV(float2 ViewportUV) { // 왜곡된 뷰포트 UV -> 왜곡된 뷰 위치(z=1) float2 DistortedViewPosition = (ViewportUV - DistortedCameraMatrix.zw) / DistortedCameraMatrix.xy; // 왜곡되지 않은 뷰 위치(z=1)을 계산합니다. float2 UndistortedViewPosition = UndistortNormalizedViewPosition(DistortedViewPosition); // 왜곡되지 않은 뷰 위치(z=1) -> 왜곡되지 않은 뷰포트 UV. return UndistortedCameraMatrix.xy * UndistortedViewPosition + UndistortedCameraMatrix.zw; } // UV의 y 컴포넌트를 뒤집습니다. float2 FlipUV(float2 UV) { return float2(UV.x, 1 - UV.y); } void MainVS( in uint GlobalVertexId : SV_VertexID, out float2 OutVertexDistortedViewportUV : TEXCOORD0, out float4 OutPosition : SV_POSITION ) { // 셀 인덱스를 계산합니다. uint GridCellIndex = GlobalVertexId / 6; // 그리드 내 셀의 행과 열 ID를 계산합니다. uint GridColumnId = GridCellIndex / GRID_SUBDIVISION_Y; uint GridRowId = GridCellIndex - GridColumnId * GRID_SUBDIVISION_Y; // 2개의 트라이앵글 그리드 셀 내 버텍스 ID를 계산합니다. uint VertexId = GlobalVertexId - GridCellIndex * 6; // 셀 내 트라이앵글의 버텍스의 최초 위치가 좌측 하단인 UV 좌표를 계산합니다. float2 CellVertexUV = float2(0x1 & ((VertexId + 1) / 3), VertexId & 0x1); // 그리드 내 버텍스의 최초 위치가 좌측 상단인 UV를 계산합니다. float2 GridInvSize = 1.f / float2(GRID_SUBDIVISION_X, GRID_SUBDIVISION_Y); float2 GridVertexUV = FlipUV( GridInvSize * (CellVertexUV + float2(GridColumnId, GridRowId))); // 반 픽셀씩 바꾸지 않는 것이 표준입니다. GridVertexUV -= PixelUVSize * 0.5; // 버텍스 위치를 출력합니다. OutPosition = float4(FlipUV( UndistortViewportUV(GridVertexUV) + PixelUVSize * 0.5) * 2 - 1, 0, 1); // 버텍스의 최초 위치가 좌측 상단인 UV를 출력합니다. OutVertexDistortedViewportUV = GridVertexUV; } void MainPS( in noperspective float2 VertexDistortedViewportUV : TEXCOORD0, in float4 SvPosition : SV_POSITION, out float4 OutColor : SV_Target0 ) { // 픽셀의 최초 위치가 좌측 상단인 UV를 계산합니다. float2 ViewportUV = SvPosition.xy * PixelUVSize; // 반 픽셀씩 바꾸지 않는 것이 표준입니다. ViewportUV -= PixelUVSize * 0.5; float2 DistortUVtoUndistortUV = (UndistortViewportUV((ViewportUV))) - ViewportUV; float2 UndistortUVtoDistortUV = VertexDistortedViewportUV - ViewportUV; // 디스플레이스먼트 채널을 출력합니다. OutColor = OutputMultiplyAndAdd.y + OutputMultiplyAndAdd.x * float4( DistortUVtoUndistortUV, UndistortUVtoDistortUV); } -
이제 Foo.uplugin 파일을 찾아 텍스트 에디터에서 열고, 해당 파일의 정보를 다음 텍스트로 대체한 후 저장합니다.
{ "FileVersion" : 3, "Version" : 1, "VersionName" : "1.0", "FriendlyName" : "Foo", "Description" : "Plugin to play around with shaders.", "Category" : "Sandbox", "CreatedBy" : "Epic Games, Inc.", "CreatedByURL" : "http://epicgames.com", "DocsURL" : "", "MarketplaceURL" : "", "SupportURL" : "", "EnabledByDefault" : false, "CanContainContent" : true, "IsBetaVersion" : false, "Installed" : false, "Modules" : [ { "Name" : "Foo", "Type" : "Developer", "LoadingPhase" : "PostConfigInit" } ] } -
그런 다음 Plugins\Foo\Source\Foo 로 이동하여 Classes 라는 새 폴더를 생성한 후 Engine\Plugins\Compositing\LensDistortion 에서 LensDistortionAPI.h 및 LensDistortionBlueprintLibrary.h 파일을 이 새로 생성된 폴더로 모두 복사합니다.
복사할 파일은 Engine\Plugins\Compositing\LensDistortion 에서 찾을 수 있습니다.
-
Classes - 새 폴더 생성
- 복사 - LensDistortionAPI.h
- 복사 - LensDistortionBlueprintLibrary.h
-
-
그런 다음 Private 폴더로 이동하여 이 Private 폴더로 LensDistortionBlueprintLibrary.cpp 및 LensDistortionRendering.cpp 파일을 모두 복사합니다.
-
Private - 기존 폴더
- 복사 - LensDistortionBlueprintLibrary.cpp
- 복사 - LensDistortionRendering.cpp
-
-
이제 언리얼 엔진 에디터와 Visual Studio를 모두 닫은 후 .U 프로젝트 파일을 찾습니다. 그런 다음 우클릭하고 Visual Studio 프로젝트 파일 생성(Generate Visual Studio project files) 옵션을 선택합니다.
-
Visual Studio 솔루션을 다시 연 후 Foo > Classes 로 이동하여 LensDistortionAPI.h 파일을 엽니다. 이 파일에서 FLensDistortionCameraModel 을 FFooCameraModel 로 대체합니다.
이 파일에서 FLensDistortionCameraModel을 FFooCameraModel로 4번 대체해야 합니다.
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "LensDistortionAPI.generated.h" /** Mathematic camera model for lens distortion/undistortion. * * Camera matrix = * | F.X 0 C.x | * | 0 F.Y C.Y | * | 0 0 1 | */ USTRUCT(BlueprintType) struct FFooCameraModel { GENERATED_USTRUCT_BODY() FFooCameraModel() { K1 = K2 = K3 = P1 = P2 = 0.f; F = FVector2D(1.f, 1.f); C = FVector2D(0.5f, 0.5f); } /** Radial parameter #1. */ UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = "Lens Distortion|Camera Model") float K1; /** Radial parameter #2. */ UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = "Lens Distortion|Camera Model") float K2; /** Radial parameter #3. */ UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = "Lens Distortion|Camera Model") float K3; /** Tangential parameter #1. */ UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = "Lens Distortion|Camera Model") float P1; /** Tangential parameter #2. */ UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = "Lens Distortion|Camera Model") float P2; /** Camera matrix's Fx and Fy. */ UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = "Lens Distortion|Camera Model") FVector2D F; /** Camera matrix's Cx and Cy. */ UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = "Lens Distortion|Camera Model") FVector2D C; /** Undistorts 3d vector (x, y, z=1.f) in the view space and returns (x', y', z'=1.f). */ FVector2D UndistortNormalizedViewPosition(FVector2D V) const; /** Returns the overscan factor required for the undistort rendering to avoid unrendered distorted pixels. */ float GetUndistortOverscanFactor( float DistortedHorizontalFOV, float DistortedAspectRatio) const; /** Draws UV displacement map within the output render target. * - Red & green channels hold the distortion displacement; * - Blue & alpha channels hold the undistortion displacement. * @param World 피처 레벨과 같이 렌더링 세팅을 얻을 현재 월드입니다. * @param DistortedHorizontalFOV 왜곡된 렌더에서 예상되는 수평 FOV입니다. * @param DistortedAspectRatio 왜곡된 렌더에서 예상되는 종횡비입니다. * @param UndistortOverscanFactor 왜곡되지 않은 렌더에 대한 오버스캔의 인수입니다. * @param OutputRenderTarget 드로할 렌더 타깃입니다. 해상도 또는 종횡비가 디스토션된 렌더링과 반드시 같을 필요는 없습니다. * @param OutputMultiply 디스플레이스먼트에 적용된 곱셈 인수입니다. * @param OutputAdd 출력 렌더 타깃을 저장하기 전에 곱해진 디스플레이스먼트에 더해진 값입니다. */ void DrawUVDisplacementToRenderTarget( class UWorld* World, float DistortedHorizontalFOV, float DistortedAspectRatio, float UndistortOverscanFactor, class UTextureRenderTarget2D* OutputRenderTarget, float OutputMultiply, float OutputAdd) const; /** Compare two lens distortion models and return whether they are equal. */ bool operator == (const FFooCameraModel& Other) const { return ( K1 == Other.K1 && K2 == Other.K2 && K3 == Other.K3 && P1 == Other.P1 && P2 == Other.P2 && F == Other.F && C == Other.C); } /** Compare two lens distortion models and return whether they are different. */ bool operator != (const FFooCameraModel& Other) const { return !(*this == Other); } }; -
그런 다음 LensDistortionBlueprintLibrary.h 파일을 엽니다. 이 파일은 이 노드가 블루프린트에 표시되는 방식을 결정하기 때문에, FLensDistortionCameraModel 을 FFooCameraModel 로 교체해야 할 뿐만 아니라 Category = "Lens Distortion" 도 Category = "Foo | Lens Distortion" 으로 변경해야 합니다.
이 파일에서 FLensDistortionCameraModel을 FFooCameraModel로 6번 대체해야 합니다.
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "UObject/ObjectMacros.h" #include "Classes/Kismet/BlueprintFunctionLibrary.h" #include "LensDistortionAPI.h" #include "LensDistortionBlueprintLibrary.generated.h" UCLASS(MinimalAPI) class ULensDistortionBlueprintLibrary : public UBlueprintFunctionLibrary { GENERATED_UCLASS_BODY() /** Returns the overscan factor required for the undistort rendering to avoid unrendered distorted pixels. */ UFUNCTION(BlueprintPure, Category = "Foo | Lens Distortion") static void GetUndistortOverscanFactor( const FFooCameraModel& CameraModel, float DistortedHorizontalFOV, float DistortedAspectRatio, float& UndistortOverscanFactor); /** Draws UV displacement map within the output render target. * - Red & green channels hold the distortion displacement; * - Blue & alpha channels hold the undistortion displacement. * @param DistortedHorizontalFOV 왜곡된 렌더에서 예상되는 수평 FOV입니다. * @param DistortedAspectRatio 왜곡된 렌더에서 예상되는 종횡비입니다. * @param UndistortOverscanFactor 왜곡되지 않은 렌더에 대한 오버스캔의 인수입니다. * @param OutputRenderTarget 드로할 렌더 타깃입니다. 해상도 또는 종횡비가 디스토션된 렌더링과 반드시 같을 필요는 없습니다. * @param OutputMultiply 디스플레이스먼트에 적용된 곱셈 인수입니다. * @param OutputAdd 출력 렌더 타깃에 저장하기 전에 곱해진 디스플레이스먼트에 더해진 값입니다. */ UFUNCTION(BlueprintCallable, Category = "Foo | Lens Distortion", meta = (WorldContext = "WorldContextObject")) static void DrawUVDisplacementToRenderTarget( const UObject* WorldContextObject, const FFooCameraModel& CameraModel, float DistortedHorizontalFOV, float DistortedAspectRatio, float UndistortOverscanFactor, class UTextureRenderTarget2D* OutputRenderTarget, float OutputMultiply = 0.5, float OutputAdd = 0.5 ); /* Returns true if A is equal to B (A == B) */ UFUNCTION(BlueprintPure, meta=(DisplayName = "Equal (LensDistortionCameraModel)", CompactNodeTitle = "==", Keywords = "== equal"), Category = "Foo | Lens Distortion") static bool EqualEqual_CompareLensDistortionModels( const FFooCameraModel& A, const FFooCameraModel& B) { return A == B; } /* Returns true if A is not equal to B (A != B) */ UFUNCTION(BlueprintPure, meta = (DisplayName = "NotEqual (LensDistortionCameraModel)", CompactNodeTitle = "!=", Keywords = "!= not equal"), Category = "Foo | Lens Distortion") static bool NotEqual_CompareLensDistortionModels( const FFooCameraModel& A, const FFooCameraModel& B) { return A != B; } }; -
이제 Private 폴더로 이동하고 LensDistortionBlueprintLibrary.cpp 파일을 열어 다음으로 대체합니다.
- FLensDistortionCameraModel 을 FFooCameraModel 로 대체
- ULensDistortionBlueprintLibrary 를 UFooBlueprintLibrary 로 대체
FLensDistortionCameraModel을 FFooCameraModel로 2번, ULensDistortionBlueprintLibrary를 UFooBlueprintLibrary로 4번 대체해야 합니다.
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. #include "LensDistortionBlueprintLibrary.h" ULensDistortionBlueprintLibrary::ULensDistortionBlueprintLibrary(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { } // 스태틱 void ULensDistortionBlueprintLibrary::GetUndistortOverscanFactor( const FFooCameraModel& CameraModel, float DistortedHorizontalFOV, float DistortedAspectRatio, float& UndistortOverscanFactor) { UndistortOverscanFactor = CameraModel.GetUndistortOverscanFactor(DistortedHorizontalFOV, DistortedAspectRatio); } // 스태틱 void ULensDistortionBlueprintLibrary::DrawUVDisplacementToRenderTarget( const UObject* WorldContextObject, const FFooCameraModel& CameraModel, float DistortedHorizontalFOV, float DistortedAspectRatio, float UndistortOverscanFactor, class UTextureRenderTarget2D* OutputRenderTarget, float OutputMultiply, float OutputAdd) { CameraModel.DrawUVDisplacementToRenderTarget( WorldContextObject->GetWorld(), DistortedHorizontalFOV, DistortedAspectRatio, UndistortOverscanFactor, OutputRenderTarget, OutputMultiply, OutputAdd); } -
그런 다음, Private 폴더에서 LensDistortionRendering.cpp 파일을 열고 FLensDistortionCameraModel 을 FFooCameraModel 로 대체합니다.
이 파일에서 FLensDistortionCameraModel을 FFooCameraModel로 6번 대체해야 합니다.
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. #include "LensDistortionAPI.h" #include "Classes/Engine/TextureRenderTarget2D.h" #include "Classes/Engine/World.h" #include "Public/GlobalShader.h" #include "Public/PipelineStateCache.h" #include "Public/RHIStaticStates.h" #include "Public/SceneUtils.h" #include "Public/SceneInterface.h" #include "Public/ShaderParameterUtils.h" static const uint32 kGridSubdivisionX = 32; static const uint32 kGridSubdivisionY = 16; /** * Internal intermediary structure derived from FFooCameraModel by the game thread * to hand to the render thread. */ struct FCompiledCameraModel { /** Orignal camera model that has generated this compiled model. */ FFooCameraModel OriginalCameraModel; /** Camera matrices of the lens distortion for the undistorted and distorted render. * XY holds the scales factors, ZW holds the translates. */ FVector4 DistortedCameraMatrix; FVector4 UndistortedCameraMatrix; /** Output multiply and add of the channel to the render target. */ FVector2D OutputMultiplyAndAdd; }; /** Undistorts top left originated viewport UV into the view space (x', y', z'=1.f) */ static FVector2D LensUndistortViewportUVIntoViewSpace( const FFooCameraModel& CameraModel, float TanHalfDistortedHorizontalFOV, float DistortedAspectRatio, FVector2D DistortedViewportUV) { FVector2D AspectRatioAwareF = CameraModel.F * FVector2D(1, -DistortedAspectRatio); return CameraModel.UndistortNormalizedViewPosition((DistortedViewportUV - CameraModel.C) / AspectRatioAwareF); } class FLensDistortionUVGenerationShader : public FGlobalShader { public: static bool ShouldCache(EShaderPlatform Platform) { return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM4); } static void ModifyCompilationEnvironment(EShaderPlatform Platform, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Platform, OutEnvironment); OutEnvironment.SetDefine(TEXT("GRID_SUBDIVISION_X"), kGridSubdivisionX); OutEnvironment.SetDefine(TEXT("GRID_SUBDIVISION_Y"), kGridSubdivisionY); } FLensDistortionUVGenerationShader() {} FLensDistortionUVGenerationShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) { PixelUVSize.Bind(Initializer.ParameterMap, TEXT("PixelUVSize")); RadialDistortionCoefs.Bind(Initializer.ParameterMap, TEXT("RadialDistortionCoefs")); TangentialDistortionCoefs.Bind(Initializer.ParameterMap, TEXT("TangentialDistortionCoefs")); DistortedCameraMatrix.Bind(Initializer.ParameterMap, TEXT("DistortedCameraMatrix")); UndistortedCameraMatrix.Bind(Initializer.ParameterMap, TEXT("UndistortedCameraMatrix")); OutputMultiplyAndAdd.Bind(Initializer.ParameterMap, TEXT("OutputMultiplyAndAdd")); } template<typename TShaderRHIParamRef> void SetParameters( FRHICommandListImmediate& RHICmdList, const TShaderRHIParamRef ShaderRHI, const FCompiledCameraModel& CompiledCameraModel, const FIntPoint& DisplacementMapResolution) { FVector2D PixelUVSizeValue( 1.f / float(DisplacementMapResolution.X), 1.f / float(DisplacementMapResolution.Y)); FVector RadialDistortionCoefsValue( CompiledCameraModel.OriginalCameraModel.K1, CompiledCameraModel.OriginalCameraModel.K2, CompiledCameraModel.OriginalCameraModel.K3); FVector2D TangentialDistortionCoefsValue( CompiledCameraModel.OriginalCameraModel.P1, CompiledCameraModel.OriginalCameraModel.P2); SetShaderValue(RHICmdList, ShaderRHI, PixelUVSize, PixelUVSizeValue); SetShaderValue(RHICmdList, ShaderRHI, DistortedCameraMatrix, CompiledCameraModel.DistortedCameraMatrix); SetShaderValue(RHICmdList, ShaderRHI, UndistortedCameraMatrix, CompiledCameraModel.UndistortedCameraMatrix); SetShaderValue(RHICmdList, ShaderRHI, RadialDistortionCoefs, RadialDistortionCoefsValue); SetShaderValue(RHICmdList, ShaderRHI, TangentialDistortionCoefs, TangentialDistortionCoefsValue); SetShaderValue(RHICmdList, ShaderRHI, OutputMultiplyAndAdd, CompiledCameraModel.OutputMultiplyAndAdd); } virtual bool Serialize(FArchive& Ar) override { bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar); Ar << PixelUVSize << RadialDistortionCoefs << TangentialDistortionCoefs << DistortedCameraMatrix << UndistortedCameraMatrix << OutputMultiplyAndAdd; return bShaderHasOutdatedParameters; } private: FShaderParameter PixelUVSize; FShaderParameter RadialDistortionCoefs; FShaderParameter TangentialDistortionCoefs; FShaderParameter DistortedCameraMatrix; FShaderParameter UndistortedCameraMatrix; FShaderParameter OutputMultiplyAndAdd; }; class FLensDistortionUVGenerationVS : public FLensDistortionUVGenerationShader { DECLARE_SHADER_TYPE(FLensDistortionUVGenerationVS, Global); public: /** Default constructor. */ FLensDistortionUVGenerationVS() {} /** Initialization constructor. */ FLensDistortionUVGenerationVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FLensDistortionUVGenerationShader(Initializer) { } }; class FLensDistortionUVGenerationPS : public FLensDistortionUVGenerationShader { DECLARE_SHADER_TYPE(FLensDistortionUVGenerationPS, Global); public: /** Default constructor. */ FLensDistortionUVGenerationPS() {} /** Initialization constructor. */ FLensDistortionUVGenerationPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FLensDistortionUVGenerationShader(Initializer) { } }; IMPLEMENT_SHADER_TYPE(, FLensDistortionUVGenerationVS, TEXT("/Plugin/Foo/Private/MyShader.usf"), TEXT("MainVS"), SF_Vertex) IMPLEMENT_SHADER_TYPE(, FLensDistortionUVGenerationPS, TEXT("/Plugin/Foo/Private/MyShader.usf"), TEXT("MainPS"), SF_Pixel) static void DrawUVDisplacementToRenderTarget_RenderThread( FRHICommandListImmediate& RHICmdList, const FCompiledCameraModel& CompiledCameraModel, const FName& TextureRenderTargetName, FTextureRenderTargetResource* OutTextureRenderTargetResource, ERHIFeatureLevel::Type FeatureLevel) { check(IsInRenderingThread()); #if WANTS_DRAW_MESH_EVENTS FString EventName; TextureRenderTargetName.ToString(EventName); SCOPED_DRAW_EVENTF(RHICmdList, SceneCapture, TEXT("LensDistortionDisplacementGeneration %s"), *EventName); #else SCOPED_DRAW_EVENT(RHICmdList, DrawUVDisplacementToRenderTarget_RenderThread); #endif // 렌더 타깃을 설정합니다. SetRenderTarget( RHICmdList, OutTextureRenderTargetResource->GetRenderTargetTexture(), FTextureRHIRef(), ESimpleRenderTargetMode::EUninitializedColorAndDepth, FExclusiveDepthStencil::DepthNop_StencilNop); FIntPoint DisplacementMapResolution(OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY()); // 뷰포트를 업데이트합니다. RHICmdList.SetViewport( 0, 0, 0.f, DisplacementMapResolution.X, DisplacementMapResolution.Y, 1.f); // 셰이더를 구합니다. TShaderMap<FGlobalShaderType>* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel); TShaderMapRef< FLensDistortionUVGenerationVS > VertexShader(GlobalShaderMap); TShaderMapRef< FLensDistortionUVGenerationPS > PixelShader(GlobalShaderMap); // 그래픽 파이프라인 스테이트를 설정합니다. FGraphicsPipelineStateInitializer GraphicsPSOInit; RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI(); GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4(); GraphicsPSOInit.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*VertexShader); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*PixelShader); SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit); // 뷰포트를 업데이트합니다. RHICmdList.SetViewport( 0, 0, 0.f, OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY(), 1.f); // 셰이더 유니폼 파라미터를 업데이트합니다. VertexShader->SetParameters(RHICmdList, VertexShader->GetVertexShader(), CompiledCameraModel, DisplacementMapResolution); PixelShader->SetParameters(RHICmdList, PixelShader->GetPixelShader(), CompiledCameraModel, DisplacementMapResolution); // 그리드를 그립니다. uint32 PrimitiveCount = kGridSubdivisionX * kGridSubdivisionY * 2; RHICmdList.DrawPrimitive(PT_TriangleList, 0, PrimitiveCount, 1); // 렌더 타깃을 해석합니다. RHICmdList.CopyToResolveTarget( OutTextureRenderTargetResource->GetRenderTargetTexture(), OutTextureRenderTargetResource->TextureRHI, false, FResolveParams()); } FVector2D FFooCameraModel::UndistortNormalizedViewPosition(FVector2D EngineV) const { // 엔진 뷰 스페이스 -> 표준 뷰 스페이스. FVector2D V = FVector2D(1, -1) * EngineV; FVector2D V2 = V * V; float R2 = V2.X + V2.Y; // 방사상 디스토션입니다(MF_Undistortion.uasset와 일치시키기 위해 괄호를 추가로 사용). FVector2D UndistortedV = V * (1.0 + (R2 * K1 + (R2 * R2) * K2 + (R2 * R2 * R2) * K3)); // 탄젠트 디스토션입니다. UndistortedV.X += P2 * (R2 + 2 * V2.X) + 2 * P1 * V.X * V.Y; UndistortedV.Y += P1 * (R2 + 2 * V2.Y) + 2 * P2 * V.X * V.Y; // 엔진 V를 반환합니다. return UndistortedV * FVector2D(1, -1); } /** Compiles the camera model. */ float FFooCameraModel::GetUndistortOverscanFactor( float DistortedHorizontalFOV, float DistortedAspectRatio) const { // 렌즈 디스토션 모델이 동일하다면, 1을 조기 반환합니다. if (*this == FFooCameraModel()) { return 1.0f; } float TanHalfDistortedHorizontalFOV = FMath::Tan(DistortedHorizontalFOV * 0.5f); // 왜곡된 뷰포트 UV 좌표 시스템의 여러 키 포인트에서 z'=1인 뷰 스페이스의 위치를 구합니다. // 이는 왜곡되지 않은 뷰포트의 필수 오버스캔 스케일 인수를 알기 위한 근사치지만, 실제로 사용할 때는 매우 효과적입니다. // // 뷰 스페이스의 왜곡되지 않은 UV 위치: // ^ 뷰 스페이스의 Y // | // 0 1 2 // // 7 0 3 --> 뷰 스페이스의 X // // 6 5 4 FVector2D UndistortCornerPos0 = LensUndistortViewportUVIntoViewSpace( *this, TanHalfDistortedHorizontalFOV, DistortedAspectRatio, FVector2D(0.0f, 0.0f)); FVector2D UndistortCornerPos1 = LensUndistortViewportUVIntoViewSpace( *this, TanHalfDistortedHorizontalFOV, DistortedAspectRatio, FVector2D(0.5f, 0.0f)); FVector2D UndistortCornerPos2 = LensUndistortViewportUVIntoViewSpace( *this, TanHalfDistortedHorizontalFOV, DistortedAspectRatio, FVector2D(1.0f, 0.0f)); FVector2D UndistortCornerPos3 = LensUndistortViewportUVIntoViewSpace( *this, TanHalfDistortedHorizontalFOV, DistortedAspectRatio, FVector2D(1.0f, 0.5f)); FVector2D UndistortCornerPos4 = LensUndistortViewportUVIntoViewSpace( *this, TanHalfDistortedHorizontalFOV, DistortedAspectRatio, FVector2D(1.0f, 1.0f)); FVector2D UndistortCornerPos5 = LensUndistortViewportUVIntoViewSpace( *this, TanHalfDistortedHorizontalFOV, DistortedAspectRatio, FVector2D(0.5f, 1.0f)); FVector2D UndistortCornerPos6 = LensUndistortViewportUVIntoViewSpace( *this, TanHalfDistortedHorizontalFOV, DistortedAspectRatio, FVector2D(0.0f, 1.0f)); FVector2D UndistortCornerPos7 = LensUndistortViewportUVIntoViewSpace( *this, TanHalfDistortedHorizontalFOV, DistortedAspectRatio, FVector2D(0.0f, 0.5f)); // z'=1인 뷰 스페이스 내 왜곡되지 않은 뷰포트의 최소, 최대 내각을 찾습니다. FVector2D MinInnerViewportRect; FVector2D MaxInnerViewportRect; MinInnerViewportRect.X = FMath::Max3(UndistortCornerPos0.X, UndistortCornerPos6.X, UndistortCornerPos7.X); MinInnerViewportRect.Y = FMath::Max3(UndistortCornerPos4.Y, UndistortCornerPos5.Y, UndistortCornerPos6.Y); MaxInnerViewportRect.X = FMath::Min3(UndistortCornerPos2.X, UndistortCornerPos3.X, UndistortCornerPos4.X); MaxInnerViewportRect.Y = FMath::Min3(UndistortCornerPos0.Y, UndistortCornerPos1.Y, UndistortCornerPos2.Y); check(MinInnerViewportRect.X < 0.f); check(MinInnerViewportRect.Y < 0.f); check(MaxInnerViewportRect.X > 0.f); check(MaxInnerViewportRect.Y > 0.f); // tan(수직 FOV * 0.5)을 계산합니다. float TanHalfDistortedVerticalFOV = TanHalfDistortedHorizontalFOV / DistortedAspectRatio; // 왜곡되지 않은 뷰포트의 필수 스케일을 축마다 계산합니다. FVector2D ViewportScaleUpFactorPerViewAxis = 0.5 * FVector2D( TanHalfDistortedHorizontalFOV / FMath::Max(-MinInnerViewportRect.X, MaxInnerViewportRect.X), TanHalfDistortedVerticalFOV / FMath::Max(-MinInnerViewportRect.Y, MaxInnerViewportRect.Y)); // 뷰 스페이스에서 왜곡되지 않은 뷰포트를 2% 확장해야 작동합니다. // 왜곡되지 않은 이상한 위치가 정확히 최소가 아닐 수도 있기 때문입니다. // 탄젠트 디스토션 배럴 렌즈 디스토션을 예로 들 수 있습니다. const float ViewportScaleUpConstMultiplier = 1.02f; return FMath::Max(ViewportScaleUpFactorPerViewAxis.X, ViewportScaleUpFactorPerViewAxis.Y) * ViewportScaleUpConstMultiplier; } void FFooCameraModel::DrawUVDisplacementToRenderTarget( UWorld* World, float DistortedHorizontalFOV, float DistortedAspectRatio, float UndistortOverscanFactor, UTextureRenderTarget2D* OutputRenderTarget, float OutputMultiply, float OutputAdd) const { check(IsInGameThread()); // 오버스캔 스케일 인수를 알기 위해 카메라 모델을 컴파일합니다. float TanHalfUndistortedHorizontalFOV = FMath::Tan(DistortedHorizontalFOV * 0.5f) * UndistortOverscanFactor; float TanHalfUndistortedVerticalFOV = TanHalfUndistortedHorizontalFOV / DistortedAspectRatio; // 출력. FCompiledCameraModel CompiledCameraModel; CompiledCameraModel.OriginalCameraModel = *this; CompiledCameraModel.DistortedCameraMatrix.X = 1.0f / TanHalfUndistortedHorizontalFOV; CompiledCameraModel.DistortedCameraMatrix.Y = 1.0f / TanHalfUndistortedVerticalFOV; CompiledCameraModel.DistortedCameraMatrix.Z = 0.5f; CompiledCameraModel.DistortedCameraMatrix.W = 0.5f; CompiledCameraModel.UndistortedCameraMatrix.X = F.X; CompiledCameraModel.UndistortedCameraMatrix.Y = F.Y * DistortedAspectRatio; CompiledCameraModel.UndistortedCameraMatrix.Z = C.X; CompiledCameraModel.UndistortedCameraMatrix.W = C.Y; CompiledCameraModel.OutputMultiplyAndAdd.X = OutputMultiply; CompiledCameraModel.OutputMultiplyAndAdd.Y = OutputAdd; const FName TextureRenderTargetName = OutputRenderTarget->GetFName(); FTextureRenderTargetResource* TextureRenderTargetResource = OutputRenderTarget->GameThread_GetRenderTargetResource(); ERHIFeatureLevel::Type FeatureLevel = World->Scene->GetFeatureLevel(); ENQUEUE_RENDER_COMMAND(CaptureCommand)( [CompiledCameraModel, TextureRenderTargetResource, TextureRenderTargetName, FeatureLevel](FRHICommandListImmediate& RHICmdList) { DrawUVDisplacementToRenderTarget_RenderThread( RHICmdList, CompiledCameraModel, TextureRenderTargetName, TextureRenderTargetResource, FeatureLevel); } ); } -
마지막으로 LensDistortionRendering.cpp 파일의 155번째, 156번째 줄이 앞서 생성한 MyShader.USF 파일을 가리키도록 변경합니다.
대체 원본:
- IMPLEMENT_SHADER_TYPE(, FLensDistortionUVGenerationVS, TEXT("/Plugin/LensDistortion/Private/UVGeneration.usf"), TEXT("MainVS"), SF_Vertex)
변경 후:
- IMPLEMENT_SHADER_TYPE(, FLensDistortionUVGenerationVS, TEXT("/Plugin/Foo/Private/MyShader.usf"), TEXT("MainVS"), SF_Vertex)
대체 원본:
- IMPLEMENT_SHADER_TYPE(, FLensDistortionUVGenerationPS, TEXT("/Plugin/LensDistortion/Private/UVGeneration.usf"), TEXT("MainPS"), SF_Pixel)
변경 후:
- IMPLEMENT_SHADER_TYPE(, FLensDistortionUVGenerationPS, TEXT("/Plugin/Foo/Private/MyShader.usf"), TEXT("MainPS"), SF_Pixel)
-
이제 Foo/Source 폴더에서 Foo.Build.cs 파일을 열고, Foo.Build.cs 파일의 PublicDependencyModuleNames.AddRange 섹션 아래 다음 코드 줄을 추가합니다.
PublicDependencyModuleNames.AddRange( new string[] { "Core", "RenderCore", "ShaderCore", "RHI", // ... 여기에 정적으로 연결할 다른 퍼블릭 종속성을 추가하세요 ... } ); -
그런 다음 Foo.Build.cs 파일의 PrivateDependencyModuleNames.AddRange 섹션에서 Slate 및 SlateCore 를 제거합니다. 완료된 Foo.Build.cs 파일은 다음과 같습니다.
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; public class Foo : ModuleRules { public Foo(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicIncludePaths.AddRange( new string[] { "Foo/Public" // ... 필요한 퍼블릭 포함 경로를 여기에 추가하세요 ... } ); PrivateIncludePaths.AddRange( new string[] { "Foo/Private", // ... 필요한 다른 프라이빗 포함 경로를 여기에 추가하세요 ... } ); PublicDependencyModuleNames.AddRange( new string[] { "Core", "RenderCore", "ShaderCore", "RHI", // ... 여기에 정적으로 연결할 다른 퍼블릭 종속성을 추가하세요 ... } ); PrivateDependencyModuleNames.AddRange( new string[] { "CoreUObject", "Engine", // ... 여기에 정적으로 연결할 프라이빗 종속성을 추가하세요 ... } ); DynamicallyLoadedModuleNames.AddRange( new string[] { // ... 자신의 모듈이 동적으로 로드할 모든 모듈을 여기에 추가하세요 ... } ); } } -
프로젝트의 Visual Studio 솔루션 파일을 다시 실행한 후 CTRL + 5 를 눌러 프로젝트를 리컴파일합니다. 컴파일이 완료되면 F5 를 눌러 언리얼 엔진 에디터를 실행합니다.
-
언리얼 엔진 에디터 로딩이 완료되면 편집 > 플러그인 에서 플러그인 매니저로 이동합니다.
-
플러그인 매니저 내부에서 하단까지 스크롤해 프로젝트(Project) 섹션을 찾습니다. 이 섹션에 새로 만든 플러그인이 있을 것입니다.
플러그인이 활성화되지 않은 경우, 해당 플러그인 이름 옆의 체크박스를 클릭하여 활성화한 후 언리얼 엔진 에디터를 재시작합니다.
-
모든 요소가 제대로 구성되었는지 확인하려면 레벨 블루프린트를 열고 이벤트 그래프에서 우클릭한 뒤 검색창에 Foo 라고 입력합니다. 그러면 Foo Camera 카테고리에 추가된 모든 항목이 보일 것입니다.
이미지를 클릭하면 전체 크기로 표시됩니다.
최종 결과
축하합니다! 렌즈 디스토션 언리얼 엔진 플러그인의 새로운 버전을 제작 및 컴파일하는 데 성공하셨습니다. 다음 단계에서는 블루프린트에서 이 새로운 글로벌 셰이더를 불러오는 방법을 배우고, 렌더 타깃에 어떻게 디스토션이 적용되는지도 살펴보겠습니다.
2 - 블루프린트 구성
플러그인 작동을 확인했으니, 다음 섹션에서는 블루프린트를 사용하여 글로벌 셰이더를 호출하는 방법을 살펴보겠습니다.
-
이 작동 방식을 확인하려면 콘텐츠 브라우저(Content Browser) 에서 우클릭한 후 부모 클래스가 액터(Actor) 이고 이름이 DrawUVDisToRenderTarget 인 새 블루프린트 클래스(Blueprint Class) 를 생성합니다.
-
그런 다음 렌더 타깃(Render Target) 을 만들어 이름을 RT_00 으로 하고, 새 머티리얼을 만들어 이름을 MAT_RT 로 합니다.
-
MAT_RT 머티리얼을 열고 머티리얼 그래프에 RT_00 렌더 타깃을 추가한 뒤, 그 출력을 메인 머티리얼 노드의 베이스 컬러 입력에 연결하고 적용(Apply) 및 저장(Save) 버튼을 누릅니다.
이미지를 클릭하면 전체 크기로 표시됩니다.
-
그런 다음 DrawUVDisToRenderTarget 블루프린트를 열고 다음 변수 및 노드를 이벤트 그래프에 생성/추가합니다.
변수 디폴트값 K1 1.0 K2 1.0 inc 1.0 노드 디폴트값 W 해당 없음 S 해당 없음 Event Tick 해당 없음 Draw UVDisplacement to Render Target FOV, Aspect Ratio, and Overscan Factor에 1 입력 Make FooCameraModel 해당 없음 Clear Render Target 2D 해당 없음
-
Draw UVDisplacement to Render Target이 제공된 렌더 타깃을 어떻게 워프하는지 한눈에 보기 위해, K1 및 K2 입력에 연결된 버튼을 누르면 증가 또는 감소되도록 블루프린트를 구성할 것입니다. 이벤트 그래프의 노드가 다음 이미지에 일치하도록 구성합니다.
Click for full image.
렌더 타깃을 Output Render Target 에 입력하지 않으면 아무것도 보이지 않는다는 점을 잊지 마세요.
-
이제 필수 블루프린트 로직이 구성되었으니, 블루프린트를 컴파일 및 저장 한 뒤 콘텐츠 브라우저 에서 블루프린트를 레벨 로 드래그합니다. 완료되면, 블루프린트를 선택하고 입력(Input) 아래 입력 자동 수신(Auto Receive Input) 을 비활성화됨(Disabled) 에서 플레이어 0(Player 0) 으로 변경합니다.
이미지를 클릭하면 전체 크기로 표시됩니다.
-
그런 다음 바닥을 선택하고 CTRL + W 를 눌러 복제한 뒤 X 축으로 -90도 회전합니다. 그리고 그 위에 MAT_RT 머티리얼을 드래그하면 Draw UVDisplacement to Render Target의 효과를 확인할 수 있습니다.
이미지를 클릭하면 전체 크기로 표시됩니다.
-
구성이 모두 완료되었으면, 플레이(Play) 버튼을 누른 뒤 W 및 S 를 눌러 다음 동영상과 같이 Draw UVDisplacement to Render Target 노드에 입력된 렌더 타깃을 워프합니다.
최종 결과
이 시점에서 블루프린트를 통해 렌더 타깃 안의 콘텐츠를 왜곡해서 다양하고 흥미로운 셰이프를 만들 수 있습니다. 이제 새로운 플러그인을 제작하거나 기존 플러그인을 편집하여 재미있는 이미지와 이펙트를 직접 만들어 보세요.