[{"data":1,"prerenderedAt":3078},["ShallowReactive",2],{"page-UE4-7":3,"page-count-UE4":3077},[4,1294,1735,2111,2417],{"id":5,"title":6,"body":7,"date":1272,"description":13,"extension":1273,"meta":1274,"navigation":385,"path":1288,"seo":1289,"stem":1290,"tags":1291,"__hash__":1293},"blogs\u002F_legacy\u002F2017\u002F2017-12-16-ue4-async-note.md","UE4异步操作总结",{"type":8,"value":9,"toc":1245},"minimark",[10,14,17,20,23,26,29,32,35,40,43,46,49,52,57,60,71,74,78,81,84,90,93,96,99,103,106,121,124,130,134,137,143,146,152,155,159,162,165,168,172,175,178,182,185,189,192,198,201,204,207,210,216,219,224,227,230,233,236,241,244,249,252,257,260,263,266,270,273,279,282,285,289,292,295,298,304,307,310,314,317,322,557,560,632,635,639,642,732,735,739,742,745,860,863,915,918,922,925,928,1125,1128,1131,1142,1220,1223,1226,1229,1232,1235,1238,1241],[11,12,13],"p",{},"虚幻本身有提供一些对异步操作的封装，这里是对这段时间接触到的“非同步”的操作进行的总结。",[11,15,16],{},"当前使用的UE4版本为4.18.2。",[11,18,19],{},"在虚幻的游戏制作中，如果不是特殊情况一般不会有用到线程的时候。但是由于实际上虚幻内部是有着许多线程机制的。",[11,21,22],{},"例如通常的游戏引擎中游戏线程和渲染线程都是独立的，相互之间会存在一个同步的机制。",[11,24,25],{},"而物理线程与游戏线程之间的同步有时候也会导致游戏的表现与预期不一致。",[11,27,28],{},"通常会有线程同步需求的地方是网络相关的操作，但是实际上UE4已经对网络操作进行了封装，无需关心这个问题。",[11,30,31],{},"而游戏线程、渲染线程、物理线程内部也都已经有了封装，对游戏逻辑的构建基本是不可见的。",[11,33,34],{},"但是有时候还是会遇到需要使用线程相关逻辑的，这里就是这段时间内累计的“非同步”相关逻辑的总结。",[36,37,39],"h2",{"id":38},"tick","Tick",[11,41,42],{},"这个其实关于Tick的，虽然Actor是有默认的Tick函数的，Component与UMG也有对应的Tick机制。",[11,44,45],{},"但是如果是自定义的UObject或者Slate，要使用Tick机制的话就会有些麻烦。",[11,47,48],{},"例如，想要让自定义的Slate控件进行某种数据更新，而数据源本身并不提供通知机制的话就会有些麻烦。",[11,50,51],{},"虽然通过各种设计可以巧妙的绕过这个问题，但是有时候在类内部构建Tick机制才是最快速的解决方案。",[53,54,56],"h3",{"id":55},"timermanager","TimerManager",[11,58,59],{},"通过使用引擎提供的定时器机制，就可以进行自定义的Tick了：",[61,62,67],"pre",{"className":63,"code":65,"language":66},[64],"language-text","GetWord()->GetTimerManager().SetTimer(\n  m_hTimerHandle,\n  this,\n  &UNetPlayManager::TimerTick,\n  1.0,\n  true\n);\n","text",[68,69,65],"code",{"__ignoreMap":70},"",[11,72,73],{},"这里需要能够获得UWorld的指针，如果是自定义的类型的话，就必须想办法提供有效的UWorld指针。",[53,75,77],{"id":76},"ftickablegameobject","FTickableGameObject",[11,79,80],{},"还有另一个方法，就是使用FTickableGameObject。",[11,82,83],{},"任何继承自FTickableGameObject的类型都会获得Tick的能力，就算不是虚幻原生的类型也可以使用，相当的便利。使用时继承自该类型，然后：",[61,85,88],{"className":86,"code":87,"language":66},[64],"public:\n\n\u002F** \u003CTick接口函数 *\u002F\nvirtual void Tick(float DeltaTime) override;\n\nvirtual bool IsTickable() const override\n{\n  return true;\n}\n\nvirtual bool IsTickableWhenPaused() const override\n{\n  return true;\n}\n\nvirtual TStatId GetStatId() const override\n{\n  RETURN_QUICK_DECLARE_CYCLE_STAT(USceneCapturer, STATGROUP_Tickables);\n}\n",[68,89,87],{"__ignoreMap":70},[11,91,92],{},"继承一下基本的函数就可以了。",[36,94,95],{"id":95},"线程同步",[11,97,98],{},"UE4对操作系统提供的线程同步相关接口进行了一定的封装。",[53,100,102],{"id":101},"atomics","Atomics",[11,104,105],{},"基本的接口可以在FPlatformAtomics找到，针对不同的平台，有不同的实现。",[107,108,109,112,115,118],"blockquote",{},[11,110,111],{},"InterlockedAdd",[11,113,114],{},"InterlockedCompareExchange (-Pointer)",[11,116,117],{},"InterlockedDecrement (-Increment)",[11,119,120],{},"InterlockedExchange (-Pointer)",[11,122,123],{},"详细的可以参看其源码。也可以参看引擎内部的使用方式：",[61,125,128],{"className":126,"code":127,"language":66},[64],"class FThreadSafeCounter\n{\npublic:\n  int32 Add( int32 Amount )\n  {\n    return FPlatformAtomics::InterlockedAdd(&Counter, Amount);\n  }\nprivate:\n  volatile int32 Counter;\n};\n",[68,129,127],{"__ignoreMap":70},[53,131,133],{"id":132},"fcriticalsection","FCriticalSection",[11,135,136],{},"用于对非线程安全的区域进行保护。",[61,138,141],{"className":139,"code":140,"language":66},[64],"FCriticalSection CriticalSection;\n",[68,142,140],{"__ignoreMap":70},[11,144,145],{},"声明之后在需要的地方进行锁操作即可，有提供作用域保护的封装：",[61,147,150],{"className":148,"code":149,"language":66},[64],"FScopeLock Lock(&CriticalSection);\n",[68,151,149],{"__ignoreMap":70},[11,153,154],{},"这样就不需要自己进行Lock和Unlock了，可以有效的防止误操作导致的Bug的出现。",[53,156,158],{"id":157},"fspinlock","FSpinLock",[11,160,161],{},"锁操作，提供Lock，Unlock以及BlockUntilUnlocked等便利的操作。",[11,163,164],{},"其实内部就是对FPlatformAtomics::InterlockedExchange的一个封装。",[11,166,167],{},"构造函数的InSpinTimeInSeconds就是默认的锁等待间隔，默认值为0.1。",[53,169,171],{"id":170},"fsemaphore","FSemaphore",[11,173,174],{},"这个是对信号量的封装，但是似乎不建议使用。",[11,176,177],{},"而且并不是对于所有的平台都有实现的，通常建议使用FEvent进行代替。",[53,179,181],{"id":180},"fevent","FEvent",[11,183,184],{},"这个相当于UE4封装的内部使用的互斥信号量机制，有基本的等待和唤醒操作。",[53,186,188],{"id":187},"fscopedevent","FScopedEvent",[11,190,191],{},"对FEvnet的封装，在注释上能够看到使用示例：",[61,193,196],{"className":194,"code":195,"language":66},[64],"{\n        FScopedEvent MyEvent;\n        SendReferenceOrPointerToSomeOtherThread(&MyEvent); \u002F\u002F Other thread calls MyEvent->Trigger();\n        \u002F\u002F MyEvent destructor is here, we wait here.\n}\n",[68,197,195],{"__ignoreMap":70},[11,199,200],{},"这个操作就是将MyEvent发送到其他线程，直到在其他的地方MyEvnet->Trigger()被调用为止，都不会离开这个作用域继续执行。",[53,202,203],{"id":203},"容器",[11,205,206],{},"包括TArray, TMap在内的几乎大部分的容器都不是线程安全的，需要自己对同步进行管理。",[11,208,209],{},"当然也能看到一些线程安全的封装，例如TArrayWithThreadsafeAdd。",[11,211,212],{},[213,214,215],"strong",{},"TLockFreePointerList",[11,217,218],{},"这个是一系列的类型，在Task Graph系统中被使用到。如其名称是LockFree的。",[11,220,221],{},[213,222,223],{},"TQueue",[11,225,226],{},"也是LockFree的，在初始化时可以指定线程同步的类型EQueueMode，分为Mpsc（多生产者单消费者）以及Spsc（单生产者单消费者）两种模式。",[11,228,229],{},"只有Spsc模式是contention free的。",[11,231,232],{},"仔细寻找的话UE4内部有实现很多便利的类型，例如TCircularQueue这种针对双线程，一个消费一个生产的线程安全类型。",[53,234,235],{"id":235},"工具类",[11,237,238],{},[213,239,240],{},"FThreadSafeCounter",[11,242,243],{},"就是前面例子中的线程安全的计数器。",[11,245,246],{},[213,247,248],{},"FThreadSingleton",[11,250,251],{},"为每一个线程创建一个实例。",[11,253,254],{},[213,255,256],{},"FThreadIdleStats",[11,258,259],{},"用于统计线程空闲状态。",[36,261,262],{"id":262},"异步执行",[11,264,265],{},"UE4中对基本的线程操作进行了一定程度的封装，使用相应的Helper就可以无需关心线程的创建这些问题。",[53,267,269],{"id":268},"asynctask","AsyncTask",[11,271,272],{},"这个函数可以将一些简单的任务扔到UE4的线程池中去进行，不必关心具体的线程同步问题。",[61,274,277],{"className":275,"code":276,"language":66},[64],"if(IsInGameThread())\n{\n  \u002F\u002F….一些操作\n}\nelse\n{\n  AsyncTask(ENamedThreads::GameThread, [=]()\n  {\n    \u002F\u002F….一些操作\n  });\n}\n",[68,278,276],{"__ignoreMap":70},[11,280,281],{},"其中第一个参数是发送到的线程的名称，通常一些工作线程是无法执行引擎中IsGameThread()保护或者其他隐形的游戏线程代码的，通过这个操作将其发送到游戏线程的话使用GameThread就可以了。",[11,283,284],{},"其实基本上的游戏逻辑中使用最多的就是这个函数了。",[53,286,288],{"id":287},"rhicmdlist","RHICmdList",[11,290,291],{},"这是一组独特的宏，用于将操作发送到渲染线程进行操作。",[11,293,294],{},"主要是对Texture之类的数据在GPU以及GPU相关的指令进行执行。",[11,296,297],{},"例如：",[61,299,302],{"className":300,"code":301,"language":66},[64],"if (IsInRenderingThread())\n{\n    \u002F\u002F Initialize the vertex factory's stream components.\n    FDataType NewData;\n    NewData.PositionComponent = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, Position, VET_Float3);\n    NewData.TangentBasisComponents[0] = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, TangentX, VET_PackedNormal);\n    NewData.TangentBasisComponents[1] = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, TangentZ, VET_PackedNormal);\n    NewData.ColorComponent = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, Color, VET_Color);\n    NewData.TextureCoordinates.Add(FVertexStreamComponent(InVertexBuffer, STRUCT_OFFSET(FPaperSpriteVertex, TexCoords), sizeof(FPaperSpriteVertex), VET_Float2));\n    SetData(NewData);\n}\nelse\n{\n    ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(\n        InitPaperSpriteVertexFactory,\n        FPaperSpriteVertexFactory*, VertexFactory, this,\n        const FPaperSpriteVertexBuffer*, VB, InVertexBuffer,\n        {\n            VertexFactory->Init(VB);\n        });\n}\n",[68,303,301],{"__ignoreMap":70},[11,305,306],{},"这样就可以保证只能在渲染线程执行的代码不会被其他线程执行到。",[11,308,309],{},"渲染线程还有一些需要注意的是，UE4中有的代码的执行其实是在渲染线程中的，如果没有留意的话会造成隐形的线程同步问题。例如通常UMG的OnPaint。",[53,311,313],{"id":312},"fasynctask","FAsyncTask",[11,315,316],{},"这个是一组任务的封装类，是基本的任务单元，最简单的使用如下：",[318,319,321],"h4",{"id":320},"fautodeleteasynctask","FAutoDeleteAsyncTask",[61,323,327],{"className":324,"code":325,"language":326,"meta":70,"style":70},"language-cpp shiki shiki-themes github-light-high-contrast github-dark monokai","class ExampleAutoDeleteAsyncTask : public FNonAbandonableTask\n{\n    friend class FAutoDeleteAsyncTask\u003CExampleAutoDeleteAsyncTask>;\n\n    int32 ExampleData;\n\n    ExampleAutoDeleteAsyncTask(int32 InExampleData)\n        : ExampleData(InExampleData)\n    {\n        UE_LOG(LogTemp, Log, TEXT(\"[ExampleAutoDeleteAsyncTask] Construct()\"));\n    }\n\n    void DoWork()\n    {\n        UE_LOG(LogTemp, Log, TEXT(\"[ExampleAutoDeleteAsyncTask] DoWork()\"));\n    }\n\n    FORCEINLINE TStatId GetStatId() const\n    {\n        RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAutoDeleteAsyncTask, STATGROUP_ThreadPoolAsyncTasks);\n    }\n};\n","cpp",[68,328,329,352,358,380,387,396,401,421,433,439,460,466,471,483,488,504,509,514,532,537,546,551],{"__ignoreMap":70},[330,331,334,338,342,346,349],"span",{"class":332,"line":333},"line",1,[330,335,337],{"class":336},"sGZGq","class",[330,339,341],{"class":340},"sfUV7"," ExampleAutoDeleteAsyncTask",[330,343,345],{"class":344},"s0idv"," : ",[330,347,348],{"class":336},"public",[330,350,351],{"class":340}," FNonAbandonableTask\n",[330,353,355],{"class":332,"line":354},2,[330,356,357],{"class":344},"{\n",[330,359,361,365,368,371,374,377],{"class":332,"line":360},3,[330,362,364],{"class":363},"sLXdl","    friend",[330,366,367],{"class":336}," class",[330,369,370],{"class":340}," FAutoDeleteAsyncTask",[330,372,373],{"class":344},"\u003C",[330,375,376],{"class":340},"ExampleAutoDeleteAsyncTask",[330,378,379],{"class":344},">;\n",[330,381,383],{"class":332,"line":382},4,[330,384,386],{"emptyLinePlaceholder":385},true,"\n",[330,388,390,393],{"class":332,"line":389},5,[330,391,392],{"class":340},"    int32",[330,394,395],{"class":344}," ExampleData;\n",[330,397,399],{"class":332,"line":398},6,[330,400,386],{"emptyLinePlaceholder":385},[330,402,404,408,411,414,418],{"class":332,"line":403},7,[330,405,407],{"class":406},"scAT4","    ExampleAutoDeleteAsyncTask",[330,409,410],{"class":344},"(",[330,412,413],{"class":340},"int32",[330,415,417],{"class":416},"sbfHu"," InExampleData",[330,419,420],{"class":344},")\n",[330,422,424,427,430],{"class":332,"line":423},8,[330,425,426],{"class":344},"        : ",[330,428,429],{"class":406},"ExampleData",[330,431,432],{"class":344},"(InExampleData)\n",[330,434,436],{"class":332,"line":435},9,[330,437,438],{"class":344},"    {\n",[330,440,442,445,448,451,453,457],{"class":332,"line":441},10,[330,443,444],{"class":406},"        UE_LOG",[330,446,447],{"class":344},"(LogTemp, Log, ",[330,449,450],{"class":406},"TEXT",[330,452,410],{"class":344},[330,454,456],{"class":455},"sZnmU","\"[ExampleAutoDeleteAsyncTask] Construct()\"",[330,458,459],{"class":344},"));\n",[330,461,463],{"class":332,"line":462},11,[330,464,465],{"class":344},"    }\n",[330,467,469],{"class":332,"line":468},12,[330,470,386],{"emptyLinePlaceholder":385},[330,472,474,477,480],{"class":332,"line":473},13,[330,475,476],{"class":336},"    void",[330,478,479],{"class":406}," DoWork",[330,481,482],{"class":344},"()\n",[330,484,486],{"class":332,"line":485},14,[330,487,438],{"class":344},[330,489,491,493,495,497,499,502],{"class":332,"line":490},15,[330,492,444],{"class":406},[330,494,447],{"class":344},[330,496,450],{"class":406},[330,498,410],{"class":344},[330,500,501],{"class":455},"\"[ExampleAutoDeleteAsyncTask] DoWork()\"",[330,503,459],{"class":344},[330,505,507],{"class":332,"line":506},16,[330,508,465],{"class":344},[330,510,512],{"class":332,"line":511},17,[330,513,386],{"emptyLinePlaceholder":385},[330,515,517,520,523,526,529],{"class":332,"line":516},18,[330,518,519],{"class":344},"    FORCEINLINE ",[330,521,522],{"class":340},"TStatId",[330,524,525],{"class":406}," GetStatId",[330,527,528],{"class":344},"() ",[330,530,531],{"class":363},"const\n",[330,533,535],{"class":332,"line":534},19,[330,536,438],{"class":344},[330,538,540,543],{"class":332,"line":539},20,[330,541,542],{"class":406},"        RETURN_QUICK_DECLARE_CYCLE_STAT",[330,544,545],{"class":344},"(ExampleAutoDeleteAsyncTask, STATGROUP_ThreadPoolAsyncTasks);\n",[330,547,549],{"class":332,"line":548},21,[330,550,465],{"class":344},[330,552,554],{"class":332,"line":553},22,[330,555,556],{"class":344},"};\n",[11,558,559],{},"在完成定义后，可以有两种使用方式：",[61,561,563],{"className":324,"code":562,"language":326,"meta":70,"style":70},"\u002F\u002F 将任务扔到线程池中去执行\n(new FAutoDeleteAsyncTask\u003CExampleAutoDeleteAsyncTask>(5))->StartBackgroundTask();\n\n\u002F\u002F 直接在当前线程执行操作\n(new FAutoDeleteAsyncTask\u003CExampleAutoDeleteAsyncTask>(5))->StartSynchronousTask();\n",[68,564,565,571,600,604,609],{"__ignoreMap":70},[330,566,567],{"class":332,"line":333},[330,568,570],{"class":569},"sZdD5","\u002F\u002F 将任务扔到线程池中去执行\n",[330,572,573,575,578,580,582,584,587,591,594,597],{"class":332,"line":354},[330,574,410],{"class":344},[330,576,577],{"class":363},"new",[330,579,370],{"class":406},[330,581,373],{"class":344},[330,583,376],{"class":340},[330,585,586],{"class":344},">(",[330,588,590],{"class":589},"stP0Q","5",[330,592,593],{"class":344},"))->",[330,595,596],{"class":406},"StartBackgroundTask",[330,598,599],{"class":344},"();\n",[330,601,602],{"class":332,"line":360},[330,603,386],{"emptyLinePlaceholder":385},[330,605,606],{"class":332,"line":382},[330,607,608],{"class":569},"\u002F\u002F 直接在当前线程执行操作\n",[330,610,611,613,615,617,619,621,623,625,627,630],{"class":332,"line":389},[330,612,410],{"class":344},[330,614,577],{"class":363},[330,616,370],{"class":406},[330,618,373],{"class":344},[330,620,376],{"class":340},[330,622,586],{"class":344},[330,624,590],{"class":589},[330,626,593],{"class":344},[330,628,629],{"class":406},"StartSynchronousTask",[330,631,599],{"class":344},[11,633,634],{},"FAutoDeleteAsyncTask的一个优点是，在执行完成后会自动销毁，无需进行额外的关注。通常文件写入或者压缩数据之类的无须进行过程管理的操作可以交付给他执行。",[318,636,638],{"id":637},"fasync-task","FAsync Task",[11,640,641],{},"这个才是本尊，由于不会自动删除，有需要进行额外操作的情况。",[61,643,645],{"className":324,"code":644,"language":326,"meta":70,"style":70},"MyTask->StartSynchronousTask();\n\n\u002F\u002Fto just do it now on this thread\n\u002F\u002FCheck if the task is done :\n\nif (MyTask->IsDone())\n{\n}\n\n\u002F\u002FSpinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.\n\u002F\u002FEnsure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.\n\nMyTask->EnsureCompletion();\ndelete Task;\n",[68,646,647,656,660,665,670,674,688,692,697,701,706,711,715,724],{"__ignoreMap":70},[330,648,649,652,654],{"class":332,"line":333},[330,650,651],{"class":344},"MyTask->",[330,653,629],{"class":406},[330,655,599],{"class":344},[330,657,658],{"class":332,"line":354},[330,659,386],{"emptyLinePlaceholder":385},[330,661,662],{"class":332,"line":360},[330,663,664],{"class":569},"\u002F\u002Fto just do it now on this thread\n",[330,666,667],{"class":332,"line":382},[330,668,669],{"class":569},"\u002F\u002FCheck if the task is done :\n",[330,671,672],{"class":332,"line":389},[330,673,386],{"emptyLinePlaceholder":385},[330,675,676,679,682,685],{"class":332,"line":398},[330,677,678],{"class":363},"if",[330,680,681],{"class":344}," (MyTask->",[330,683,684],{"class":406},"IsDone",[330,686,687],{"class":344},"())\n",[330,689,690],{"class":332,"line":403},[330,691,357],{"class":344},[330,693,694],{"class":332,"line":423},[330,695,696],{"class":344},"}\n",[330,698,699],{"class":332,"line":435},[330,700,386],{"emptyLinePlaceholder":385},[330,702,703],{"class":332,"line":441},[330,704,705],{"class":569},"\u002F\u002FSpinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.\n",[330,707,708],{"class":332,"line":462},[330,709,710],{"class":569},"\u002F\u002FEnsure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.\n",[330,712,713],{"class":332,"line":468},[330,714,386],{"emptyLinePlaceholder":385},[330,716,717,719,722],{"class":332,"line":473},[330,718,651],{"class":344},[330,720,721],{"class":406},"EnsureCompletion",[330,723,599],{"class":344},[330,725,726,729],{"class":332,"line":485},[330,727,728],{"class":363},"delete",[330,730,731],{"class":344}," Task;\n",[11,733,734],{},"但是如果是使用StartBackgroundTask()的话依然不需要自己进行管理。",[53,736,738],{"id":737},"frunnable","FRunnable",[11,740,741],{},"这个是交付给线程的执行体封装，通常用于比AsyncTask更加复杂的操作。",[11,743,744],{},"分为Init(), Run(), Exit()三个操作，如果Init失败就不会执行Run()，Run()执行完成就会执行Exit()。",[61,746,748],{"className":324,"code":747,"language":326,"meta":70,"style":70},"class FRunAbleTest : public FRunnable\n{\n    virtual uint32 Run() override\n    {\n        UE_LOG(LogTemp, Log, TEXT(\"[FRunAbleTest] Run()\"));\n        FPlatformProcess::Sleep(30);\n        UE_LOG(LogTemp, Log, TEXT(\"[FRunAbleTest] Run(): Comp\"));\n        return 0;\n    }\n\n};\n",[68,749,750,764,768,784,788,803,822,837,848,852,856],{"__ignoreMap":70},[330,751,752,754,757,759,761],{"class":332,"line":333},[330,753,337],{"class":336},[330,755,756],{"class":340}," FRunAbleTest",[330,758,345],{"class":344},[330,760,348],{"class":336},[330,762,763],{"class":340}," FRunnable\n",[330,765,766],{"class":332,"line":354},[330,767,357],{"class":344},[330,769,770,773,776,779,781],{"class":332,"line":360},[330,771,772],{"class":363},"    virtual",[330,774,775],{"class":340}," uint32",[330,777,778],{"class":406}," Run",[330,780,528],{"class":344},[330,782,783],{"class":363},"override\n",[330,785,786],{"class":332,"line":382},[330,787,438],{"class":344},[330,789,790,792,794,796,798,801],{"class":332,"line":389},[330,791,444],{"class":406},[330,793,447],{"class":344},[330,795,450],{"class":406},[330,797,410],{"class":344},[330,799,800],{"class":455},"\"[FRunAbleTest] Run()\"",[330,802,459],{"class":344},[330,804,805,808,811,814,816,819],{"class":332,"line":398},[330,806,807],{"class":340},"        FPlatformProcess",[330,809,810],{"class":344},"::",[330,812,813],{"class":406},"Sleep",[330,815,410],{"class":344},[330,817,818],{"class":589},"30",[330,820,821],{"class":344},");\n",[330,823,824,826,828,830,832,835],{"class":332,"line":403},[330,825,444],{"class":406},[330,827,447],{"class":344},[330,829,450],{"class":406},[330,831,410],{"class":344},[330,833,834],{"class":455},"\"[FRunAbleTest] Run(): Comp\"",[330,836,459],{"class":344},[330,838,839,842,845],{"class":332,"line":423},[330,840,841],{"class":363},"        return",[330,843,844],{"class":589}," 0",[330,846,847],{"class":344},";\n",[330,849,850],{"class":332,"line":435},[330,851,465],{"class":344},[330,853,854],{"class":332,"line":441},[330,855,386],{"emptyLinePlaceholder":385},[330,857,858],{"class":332,"line":462},[330,859,556],{"class":344},[11,861,862],{},"通常也可以只指定Run()，然后交付给线程：",[61,864,866],{"className":324,"code":865,"language":326,"meta":70,"style":70},"FRunnable* tp_Runable = new FRunAbleTest();\nmp_TestThread = FRunnableThread::Create(tp_Runable, TEXT(\"Test_01\"));\n",[68,867,868,888],{"__ignoreMap":70},[330,869,870,872,875,878,881,884,886],{"class":332,"line":333},[330,871,738],{"class":344},[330,873,874],{"class":363},"*",[330,876,877],{"class":344}," tp_Runable ",[330,879,880],{"class":363},"=",[330,882,883],{"class":363}," new",[330,885,756],{"class":406},[330,887,599],{"class":344},[330,889,890,893,895,898,900,903,906,908,910,913],{"class":332,"line":354},[330,891,892],{"class":344},"mp_TestThread ",[330,894,880],{"class":363},[330,896,897],{"class":340}," FRunnableThread",[330,899,810],{"class":344},[330,901,902],{"class":406},"Create",[330,904,905],{"class":344},"(tp_Runable, ",[330,907,450],{"class":406},[330,909,410],{"class":344},[330,911,912],{"class":455},"\"Test_01\"",[330,914,459],{"class":344},[11,916,917],{},"就可以了。",[53,919,921],{"id":920},"async","Async",[11,923,924],{},"这是另一个异步执行的宏，与AsyncTask有少许不同。",[11,926,927],{},"Async的简单的使用方式在注释中有提到",[61,929,931],{"className":324,"code":930,"language":326,"meta":70,"style":70},"    \u002F\u002F 使用全局函数\n    int TestFunc()\n    {\n        return 123;\n    }\n\n    TFunction\u003Cint()> Task = TestFunc();\n    auto Result = Async(EAsyncExecution::Thread, Task);\n\n    \u002F\u002F 使用lambda\n    TFunction\u003Cint()> Task = []()\n    {\n        return 123;\n    }\n\n    auto Result = Async(EAsyncExecution::Thread, Task);\n\n\n    \u002F\u002F 使用inline lambda\n    auto Result = Async\u003Cint>(EAsyncExecution::Thread, []() {\n        return 123;\n    }\n",[68,932,933,938,948,952,961,965,969,994,1015,1019,1024,1043,1047,1055,1059,1063,1079,1083,1087,1092,1113,1121],{"__ignoreMap":70},[330,934,935],{"class":332,"line":333},[330,936,937],{"class":569},"    \u002F\u002F 使用全局函数\n",[330,939,940,943,946],{"class":332,"line":354},[330,941,942],{"class":336},"    int",[330,944,945],{"class":406}," TestFunc",[330,947,482],{"class":344},[330,949,950],{"class":332,"line":360},[330,951,438],{"class":344},[330,953,954,956,959],{"class":332,"line":382},[330,955,841],{"class":363},[330,957,958],{"class":589}," 123",[330,960,847],{"class":344},[330,962,963],{"class":332,"line":389},[330,964,465],{"class":344},[330,966,967],{"class":332,"line":398},[330,968,386],{"emptyLinePlaceholder":385},[330,970,971,974,976,979,982,985,988,990,992],{"class":332,"line":403},[330,972,973],{"class":344},"    TFunction",[330,975,373],{"class":363},[330,977,978],{"class":336},"int",[330,980,981],{"class":344},"()",[330,983,984],{"class":363},">",[330,986,987],{"class":344}," Task ",[330,989,880],{"class":363},[330,991,945],{"class":406},[330,993,599],{"class":344},[330,995,996,999,1002,1004,1007,1009,1012],{"class":332,"line":423},[330,997,998],{"class":336},"    auto",[330,1000,1001],{"class":344}," Result ",[330,1003,880],{"class":363},[330,1005,1006],{"class":406}," Async",[330,1008,410],{"class":344},[330,1010,1011],{"class":340},"EAsyncExecution",[330,1013,1014],{"class":344},"::Thread, Task);\n",[330,1016,1017],{"class":332,"line":435},[330,1018,386],{"emptyLinePlaceholder":385},[330,1020,1021],{"class":332,"line":441},[330,1022,1023],{"class":569},"    \u002F\u002F 使用lambda\n",[330,1025,1026,1028,1030,1032,1034,1036,1038,1040],{"class":332,"line":462},[330,1027,973],{"class":344},[330,1029,373],{"class":363},[330,1031,978],{"class":336},[330,1033,981],{"class":344},[330,1035,984],{"class":363},[330,1037,987],{"class":344},[330,1039,880],{"class":363},[330,1041,1042],{"class":344}," []()\n",[330,1044,1045],{"class":332,"line":468},[330,1046,438],{"class":344},[330,1048,1049,1051,1053],{"class":332,"line":473},[330,1050,841],{"class":363},[330,1052,958],{"class":589},[330,1054,847],{"class":344},[330,1056,1057],{"class":332,"line":485},[330,1058,465],{"class":344},[330,1060,1061],{"class":332,"line":490},[330,1062,386],{"emptyLinePlaceholder":385},[330,1064,1065,1067,1069,1071,1073,1075,1077],{"class":332,"line":506},[330,1066,998],{"class":336},[330,1068,1001],{"class":344},[330,1070,880],{"class":363},[330,1072,1006],{"class":406},[330,1074,410],{"class":344},[330,1076,1011],{"class":340},[330,1078,1014],{"class":344},[330,1080,1081],{"class":332,"line":511},[330,1082,386],{"emptyLinePlaceholder":385},[330,1084,1085],{"class":332,"line":516},[330,1086,386],{"emptyLinePlaceholder":385},[330,1088,1089],{"class":332,"line":534},[330,1090,1091],{"class":569},"    \u002F\u002F 使用inline lambda\n",[330,1093,1094,1096,1098,1100,1102,1104,1106,1108,1110],{"class":332,"line":539},[330,1095,998],{"class":336},[330,1097,1001],{"class":344},[330,1099,880],{"class":363},[330,1101,1006],{"class":406},[330,1103,373],{"class":344},[330,1105,978],{"class":336},[330,1107,586],{"class":344},[330,1109,1011],{"class":340},[330,1111,1112],{"class":344},"::Thread, []() {\n",[330,1114,1115,1117,1119],{"class":332,"line":548},[330,1116,841],{"class":363},[330,1118,958],{"class":589},[330,1120,847],{"class":344},[330,1122,1123],{"class":332,"line":553},[330,1124,465],{"class":344},[11,1126,1127],{},"第一个参数为执行的类型，TaskGraph是将其放到任务图中去执行，Thread则是在单独的线程中执行，TreadPool则是放入线程池中去执行。",[11,1129,1130],{},"这里并不能像AsyncTask一样指定目标的线程。",[11,1132,1133,1134,1137,1138,1141],{},"同时Async会返回一个",[68,1135,1136],{},"TFuture\u003CResultType>","，而",[68,1139,1140],{},"ResultType","则是传入的执行函数的返回值。",[61,1143,1145],{"className":324,"code":1144,"language":326,"meta":70,"style":70},"TFunction\u003Cint()> My_Task= []() {\n    return 123;\n};\n\nauto Future = Async(EAsyncExecution::TaskGraph, My_Task);\nint Result = Future.Get();\n",[68,1146,1147,1168,1177,1181,1185,1204],{"__ignoreMap":70},[330,1148,1149,1152,1154,1156,1158,1160,1163,1165],{"class":332,"line":333},[330,1150,1151],{"class":344},"TFunction",[330,1153,373],{"class":363},[330,1155,978],{"class":336},[330,1157,981],{"class":344},[330,1159,984],{"class":363},[330,1161,1162],{"class":344}," My_Task",[330,1164,880],{"class":363},[330,1166,1167],{"class":344}," []() {\n",[330,1169,1170,1173,1175],{"class":332,"line":354},[330,1171,1172],{"class":363},"    return",[330,1174,958],{"class":589},[330,1176,847],{"class":344},[330,1178,1179],{"class":332,"line":360},[330,1180,556],{"class":344},[330,1182,1183],{"class":332,"line":382},[330,1184,386],{"emptyLinePlaceholder":385},[330,1186,1187,1190,1193,1195,1197,1199,1201],{"class":332,"line":389},[330,1188,1189],{"class":336},"auto",[330,1191,1192],{"class":344}," Future ",[330,1194,880],{"class":363},[330,1196,1006],{"class":406},[330,1198,410],{"class":344},[330,1200,1011],{"class":340},[330,1202,1203],{"class":344},"::TaskGraph, My_Task);\n",[330,1205,1206,1208,1210,1212,1215,1218],{"class":332,"line":398},[330,1207,978],{"class":336},[330,1209,1001],{"class":344},[330,1211,880],{"class":363},[330,1213,1214],{"class":344}," Future.",[330,1216,1217],{"class":406},"Get",[330,1219,599],{"class":344},[11,1221,1222],{},"类似这样的调用即可。",[36,1224,1225],{"id":1225},"总结",[11,1227,1228],{},"UE4提供的异步操作大体上分为TaskGraph和TreadPool的管理方式，通常较简单的任务交付给TaskGraph，复杂的任务交付给Thread。",[11,1230,1231],{},"对于Task，引擎会有自己的管理，将其分配给空闲的Worker Thread。同时Task之间的依赖关系也会被管理，并按照需要的顺序被执行。",[11,1233,1234],{},"其实TaskGroup和ThreadPool都是可以自己进行申请和管理的，但是并没有实际的进行研究。",[11,1236,1237],{},"因为理论上，除非有需求，应当尽量的让游戏逻辑保持简洁。再加上线程同步是要支付额外的成本的，因此，要尽量避免对异步逻辑的使用，即使使用，也要尽量的保持逻辑单纯。而且这两个系统本身是虚幻为编辑器而设计的，虽然开放给用户使用，但是就像GamePlayAbility系统一样。本身每个程序员都有自己的实现思路，也没有必要一定要使用这套系统。",[11,1239,1240],{},"毕竟游戏最终是用户体验，没有用户在意屏幕背后的逻辑实现是否”Geek”。",[1242,1243,1244],"style",{},"html pre.shiki code .sGZGq, html code.shiki .sGZGq{--shiki-default:#A0111F;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .sfUV7, html code.shiki .sfUV7{--shiki-default:#702C00;--shiki-default-text-decoration:inherit;--shiki-dark:#B392F0;--shiki-dark-text-decoration:inherit;--shiki-sepia:#A6E22E;--shiki-sepia-text-decoration:underline}html pre.shiki code .s0idv, html code.shiki .s0idv{--shiki-default:#0E1116;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .sLXdl, html code.shiki .sLXdl{--shiki-default:#A0111F;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .scAT4, html code.shiki .scAT4{--shiki-default:#622CBC;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .sbfHu, html code.shiki .sbfHu{--shiki-default:#702C00;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit;--shiki-sepia:#FD971F;--shiki-sepia-font-style:italic}html pre.shiki code .sZnmU, html code.shiki .sZnmU{--shiki-default:#032563;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html pre.shiki code .sZdD5, html code.shiki .sZdD5{--shiki-default:#66707B;--shiki-dark:#6A737D;--shiki-sepia:#88846F}html pre.shiki code .stP0Q, html code.shiki .stP0Q{--shiki-default:#023B95;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}",{"title":70,"searchDepth":354,"depth":360,"links":1246},[1247,1251,1261,1271],{"id":38,"depth":354,"text":39,"children":1248},[1249,1250],{"id":55,"depth":360,"text":56},{"id":76,"depth":360,"text":77},{"id":95,"depth":354,"text":95,"children":1252},[1253,1254,1255,1256,1257,1258,1259,1260],{"id":101,"depth":360,"text":102},{"id":132,"depth":360,"text":133},{"id":157,"depth":360,"text":158},{"id":170,"depth":360,"text":171},{"id":180,"depth":360,"text":181},{"id":187,"depth":360,"text":188},{"id":203,"depth":360,"text":203},{"id":235,"depth":360,"text":235},{"id":262,"depth":354,"text":262,"children":1262},[1263,1264,1265,1269,1270],{"id":268,"depth":360,"text":269},{"id":287,"depth":360,"text":288},{"id":312,"depth":360,"text":313,"children":1266},[1267,1268],{"id":320,"depth":382,"text":321},{"id":637,"depth":382,"text":638},{"id":737,"depth":360,"text":738},{"id":920,"depth":360,"text":921},{"id":1225,"depth":354,"text":1225},"2017-12-16","md",{"layout":1275,"status":1276,"published":385,"author":1277,"author_login":1279,"author_email":1280,"wordpress_id":1281,"wordpress_url":1282,"date_gmt":1283,"excerpt":1284},"post","publish",{"display_name":1278,"login":1279,"email":1280,"url":70},"风铃","flinkor","flinkor@foxmail.com",2150,"\u002F\u002F?p=2150","2017-12-16 08:37:44 +0000",{"type":8,"value":1285},[1286],[11,1287,13],{},"\u002F2017-12-16-ue4-async-note",{"title":6,"description":13},"_legacy\u002F2017\u002F2017-12-16-ue4-async-note",[1292,921],"UE4","rkyu0u7S8Wi9q-cnfwEdHesRioNVI7qK3cet5qvrtlQ",{"id":1295,"title":1296,"body":1297,"date":1719,"description":1301,"extension":1273,"meta":1720,"navigation":385,"path":1729,"seo":1730,"stem":1731,"tags":1732,"__hash__":1734},"blogs\u002F_legacy\u002F2017\u002F2017-11-18-ue4-render-flow-overview.md","UE4 Render Flow纵览",{"type":8,"value":1298,"toc":1698},[1299,1302,1305,1308,1311,1318,1321,1329,1333,1336,1339,1345,1348,1352,1355,1358,1366,1369,1373,1376,1379,1382,1385,1388,1391,1394,1397,1400,1404,1407,1410,1413,1420,1423,1427,1430,1433,1436,1442,1445,1451,1454,1457,1461,1464,1467,1470,1476,1479,1482,1486,1489,1492,1498,1501,1504,1509,1512,1515,1519,1522,1526,1529,1532,1538,1541,1544,1550,1553,1556,1559,1562,1565,1568,1572,1575,1578,1581,1585,1588,1591,1611,1614,1618,1621,1624,1627,1630,1633,1639,1642,1645,1648,1651,1654,1657,1661,1664,1669,1673,1676,1682,1685,1688],[11,1300,1301],{},"渲染优化时质量和效率的平衡，虽然按照官方的建议进行相应的调整即可，但是不稍微了解其内部的原理的话还是有些许让人困惑的。",[11,1303,1304],{},"本文基于CEDEC2016的一篇讲稿，目标UE4版本为4.13。",[11,1306,1307],{},"由于到目前的版本（4.18）引擎渲染已经有了很大的变动，所以有的内容只有参考作用。",[11,1309,1310],{},"从概览的角度来看，UE4的渲染可以划分成以下的阶段：",[11,1312,1313],{},[1314,1315],"img",{"alt":1316,"src":1317},"clip_image001","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image001_thumb-2.png",[11,1319,1320],{},"这个通路是针对延迟渲染的，与目前主要针对VR设备的前向渲染并不对应。",[11,1322,1323,1328],{},[1324,1325,1327],"a",{"href":1326},"\u002F2017-07-30-ue4-profiling-preview\u002F#i-4","针对渲染通道的优化","虽然之前有做过总结，但是并没有详细的研究过各个通道在整体渲染中的地位。",[36,1330,1332],{"id":1331},"base-pass","Base Pass",[11,1334,1335],{},"作为最重要的基础性通道，Base Pass运算的结果作为之后所有通道运算的基础。",[11,1337,1338],{},"基础通道里主要的可见操作是对Opaque\u002FMasked材质的物体进行的遮蔽运算并完成G-Buffer的生成，VS和PS也在这个阶段进行计算。",[11,1340,1341],{},[1314,1342],{"alt":1343,"src":1344},"image3","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fimage3_thumb.png",[11,1346,1347],{},"针对Base Pass的优化方向主要针对Vertex Shader和Pixel Shader两个阶段，另外在材质的Shader制作本身上也需要注意降低运算量。",[53,1349,1351],{"id":1350},"vertex-shader","Vertex Shader",[11,1353,1354],{},"顶点计算的优化主要就是一些通常的建议：对物体的Bound进行规划，不要让bound过大，避免使用覆盖视野前后的物体。以使得Culling能够在早期就剪除掉不需要的顶点计算。",[11,1356,1357],{},"在Console中可以使用一下命令辅助优化：",[107,1359,1360,1363],{},[11,1361,1362],{},"Stat InitViews可以查看裁剪计算的效果",[11,1364,1365],{},"FreezeRendering可以冻结裁剪，对裁剪结果进行可视化分析",[11,1367,1368],{},"更进一步的可以根据平台的GPU特性不同进行对应的优化。",[53,1370,1372],{"id":1371},"pixel-shader","Pixel Shader",[11,1374,1375],{},"由于Base Pass是后面所有通道的基础，所以会有较高的固有消耗。同时也是在场景中添加物品、Shader等产生性能消耗最直观的地方。",[11,1377,1378],{},"在理想的状态下，在没有Masked或者Translucent的情况，在PreZ阶段完成时就可以决定各个像素的深度并形成遮蔽计算了。在这种情况下，就可以极好的减小Piexel Shader阶段的运算量。但是实际上，为了场景中的特效质量，不能光依靠Opaque的材质，事情就没有那么简单了。",[11,1380,1381],{},"会导致PreZ完成时深度计算结果不完全的运算有，Masked材质的Alpha Test以及在Pixel Shader内部对深度数据的重写。因此需要有PostZ阶段对深度数据进行重新处理。",[11,1383,1384],{},"因此在这里容易形成两种造成性能影响的错误操作：在制作通用的材质时，明明有的不使用半透明蒙版通道情况却开启了半透明蒙版并往其上连接一个参数或者将参数连接到Piexel Depth Offset上。",[11,1386,1387],{},"由于PreZ和PostZ的决策是在GPU中完成的，无法在UE4中进行预览，因此在进行优化的时候要注意对上面的两种情况进行观察。",[11,1389,1390],{},"总体而言，作为G-Buffer的生成阶段，BassPass会直接的受到物体增加的影响，在添加物体时要注意检查以下两项：",[11,1392,1393],{},"Bounds的设置是否很好的完成了Culling。",[11,1395,1396],{},"Material的设置是否很好的避免了不必要的Pixel Shader计算。",[11,1398,1399],{},"另外，在项目设置中可以对G-Buffer的精度进行设定，对于需要高质量运算结果的情况或者想要降低性能消耗的情况，可以在这里进行调整。",[36,1401,1403],{"id":1402},"z-prepass","Z PrePass",[11,1405,1406],{},"这是在BasePass之前尝试进行深度计算。经过Z PrePass计算之后，可以减少到达Vertex Shader的顶点数量，以提高效率。",[11,1408,1409],{},"在项目设置中可以对Early Z-Pass相关的选型进行调整。",[11,1411,1412],{},"对于单个物体，这里的Use as Occluder默认是开启的，将其去掉就不会参与Early Z-Pass的计算。这个大部分时间应该保持默认，让引擎自行决定是否让物体参与深度计算。",[11,1414,1415,1416],{},"ll\n",[1314,1417],{"alt":1418,"src":1419},"clip_image0015","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0015_thumb-1.png",[11,1421,1422],{},"早期深度计算是为了减少Base Pass的运算负荷而存在的，但有时场景中物体布局可能会导致这个阶段形成瓶颈，可以在这里针对物体进行开启关闭来调整效果。",[36,1424,1426],{"id":1425},"custom-depthstencil","Custom Depth\u002FStencil",[11,1428,1429],{},"在BasePass之后有一个可以自行进行定义的阶段，就是自定义深度。",[11,1431,1432],{},"自定义深度可以使得用户为物体在渲染时额外的生成一张深度贴图，可以很好的对需要的物体进行裁剪。",[11,1434,1435],{},"在项目设置中开启自定义深度",[11,1437,1438],{},[1314,1439],{"alt":1440,"src":1441},"clip_image002","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image002_thumb-2.png",[11,1443,1444],{},"然后在需要自定义深度的物体上打开",[11,1446,1447],{},[1314,1448],{"alt":1449,"src":1450},"clip_image003","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image003_thumb-2.png",[11,1452,1453],{},"需要注意的是，由于是额外进行的深度计算，基本上等同于对Base Pass进行了一次重新计算，自定义深度在场景较大的开放世界或者物体较多的场景中尤其会造成大的性能损失。",[11,1455,1456],{},"因此虽然使用Custom Depth可以相对简单的实现一些效果，但是却是以性能为代价的，应当尽量避免使用这个思路，如果非用不可的话，需要进行更加严格的Profiling。",[36,1458,1460],{"id":1459},"pre-lighting","Pre-Lighting",[11,1462,1463],{},"这是光照计算之前的一个运算阶段，主要的作用是Decal和AO的计算。",[11,1465,1466],{},"在过去的版本中Decal经常与光照计算产生冲突，造成一些奇特的明显不符合预期的最终结果。",[11,1468,1469],{},"因此后来加入了",[11,1471,1472],{},[1314,1473],{"alt":1474,"src":1475},"clip_image0019","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0019_thumb.png",[11,1477,1478],{},"的选项，目前的引擎中是默认开启的，无需太多关心。由于之前的项目中Decal的使用似乎没有遇到过什么问题，想来目前的默认选项已经很好的解决了问题。",[11,1480,1481],{},"如果在Decal的使用过程中遇到了问题，可以针对性的进行搜索。",[36,1483,1485],{"id":1484},"lighting","Lighting",[11,1487,1488],{},"就是光照计算阶段，光照的优化其实能找到很多资料。光照在UE4的操作上分为三种，StaticLight是全静态光照，全部使用预计算的结果进行光照。而Movable的光照则是全动态的，所有的光照都在运行时进行计算。Stational的光照则介于两者之间，静态物体的阴影会在预计算阶段进行缓存。",[11,1490,1491],{},"另外Stationary Light有同一个区域只受5个光照作用的限制，多出来的范围最小的那个会变成红叉叉，变成动态光照，在使用时需要注意。在视图选项中可以使用",[11,1493,1494],{},[1314,1495],{"alt":1496,"src":1497},"image","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fimage_thumb.png",[11,1499,1500],{},"来对整体场景进行排查。",[11,1502,1503],{},"在动态光照的优化上，动态光照是重叠的越多性能消耗就越高的，相反的个数很多却相互不重叠的话光照复杂度的上升却不是很快。可以在编辑器中使用光照复杂度视图进行确认和优化。",[11,1505,1506],{},[1314,1507],{"alt":1496,"src":1508},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fimage_thumb-1.png",[11,1510,1511],{},"还有一点是，静态光照并不是在运行时完全没有性能消耗。静态光照在运行时InitDynamic Setup计算阶段是会造成CPU消耗的，因此并不是由于是预计算的就可以无计划的放置。另外，据说StaticLight在被移动等时会自动的被变更为Movable的，没有进行过测试所以并不是很确定呢。",[11,1513,1514],{},"总之在进行光照布局时，首先使用Stationary是比较合理的策略。",[36,1516,1518],{"id":1517},"reflect","Reflect",[11,1520,1521],{},"反射计算虽然在概念上算是光照的一部分，但是其实在运算中是一个额外的阶段。",[53,1523,1525],{"id":1524},"reflection-probe","Reflection Probe",[11,1527,1528],{},"反射捕获，是预计算的反射。在引擎中提供了球体反射捕获和盒体反射捕获两个选择，在使用反射捕获时，可以在拖入后对所在区域进行手动的重新捕获，也可以自己在其中指定CubeMap。",[11,1530,1531],{},"在项目设置中可以进行设置来调整反射捕获的精度。",[11,1533,1534],{},[1314,1535],{"alt":1536,"src":1537},"clip_image00111","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image00111_thumb.png",[11,1539,1540],{},"这部分的消耗是在Reflection Environment Compute Shader XXXX中反映的，与动态光照相同，个数对其性能消耗的影响不如区域重叠造成的影响。",[11,1542,1543],{},"官方的建议是，在场景全体放置一个总的反射捕捉，然后在一个单位房间内放一个整合性的捕捉，最后在反射性的物体上针对性的放置。",[11,1545,1546],{},[1314,1547],{"alt":1548,"src":1549},"clip_image0024","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0024_thumb-2.png",[11,1551,1552],{},"覆盖全体的捕捉",[11,1554,1555],{},"防止在场景内完全丢失反射信息的情况",[11,1557,1558],{},"房间单位的捕捉",[11,1560,1561],{},"在场景单元内形成详细的反射信息",[11,1563,1564],{},"物体单位的捕捉",[11,1566,1567],{},"反射要求较高的物体附近进行更加详细的捕捉",[53,1569,1571],{"id":1570},"screen-space-reflection","Screen Space Reflection",[11,1573,1574],{},"动态反射计算，没有深入看过其实现。",[11,1576,1577],{},"这里的主要问题是，由于是在屏幕空间内进行的计算，在屏幕外的反射无法正确的反映，同时有较多的噪点而且对Translucent的材质在计算时容易出现问题。",[11,1579,1580],{},"因此通常是与上面的反射捕获共同使用，作为其补充而存在的。因此如果出现了反射表面投影质量比较奇怪的问题，通常也可以检查一下是否该区域没有放置反射捕获，而不是一味的去加强动态光照和间接光照的次数，毕竟他们的性能消耗还是非常可观的。",[53,1582,1584],{"id":1583},"planar-reflection","Planar Reflection",[11,1586,1587],{},"效果很好的反射，全动态计算。",[11,1589,1590],{},"但是其负荷相当的高，如果场景中有两个以上的话，会有目视可见的性能消耗。",[107,1592,1593,1596,1599,1602,1605,1608],{},[11,1594,1595],{},"无法控制反射通道中启用的渲染功能。",[11,1597,1598],{},"反射通道中的动态阴影不正确。",[11,1600,1601],{},"为保证达到目标帧率，需计算资源是否足以使用平面反射。",[11,1603,1604],{},"只支持恒定的粗糙系数，其在平面反射组件上（而非在材质上）进行指定。",[11,1606,1607],{},"如可能，须尽量将世界场景中的平面反射 Actor 数量限制为 1 个，将其移动、旋转、缩放，和世界场景搭配。也可使用多个平面反射 Actor，但需多加注意，因为平面反射 Actor 不执行任何距离剔除，只进行视锥和遮蔽剔除。因此，如果画面中同时存在两个平面反射 Actor，项目的帧率将受到严重影响。",[11,1609,1610],{},"渲染平面反射 Actor 的开销直接来自当前关卡中渲染的内容。启用此功能后，由三角形组成、绘制调用较大的场景将遭受严重的性能影响，因为这些开销不会随屏幕百分比变化。",[11,1612,1613],{},"以上内容引用自官方文档，在使用时需要额外的进行留意。",[36,1615,1617],{"id":1616},"translucent","Translucent",[11,1619,1620],{},"由于深度计算的效率等问题，Translucent单独在另一条路径上进行处理。因此在半透明的计算在延迟渲染中，总是会有很多的问题。",[53,1622,1623],{"id":1623},"深度计算",[11,1625,1626],{},"将半透明的粒子投放到场景中时，可以看到并不会在深度数据中产生影响。这样在一些使用深度数据进行的效果如DOF中就会出现BUG。",[11,1628,1629],{},"因此UE4使用Separate Translucency来对半透明物体的深度进行处理",[11,1631,1632],{},"在项目设置中可以看到开关",[11,1634,1635],{},[1314,1636],{"alt":1637,"src":1638},"clip_image00113","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image00113_thumb.png",[11,1640,1641],{},"关于DepthOfField,在材质中有其运算结果的节点。",[53,1643,1644],{"id":1644},"成本问题",[11,1646,1647],{},"半透明物体会极大的加重场景的渲染负担，在着色器复杂度中能够看到，通常半透明的粒子会导致复杂度变为红色。",[11,1649,1650],{},"优化上可以考虑降低Separate Translucency的分辨率，使用r.SeparateTranslucencyScreenPercentage指令可以通过降低分辨率来减小其消耗。",[11,1652,1653],{},"另一个解决方案是使用Particle CutOut有效的减少半透明计算的区域。",[11,1655,1656],{},"似乎只要使用Create SubUV Animation就会自动应用，没有测试所以并不清楚。",[53,1658,1660],{"id":1659},"responsive-aa","Responsive AA",[11,1662,1663],{},"在半透明材质中可以看到这个选项，主要是针对使用了半透明材质的粒子的。",[11,1665,1666],{},[1314,1667],{"alt":1496,"src":1668},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fimage_thumb-2.png",[36,1670,1672],{"id":1671},"post-process","Post Process",[11,1674,1675],{},"pp是渲染的最后一个阶段，可以在这里对渲染结果进行进一步的加工。",[11,1677,1678],{},[1314,1679],{"alt":1680,"src":1681},"clip_image0026","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0026_thumb-1.png",[11,1683,1684],{},"PostProcess的成本消耗与添加的特效相关，每一个特效都会产生额外的消耗。如果自己使用了pp的材质来进行控制的话，其Shader复杂度也会对性能产生影响。",[11,1686,1687],{},"自带的PP特效可以通过指令来调整其效果，例如r.BloomQuality，其运算负荷是作为PostProcessWeightedSampleSum显示。",[11,1689,1690,1691,1697],{},"UE4的后期处理能在官方找到很多详细的[",[1324,1692,1696],{"href":1693,"rel":1694},"https:\u002F\u002Fdocs.unrealengine.com\u002Flatest\u002FCHN\u002FEngine\u002FRendering\u002FPostProcessEffects\u002Findex.html",[1695],"nofollow","文档","]。",{"title":70,"searchDepth":354,"depth":360,"links":1699},[1700,1704,1705,1706,1707,1708,1713,1718],{"id":1331,"depth":354,"text":1332,"children":1701},[1702,1703],{"id":1350,"depth":360,"text":1351},{"id":1371,"depth":360,"text":1372},{"id":1402,"depth":354,"text":1403},{"id":1425,"depth":354,"text":1426},{"id":1459,"depth":354,"text":1460},{"id":1484,"depth":354,"text":1485},{"id":1517,"depth":354,"text":1518,"children":1709},[1710,1711,1712],{"id":1524,"depth":360,"text":1525},{"id":1570,"depth":360,"text":1571},{"id":1583,"depth":360,"text":1584},{"id":1616,"depth":354,"text":1617,"children":1714},[1715,1716,1717],{"id":1623,"depth":360,"text":1623},{"id":1644,"depth":360,"text":1644},{"id":1659,"depth":360,"text":1660},{"id":1671,"depth":354,"text":1672},"2017-11-18",{"layout":1275,"status":1276,"published":385,"author":1721,"author_login":1279,"author_email":1280,"wordpress_id":1722,"wordpress_url":1723,"date_gmt":1724,"excerpt":1725},{"display_name":1278,"login":1279,"email":1280,"url":70},2126,"\u002F\u002F?p=2126","2017-11-18 09:45:59 +0000",{"type":8,"value":1726},[1727],[11,1728,1301],{},"\u002F2017-11-18-ue4-render-flow-overview",{"title":1296,"description":1301},"_legacy\u002F2017\u002F2017-11-18-ue4-render-flow-overview",[1292,1733],"Rendering","FhVs1eAqGGNQqiSWGnuupyS0M44voIuEFC_oLNsA6WU",{"id":1736,"title":1737,"body":1738,"date":2095,"description":1742,"extension":1273,"meta":2096,"navigation":385,"path":2105,"seo":2106,"stem":2107,"tags":2108,"__hash__":2110},"blogs\u002F_legacy\u002F2017\u002F2017-11-11-animatioinstarterpack-note-01.md","AnimatioinStarterPack的使用（上）",{"type":8,"value":1739,"toc":2077},[1740,1743,1746,1749,1752,1755,1758,1761,1764,1768,1771,1774,1779,1782,1785,1788,1791,1796,1799,1802,1807,1810,1813,1816,1819,1822,1828,1831,1834,1837,1840,1843,1846,1849,1852,1855,1858,1861,1865,1868,1874,1877,1880,1883,1886,1889,1892,1895,1898,1901,1904,1907,1910,1913,1916,1919,1924,1927,1930,1933,1936,1942,1945,1948,1951,1954,1957,1960,1963,1969,1972,1977,1980,1986,1989,1995,1998,2001,2004,2010,2013,2017,2020,2023,2026,2030,2033,2036,2042,2045,2051,2054,2057,2060,2063,2069,2071,2074],[11,1741,1742],{},"虚幻商城能看到官方提供的AnimationStarterPack，对于制作游戏原型非常的有用，毕竟如果一直用一个立方体来代替角色多少还是有些不足的。",[11,1744,1745],{},"当前使用的UE4版本为4.18.0。",[11,1747,1748],{},"4.18的升级中对角色动画相关的功能进行了改进，不过主要的改动是在PhysicAsset上的。刚好趁着这次开坑，对动画系统重新熟悉一下。",[11,1750,1751],{},"PS：这里只是操作笔记，并不会有很多细节上的说明哦~",[36,1753,1754],{"id":1754},"基础准备",[11,1756,1757],{},"首先当然是通过EpicLaucher添加AnimationStarterPack到项目中。",[11,1759,1760],{},"添加完成后能够看到动画包中有做好的基础角色蓝图，那么第一步就是参照着自己实现一遍。",[11,1762,1763],{},"动画蓝图的操作分别位于角色的蓝图和动画蓝图本身两个部分，这两者会互相交互来对状态进行更新。",[53,1765,1767],{"id":1766},"character","Character",[11,1769,1770],{},"角色蓝图直接新建一个Character即可，由于目标的系统是顶部的上帝视角的，所以会和官方的第一人称(?)的有些不同。",[11,1772,1773],{},"照例，先添加SpringArm和摄像",[11,1775,1776],{},[1314,1777],{"alt":1316,"src":1778},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image001_thumb.png",[11,1780,1781],{},"记得在SpringArm上设置Use Pawn Control Rotation。",[11,1783,1784],{},"之后，在Mesh中将Skeletal Mesh指定为SK_Mannequin，动画蓝图那里随便指定一个Asset，方便预览。",[11,1786,1787],{},"根据官方提供的Character将位置设为(0,0,-100)，同时将旋转设为(0,0,-90)以保持朝向与Arrow一致。",[11,1789,1790],{},"这里如果直接运行的话会发现角色穿到地面以下了，原因是胶囊体的大小不一致。由于不清楚官方设定的理由，这里暂且按照官方的来，修改为",[11,1792,1793],{},[1314,1794],{"alt":1440,"src":1795},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image002_thumb.png",[11,1797,1798],{},"这样就基本差不多了，然后将官方的角色蓝图中事件图表全部拷贝过来。这些蓝图都是些操作角色的输入处理，从Input拉到Character以及标志位的设置之类的，没有什么特别的地方呢。",[11,1800,1801],{},"没有的变量直接点右键生成，没有的输入直接到项目设置中添加。唯一要注意的是",[11,1803,1804],{},[1314,1805],{"alt":1449,"src":1806},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image003_thumb.png",[11,1808,1809],{},"Lookup的Scale是-1，要不然操作起来和通常的FPS是相反的。",[53,1811,1812],{"id":1812},"动画蓝图",[11,1814,1815],{},"然后新建一个C++类，继承自AnimInstance，这样做主要是考虑到之后动画蓝图的逻辑可能会变得复杂。",[11,1817,1818],{},"接下来在蓝图中新建一个动画蓝图，父类选择刚刚构建的CharaAnimate，骨架选择UE4_Mannequin_Skeleton。",[11,1820,1821],{},"然后在Character蓝图的Mesh里面将动画指定为刚刚新建的动画蓝图",[11,1823,1824],{},[1314,1825],{"alt":1826,"src":1827},"clip_image0014","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0014_thumb.png",[11,1829,1830],{},"指定完成后运行时就是个T字了，因为动画蓝图还是空的。",[11,1832,1833],{},"到达动画图表，添加一个简单的状态机。",[11,1835,1836],{},"状态机是动画蓝图的核心功能之一，可以通过设定的条件，根据变量的值等，跳转到不同的状态值，而不用自己根据众多的值对状态进行控制。",[11,1838,1839],{},"在动画图表中右键，新建一个状态机，名字就模仿官方的动画蓝图叫LocoMotion，并将它连到“最终动画姿势”上。",[11,1841,1842],{},"然后打开LocoMotion，右键新建一个状态，名为Idle。",[11,1844,1845],{},"打开Idle的状态，从右边的动画列表直接拖一个Idle_Rifle_Hip连接到Result上。",[11,1847,1848],{},"这样的话预览有些就能看到角色播放静止动画了。",[36,1850,1851],{"id":1851},"混合空间",[11,1853,1854],{},"混合空间是最基础的动画蓝图操作，大部分时候行走的动画都是靠其实现的。",[11,1856,1857],{},"新建一个混合空间，骨骼选择SK_Mannequin。",[11,1859,1860],{},"混合空间是2D的，通常的行走混合就是通过前进速度和前进方向来混合出八方向行走动画。",[53,1862,1864],{"id":1863},"blendspace","BlendSpace",[11,1866,1867],{},"打开混合空间，在左侧切换到Asset Detail标签，首先将混合用的两个坐标轴设置好。",[11,1869,1870],{},[1314,1871],{"alt":1872,"src":1873},"clip_image0016","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0016_thumb.png",[11,1875,1876],{},"方向是从-180~180之间，而速度则需要根据Character的设定来决定，由于这里是默认的，就保持和官方示例的一样就好了。",[11,1878,1879],{},"这里的坐标名称是随意的，之后会通过蓝图来设置当前的坐标值。",[53,1881,1882],{"id":1882},"状态机",[11,1884,1885],{},"在进一步操作混合空间前，先将动画蓝图设置好。",[11,1887,1888],{},"动画图表的状态机中新建一个状态，命名为Move。",[11,1890,1891],{},"新建两个变量Speed、Direction，",[11,1893,1894],{},"从Idle的边缘拉一根线到Move，就会自动生成一个Transition。然后在Transition中添加条件为Speed>10.0则执行状态迁移，也就是由Idle变为Move。",[11,1896,1897],{},"然后从Move拉一个Transition到Idle，条件设置为Speed\u003C=10.0。",[11,1899,1900],{},"打开Move的状态，直接拖入刚刚新建的混合空间，把Speed和Direction分别接到混合空间的两个坐标轴上。可以在右边的动画预览页调整两个值来查看效果，不过现在混合空间是空的，会变成摆T字。",[11,1902,1903],{},"这样状态机的部分就设置完了。",[53,1905,1906],{"id":1906},"关键帧",[11,1908,1909],{},"然后回到混合空间中，在坐标系的各个位置添加混合节点。",[11,1911,1912],{},"一般情况下在角度(-180, -90, 0, 90, 180)和(行走速度,跑步速度)上添加关键性的混合用节点就可以了。",[11,1914,1915],{},"但是AnimationStarterPack中似乎没有跑步动画，不过这个版本的引擎中可以在左侧对节点的速度缩放进行调节，某种程度上可以代替跑步，不过反正是用来做原型的，也不用太在意。",[11,1917,1918],{},"在Speed 270上拖放动画，按角度来区分的话-180和180都是向后采用BWD，0则采用FWD，-90是LT而90是RT，然后在Speed 0上放一样的动画，但是将Rate Scale调低一些，变成0.8。",[11,1920,1921],{},[1314,1922],{"alt":1548,"src":1923},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0024_thumb.png",[11,1925,1926],{},"然后将Target Weight Interpolation Speed Per Sec设定为2.0，这样混合空间的准备就完成了。",[53,1928,1929],{"id":1929},"状态绑定",[11,1931,1932],{},"此时在动画蓝图中调节预览的两个值，就可以看到效果了。",[11,1934,1935],{},"然后参照官方的动画蓝图中的节点，将速度和方向从Character那边读取过来。",[11,1937,1938],{},[1314,1939],{"alt":1940,"src":1941},"clip_image0034","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0034_thumb.png",[11,1943,1944],{},"直接运行游戏，就可以看到行走的动画切换了。",[36,1946,1947],{"id":1947},"额外动作",[11,1949,1950],{},"在基本的行走动画之外，还有一些额外的动作可以加入到动画蓝图的状态机中，通常的FPS中会有跳跃、蹲伏、趴下之类的动作。",[11,1952,1953],{},"AnimationStarterPack中虽然有提供趴下的动作，但是是没有移动动画的，所以是一个静止的状态，这里并没有做，其实动画蓝图这边只是新建一个孤立的状态机就可以。主要还是要在角色蓝图中添加静止移动的逻辑，由于并没有做这种功能的打算，这里就放弃了。",[53,1955,1956],{"id":1956},"静止跳跃",[11,1958,1959],{},"这里官方的动画蓝图有一个问题，由于在Idle->Jump的管道中有加上速度条件，而实际上这个速度是包含跳跃速度的，因此导致状态机沿着Idle->Jog->Run Jump的路径前进，Jump这个状态是永远都不可达的。",[11,1961,1962],{},"由于不知道官方的原始设计时什么样的，这里姑且在Speed计算时忽略掉Z轴的速度。",[11,1964,1965],{},[1314,1966],{"alt":1967,"src":1968},"clip_image0018","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0018_thumb.png",[11,1970,1971],{},"然后在检测到玩家按下跳跃键后，在动画蓝图中记录跳跃标记",[11,1973,1974],{},[1314,1975],{"alt":1680,"src":1976},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0026_thumb.png",[11,1978,1979],{},"基本可以将官方的蓝图中的Jump相关的部分直接抄过来，另外由于修改了Jump的逻辑，要为Jump添加状态进入事件",[11,1981,1982],{},[1314,1983],{"alt":1984,"src":1985},"clip_image0036","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0036_thumb.png",[11,1987,1988],{},"并在事件蓝图中相应",[11,1990,1991],{},[1314,1992],{"alt":1993,"src":1994},"clip_image004","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image004_thumb.png",[11,1996,1997],{},"在这里的话就能在静止状态下跳起来了，但是如果在行走中按跳跃的话，就会有BUG：在停止运动后额外的播放了一次动画。",[11,1999,2000],{},"这个是因为没有加RunJump状态造成的，不过为了以后添加别的状态不出现这个Bug，需要对CanJump()的实现多加留意。不能跳跃的状态就不要将跳跃标记置为True。",[11,2002,2003],{},"另外一个方面就是，Jump这个动画有一个前摇的过程，但是实际上蓝图的实现是已经跳跃起来了。这里需要对设计进行调整，或者让动画从0.3秒开始播放",[11,2005,2006],{},[1314,2007],{"alt":2008,"src":2009},"clip_image005","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image005_thumb.png",[11,2011,2012],{},"这样调整之后会造成落地有些许的违和感，这个主要是官方没有准备浮空动画而是在跳跃动画播放完后直接切回非跳跃状态造成的。因此大概是为了防止这里的穿帮，官方给的默认跳跃高度还是比较低的。记得以前的某个示例中是有的，但是在AnimationStarterPack中并没有找到这个呢。由于是用来做游戏原型的，这个细节就不管了。",[53,2014,2016],{"id":2015},"runjump","RunJump",[11,2018,2019],{},"然后就是RunJump的添加。",[11,2021,2022],{},"这里大部分的逻辑可以按照Jump的一样的流程，也可以从官方的示例里面抄过来。不过从Move->Jump的状态其实没有必要对速度进行限制了。直接检测到跳跃标记就切换到RunJump状态就可以了。",[11,2024,2025],{},"RunJump的切换非常的流畅，所以很怀疑官方让Jump状态失效是故意而为的。",[53,2027,2029],{"id":2028},"crouching","Crouching",[11,2031,2032],{},"下蹲状态和跳跃状态有些类似，不过下蹲状态被当作了一个持续性状态来处理。",[11,2034,2035],{},"所以下蹲分为静止下蹲和下蹲移动两个状态，下蹲移动中也还是一个负责移动处理的BlendSpace，参照Move的BlendeSpace即可",[11,2037,2038],{},[1314,2039],{"alt":2040,"src":2041},"clip_image006","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image006_thumb.png",[11,2043,2044],{},"总体上而言下蹲的静止和移动与Stand->Run是一个对应的关系。",[11,2046,2047],{},[1314,2048],{"alt":2049,"src":2050},"clip_image007","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image007_thumb.png",[11,2052,2053],{},"因此这里基本上没有什么新的东西。",[11,2055,2056],{},"唯一的不同是，由于前面修改过Speed运算方式。在Crouch的状态下按跳跃，只会有位置上升，而不会有动画的变更。",[11,2058,2059],{},"官方原始的类似于蹲下跳跃的效果，是由于Z轴速度大于10导致状态迁移到Crouch Move而产生的。",[11,2061,2062],{},"在CanJump()中添加CrouchingButtonDown时不允许设定Jump标志的逻辑，就可以防止在站起身之后进入跳跃的问题了。",[61,2064,2067],{"className":2065,"code":2066,"language":66},[64],"bool UCharaAnimate::CanJump(bool ShouldJump)\n{\n  return !EnableJump && ShouldJump && !Crouching;\n}\n",[68,2068,2066],{"__ignoreMap":70},[36,2070,1225],{"id":1225},[11,2072,2073],{},"至此官方的动画蓝图中实现的功能就基本完成了，虽然在实现上和官方少许有些不同，不过功能上已经没有什么缺陷了。",[11,2075,2076],{},"接下来，就是添加上一些额外的动画来与游戏模式相对应了。",{"title":70,"searchDepth":354,"depth":360,"links":2078},[2079,2083,2089,2094],{"id":1754,"depth":354,"text":1754,"children":2080},[2081,2082],{"id":1766,"depth":360,"text":1767},{"id":1812,"depth":360,"text":1812},{"id":1851,"depth":354,"text":1851,"children":2084},[2085,2086,2087,2088],{"id":1863,"depth":360,"text":1864},{"id":1882,"depth":360,"text":1882},{"id":1906,"depth":360,"text":1906},{"id":1929,"depth":360,"text":1929},{"id":1947,"depth":354,"text":1947,"children":2090},[2091,2092,2093],{"id":1956,"depth":360,"text":1956},{"id":2015,"depth":360,"text":2016},{"id":2028,"depth":360,"text":2029},{"id":1225,"depth":354,"text":1225},"2017-11-11",{"layout":1275,"status":1276,"published":385,"author":2097,"author_login":1279,"author_email":1280,"wordpress_id":2098,"wordpress_url":2099,"date_gmt":2100,"excerpt":2101},{"display_name":1278,"login":1279,"email":1280,"url":70},2066,"\u002F\u002F?p=2066","2017-11-11 02:51:55 +0000",{"type":8,"value":2102},[2103],[11,2104,1742],{},"\u002F2017-11-11-animatioinstarterpack-note-01",{"title":1737,"description":1742},"_legacy\u002F2017\u002F2017-11-11-animatioinstarterpack-note-01",[1292,2109],"Animation","pkfBqzdfLonxqYHExxcc5TCbq5IXzXFvboJ8DjRg40o",{"id":2112,"title":2113,"body":2114,"date":2095,"description":2118,"extension":1273,"meta":2403,"navigation":385,"path":2412,"seo":2413,"stem":2414,"tags":2415,"__hash__":2416},"blogs\u002F_legacy\u002F2017\u002F2017-11-11-animatioinstarterpack-note-02.md","AnimatioinStarterPack的使用（下）",{"type":8,"value":2115,"toc":2392},[2116,2119,2122,2125,2129,2132,2135,2138,2141,2144,2149,2152,2157,2159,2162,2165,2168,2171,2174,2179,2182,2187,2190,2194,2197,2200,2203,2206,2295,2298,2303,2306,2311,2314,2319,2322,2328,2331,2336,2339,2344,2347,2350,2353,2356,2359,2364,2367,2370,2374,2377,2380,2383,2386,2389],[11,2117,2118],{},"前面已经对官方的示例动画蓝图进行了还原，但是要作为原型测试时使用还是有些不足。",[11,2120,2121],{},"当前UE4版本为UE4.18.0。",[11,2123,2124],{},"这里继续对动画蓝图添加一些基本的功能。",[36,2126,2128],{"id":2127},"dead","Dead",[11,2130,2131],{},"死亡动画也是可以放到状态机的，但是如果状态机的结构很复杂的话，就会陷入需要从每个状态拉到Dead状态的窘境。",[11,2133,2134],{},"这里就需要在制作之前对状态机进行规划，例如其实静止和移动这两个状态是可以合并到一个BlendSpace中去的，再加上下蹲动画的合并，同时，将Jump和RunJump状态进行统一，状态机的数量就会减少。这里由于是原型用的，之后动画资源可能会不一样，就不会进行详细的设计和修改了。",[53,2136,2137],{"id":2137},"逻辑绑定",[11,2139,2140],{},"由于这里没有游戏逻辑，所以直接按下P键就判定玩家死亡。",[11,2142,2143],{},"在Character蓝图中，调用Disable Input来关闭玩家输入，设置标志位，3秒之后重置这些状态。在此就不做其他逻辑了。",[11,2145,2146],{},[1314,2147],{"alt":1316,"src":2148},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image001_thumb-1.png",[11,2150,2151],{},"在动画蓝图的更新函数中，取得Character的死亡标记，并设置到动画蓝图中",[11,2153,2154],{},[1314,2155],{"alt":1440,"src":2156},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image002_thumb-1.png",[53,2158,1812],{"id":1812},[11,2160,2161],{},"在状态机中添加Dead状态，随便拉一个Dead动画出来播放，不要循环。",[11,2163,2164],{},"然后从Idle->Dead设定条件为PawnDead，而从Dead->Idle为非PawnDead。",[11,2166,2167],{},"这样就有了基本的死亡动画逻辑。",[11,2169,2170],{},"其实官方总共准备了3种略有不同的站立死亡动画，这里可以选择随机的播放一种，使用名为Blend Poses by int的节点即可。",[11,2172,2173],{},"在Dead状态中，对动画进行操作",[11,2175,2176],{},[1314,2177],{"alt":1449,"src":2178},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image003_thumb-1.png",[11,2180,2181],{},"DeadAnimatType是新建的Int变量，需要在检测到死亡标志位时随机指定到0~2之间。",[11,2183,2184],{},[1314,2185],{"alt":1993,"src":2186},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image004_thumb-1.png",[11,2188,2189],{},"这样就有了随机死亡动画的功能了，不过这里由于没有添加Respawn的逻辑，3秒的Delay之后是直接原地站起来复活的:D",[36,2191,2193],{"id":2192},"aimoffset","AimOffset",[11,2195,2196],{},"这也是UE4提供的动画Asset之一，其主要作用是在移动-静止动画上叠加的额外的瞄准动画。",[53,2198,2199],{"id":2199},"资源准备",[11,2201,2202],{},"新建一个AimOffset打开编辑，能看到它和BlendSpace一样有两个轴向对动画进行Blend，不同之处在于AimOffset最终是叠加到已有的基础动作之上的。",[11,2204,2205],{},"要做AimOffset首先需要的是制作各个瞄准方向的基础动作，在AnimationStarterPack中，瞄准动画可以从Aim_Space_Hip的动画中取出，其中共有9个动作，帧数分别为：",[2207,2208,2209,2220],"table",{},[2210,2211,2212],"thead",{},[2213,2214,2215,2218],"tr",{},[2216,2217],"th",{},[2216,2219],{},[2221,2222,2223,2232,2240,2248,2255,2263,2271,2279,2287],"tbody",{},[2213,2224,2225,2229],{},[2226,2227,2228],"td",{},"0",[2226,2230,2231],{},"Aim_Center",[2213,2233,2234,2237],{},[2226,2235,2236],{},"10",[2226,2238,2239],{},"Aim_Center_Up",[2213,2241,2242,2245],{},[2226,2243,2244],{},"20",[2226,2246,2247],{},"Aim_Center_Down",[2213,2249,2250,2252],{},[2226,2251,818],{},[2226,2253,2254],{},"Aim_Left_Center",[2213,2256,2257,2260],{},[2226,2258,2259],{},"40",[2226,2261,2262],{},"Aim_Left_Up",[2213,2264,2265,2268],{},[2226,2266,2267],{},"50",[2226,2269,2270],{},"Aim_Left_Down",[2213,2272,2273,2276],{},[2226,2274,2275],{},"60",[2226,2277,2278],{},"Aim_Right_Center",[2213,2280,2281,2284],{},[2226,2282,2283],{},"70",[2226,2285,2286],{},"Aim_Right_Up",[2213,2288,2289,2292],{},[2226,2290,2291],{},"80",[2226,2293,2294],{},"Aim_Right_Down",[11,2296,2297],{},"操作上，对Anim_Space_Hip进行复制，通过最下方的帧导航到想要的帧。",[11,2299,2300],{},[1314,2301],{"alt":1418,"src":2302},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0015_thumb.png",[11,2304,2305],{},"然后在帧导航条上点击右键，先删除左边的所有帧，再删除右边的所有帧",[11,2307,2308],{},[1314,2309],{"alt":1548,"src":2310},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0024_thumb-1.png",[11,2312,2313],{},"这样就能得到9个只有一帧的动画了，全部选中，然后在右键菜单中使用集合编辑功能。",[11,2315,2316],{},[1314,2317],{"alt":1940,"src":2318},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0034_thumb-1.png",[11,2320,2321],{},"然后对AdditiveSettings属性进行调整",[11,2323,2324],{},[1314,2325],{"alt":2326,"src":2327},"clip_image0044","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0044_thumb.png",[11,2329,2330],{},"回到刚刚新建的AnimOffset，与BlendSpace类似，设定两个轴向的名称和范围",[11,2332,2333],{},[1314,2334],{"alt":2008,"src":2335},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image005_thumb-1.png",[11,2337,2338],{},"然后将刚刚生成的9个动作拖放到关键插值点上去",[11,2340,2341],{},[1314,2342],{"alt":2040,"src":2343},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image006_thumb-1.png",[11,2345,2346],{},"这样就可以了。",[53,2348,1812],{"id":2349},"动画蓝图-1",[11,2351,2352],{},"接下来，到动画蓝图中对AnimOffset进行使用。",[11,2354,2355],{},"首先添加两个Float变量，分别命名Aim_Yaw和Aim_Pitch",[11,2357,2358],{},"然后到动画图表中，添加AnimOffset的使用",[11,2360,2361],{},[1314,2362],{"alt":2049,"src":2363},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image007_thumb-1.png",[11,2365,2366],{},"这时，在动画蓝图的预览中就可以对AimOffset进行调试了。",[11,2368,2369],{},"如果要实际使用的话，还要在BlueprintUpdateAnimation事件或者其他地方，对这两个值进行更新。由于我这边是做的TopDown，就不继续操作了。",[36,2371,2373],{"id":2372},"montage","Montage",[11,2375,2376],{},"其他的一些动画，包括射击动画、装备切换动画都需要用到Montage。",[11,2378,2379],{},"这些动画都是与具体的资源相关性比较强的，在原型制作阶段制作的话就不会进行更多的时间投入了。",[11,2381,2382],{},"包括换枪、持枪、射击在内的动画要使用Motage，是因为身体的基本动作是一致的，不可能为每一个状态做一个对应的动画。",[11,2384,2385],{},"基本的装备切换，都是通过在Montage中添加动画通知，然后将物品切换Socket来做的。",[11,2387,2388],{},"由于只是要做一个动画蓝图来做原型用，而且要使用Montage的话，就必须从资源开始就有做好的规划，在没有进一步需求的情况下暂时就不会继续做下去了。",[11,2390,2391],{},"Montage的教程很多，直接搜索就可以了:D。",{"title":70,"searchDepth":354,"depth":360,"links":2393},[2394,2398,2402],{"id":2127,"depth":354,"text":2128,"children":2395},[2396,2397],{"id":2137,"depth":360,"text":2137},{"id":1812,"depth":360,"text":1812},{"id":2192,"depth":354,"text":2193,"children":2399},[2400,2401],{"id":2199,"depth":360,"text":2199},{"id":2349,"depth":360,"text":1812},{"id":2372,"depth":354,"text":2373},{"layout":1275,"status":1276,"published":385,"author":2404,"author_login":1279,"author_email":1280,"wordpress_id":2405,"wordpress_url":2406,"date_gmt":2407,"excerpt":2408},{"display_name":1278,"login":1279,"email":1280,"url":70},2091,"\u002F\u002F?p=2091","2017-11-11 02:52:21 +0000",{"type":8,"value":2409},[2410],[11,2411,2118],{},"\u002F2017-11-11-animatioinstarterpack-note-02",{"title":2113,"description":2118},"_legacy\u002F2017\u002F2017-11-11-animatioinstarterpack-note-02",[1292,2109],"yvxgzW9FrlIGR1YF3sTA3p7Scjk6Z2_J6JX0053oKt8",{"id":2418,"title":2419,"body":2420,"date":3062,"description":2424,"extension":1273,"meta":3063,"navigation":385,"path":3072,"seo":3073,"stem":3074,"tags":3075,"__hash__":3076},"blogs\u002F_legacy\u002F2017\u002F2017-10-21-ue4-lighting-troubleshooting-note.md","UE4光照问题排查指南",{"type":8,"value":2421,"toc":3029},[2422,2425,2433,2436,2440,2443,2448,2452,2455,2458,2463,2466,2471,2475,2478,2483,2486,2491,2494,2498,2501,2504,2507,2510,2515,2518,2521,2524,2529,2533,2536,2542,2545,2549,2552,2558,2561,2564,2567,2570,2573,2576,2601,2609,2613,2616,2622,2626,2634,2637,2640,2644,2650,2653,2659,2662,2665,2668,2677,2681,2688,2691,2697,2700,2704,2707,2715,2718,2721,2724,2730,2733,2739,2743,2746,2752,2755,2761,2764,2767,2770,2773,2776,2779,2782,2785,2788,2791,2795,2801,2804,2807,2810,2813,2816,2819,2827,2831,2834,2842,2846,2849,2855,2858,2861,2864,2867,2870,2876,2879,2882,2888,2891,2894,2897,2900,2903,2909,2912,2915,2918,2922,2925,2931,2935,2938,2941,2944,2948,2954,2957,2961,2964,2970,2973,2976,2979,2989,2992,2996,2999,3005,3008,3015,3018],[11,2423,2424],{},"这个是官方Wiki中光照问题排查指南的总结，对于排查光照问题很有帮助。",[11,2426,2427,2428,1697],{},"由于文章实在太长，所以并不是逐字翻译。但是会尽量保留所有的内容，之前的文章有重复的部分会导向以前的部分。官方Wiki的原始文章地址在[",[1324,2429,2432],{"href":2430,"rel":2431},"https:\u002F\u002Fwiki.unrealengine.com\u002FLightingTroubleshootingGuide",[1695],"这里",[36,2434,2435],{"id":2435},"通用设定",[53,2437,2439],{"id":2438},"为何我的阴影是黑色的","为何我的阴影是黑色的？",[11,2441,2442],{},"通常阴影纯黑是由于没有辅助的填充光照造成的，这种情况通常发生在开放场景中只有一个代替日光的直射光时。这时需要在场景中添加一个Sky Light作为全局光照，由于光照的反射模拟是有限的，天光可以有效的对现实中的光照进行模拟。",[11,2444,2445],{},[1314,2446],{"alt":1316,"src":2447},"\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image001_thumb.png",[53,2449,2451],{"id":2450},"将bsp转化为static-mesh时光照贴图设置错误","将BSP转化为Static Mesh时光照贴图设置错误",[11,2453,2454],{},"这个似乎是早期版本的问题，现在（4.18）已经不会有这个问题了。不过光照贴图的自动设置Index是0，出现问题时可以排查这里。",[11,2456,2457],{},"当将BSP物体转化为模型体时，可能会看到这样的错误",[11,2459,2460],{},[1314,2461],{"alt":1440,"src":2462},"\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image002_thumb.png",[11,2464,2465],{},"这个是由于光照贴图没有完全的自动设定造成的，需要在SM的设定中手动的指定光照贴图分辨率和光照贴图的UV Channel。",[11,2467,2468],{},[1314,2469],{"alt":1449,"src":2470},"\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image003_thumb.png",[53,2472,2474],{"id":2473},"如何停止使用光照贴图","如何停止使用光照贴图？",[11,2476,2477],{},"如果整个项目都不使用的话，可以在项目设定中关闭静态光照：",[11,2479,2480],{},[1314,2481],{"alt":1993,"src":2482},"\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image004_thumb.png",[11,2484,2485],{},"或者只是在单独的地图的世界设置中禁用预计算光照：",[11,2487,2488],{},[1314,2489],{"alt":2008,"src":2490},"\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image005_thumb.png",[11,2492,2493],{},"会有提示，需要重新构建一次光照才会清除当前地图内的光照数据。",[53,2495,2497],{"id":2496},"为何我的模型一部分不见了","为何我的模型一部分不见了？",[11,2499,2500],{},"和在建模软件中不同，UE4会对模型的面进行可见度裁剪。如果模型的一部分在UE4中不可见，可以按Alt+2进入线框模式，查看导入是否正确。",[11,2502,2503],{},"通常的情况下对模型进行修改比较好。",[11,2505,2506],{},"如果不想修改模型的话有两个解决方案：",[11,2508,2509],{},"选中对应的物体，打开双面光照",[11,2511,2512],{},[1314,2513],{"alt":2040,"src":2514},"\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image006_thumb.png",[11,2516,2517],{},"Lightmass中的是静态光照的开关，Shadow Two Sided则决定了是否作为双面物体进行动态光照阴影计算。",[53,2519,2520],{"id":2520},"将物体材质设定为双面的",[11,2522,2523],{},"在对应物体的材质中将其修改为双面材质",[11,2525,2526],{},[1314,2527],{"alt":2049,"src":2528},"\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image007_thumb.png",[53,2530,2532],{"id":2531},"为何突然光照效果变得不一样了","为何突然光照效果变得不一样了？",[11,2534,2535],{},"编辑器内部有自动调整渲染级别的设定，在渲染帧率下降时，编辑器会提示是否下调渲染等级，如果没有留意的话会发现渲染结果突然变得不一样了。直接对设置进行修改就好了",[11,2537,2538],{},[1314,2539],{"alt":2540,"src":2541},"clip_image008","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image008_thumb.png",[11,2543,2544],{},"去掉“显示器编辑器偏好设置？”的勾选，就不会自动进行渲染级别调整了",[53,2546,2548],{"id":2547},"为何光照上有个红叉","为何光照上有个红叉？",[11,2550,2551],{},"Stationary Light在同一个被覆盖区域中只能有4个",[11,2553,2554],{},[1314,2555],{"alt":2556,"src":2557},"clip_image009","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image009_thumb.png",[11,2559,2560],{},"当超过这个数目时，范围最小的那个Stationary Light会被转化为动态光源。",[11,2562,2563],{},"由于动态光源拥有更高的运行时开销，所以需要注意。",[11,2565,2566],{},"这里的修正上只能对光源的布局进行重新规划。",[53,2568,2569],{"id":2569},"光照贴图错误",[11,2571,2572],{},"光照贴图在设定上必须在0~1的UV空间内，超过的话就会在烘焙时提示警告Lightmap Overlapping by xx%。",[11,2574,2575],{},"当然还有一些其他的报错，光照贴图需要注意以下问题：",[2577,2578,2579,2583,2586,2589,2592,2595,2598],"ul",{},[2580,2581,2582],"li",{},"不要有重叠的部分",[2580,2584,2585],{},"不要超过0~1的UV空间",[2580,2587,2588],{},"Flag-Mapping并不是最好的方式且经常导致光照贴图错误",[2580,2590,2591],{},"尽量占满UV空间",[2580,2593,2594],{},"如果模型很大而且复杂，最后分成数个物体，这样也能有助于裁剪等机制",[2580,2596,2597],{},"尽量减小光照贴图分辨率以减少贴图尺寸",[2580,2599,2600],{},"相互不接触的线之间要保持至少2像素的距离，以防止光照污染",[11,2602,2603,2604,1697],{},"更多的内容可以参照官方的[",[1324,2605,2608],{"href":2606,"rel":2607},"https:\u002F\u002Fdocs.unrealengine.com\u002Flatest\u002FINT\u002FEngine\u002FContent\u002FTypes\u002FStaticMeshes\u002FLightmapUnwrapping\u002F",[1695],"光照贴图指南",[53,2610,2612],{"id":2611},"post-process中gi的设定","Post Process中GI的设定",[11,2614,2615],{},"PP中的GI设定可以对光照效果产生很大的影响，官方有一个示例就是通过两个PP的动态切换来实现灯光效果的变更。",[11,2617,2618],{},[1314,2619],{"alt":2620,"src":2621},"clip_image010","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image010_thumb.png",[53,2623,2625],{"id":2624},"capsule-shadows","Capsule Shadows",[11,2627,2628,2629,2633],{},"这个是4.11版本的新特性，似乎和问题排查关系不大，可以参考之前的",[1324,2630,2632],{"href":2631},"\u002F2017-08-23-ue4-lighting-and-optimize\u002F#Capsule_Shadows","介绍","。",[36,2635,2636],{"id":2636},"动态光照",[11,2638,2639],{},"动态光照在使用上比静态光照直观些，不需要每次都重新构建就能看到效果。",[53,2641,2643],{"id":2642},"直射光独有属性csm","直射光独有属性：CSM",[11,2645,2646],{},[1314,2647],{"alt":2648,"src":2649},"clip_image011","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image011_thumb.png",[11,2651,2652],{},"这些属性都是决定动态光照级联切换的，在显示中打开",[11,2654,2655],{},[1314,2656],{"alt":2657,"src":2658},"clip_image012","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image012_thumb.png",[11,2660,2661],{},"可以更直观的看到效果",[11,2663,2664],{},"Dyamic Shadow Distance为光照的覆盖的范围，设置为0的话等于禁用这个光照。Stational光照的默认设定为0，也就是不开启动态光照。",[11,2666,2667],{},"Num Dynamic Shadow Cascades为动态贴图的过渡级别，越多的话动态光照的效果就越好，但是也更消耗性能，设置为0的话等同于禁用动态光照。",[11,2669,2670,2671,2676],{},"由于原文章中能够很直观的看到各个属性的调节的",[1324,2672,2675],{"href":2673,"rel":2674},"https:\u002F\u002Fwiki.unrealengine.com\u002FLightingTroubleshootingGuide#Directional_Light_ONLY:_Cascaded_Shadow_Maps_Settings:",[1695],"效果","，打开Shadow Frustrums就可以很好的进行观察，这里就不继续介绍了。",[53,2678,2680],{"id":2679},"far-shadow","Far Shadow",[11,2682,2683,2684,2687],{},"远景物体的阴影投射，这一部分之前有总结过，可以移步以前",[1324,2685,2680],{"href":2686},"\u002F2017-08-23-ue4-lighting-and-optimize\u002F#Far_Shadow","的文章。",[11,2689,2690],{},"这里官方提到了额外的注意事项，就是Far Shadow Distance应当比CSM的距离要远，在运算上Far Shadow会在设定距离与CSM距离之间进行Cascade Count次级联运算。",[11,2692,2693],{},[1314,2694],{"alt":2695,"src":2696},"clip_image013","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image013_thumb.png",[11,2698,2699],{},"同时尽量只给大的物体投射Far Shadow，因为对于小的物体而言，Far Shadow其实是不是很必要却有额外消耗的。",[53,2701,2703],{"id":2702},"csm调整","CSM调整",[11,2705,2706],{},"动态光照的污染等问题，通常通过调整CSM的属性来进行排查。",[11,2708,2709,2714],{},[1324,2710,2713],{"href":2711,"rel":2712},"https:\u002F\u002Fwiki.unrealengine.com\u002FLightingTroubleshootingGuide#Adjusting_Cascades_for_better_Quality:",[1695],"官方的示例","中是通过调整Cascade Distribution Exponent来解决问题的，但是由于室外场景比较多变，并没有",[11,2716,2717],{},"统一的解决方案，通过Shadow Frustrums工具的配合，对上面的几个属性进行调整比较好。",[53,2719,2720],{"id":2720},"动态光照通用属性",[11,2722,2723],{},"Shadow BIas通常用于调整动态阴影导致的自投影问题，一个过小的值将会导致阴影从很近的地方开始计算。",[11,2725,2726],{},[1314,2727],{"alt":2728,"src":2729},"clip_image014","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image014_thumb.png",[11,2731,2732],{},"Shadow Filter Sharpness则用于调整阴影的边缘是否尖锐",[11,2734,2735],{},[1314,2736],{"alt":2737,"src":2738},"clip_image015","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image015_thumb.png",[53,2740,2742],{"id":2741},"为何光照在远处时穿过物体","为何光照在远处时穿过物体？",[11,2744,2745],{},"这个问题是由于用于遮挡动态光照的物体在距离很远时已经被裁剪造成的。",[11,2747,2748],{},[1314,2749],{"alt":2750,"src":2751},"clip_image016","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image016_thumb.png",[11,2753,2754],{},"比较直观的解决方案是直接拉大那个物体的Bound来避免其被裁剪。可以直接在物体实例中设置",[11,2756,2757],{},[1314,2758],{"alt":2759,"src":2760},"clip_image017","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image017_thumb.png",[11,2762,2763],{},"放大Bounds Scale，在显示中打开Bounds的预览可以很好的对其进行规划。",[11,2765,2766],{},"但是这里通常情况下的建议是避免这种光照使用方式，这里可以使用聚光灯或者光照函数、IIES来更加高效的实现这个效果。",[11,2768,2769],{},"使用物体遮挡来对光源的形状进行控制并不是一个好主意。",[36,2771,2772],{"id":2772},"静态光照",[11,2774,2775],{},"光照贴图分辨率\u002F阴影质量",[11,2777,2778],{},"这里的建议虽然是官方的，但是并不能对所有的情况通用，只是作为光照问题排查的基础。",[11,2780,2781],{},"光照贴图分辨率用于调整阴影的质量，这里需要注意的是，这个分辨率是决定投影在其上的阴影的精度的，并不是决定其自身的阴影精度的。也就是说物体的阴影的质量是更加这个阴影投射到的物体的光照贴图分辨率决定的。",[11,2783,2784],{},"网格物体的分辨率必须是POT的，且数值越大阴影质量越高",[11,2786,2787],{},"BSP物体的分辨率则相反，数值越低阴影质量越高",[11,2789,2790],{},"Landscapes的分辨率调节则是一个乘数，越大质量越高",[53,2792,2794],{"id":2793},"seams","Seams",[11,2796,2797],{},[1314,2798],{"alt":2799,"src":2800},"clip_image018","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image018_thumb.png",[11,2802,2803],{},"就是物体接合处的光照效果不真实的问题，这个问题通常是由间接光照引起的，因为间接光照计算时各个物体相互之间是独立的，因此计算的结果就会不合理。",[11,2805,2806],{},"官方的建议之一是调整世界设置中的静态光照参数",[11,2808,2809],{},"Indirect Lighting Quality设置到2或者更高",[11,2811,2812],{},"Indirect Lighting Smoothness设置到0.65~0.75之间",[11,2814,2815],{},"Static Lighting Level Scale这个属性由于是对计算单位进行缩放，虽然调小会有更好的阴影过度，但是会明显的提高光照计算量。通常应用于建筑展示领域，不建议在游戏开发中使用。",[11,2817,2818],{},"通常上面的调整效果是有限的，其实这个问题更多的应该从关卡构建上入手：",[2577,2820,2821,2824],{},[2580,2822,2823],{},"不要过分的将关卡模块化，当可以有一整面墙的时候就不要将其拆分为好几个部分。这样可以防止问题的同时减少绘制调用次数。",[2580,2825,2826],{},"使用物体对接缝处进行遮蔽。",[53,2828,2830],{"id":2829},"构建光照时报错overlapping-uv-error","构建光照时报错Overlapping UV error",[11,2832,2833],{},"这个之前就有提到，大概是专门又详细的说明一次。",[11,2835,2836,2837,2841],{},"Overlapping和Wrapping都是UV错误的形式，可以使用",[1324,2838,2840],{"href":2839},"\u002F2017-08-23-ue4-lighting-and-optimize\u002F#Error_Coloring","错误着色","进行可视化排查，但是通常直接查看UV就能看出问题。",[53,2843,2845],{"id":2844},"如何使用编辑器生成光照uv","如何使用编辑器生成光照UV",[11,2847,2848],{},"在Mesh的编辑窗口",[11,2850,2851],{},[1314,2852],{"alt":2853,"src":2854},"clip_image019","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image019_thumb.png",[11,2856,2857],{},"勾选生成光照贴图UV后，设置下面三个属性就可以了。",[11,2859,2860],{},"通常源光照贴图索引使用的应当是用于贴图渲染的那个UV通道。",[11,2862,2863],{},"目标索引则是要生成的UV索引。",[11,2865,2866],{},"点击应用修改后就会生成光照UV。",[11,2868,2869],{},"这时候就可以在上面的通用设定中对光照UV的索引进行设置了。",[11,2871,2872],{},[1314,2873],{"alt":2874,"src":2875},"clip_image020","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image020_thumb.png",[53,2877,2878],{"id":2878},"间接光照回弹次数",[11,2880,2881],{},"可以在世界设定中对间接光照的回弹次数进行设定，回弹计算的次数越多，间接光照的效果越好。",[11,2883,2884],{},[1314,2885],{"alt":2886,"src":2887},"clip_image021","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image021_thumb.png",[11,2889,2890],{},"设定上拖动只能在1~4之间，但是可以手动输入更大的值。",[11,2892,2893],{},"回弹计算在第一次计算时成本最高，之后计算的话成本不高，但是也不会有什么太大的结果变化。通常4次计算就足够了。",[11,2895,2896],{},"为何间接光照产生了很多“斑点”？",[11,2898,2899],{},"通过调节光照贴图分辨率可以缓解这个问题。",[11,2901,2902],{},"虽然调整光照贴图质量以及平滑度也能解决这个问题",[11,2904,2905],{},[1314,2906],{"alt":2907,"src":2908},"clip_image022","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image022_thumb.png",[11,2910,2911],{},"但是通常会导致过高的光照构建消耗，并不推荐。",[11,2913,2914],{},"提高间接光照回弹次数能够起到比提高光照贴图分辨率更好的效果。",[11,2916,2917],{},"一个更好的解决方案是，不要让区域只受间接光照的影响，添加一个不投影的光源就能很好的防止斑点的出现。",[53,2919,2921],{"id":2920},"lighting-needs-to-be-rebuilt是什么","\"Lighting needs to be rebuilt\"是什么？",[11,2923,2924],{},"光照需要重新构建，点击构建按钮下的仅构建光照即可。",[11,2926,2927],{},[1314,2928],{"alt":2929,"src":2930},"clip_image023","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image023_thumb.png",[53,2932,2934],{"id":2933},"unbuilt-actors-list","Unbuilt Actors List",[11,2936,2937],{},"如果构建后依然出现这个提示，可以在输出日志中敲入命令DumpUnbuiltLightInteractions来列出未构建的物体。",[11,2939,2940],{},"通常会出现这类物体可能还是由于蓝图或代码在物体摆放后对其移动造成的。",[11,2942,2943],{},"通过这个列表对问题进行排查即可。",[53,2945,2947],{"id":2946},"statistics-window","Statistics Window",[11,2949,2950],{},[1314,2951],{"alt":2952,"src":2953},"clip_image024","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image024_thumb.png",[11,2955,2956],{},"这个窗口可以排查是哪些物体过度的消耗了光照构建的时间。",[53,2958,2960],{"id":2959},"lanscape上草的阴影问题","Lanscape上草的阴影问题",[11,2962,2963],{},"可以在GrassType上找到一个选项，",[11,2965,2966],{},[1314,2967],{"alt":2968,"src":2969},"clip_image025","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image025_thumb.png",[11,2971,2972],{},"让草直接使用Landscape的光照贴图即可。",[53,2974,2975],{"id":2975},"通过命令行构建构造",[11,2977,2978],{},"4.10.2后的功能",[11,2980,2981,2982,2985,2986],{},"UE4-Editor.exe ",[330,2983,2984],{},"Project Folder Path"," -run=resavepackages -buildlighting -MapsOnly -ProjectOnly -AllowCommandletRendering -Map=",[330,2987,2988],{},"Name of map",[11,2990,2991],{},"其中-Map是可选项，不过去掉的话会转而构建所有的地图。",[53,2993,2995],{"id":2994},"foliage的光照","Foliage的光照",[11,2997,2998],{},"在Foliage中可以使用Light Map Resolution选项覆盖掉Mesh本身的光照贴图分辨率。",[11,3000,3001],{},[1314,3002],{"alt":3003,"src":3004},"clip_image026","\u002Fwp-content\u002Fuploads\u002F2017\u002F10\u002Fclip_image026_thumb.png",[11,3006,3007],{},"由于Foliage实际上是被打包在一起的，所以所有的FoliageInstance的光照贴图将会合在一张里面，所以缩小其分辨率是有一定必要的。",[11,3009,3010,3011,3014],{},"尤其是在构建时出现提示Instanced_Foliage_Actor_",[330,3012,3013],{},"X"," lightmap is too large and should be reduced的时候，就可以在这里进行修改。",[11,3016,3017],{},"另外还有一些官方的Foliage官方建议：",[2577,3019,3020,3023,3026],{},[2580,3021,3022],{},"如果要让Foliage使用风相关的特性的话，要避免对Foliage使用静态光照，因为阴影不会随风而动。",[2580,3024,3025],{},"减少光照贴图分辨率以减小贴图空间消耗",[2580,3027,3028],{},"在Grass上禁用静态光照，因为通常Grass是数目极大，会造成明显的构建消耗",{"title":70,"searchDepth":354,"depth":360,"links":3030},[3031,3043,3050],{"id":2435,"depth":354,"text":2435,"children":3032},[3033,3034,3035,3036,3037,3038,3039,3040,3041,3042],{"id":2438,"depth":360,"text":2439},{"id":2450,"depth":360,"text":2451},{"id":2473,"depth":360,"text":2474},{"id":2496,"depth":360,"text":2497},{"id":2520,"depth":360,"text":2520},{"id":2531,"depth":360,"text":2532},{"id":2547,"depth":360,"text":2548},{"id":2569,"depth":360,"text":2569},{"id":2611,"depth":360,"text":2612},{"id":2624,"depth":360,"text":2625},{"id":2636,"depth":354,"text":2636,"children":3044},[3045,3046,3047,3048,3049],{"id":2642,"depth":360,"text":2643},{"id":2679,"depth":360,"text":2680},{"id":2702,"depth":360,"text":2703},{"id":2720,"depth":360,"text":2720},{"id":2741,"depth":360,"text":2742},{"id":2772,"depth":354,"text":2772,"children":3051},[3052,3053,3054,3055,3056,3057,3058,3059,3060,3061],{"id":2793,"depth":360,"text":2794},{"id":2829,"depth":360,"text":2830},{"id":2844,"depth":360,"text":2845},{"id":2878,"depth":360,"text":2878},{"id":2920,"depth":360,"text":2921},{"id":2933,"depth":360,"text":2934},{"id":2946,"depth":360,"text":2947},{"id":2959,"depth":360,"text":2960},{"id":2975,"depth":360,"text":2975},{"id":2994,"depth":360,"text":2995},"2017-10-21",{"layout":1275,"status":1276,"published":385,"author":3064,"author_login":1279,"author_email":1280,"wordpress_id":3065,"wordpress_url":3066,"date_gmt":3067,"excerpt":3068},{"display_name":1278,"login":1279,"email":1280,"url":70},2031,"\u002F\u002F?p=2031","2017-10-21 05:49:23 +0000",{"type":8,"value":3069},[3070],[11,3071,2424],{},"\u002F2017-10-21-ue4-lighting-troubleshooting-note",{"title":2419,"description":2424},"_legacy\u002F2017\u002F2017-10-21-ue4-lighting-troubleshooting-note",[1292,1485],"o1IWVGPJuUDTqsLwdahOGRXsZxnTw16Zw2Dxk2LTmUU",85,1788763178256]