[{"data":1,"prerenderedAt":1176},["ShallowReactive",2],{"page-UE4-1":3,"page-count-UE4":1175},[4,638,765,970,1059],{"id":5,"title":6,"body":7,"date":618,"description":13,"extension":619,"meta":620,"navigation":623,"path":631,"seo":632,"stem":633,"tags":634,"__hash__":637},"blogs\u002F_legacy\u002F2021\u002F2021-11-16-note-blue-protocol-anime-render.md","CEDEC2021-《Blueprotocol》的渲染实现笔记",{"type":8,"value":9,"toc":592},"minimark",[10,14,25,28,32,35,38,41,48,51,56,59,62,193,196,199,202,207,210,220,223,228,231,243,248,251,256,261,264,267,272,275,278,281,339,342,346,349,352,360,363,368,371,374,377,385,390,393,396,399,404,407,410,413,418,420,424,427,430,433,438,441,446,449,452,455,469,472,475,480,483,486,489,492,495,498,501,504,507,510,513,516,520,523,526,531,535,538,541,544,549,552,557,561,564,569,579,582,585,588],[11,12,13],"p",{},"CEDEC2021上《BLUE PROTOCOL》的分享的渲染部分。",[11,15,16,17,24],{},"PPT：[",[18,19,23],"a",{"href":20,"rel":21},"https:\u002F\u002Fcedil.cesa.or.jp\u002Fcedil_sessions\u002Fview\u002F2389",[22],"nofollow","BLUE PROTOCOLにおけるアニメ表現技法について　～実装編～"," ]。",[11,26,27],{},"《BLUE PROTOCOL》虽然是用UE4进行实现的，但是由于他们的方法有对引擎进行改造，所以对于不希望进行引擎修改的情况可能不是很有参考作用。",[29,30,31],"h2",{"id":31},"简介",[11,33,34],{},"渲染实现上，是基于UE4的PBR渲染的。为了实现动画风格的渲染，在UE4的材质系统上，添加了一个新的ShaderModel，对BasePass和PostProcess进行了改造。",[11,36,37],{},"这样做的主要目的，一个是可以结合PBR的渲染效果，另一个是降低渲染的成本。",[11,39,40],{},"原始的PTT中有一定的篇幅在介绍UE4原本的渲染过程，由于没有现场的视频，不是很好推断想要表达的内容。总体而言，应当是对UE4原有的渲染过程进行一个总结，为之后介绍修改的部分进行铺垫。",[11,42,43],{},[44,45],"img",{"alt":46,"src":47},"","\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fue4-default-pass-300x156.png",[11,49,50],{},"实际的修改主要集中在对现有的Pass进行改造以及添加一些新的Pass，改造后的Pass流程：",[11,52,53],{},[44,54],{"alt":46,"src":55},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fimage-300x131.png",[11,57,58],{},"详细的修改部分在后面会有，总体而言感觉改动不大。主要修改如介绍说的，主要集中在BasePass上，所以其实单看BasePass的话，修改还是挺大的。",[11,60,61],{},"GBuffer的使用情况：",[63,64,65,87],"table",{},[66,67,68],"thead",{},[69,70,71,75,78,81,84],"tr",{},[72,73,74],"th",{},"#",[72,76,77],{},"Red",[72,79,80],{},"Green",[72,82,83],{},"Blue",[72,85,86],{},"Alpha",[88,89,90,108,125,142,159,176],"tbody",{},[69,91,92,96,99,102,105],{},[93,94,95],"td",{},"GBufferA",[93,97,98],{},"ComputeNomal.x",[93,100,101],{},"ComputeNomal.y",[93,103,104],{},"ComputeNomal.z",[93,106,107],{},"PerObjectGBufferData",[69,109,110,113,116,119,122],{},[93,111,112],{},"GBufferB",[93,114,115],{},"ToonHairMaskOffsetShadowMask",[93,117,118],{},"SpecularMask",[93,120,121],{},"SpecularValue",[93,123,124],{},"ShadingModelIDSelectiveOutputMask",[69,126,127,130,133,136,139],{},[93,128,129],{},"GBufferC",[93,131,132],{},"BaseColor.r",[93,134,135],{},"BaseColor.g",[93,137,138],{},"BaseColor.b",[93,140,141],{},"AmbientOcclusion",[69,143,144,147,150,153,156],{},[93,145,146],{},"GBufferD",[93,148,149],{},"ShadowColor.r",[93,151,152],{},"ShadowColor.g",[93,154,155],{},"ShadowColor.b",[93,157,158],{},"NdotL",[69,160,161,164,167,170,173],{},[93,162,163],{},"GBufferE",[93,165,166],{},"-",[93,168,169],{},"RimLightMask",[93,171,172],{},"DiffuseOffset",[93,174,175],{},"RimLightWidth",[69,177,178,181,184,187,190],{},[93,179,180],{},"GBufferF",[93,182,183],{},"OutlineWidth",[93,185,186],{},"OutlineID",[93,188,189],{},"OutlinePaint",[93,191,192],{},"OutlineZShift",[29,194,195],{"id":195},"光照",[11,197,198],{},"光照使用的Mesh资源总共有两组法线以及顶点颜色数据，两组法线分别是在模型制作工具中预先处理的用于动画风格阴影的法线，以及在导入时自动计算的法线。",[11,200,201],{},"动画风格阴影的法线(Import Normal)没有介绍具体采用的方案，从效果图上看应该采用的是比较通用的处理方式。",[11,203,204],{},[44,205],{"alt":46,"src":206},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fimport-normal-300x240.png",[11,208,209],{},"导入计算的法线(Compute Normal)，有描述计算方式：",[211,212,213,217],"ol",{},[214,215,216],"li",{},"计算三角面的法线",[214,218,219],{},"每个顶点的法线是相邻三个面的平均",[11,221,222],{},"应该用的就是UE4默认的那种方式，没有考究过。导入计算方面，对于脸部这种分成了好几个部件的情况，会统一导入，防止在边缘出现计算问题。",[11,224,225],{},[44,226],{"alt":46,"src":227},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fcompute-normal-merge-300x245.png",[11,229,230],{},"顶点颜色方面，使用情况如下：",[232,233,234,237,240],"blockquote",{},[11,235,236],{},"Red: Diffuse Offset",[11,238,239],{},"Blue: Ambient Occlusion",[11,241,242],{},"Green: DepthShift(轮廓线用)",[244,245,247],"h3",{"id":246},"basepass","BasePass",[11,249,250],{},"主要计算用于后面光照计算的必要数据：",[11,252,253],{},[44,254],{"alt":46,"src":255},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fbase-pass-300x124.png",[257,258,260],"h4",{"id":259},"basecolor与shadowcolor","BaseColor与ShadowColor",[11,262,263],{},"BaseColor为明亮处的颜色，ShadowColor为暗处的颜色。",[11,265,266],{},"ShadowColor基于BaseColor进行计算：",[11,268,269],{},[44,270],{"alt":46,"src":271},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fshadow-color-300x156.png",[11,273,274],{},"针对头发的颜色，BaseColor和ShadowColor都是直接在材质中指定颜色。如果头发是两种颜色的情况，会采用梯度图来进行过渡。头发的BaseColor和ShadowColor可以不依赖上面的规则进行设定。",[257,276,158],{"id":277},"ndotl",[11,279,280],{},"基于光源方向和ImportNormal进行计算：",[282,283,287],"pre",{"className":284,"code":285,"language":286,"meta":46,"style":46},"language-cpp shiki shiki-themes github-light-high-contrast github-dark monokai","HalfNoL = dot(Normal, Light) * 0.5 + 0.5;\nToonNoL = step(Threshold, HalfNoL);\n","cpp",[288,289,290,325],"code",{"__ignoreMap":46},[291,292,295,299,303,307,310,313,317,320,322],"span",{"class":293,"line":294},"line",1,[291,296,298],{"class":297},"s0idv","HalfNoL ",[291,300,302],{"class":301},"sLXdl","=",[291,304,306],{"class":305},"scAT4"," dot",[291,308,309],{"class":297},"(Normal, Light) ",[291,311,312],{"class":301},"*",[291,314,316],{"class":315},"stP0Q"," 0.5",[291,318,319],{"class":301}," +",[291,321,316],{"class":315},[291,323,324],{"class":297},";\n",[291,326,328,331,333,336],{"class":293,"line":327},2,[291,329,330],{"class":297},"ToonNoL ",[291,332,302],{"class":301},[291,334,335],{"class":305}," step",[291,337,338],{"class":297},"(Threshold, HalfNoL);\n",[11,340,341],{},"PPT中解释了为什么要在BasePass中进行NdotL，除了精度上的考虑之外，主要是对要将什么搭载到GBuffer上的设计考量。ImportNormal只会在这个步骤中用，在这里计算完之后就不需要在GBuffer中传递ImportNormal了。",[257,343,345],{"id":344},"specular","Specular",[11,347,348],{},"主要是材质中直接传过来的SpecularValue，以及计算SpecularMask。",[11,350,351],{},"SpecularMask用于表达主光源的高光，基于Blinn Phong反射模型进行：",[282,353,358],{"className":354,"code":356,"language":357},[355],"language-text","H = normalize(Light + View);\nNoH = dot(Normal,\nToonNoH = step(Threshold, NoH);\n","text",[288,359,356],{"__ignoreMap":46},[11,361,362],{},"头发的高光有另外的处理逻辑：",[11,364,365],{},[44,366],{"alt":46,"src":367},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fhair-specular-mask-300x134.png",[11,369,370],{},"似乎会根据头发本身与重心的距离以及其ID，与光源进行配合动态的缩放。",[244,372,373],{"id":373},"方向光",[11,375,376],{},"方向光的计算主要分为两个过程：",[211,378,379,382],{},[214,380,381],{},"使用NdotL(GBufferD.Alpha)来处理BaseColor和ShadowColor(ShadowColor作为Subsurface处理)",[214,383,384],{},"通过BaseColor的亮度结合SpecularValue以及SpecularMask计算高光",[11,386,387],{},[44,388],{"alt":46,"src":389},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fdirectional-light-300x164.png",[11,391,392],{},"使用的就是上面BasePass过程中生成的5张结果。",[244,394,395],{"id":395},"点光源",[11,397,398],{},"无视法线，直接按照与光源的距离来进行计算。",[11,400,401],{},[44,402],{"alt":46,"src":403},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fpoint-light-300x183.png",[11,405,406],{},"同时为Toon添加专用的参数来进行效果调节。",[244,408,409],{"id":409},"天光和间接光",[11,411,412],{},"将法线视为向上(0,0,1)来处理。",[11,414,415],{},[44,416],{"alt":46,"src":417},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fskylight-300x182.png",[11,419,406],{},[244,421,423],{"id":422},"rimlight","RimLight",[11,425,426],{},"在光照后添加了一个新的PostRimLight的Pass来处理RimLight计算。",[11,428,429],{},"计算使用RimLightWidth(GBufferE.Alpha)和RimLightMask(GBufferE.Green)来进行。",[11,431,432],{},"通过深度信息作Sobel filter计算",[11,434,435],{},[44,436],{"alt":46,"src":437},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Frim-light-depth-300x134.png",[11,439,440],{},"再通过RimLightMask屏蔽掉不希望的部分之后，叠加到Specular上。",[11,442,443],{},[44,444],{"alt":46,"src":445},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Frim-light-mask-300x127.png",[29,447,448],{"id":448},"轮廓线",[11,450,451],{},"轮廓线的绘制在PostOutlinePass中，这个Pass放在了光照和半透明计算之间，确保轮廓线能在水中等情况下受到折射的影响，同时保证半透明物体上不绘制轮廓线。",[11,453,454],{},"轮廓线主要有以下的计算来源：",[211,456,457,460,463,466],{},[214,458,459],{},"基于深度： sobel filter，比较深度并只在深度更大的地方绘制，通过depthshift限制一些地方(脸上)的绘制",[214,461,462],{},"基于ID：sobel filter，在值较大的上绘制",[214,464,465],{},"基于法线： 内积，主要针对深度差不多，ID也分不开的地方",[214,467,468],{},"预先绘制的：从G-Buffer中取出",[11,470,471],{},"使用到的资源包括之前的顶点颜色G通道中的DepthShift，以及额外的通过TexCoord1来获取的第二组顶点颜色。另外，有一张预先绘制的轮廓线贴图。",[11,473,474],{},"绘制过程主要使用GBufferF：",[11,476,477],{},[44,478],{"alt":46,"src":479},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fline-300x129.png",[11,481,482],{},"Red: 从TexCoord1.x中获取轮廓线粗细，并根据摄像距离和分辨率进行调整",[11,484,485],{},"Green: 从TexCoord1.y中获取轮廓线ID",[11,487,488],{},"Blue: 从预先绘制的贴图来绘制，同时使用轮廓模型防止绘制溢出",[11,490,491],{},"Alpha: 从VertexColor.g获取DepthShift，在基于深度计算时使用，绘制时基于参数进行反相",[244,493,494],{"id":494},"眉轮廓线",[11,496,497],{},"动画风格中常见的将眉毛绘制到前发之上的手法。",[11,499,500],{},"绘制上，将除头发以外的部分在BasePass中绘制，而头发单独在上面图中的ToonHairPass绘制。",[11,502,503],{},"眉毛的罗廓线由基于ID的方法生成，首先，在BasePass中生成一个眉毛本身的Mask，之后头发绘制时在更新轮廓用的ID的时候，在这个Mask上的部分不再更新。由此，在进行轮廓线生成的时候，就能够通过ID来产生眉毛的轮廓了。",[244,505,506],{"id":506},"抗锯齿",[11,508,509],{},"用上面的方法绘制的轮廓线会由于TAA导致模糊。",[11,511,512],{},"这里通过利用ResponsiveAA来解决，将轮廓线绘制到stencil来减轻轮廓线被TAA模糊的情况。",[29,514,515],{"id":515},"阴影",[244,517,519],{"id":518},"shadowmap","ShadowMap",[11,521,522],{},"对于本身的ShadowMap的逻辑改造，主要目的是移除Toon材质上的自阴影。",[11,524,525],{},"处理方式上，根据是否是Toon材质将ShadowMap分开处理，这样就可以在移除自阴影的同时接受背景物体的阴影。",[11,527,528],{},[44,529],{"alt":46,"src":530},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002FShadowMaps-284x300.png",[244,532,534],{"id":533},"offsetshadow","OffsetShadow",[11,536,537],{},"为了补偿由于没有自阴影导致的图像信息不足，添加了一个在屏幕空间进行偏移的阴影。",[11,539,540],{},"处理这个的是最上面图中的OffsetShadowDepth和OffsetShadowMerge。",[11,542,543],{},"过程的原理描述起来其实很简单，首先，在OffsetShadowDepth中，根据深度信息产生一个向下的偏移。",[11,545,546],{},[44,547],{"alt":46,"src":548},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002FOffset-Shadow-Depth--300x169.png",[11,550,551],{},"然后在OffsetShadowMerge中，根据深度对比将信息描绘到G-Buffer中的NdotL中去(GBufferD.Alpha)。",[11,553,554],{},[44,555],{"alt":46,"src":556},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002FOffset-Shadow-Merge-300x149.png",[29,558,560],{"id":559},"postprocess","PostProcess",[11,562,563],{},"后处理的方面比较简单，使用了柔光滤镜。在动画风格中经常使用到，虽然Bloom也能达到类似的效果，但还是另外的进行了实现。",[11,565,566],{},[44,567],{"alt":46,"src":568},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fpost-process-300x154.png",[232,570,571],{},[11,572,573,574,578],{},"柔光滤镜，应该和[",[18,575,577],{"href":576},"\u002F2021-01-24-ue4-fest-npr-note\u002F#i-10","这个","]是一样的。",[29,580,581],{"id":581},"总结",[11,583,584],{},"虽然改造引擎的方法可能不是很通用，但是其中的一些思路还是比较有参考作用的。对于动画风格渲染，需要注意的部分有很多共通的地方，不过由于UE4的渲染流程是写死的，所以不会像U3D之类的那样灵活。",[11,586,587],{},"PPT后面还有一些关于Actor的动态Instance相关的优化，由于不是很关注这块，所以没有详细的看过，这里就不记录了。",[589,590,591],"style",{},"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 .stP0Q, html code.shiki .stP0Q{--shiki-default:#023B95;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}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);}",{"title":46,"searchDepth":327,"depth":593,"links":594},3,[595,596,608,612,616,617],{"id":31,"depth":327,"text":31},{"id":195,"depth":327,"text":195,"children":597},[598,604,605,606,607],{"id":246,"depth":593,"text":247,"children":599},[600,602,603],{"id":259,"depth":601,"text":260},4,{"id":277,"depth":601,"text":158},{"id":344,"depth":601,"text":345},{"id":373,"depth":593,"text":373},{"id":395,"depth":593,"text":395},{"id":409,"depth":593,"text":409},{"id":422,"depth":593,"text":423},{"id":448,"depth":327,"text":448,"children":609},[610,611],{"id":494,"depth":593,"text":494},{"id":506,"depth":593,"text":506},{"id":515,"depth":327,"text":515,"children":613},[614,615],{"id":518,"depth":593,"text":519},{"id":533,"depth":593,"text":534},{"id":559,"depth":327,"text":560},{"id":581,"depth":327,"text":581},"2021-11-16","md",{"layout":621,"status":622,"published":623,"wordpress_id":624,"wordpress_url":625,"date_gmt":626,"excerpt":627},"post","publish",true,3557,"\u002F?p=3557","2021-11-15 17:18:59 +0000",{"type":8,"value":628},[629],[11,630,13],{},"\u002F2021-11-16-note-blue-protocol-anime-render",{"title":6,"description":13},"_legacy\u002F2021\u002F2021-11-16-note-blue-protocol-anime-render",[635,636],"UE4","CEDEC","k3Wf7wAjkskIvTzdrhF42jFwPhW8WAtbJHzIkxt3V-4",{"id":639,"title":640,"body":641,"date":747,"description":645,"extension":619,"meta":748,"navigation":623,"path":760,"seo":761,"stem":762,"tags":763,"__hash__":764},"blogs\u002F_legacy\u002F2021\u002F2021-05-30-ue4-add-new-asset-type.md","UE4新增Asset类型",{"type":8,"value":642,"toc":742},[643,646,649,652,656,659,662,668,671,674,680,683,689,692,698,702,705,716,722,725,731,734,736,739],[11,644,645],{},"想要在编辑器里面添加新的Asset类型，算是比较常见又不是经常使用的需求。",[11,647,648],{},"当前UE4版本为4.26.2。",[11,650,651],{},"虽然本身并不复杂，但是每次添加的时候如果项目中没有相关的代码，就又要去查一次。不然总是会在一些奇怪的地方卡住导致在编辑器中无法使用的情况。因此在这里记录一下，方便下次查找。",[29,653,655],{"id":654},"ufactory继承","UFactory继承",[11,657,658],{},"首先需要继承UFactory，直接用UE4添加类，或者自己写即可。",[11,660,661],{},"覆盖掉下面的函数：",[282,663,666],{"className":664,"code":665,"language":357},[355],"virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;\nvirtual bool ShouldShowInNewMenu() const override;\n",[288,667,665],{"__ignoreMap":46},[11,669,670],{},"ShouldShowInNewMenu直接返回True则可用在编辑器的右键新建的菜单中出现。",[11,672,673],{},"FactoryCreateNew则直接新建一个对应类型的Object即可：",[282,675,678],{"className":676,"code":677,"language":357},[355],"return NewObject\u003CUMyObjectType>(InParent, InClass, InName, Flags);\n",[288,679,677],{"__ignoreMap":46},[11,681,682],{},"同时需要在构造函数中初始化一些设置",[282,684,687],{"className":685,"code":686,"language":357},[355],"\nSupportedClass = UMyObjectType::StaticClass();\nbCreateNew = true;\nbEditAfterNew = true;\n\n",[288,688,686],{"__ignoreMap":46},[11,690,691],{},"如果遇到了一些奇怪的链接问题，可以先检查下是否在模块的build.cs中包含了UnrealEd。",[282,693,696],{"className":694,"code":695,"language":357},[355],"PrivateDependencyModuleNames.AddRange(\n  new string[]\n  {\n    \"CoreUObject\",\n    \"Engine\",\n    \"UnrealEd\",\n  }\n);\n\n",[288,697,695],{"__ignoreMap":46},[29,699,701],{"id":700},"assettypeactions","AssetTypeActions",[11,703,704],{},"之前的版本有上面的操作就可以了，但是目前4.26的版本需要额外的操作才能在编辑器中看到新建按钮。",[11,706,707,708,711,712,715],{},"定义一个新的类继承自",[288,709,710],{},"FAssetTypeActions_Base","，一个比较好的参考是",[288,713,714],{},"FAssetTypeActions_DataAsset","。",[282,717,720],{"className":718,"code":719,"language":357},[355],"class FAssetTypeActions_DataAsset : public FAssetTypeActions_Base\n{\npublic:\n  \u002F\u002F IAssetTypeActions Implementation\n  virtual FText GetName() const override { return NSLOCTEXT(\"AssetTypeActions\", \"AssetTypeActions_DataAsset\", \"Data Asset\"); }\n  virtual FColor GetTypeColor() const override { return FColor(201, 29, 85); }\n  virtual UClass* GetSupportedClass() const override { return UDataAsset::StaticClass(); }\n  virtual uint32 GetCategories() override { return EAssetTypeCategories::Misc; }\n};\n",[288,721,719],{"__ignoreMap":46},[11,723,724],{},"无需额外的定义，之后在Module的Start的地方进行注册即可",[282,726,729],{"className":727,"code":728,"language":357},[355],"void FAwesomeEdModule::StartupModule()\n{\n    IAssetTools& AssetTools = FModuleManager::LoadModuleChecked\u003CFAssetToolsModule>(\"AssetTools\").Get();\n    AssetTools.RegisterAssetTypeActions(MakeShareable(new FAwesomeAssetAction));\n}\n",[288,730,728],{"__ignoreMap":46},[11,732,733],{},"其中FAwesomeAssetAction就是新定义的AssetTypeAction。",[29,735,581],{"id":581},[11,737,738],{},"由于这种功能一般加一次就会不管了，下一次在别的地方要用到的时候才发现API已经变了。由于本身操作不是很熟悉，很难把握是自己操作有误还是其他因素引起的。",[11,740,741],{},"总之这边记录的是当前版本的可行操作，之后的版本不行的话就可以判断可能是API有改变。",{"title":46,"searchDepth":327,"depth":593,"links":743},[744,745,746],{"id":654,"depth":327,"text":655},{"id":700,"depth":327,"text":701},{"id":581,"depth":327,"text":581},"2021-05-30",{"layout":621,"status":622,"published":623,"author":749,"author_login":750,"author_email":751,"author_url":752,"wordpress_id":753,"wordpress_url":754,"date_gmt":755,"excerpt":756},{"display_name":750,"login":750,"email":751,"url":752},"chaoshikari","chaoshikari@gmail.com","\u002F",3508,"\u002F?p=3508","2021-05-30 13:15:20 +0000",{"type":8,"value":757},[758],[11,759,645],{},"\u002F2021-05-30-ue4-add-new-asset-type",{"title":640,"description":645},"_legacy\u002F2021\u002F2021-05-30-ue4-add-new-asset-type",[635],"LHeIYSAW7BjCYwrB_XV063tftDjPQLdpuY96Lu-v7xc",{"id":766,"title":767,"body":768,"date":952,"description":772,"extension":619,"meta":953,"navigation":623,"path":965,"seo":966,"stem":967,"tags":968,"__hash__":969},"blogs\u002F_legacy\u002F2021\u002F2021-01-24-ue4-level-editor-notes.md","UE4关卡编辑问题记录",{"type":8,"value":769,"toc":940},[770,773,776,780,783,786,789,795,798,804,807,810,816,819,825,828,834,837,843,846,849,855,858,861,864,868,871,874,877,880,883,894,898,901,904,908,911,916,919,925,928,934,937],[11,771,772],{},"最近自己摆弄关卡遇到的一些问题的记录。",[11,774,775],{},"由于近段时间基本一直在作逻辑开发，所以很少自己碰编辑器，发现一些常见的问题也不知道该如何应对。于是统一的记录了起来，方便下次查找原因。",[29,777,779],{"id":778},"texture-streaming-over-budget","Texture streaming over budget",[11,781,782],{},"结论写在前面，基本原因是默认配置太小，调大就可以了。",[11,784,785],{},"主要是直接输出一个红色的错误日志在顶部，让人担心自己是不是做错了什么。",[11,787,788],{},"首先，找到日志输出的地方：",[282,790,793],{"className":791,"code":792,"language":357},[355],"SmallTextItem.SetColor(FLinearColor::Red); \nSmallTextItem.Text = FText::Format(LOCTEXT(\"TEXTURE_STREAMING_POOL_OVER_BUDGET_FMT\", \"TEXTURE STREAMING POOL OVER {0} BUDGET\"), FText::AsMemory(MemOver)); \nCanvas->DrawItem(SmallTextItem, FVector2D(MessageX, MessageY));\n",[288,794,792],{"__ignoreMap":46},[11,796,797],{},"这个东西的计算在：",[282,799,802],{"className":800,"code":801,"language":357},[355],"FRenderAssetStreamingMipCalcTask::UpdateStats_Async\n",[288,803,801],{"__ignoreMap":46},[11,805,806],{},"中，主要有两个来源：",[244,808,809],{"id":809},"内存池超过限制",[282,811,814],{"className":812,"code":813,"language":357},[355],"Stats.OverBudget += FMath::Max\u003Cint64>(Stats.RequiredPool - Stats.StreamingPool, 0);\n",[288,815,813],{"__ignoreMap":46},[11,817,818],{},"用实际消耗的内存大小减去MemoryBudget，RequairedPool就是每一个FStreamingRenderAsset的消耗加起来",[282,820,823],{"className":821,"code":822,"language":357},[355],"Stats.RequiredPool += RequiredSize;\n",[288,824,822],{"__ignoreMap":46},[244,826,827],{"id":827},"单个物体超过限制",[282,829,832],{"className":830,"code":831,"language":357},[355],"\u002F\u002F All persistent mip bias bigger than the expected is considered overbudget. \nconst int32 OverBudgetBias = FMath::Max\u003Cint32>(0, StreamingRenderAsset.BudgetMipBias - Settings.GlobalMipBias); \nStats.OverBudget += StreamingRenderAsset.GetSize(StreamingRenderAsset.MaxAllowedMips + OverBudgetBias) - MaxSize;\n",[288,833,831],{"__ignoreMap":46},[11,835,836],{},"这里累计进去的就是OverBudgetBias，因为前面",[282,838,841],{"className":839,"code":840,"language":357},[355],"const int64 MaxSize = StreamingRenderAsset.GetSize(StreamingRenderAsset.MaxAllowedMips);\n",[288,842,840],{"__ignoreMap":46},[244,844,845],{"id":845},"实际分析",[11,847,848],{},"这样的话，直接stat streaming，看结果",[11,850,851],{},[44,852],{"alt":853,"src":854},"image-20201227191346947","\u002Fwp-content\u002Fuploads\u002F2021\u002F01\u002Fimage-20201227191346947_thumb.png",[11,856,857],{},"这里就是配置太小了，不知为什么默认只有76M，这样随便上几个大点的pbr贴图就超过了。通过指令上调配置就好。",[11,859,860],{},"r.Streaming.PoolSize 2000",[11,862,863],{},"也就是说，实际上是默认配置太小导致的，不是哪里操作有问题。",[29,865,867],{"id":866},"instancemesh","InstanceMesh",[11,869,870],{},"目前共有UInstancedStaticMeshComponent和UHierarchicalInstancedStaticMeshComponent两个组件，但是他们有什么区别呢？一旦开始纠结就不知道应该选哪一个了。",[11,872,873],{},"稍微查了下资料，主要的区别是。UHierarchicalInstancedStaticMeshComponent是会按instance更新LOD的，而UInstancedStaticMeshComponent则全部统一使用同一个LOD。",[244,875,876],{"id":876},"本身的限制",[11,878,879],{},"由于目前的版本会自动合批，不是很确定UInstancedStaticMeshComponent还有没有必要性。不过至少会减少Actor的数量，也方便使用Cluster来减少GC负担。",[11,881,882],{},"InstanceMesh的限制：",[211,884,885,888,891],{},[214,886,887],{},"无法使用负数的Scale",[214,889,890],{},"无法分别指定材质(per instance random可用于一些效果)",[214,892,893],{},"碰撞不会在运行时更新(这个现在应该没有了)",[244,895,897],{"id":896},"crash","Crash",[11,899,900],{},"实际使用instancemesh，发现在编辑器里面直接ctrl+w会崩溃，主要原因是instance的生成直接放到构造函数里面了。",[11,902,903],{},"正确的做法是，将生成放到OnConstruction里面，还能在改变属性时自动更新。",[29,905,907],{"id":906},"bound","Bound",[11,909,910],{},"在愉快的拖动关卡的时候，突然出现了这样的报错：",[232,912,913],{},[11,914,915],{},"The actor will be placed outside the bounds of the current level. Continue?",[11,917,918],{},"于是找了下输出来源，这个Bound似乎是实时计算的",[282,920,923],{"className":921,"code":922,"language":357},[355],"FBox CurrentLevelBounds(ForceInit);\nif (InLevel-&gt;LevelBoundsActor.IsValid())\n{\n    CurrentLevelBounds = InLevel-&gt;LevelBoundsActor.Get()-&gt;GetComponentsBoundingBox();\n}\nelse\n{\n    CurrentLevelBounds = ALevelBounds::CalculateLevelBounds(InLevel);\n}\n",[288,924,922],{"__ignoreMap":46},[11,926,927],{},"在有LevelBoundsActor的情况下，会使用指定的Bound",[282,929,932],{"className":930,"code":931,"language":357},[355],"\u002F**\n *\n * Defines level bounds\n * Updates bounding box automatically based on actors transformation changes or holds fixed user defined bounding box\n * Uses only actors where AActor::IsLevelBoundsRelevant() == true\n *\u002F\n",[288,933,931],{"__ignoreMap":46},[11,935,936],{},"也就是说，没有自己指定边界Actor为自定义的话，其实没什么太大的意义。",[11,938,939],{},"可以在编辑器设置中关闭bPromptWhenAddingToLevelOutsideBounds。",{"title":46,"searchDepth":327,"depth":593,"links":941},[942,947,951],{"id":778,"depth":327,"text":779,"children":943},[944,945,946],{"id":809,"depth":593,"text":809},{"id":827,"depth":593,"text":827},{"id":845,"depth":593,"text":845},{"id":866,"depth":327,"text":867,"children":948},[949,950],{"id":876,"depth":593,"text":876},{"id":896,"depth":593,"text":897},{"id":906,"depth":327,"text":907},"2021-01-24",{"layout":621,"status":622,"published":623,"author":954,"author_login":956,"author_email":957,"wordpress_id":958,"wordpress_url":959,"date_gmt":960,"excerpt":961},{"display_name":955,"login":956,"email":957,"url":46},"风铃","flinkor","flinkor@foxmail.com",3226,"\u002F?p=3226","2021-01-24 03:19:43 +0000",{"type":8,"value":962},[963],[11,964,772],{},"\u002F2021-01-24-ue4-level-editor-notes",{"title":767,"description":772},"_legacy\u002F2021\u002F2021-01-24-ue4-level-editor-notes",[635],"aC2fbb4Y1BSs1AdibFkWM6TlG677lO2kh_AymzB3kJo",{"id":971,"title":972,"body":973,"date":1043,"description":977,"extension":619,"meta":1044,"navigation":623,"path":1053,"seo":1054,"stem":1055,"tags":1056,"__hash__":1058},"blogs\u002F_legacy\u002F2020\u002F2020-06-13-ue4-crash-log-on-direct12-rhi.md","UE4在DX12 RHI上崩溃两例记录",{"type":8,"value":974,"toc":1039},[975,978,981,984,987,990,993,999,1002,1008,1012,1015,1021,1030,1036],[11,976,977],{},"主要是发生在global shader的自定义上，如果你没有使用的话估计原因不太一样~",[11,979,980],{},"当前使用UE版本为UE4.25.1。",[11,982,983],{},"原本在理解上感觉UE4的RHI封装应当是一致的，但实际情况还是会有些不同。",[11,985,986],{},"结论上看，在Direct12的RHI上有一些更加严格的限制，如果不注意的话就会出现问题。",[29,988,989],{"id":989},"参数顺序",[11,991,992],{},"Dx12的RHI要求Vertex Shader和Pixel Shader的参数顺序是一致的，也就是说OutPut的次序就是Input的次序，否则就会出现以下错误",[282,994,997],{"className":995,"code":996,"language":357},[355],"Fatal error: [File:D:\u002FBuild\u002F++UE4+Licensee\u002FSync\u002FEngine\u002FSource\u002FRuntime\u002FD3D12RHI\u002FPrivate\u002FD3D12Util.cpp] [Line: 581] hr failed at D:\u002FBuild\u002F++UE4+Licensee\u002FSync\u002FEngine\u002FSource\u002FRuntime\u002FD3D12RHI\u002FPrivate\u002FWindows\u002FWindowsD3D12PipelineState.cpp:697 with error E_INVALIDARG\nUE4Editor.exe has triggered a breakpoint.\n",[288,998,996],{"__ignoreMap":46},[11,1000,1001],{},"调整参数次序后不再出现。",[282,1003,1006],{"className":1004,"code":1005,"language":357},[355],"void MainVS(\n    in uint GlobalVertexId : SV_VertexID,\n-    out float4 OutPosition : SV_POSITION,\n-    out float2 OutUV : TEXCOORD0   \n+    out float2 OutUV : TEXCOORD0,\n+    out float4 OutPosition : SV_POSITION\n    )\n{\n    OutPosition = float4(kSpriteVertices[GlobalVertexId], 0, 1);\n    OutUV = kQuadUVs[GlobalVertexId];\n}\n\n\n\nvoid MainPS(\n    in noperspective float2 IntUV : TEXCOORD0,\n    in float4 SvPosition : SV_POSITION,\n    out float4 OutColor : SV_Target0\n    )\n{\n    OutColor = float4(1,IntUV.r,0,1);\n}\n",[288,1007,1005],{"__ignoreMap":46},[29,1009,1011],{"id":1010},"setgraphicspipelinestate","SetGraphicsPipelineState",[11,1013,1014],{},"这个错误应当与初始化次序有关，主要是Vertex Shader等的参数必须在SetGraphicsPipelineState之后调用，否则就会出现一下错误",[282,1016,1019],{"className":1017,"code":1018,"language":357},[355],"Ensure condition failed: CachedShader == VertexShader [File:D:\u002FBuild\u002F++UE4+Licensee\u002FSync\u002FEngine\u002FSource\u002FRuntime\u002FD3D12RHI\u002FPrivate\u002FD3D12Commands.cpp] [Line: 38] Parameters are being set for a VertexShader which is not currently bound\nUE4Editor.exe has triggered a breakpoint.\n",[288,1020,1018],{"__ignoreMap":46},[11,1022,1023,1024,1029],{},"注意调整下次序就可以了，类似这样的[",[18,1025,1028],{"href":1026,"rel":1027},"https:\u002F\u002Fgithub.com\u002FArisego\u002FUnrealLive2D\u002Fcommit\u002Fab8f25a34a2f9c491014ba33474eadd06c9a288a",[22],"改动","]就可以了。",[282,1031,1034],{"className":1032,"code":1033,"language":357},[355],"        GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();\n        GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();\n\n+       SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);\n\n\n        VertexShader->SetParameters(RHICmdList, VertexShader.GetVertexShader(), ts_BaseColor, tsr_TextureRHI);\n        PixelShader->SetParameters(RHICmdList, PixelShader.GetPixelShader(), ts_BaseColor, tsr_TextureRHI);\n\n\n        \u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\n        \u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\n-       SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);\n\n\n\n        RHICmdList.SetStreamSource(0, ScratchVertexBufferRHI, 0);\n",[288,1035,1033],{"__ignoreMap":46},[11,1037,1038],{},"大的逻辑上并没有什么问题，应该也不会出现RHI封装语义不一致的问题。都是一些细节上的约定没搞明白造成的。",{"title":46,"searchDepth":327,"depth":593,"links":1040},[1041,1042],{"id":989,"depth":327,"text":989},{"id":1010,"depth":327,"text":1011},"2020-06-13",{"layout":621,"status":622,"published":623,"author":1045,"author_login":956,"author_email":957,"wordpress_id":1046,"wordpress_url":1047,"date_gmt":1048,"excerpt":1049},{"display_name":955,"login":956,"email":957,"url":46},2917,"\u002F?p=2917","2020-06-13 03:22:38 +0000",{"type":8,"value":1050},[1051],[11,1052,977],{},"\u002F2020-06-13-ue4-crash-log-on-direct12-rhi",{"title":972,"description":977},"_legacy\u002F2020\u002F2020-06-13-ue4-crash-log-on-direct12-rhi",[635,1057],"Direct12","3fhLcl2u7jyfS6j562ueYgKWvVKuAmOkN-Ws-IHxJxY",{"id":1060,"title":1061,"body":1062,"date":1160,"description":1066,"extension":619,"meta":1161,"navigation":623,"path":1170,"seo":1171,"stem":1172,"tags":1173,"__hash__":1174},"blogs\u002F_legacy\u002F2020\u002F2020-06-01-ue4-editor-helper-dev-note.md","UE4编辑器帮助开发记录",{"type":8,"value":1063,"toc":1153},[1064,1067,1070,1073,1076,1081,1084,1090,1093,1099,1103,1106,1110,1113,1116,1119,1122,1125,1128,1131,1134,1138,1141,1147,1150],[11,1065,1066],{},"UE4的编辑器帮助类对于开发一些小功能加快开发速度挺有用的。",[11,1068,1069],{},"当前使用的UE4版本为4.25。",[29,1071,1072],{"id":1072},"命令行支持",[11,1074,1075],{},"要支持命令行，不仅仅需要继承UCommandlet。还要注意类的命名，否则可能会无法正确的执行：",[232,1077,1078],{},[11,1079,1080],{},"LogInit: Error: xxxxx looked like a commandlet, but we could not find the class.",[11,1082,1083],{},"主要是因为命名必须采用xxxxxxCommandlet的形式，因为在FEngineLoop::PreInitPreStartupScreen中作了特殊处理",[282,1085,1088],{"className":1086,"code":1087,"language":357},[355],"if (Token.StartsWith(TEXT(\"run=\")))\n{\n    Token.RightChopInline(4, false);\n    if (!Token.EndsWith(TEXT(\"Commandlet\")))\n    {\n        Token += TEXT(\"Commandlet\");\n    }\n}\n",[288,1089,1087],{"__ignoreMap":46},[11,1091,1092],{},"对于传入的参数可以使用帮助函数进行解析",[282,1094,1097],{"className":1095,"code":1096,"language":357},[355],"TArray\u003CFString> Tokens;\nTArray\u003CFString> Switches;\nTMap\u003CFString, FString> ParamVals;\n\nconst TCHAR* Parms = *Params;\nUCommandlet::ParseCommandLine(Parms, Tokens, Switches, ParamVals);\n",[288,1098,1096],{"__ignoreMap":46},[29,1100,1102],{"id":1101},"console相关","Console相关",[11,1104,1105],{},"console对于编辑器内调试还是挺有帮助的，尤其是console var，简直调试利器，尤其是你的项目编译一次要十几分钟的时候……",[244,1107,1109],{"id":1108},"console帮助","Console帮助",[11,1111,1112],{},"这个记得以前在社区有看到生成的方法，但是这次在网上搜了很久才找到方法。",[11,1114,1115],{},"其实很简单，在console里面输入help，就会自动在你项目目录里面生成几乎所有的console的说明文档了。还自带搜索过滤功能……",[11,1117,1118],{},"不过这个生成的其实不全，这次刚好用到的几个没提到的有：",[11,1120,1121],{},"net SOCKETS 输出所有打开的UChannel",[11,1123,1124],{},"net DUMPSERVERRPC 输出所有的ServerRpc",[11,1126,1127],{},"net NETFLOOD 发送 NMT_Netspeed 256次",[11,1129,1130],{},"net NETDEBUGTEXT 使用DebugText发送数据，参考NotifyControlMessage",[11,1132,1133],{},"net NETDISCONNECT 触发连接断开",[244,1135,1137],{"id":1136},"添加console支持","添加Console支持",[11,1139,1140],{},"虽然现在编辑器有Python支持了，但是自定义的函数注册Console会比python轻快些，尤其只是一些不成体系的小帮助函数。",[282,1142,1145],{"className":1143,"code":1144,"language":357},[355],"static void _Import_Curve(const TArray\u003C FString >& InArgs);\n\nstatic FAutoConsoleCommand ImportXxxToCurve(\n    TEXT(\"aio.curvim\"),\n    TEXT(\"Import xxx to target, argument [curve asset ref] \"),\n    FConsoleCommandWithArgsDelegate::CreateStatic(&_Import_Curve),\n    ECVF_Cheat);\n",[288,1146,1144],{"__ignoreMap":46},[11,1148,1149],{},"传入的数组就是入参表。同类的还有FAutoConsoleCommandWithWorld和FAutoConsoleCommandWithOutputDevice的样子，不过没有使用过。",[11,1151,1152],{},"这边主要做一些自定义的资源导入导出指令，所以并不需要World呢。",{"title":46,"searchDepth":327,"depth":593,"links":1154},[1155,1156],{"id":1072,"depth":327,"text":1072},{"id":1101,"depth":327,"text":1102,"children":1157},[1158,1159],{"id":1108,"depth":593,"text":1109},{"id":1136,"depth":593,"text":1137},"2020-06-01",{"layout":621,"status":622,"published":623,"author":1162,"author_login":956,"author_email":957,"wordpress_id":1163,"wordpress_url":1164,"date_gmt":1165,"excerpt":1166},{"display_name":955,"login":956,"email":957,"url":46},2911,"\u002F?p=2911","2020-06-01 08:09:00 +0000",{"type":8,"value":1167},[1168],[11,1169,1066],{},"\u002F2020-06-01-ue4-editor-helper-dev-note",{"title":1061,"description":1066},"_legacy\u002F2020\u002F2020-06-01-ue4-editor-helper-dev-note",[635],"AISPz3jz3z2Nb4LCj272Ak4Yv4QLvUuWVGfEzauJS7Y",85,1788763177231]