[{"data":1,"prerenderedAt":924},["ShallowReactive",2],{"page-UE4-2":3,"page-count-UE4":923},[4,174,498,726,833],{"id":5,"title":6,"body":7,"date":150,"description":13,"extension":151,"meta":152,"navigation":155,"path":167,"seo":168,"stem":169,"tags":170,"__hash__":173},"blogs\u002F_legacy\u002F2020\u002F2020-05-24-ue4-position-deviation-notes.md","UE4物理位置偏差小记两则",{"type":8,"value":9,"toc":139},"minimark",[10,14,17,20,24,27,30,33,44,47,53,56,59,62,65,68,73,76,79,82,85,88,92,95,106,118,121,124,127,130,133,136],[11,12,13],"p",{},"UE4的运动模拟在使用过程中有时会出现位置不在预期的情况。",[11,15,16],{},"虽然并不是会造成很大的问题，但是在需要使用精确位置的时候就会出现一些偏差。",[11,18,19],{},"当前UE4版本为4.25.0。",[21,22,23],"h2",{"id":23},"抛体运动",[11,25,26],{},"在不使用物理模拟移动物体时，包括SetActorLocation的sweep，都会使用到UPrimitiveComponent::MoveComponentImpl进行移动。sweep的好处是移动的结果会保证不会有穿插。",[11,28,29],{},"但是这种运动方式在快速移动时会有一点小问题，就是它在作碰撞回避之后的位置并不是完全贴紧目标碰撞体表面的。",[11,31,32],{},"主要原因是在发生碰撞后的PullBackHit中：",[34,35,40],"pre",{"className":36,"code":38,"language":39},[37],"language-text","static void PullBackHit(FHitResult& Hit, const FVector& Start, const FVector& End, const float Dist)\n{\n    const float DesiredTimeBack = FMath::Clamp(0.1f, 0.1f\u002FDist, 1.f\u002FDist) + 0.001f;\n    Hit.Time = FMath::Clamp( Hit.Time - DesiredTimeBack, 0.f, 1.f );\n}\n","text",[41,42,38],"code",{"__ignoreMap":43},"",[11,45,46],{},"之后执行位置回退时，会使用这个计算的Time",[34,48,51],{"className":49,"code":50,"language":39},[37],"NewLocation = TraceStart + (BlockingHit.Time * (TraceEnd - TraceStart));\n",[41,52,50],{"__ignoreMap":43},[11,54,55],{},"也就是说，如果你是在进行子弹模拟的话，这个回避之后的位置有可能离碰撞表面会比较远。",[11,57,58],{},"这个时候使用返回的HitResult中的ImpactPoint或者Location反而更加接近一些。",[21,60,61],{"id":61},"骨骼位置",[11,63,64],{},"对于依赖于动画更新的骨骼而言，其物理体的位置与其渲染位置是会存在一个微小的偏差的。如果项目中使用了一些特定的优化的话，这个偏差可能会被放大。在使用的时候需要注意。",[11,66,67],{},"取到的位置不在预期点主要由以下两个函数引发：",[69,70,72],"h3",{"id":71},"updatekinematicbonestoanim","UpdateKinematicBonesToAnim",[11,74,75],{},"当我们调用USkinnedMeshComponent::GetBoneTransform的时候，其实并不是去取物理世界中取骨骼的位置。我们取到的结果决定于两个值，一个是当前的ComponentToWorld，另一个是缓存的GetComponentSpaceTransforms。",[11,77,78],{},"而GetComponentSpaceTransforms本身是一个维护的双缓冲系统，其更新依赖于USkinnedMeshComponent::FlipEditableSpaceBases。",[11,80,81],{},"也就是说，在实际更新过程中，这两者有一个过期就会导致出现偏差。",[11,83,84],{},"实际去更新物理位置的逻辑在USkeletalMeshComponent::UpdateKinematicBonesToAnim中能够找到。",[11,86,87],{},"作一个简单的测试，在USkeletalMeshComponent::UpdateKinematicBonesToAnim中直接return。然后在运行时使用pvd连接引擎，你会发现移动角色之后，骨骼在物理世界的位置并没有发生移动。此时渲染的位置却已经移动了。",[69,89,91],{"id":90},"setkinematictarget","setKinematicTarget",[11,93,94],{},"在UpdateKinematicBonesToAnim中还有另一个可能会影响到实际使用的更新，就是setKinematicTarget，如果在移动骨骼的时候没有设置Teleport的话，就会使用这个函数进行物理设置。",[11,96,97,98,105],{},"按照[",[99,100,104],"a",{"href":101,"rel":102},"https:\u002F\u002Fdocs.nvidia.com\u002Fgameworks\u002Fcontent\u002Fgameworkslibrary\u002Fphysx\u002Fapireference\u002Ffiles\u002FclassPxRigidDynamic.html#4464d188e7a1e94582c9cf35da9bbc93",[103],"nofollow","Physx的文档","]描述，这里面其实有个类似缓存的机制",[107,108,109,112,115],"blockquote",{},[11,110,111],{},"The move command will result in a velocity that will move the body into the desired pose. After the move is carried out during a single time step, the velocity is returned to zero. Thus, you must continuously call this in every time step for kinematic actors so that they move along a path.",[11,113,114],{},"This function simply stores the move destination until the next simulation step is processed, so consecutive calls will simply overwrite the stored target variable.",[11,116,117],{},"The motion is always fully carried out.",[11,119,120],{},"也就是说，“真实”的物理位置会在下一次模拟之后才会更新。",[69,122,123],{"id":123},"安全的获取方式",[11,125,126],{},"要“正确”的取到想要的位置，只要使用bodyinstance的GetUnrealWorldTransform就可以了。其中第二个参数bForceGlobalPose就是区分是否取setKineMaticTarget的位置。",[11,128,129],{},"如果是要渲染位置，还是用GetBoneTransform就可以。",[21,131,132],{"id":132},"总结",[11,134,135],{},"由于引擎其实是按帧模拟的，更多的时候，位置偏差其实发生在更新次序问题上。",[11,137,138],{},"所以除非在特殊情况下，还是不要在意比较好……",{"title":43,"searchDepth":140,"depth":141,"links":142},2,3,[143,144,149],{"id":23,"depth":140,"text":23},{"id":61,"depth":140,"text":61,"children":145},[146,147,148],{"id":71,"depth":141,"text":72},{"id":90,"depth":141,"text":91},{"id":123,"depth":141,"text":123},{"id":132,"depth":140,"text":132},"2020-05-24","md",{"layout":153,"status":154,"published":155,"author":156,"author_login":158,"author_email":159,"wordpress_id":160,"wordpress_url":161,"date_gmt":162,"excerpt":163},"post","publish",true,{"display_name":157,"login":158,"email":159,"url":43},"风铃","flinkor","flinkor@foxmail.com",2901,"\u002F?p=2901","2020-05-24 03:20:32 +0000",{"type":8,"value":164},[165],[11,166,13],{},"\u002F2020-05-24-ue4-position-deviation-notes",{"title":6,"description":13},"_legacy\u002F2020\u002F2020-05-24-ue4-position-deviation-notes",[171,172],"UE4","Physx","l-g_jPlFybyQICc4OAhNVRgImE-5AJNP0ExgK98W2OQ",{"id":175,"title":176,"body":177,"date":482,"description":181,"extension":151,"meta":483,"navigation":155,"path":492,"seo":493,"stem":494,"tags":495,"__hash__":497},"blogs\u002F_legacy\u002F2020\u002F2020-05-10-ue4-blender-hair-groom.md","UE4使用Blender的HairGroom笔记",{"type":8,"value":178,"toc":469},[179,182,185,200,204,213,216,219,226,229,233,236,239,245,248,261,264,267,273,276,281,284,290,293,299,303,306,309,315,318,324,327,330,333,336,339,345,348,351,354,360,366,369,375,378,381,384,387,393,396,402,405,411,414,419,422,427,430,433,438,441,446,449,452,455,458,463,466],[11,180,181],{},"UE4最近更新了对HairGroom的支持，于是对其进行了一些测试。",[11,183,184],{},"当前使用的UE4版本为4.25.0，Blender版本为2.82a。",[11,186,187,188,193,194,199],{},"本文主要参考Jayanam的教程[",[99,189,192],{"href":190,"rel":191},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=Oq0q8AmTnDU",[103],"Blender 2.8 Hair Beginner Tutorial","]和Marvel Master的教程[",[99,195,198],{"href":196,"rel":197},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=vQIkyk0-TD4",[103],"Dynamic Hair Tutorial - Blender to Unreal Engine 4 Groom UE4.25","]。",[21,201,203],{"id":202},"ue4设置","UE4设置",[11,205,206,207,212],{},"这部分按照[",[99,208,211],{"href":209,"rel":210},"https:\u002F\u002Fdocs.unrealengine.com\u002Fzh-CN\u002FEngine\u002FHairRendering\u002FOverview\u002Findex.html",[103],"官方文档","]操作就可以了。",[11,214,215],{},"当然，跟着Marvel Master的教程做也是可以的。需要注意的是，当前Groom支持在实验阶段，所以有些部分可能会与实际操作不一致。",[11,217,218],{},"例如Niagra发射器的部分，按照官方文档操作之后",[11,220,221],{},[222,223],"img",{"alt":224,"src":225},"image","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fimage_thumb.png",[11,227,228],{},"Override Parameters并没有出现，由于并不影响实际效果，所以就先放置了。",[21,230,232],{"id":231},"blender内制作groom","Blender内制作Groom",[11,234,235],{},"教程里面是直接在导出的SkeletonMesh上做Groom的，默认情况下会从全体表面来发射头发，以功能验证而言倒是无所谓。",[11,237,238],{},"总之首先就是加个粒子系统，然后改成头发",[11,240,241],{},[222,242],{"alt":243,"src":244},"clip_image001","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image001_thumb.png",[11,246,247],{},"对于需要控制生长区域的情况，目前看主要有三种方法：",[249,250,251,255,258],"ol",{},[252,253,254],"li",{},"单独拉出Mesh用于头发发射",[252,256,257],{},"使用头发发射器的顶点组功能",[252,259,260],{},"使用贴图进行权重控制",[11,262,263],{},"似乎第三个方法主要用于微调的样子。",[11,265,266],{},"定点组的设置比较直观，使用贴图Mask的时候需要注意，必须调整贴图的影响方式",[11,268,269],{},[222,270],{"alt":271,"src":272},"clip_image0014","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image0014_thumb.png",[11,274,275],{},"完成粒子编辑后，将粒子转换为Curv",[11,277,278],{},[222,279],{"alt":224,"src":280},"\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fimage_thumb-2.png",[11,282,283],{},"然后作Curv转换",[11,285,286],{},[222,287],{"alt":288,"src":289},"clip_image0016","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image0016_thumb.png",[11,291,292],{},"导出Abc，注意调整缩放",[11,294,295],{},[222,296],{"alt":297,"src":298},"clip_image0018","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image0018_thumb.png",[21,300,302],{"id":301},"导入ue4","导入UE4",[69,304,305],{"id":305},"头发导入",[11,307,308],{},"导入的选项直接参照教程即可，流程上似乎有些奇怪，不过现在似乎就是这样工作的",[11,310,311],{},[222,312],{"alt":313,"src":314},"clip_image00110","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image00110_thumb.png",[11,316,317],{},"导入之后，在Asset里面打开物理模拟",[11,319,320],{},[222,321],{"alt":322,"src":323},"clip_image00112","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image00112_thumb.png",[11,325,326],{},"此时可以将资源拖到地图中预览。",[11,328,329],{},"这边遇到问题是点开之后拖到地图里面没有反应，此时，将Asset直接添加到角色蓝图内，会发现能动了，但是没有跟随骨骼限制运动。这时再拖到地图里面就会有物理反应了，可能是哪里有bug。",[69,331,332],{"id":332},"基础导入",[11,334,335],{},"无论如何，要让效果跟随物理基础，需要再次做一下导入工作。感觉之后的版本应该会改进，不然这样操作很奇怪",[11,337,338],{},"在blender中删除转换后的Mesh，选中角色模型",[11,340,341],{},[222,342],{"alt":343,"src":344},"clip_image00114","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image00114_thumb.png",[11,346,347],{},"再导出一次abc，覆盖掉旧文件",[11,349,350],{},"在UE4的旧Asset上点击重新导入",[11,352,353],{},"需要重新设置物理模拟",[11,355,356],{},[222,357],{"alt":358,"src":359},"clip_image002","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image002_thumb.png",[11,361,362],{},[222,363],{"alt":364,"src":365},"clip_image003","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image003_thumb.png",[11,367,368],{},"这个时候就可以看到Groom的跟随骨骼动画的效果了",[11,370,371],{},[222,372],{"alt":373,"src":374},"clip_image004","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image004_thumb.png",[21,376,377],{"id":377},"头发试作",[11,379,380],{},"为了实际看看Groom的头发大概是什么风格，这边简单的进行了下测试",[69,382,383],{"id":383},"顶点组绘制控制",[11,385,386],{},"在模型上添加一个顶点组",[11,388,389],{},[222,390],{"alt":391,"src":392},"clip_image00116","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image00116_thumb.png",[11,394,395],{},"然后将模式切换到权重绘制模式",[11,397,398],{},[222,399],{"alt":400,"src":401},"clip_image0024","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image0024_thumb.png",[11,403,404],{},"这个时候就可以在模型上绘制顶点组的范围了",[11,406,407],{},[222,408],{"alt":409,"src":410},"clip_image0034","\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fclip_image0034_thumb.png",[11,412,413],{},"绘制完成后在粒子的顶点组里面将density指定到这个顶点组",[11,415,416],{},[222,417],{"alt":224,"src":418},"\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fimage_thumb-3.png",[11,420,421],{},"就不会从整个头上发射头发了",[11,423,424],{},[222,425],{"alt":224,"src":426},"\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fimage_thumb-4.png",[69,428,429],{"id":429},"粒子调整",[11,431,432],{},"切换到粒子编辑模式随便梳理一下头发",[11,434,435],{},[222,436],{"alt":224,"src":437},"\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fimage_thumb-5.png",[11,439,440],{},"这里可以通过选项让梳理的时候能够看到子粒子的效果",[11,442,443],{},[222,444],{"alt":224,"src":445},"\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fimage_thumb-7.png",[11,447,448],{},"这个梳理模式(粒子编辑器)还是挺强大的，有看到教程是直接从空白处开始刷出粒子的，不过由于没有什么美术细胞，就只能从简了。",[11,450,451],{},"按照教程里面的进行导出操作，就可以看到效果了",[69,453,454],{"id":454},"效果",[11,456,457],{},"努力进行了一番效果调整后，结果是这样的：",[11,459,460],{},[222,461],{"alt":224,"src":462},"\u002Fwp-content\u002Fuploads\u002F2020\u002F05\u002Fimage_thumb-1.png",[11,464,465],{},"由于全局只用了一个发射器，尤其是没有做前发的部分，所以效果不是很好。另外，不知是没有处理好头发的效果，还是过于写实的原因，感觉Groom的和动画风格的模型有些不匹配。",[11,467,468],{},"本来是想拉个双马尾的，但是由于对Blender的操作还不是很熟悉，最后放弃了……感觉要达到ArtStation上看到的大神们做出来的效果对我而言暂时似乎是不太可能的。",{"title":43,"searchDepth":140,"depth":141,"links":470},[471,472,473,477],{"id":202,"depth":140,"text":203},{"id":231,"depth":140,"text":232},{"id":301,"depth":140,"text":302,"children":474},[475,476],{"id":305,"depth":141,"text":305},{"id":332,"depth":141,"text":332},{"id":377,"depth":140,"text":377,"children":478},[479,480,481],{"id":383,"depth":141,"text":383},{"id":429,"depth":141,"text":429},{"id":454,"depth":141,"text":454},"2020-05-10",{"layout":153,"status":154,"published":155,"author":484,"author_login":158,"author_email":159,"wordpress_id":485,"wordpress_url":486,"date_gmt":487,"excerpt":488},{"display_name":157,"login":158,"email":159,"url":43},2876,"\u002F?p=2876","2020-05-10 12:56:56 +0000",{"type":8,"value":489},[490],[11,491,181],{},"\u002F2020-05-10-ue4-blender-hair-groom",{"title":176,"description":181},"_legacy\u002F2020\u002F2020-05-10-ue4-blender-hair-groom",[171,496],"Blender","Yspt_F27sh5kZzkyHIEkNbC-a3oyN24-P5N4FvzBm1c",{"id":499,"title":500,"body":501,"date":711,"description":505,"extension":151,"meta":712,"navigation":155,"path":721,"seo":722,"stem":723,"tags":724,"__hash__":725},"blogs\u002F_legacy\u002F2020\u002F2020-04-11-blender-ue4-pipeline-note.md","Blender到UE4笔记",{"type":8,"value":502,"toc":696},[503,506,509,512,516,519,524,527,531,534,545,548,551,554,557,566,575,578,581,584,587,593,596,599,602,607,610,613,616,619,623,632,638,641,646,650,658,664,667,671,674,677,680,685,688,693],[11,504,505],{},"Blender导出FBX到UE4的路径有些常见的问题，每次碰到都重新去找原因造成时间浪费，于是这边记录下。",[11,507,508],{},"当前UE4版本为UE4.25.0；当前Blender版本为Blender2.82a。\nBlender最近升级到2.8了，有不少变化，不过一些基本的操作还是一样的。",[21,510,511],{"id":511},"导出导入",[69,513,515],{"id":514},"smooth-group","Smooth Group",[11,517,518],{},"导入的时候提示没有SmoothGroup，需要修改FBX的默认导出选项",[11,520,521],{},[222,522],{"alt":243,"src":523},"\u002Fwp-content\u002Fuploads\u002F2020\u002F04\u002Fclip_image001_thumb.png",[11,525,526],{},"这样就不会提示了",[69,528,530],{"id":529},"bind-pose","Bind pose",[11,532,533],{},"导入UE4时提示",[107,535,536,539,542],{},[11,537,538],{},"The following bones are missing from the bind pose:",[11,540,541],{},"This can happen for bones that are not vert weighted. If they are not in the correct orientation after importing,",[11,543,544],{},"please set the \"Use T0 as ref pose\" option or add them to the bind pose and reimport the skeletal mesh.",[11,546,547],{},"按照提示直接勾选Use T0 as ref pose，就不会提示了，结果没有明显变化。",[11,549,550],{},"是否有隐藏问题还不是很确定。",[69,552,553],{"id":553},"贴图丢了",[11,555,556],{},"这个是Blender2.8的新坑，主要是材质节点的组织方式变了。",[11,558,559,560,565],{},"从[",[99,561,564],{"href":562,"rel":563},"https:\u002F\u002Fdeveloper.blender.org\u002FT68399",[103],"官方社区的讨论","]来看，这个似乎不是BUG而是一个特性。",[11,567,568,569,574],{},"2.8的[",[99,570,573],{"href":571,"rel":572},"https:\u002F\u002Fwiki.blender.org\u002Fwiki\u002FReference\u002FRelease_Notes\u002F2.80\u002FPython_API\u002FModules",[103],"API变更记录","]里面可以看到，导出使用的帮助类为bpy_extras.node_shader_utils。对于导出失败的材质，实际使用这个helper去尝试获取材质，就会发现无法正确的获取到贴图信息。主要原因是一些复杂的材质使用逻辑导致的，对于只是将Blender当作文件转换工具的用户而言就有些困扰了呢。",[11,576,577],{},"暂时没有什么特别好的解决方案，对于没有特别必要升级2.8的情况可以回到2.79……",[69,579,580],{"id":580},"贴图扭曲",[11,582,583],{},"模型导入后，部分贴图效果有微妙的扭曲感。在一些部位能明显的看到贴图出现了偏移。在对Blender内的UV进行查看后，发现在整个路径中，UV的值过大了。虽然说在理论上这并不会造成问题，但是实际是在导入UE4后出现了较为明显的UV偏差。",[11,585,586],{},"详细的发生原因没有时间去细究，不确定问题是在Blender侧还是UE侧，通过使用脚本在Blender中直接将UV全部裁剪到0~1的范围内解决了问题。",[34,588,591],{"className":589,"code":590,"language":39},[37],"import bpy\n\nfor ts_mesh in bpy.data.meshes:\n    print(ts_mesh.name)\n    for ts_uvlayer in ts_mesh.uv_layers:\n        print(ts_uvlayer)\n        for ts_data in ts_uvlayer.data:\n            ts_uv = ts_data.uv\n            ts_uv.x = ts_uv.x % 1.0\n            ts_uv.y = ts_uv.y % 1.0\n            print(ts_data.uv)\n\n\n",[41,592,590],{"__ignoreMap":43},[69,594,595],{"id":595},"光照模式下出现断层",[11,597,598],{},"在UnLit模式下模型正常，光照模式下出现一个明显的断层。",[11,600,601],{},"这种时候可以对GBuffer进行可视化来排查问题",[11,603,604],{},[222,605],{"alt":224,"src":606},"\u002Fwp-content\u002Fuploads\u002F2020\u002F04\u002Fimage_thumb-4.png",[11,608,609],{},"这次遇到的是法线贴图的处理方式有问题，能够明显看到在World Normal的可视化里面有一个断层，导致Detail Light那边同样出现了断层。",[11,611,612],{},"问题是法线贴图的混合，法线贴图不能通过简单的加法或者乘法来混合，UE4有提供混合节点BlendAngleCorrectedNormals，直接使用即可~",[21,614,615],{"id":615},"材质节点",[11,617,618],{},"Blender和UE4的材质节点有很多不同的地方，需要至少保证一些数学计算是一致的",[69,620,622],{"id":621},"rgb-to-bw","Rgb to bw",[11,624,625,626,631],{},"似乎是在做灰度转换的样子，[",[99,627,630],{"href":628,"rel":629},"https:\u002F\u002Fdocs.blender.org\u002Fmanual\u002Fen\u002Flatest\u002Fcompositing\u002Ftypes\u002Fconverter\u002Frgb_to_bw.html",[103],"官方Rgb_to_bw文档","]没有给出详细的计算方式。到源码里面的utils.py找到了",[34,633,636],{"className":634,"code":635,"language":39},[37],"def rgb_to_bw(r, g, b):\n    \"\"\"Method to convert rgb to a bw intensity value.\"\"\"\n    return 0.35 * r + 0.45 * g + 0.2 * b\n",[41,637,635],{"__ignoreMap":43},[11,639,640],{},"参照着实现",[11,642,643],{},[222,644],{"alt":224,"src":645},"\u002Fwp-content\u002Fuploads\u002F2020\u002F04\u002Fimage_thumb-3.png",[69,647,649],{"id":648},"invert","Invert",[11,651,97,652,657],{},[99,653,656],{"href":654,"rel":655},"https:\u002F\u002Fdocs.blender.org\u002Fmanual\u002Fen\u002Flatest\u002Frender\u002Fshader_nodes\u002Fcolor\u002Finvert.html",[103],"Invert Node","]的描述，就是反转，但是有一个factor的参数，暂时采用svm_invert.h的实现",[34,659,662],{"className":660,"code":661,"language":39},[37],"ccl_device float invert(float color, float factor)\n{\n  return factor * (1.0f - color) + (1.0f - factor) * color;\n}\n",[41,663,661],{"__ignoreMap":43},[11,665,666],{},"没有配置factor的话就是OneMinus",[69,668,670],{"id":669},"mix","Mix",[11,672,673],{},"Mix等效于UE4的Interp节点。",[21,675,676],{"id":676},"功能差异",[11,678,679],{},"这次遇到一个Blender和UE4之间节点上的表现差异导致的误操作，在UE4里面，材质节点是这样的：",[11,681,682],{},[222,683],{"alt":224,"src":684},"\u002Fwp-content\u002Fuploads\u002F2020\u002F04\u002Fimage_thumb-5.png",[11,686,687],{},"这里面RGB和下面的R\\G\\B是指向同样的值的，而在Blender里面，如果有这样的节点",[11,689,690],{},[222,691],{"alt":224,"src":692},"\u002Fwp-content\u002Fuploads\u002F2020\u002F04\u002Fimage_thumb-6.png",[11,694,695],{},"这个时候Color和R\\G\\B其实是完全不同的东西，如果在进行材质功能映射的时候一定不要按照UE4这边的习惯去理解~",{"title":43,"searchDepth":140,"depth":141,"links":697},[698,705,710],{"id":511,"depth":140,"text":511,"children":699},[700,701,702,703,704],{"id":514,"depth":141,"text":515},{"id":529,"depth":141,"text":530},{"id":553,"depth":141,"text":553},{"id":580,"depth":141,"text":580},{"id":595,"depth":141,"text":595},{"id":615,"depth":140,"text":615,"children":706},[707,708,709],{"id":621,"depth":141,"text":622},{"id":648,"depth":141,"text":649},{"id":669,"depth":141,"text":670},{"id":676,"depth":140,"text":676},"2020-04-11",{"layout":153,"status":154,"published":155,"author":713,"author_login":158,"author_email":159,"wordpress_id":714,"wordpress_url":715,"date_gmt":716,"excerpt":717},{"display_name":157,"login":158,"email":159,"url":43},2831,"\u002F?p=2831","2020-04-11 11:51:28 +0000",{"type":8,"value":718},[719],[11,720,505],{},"\u002F2020-04-11-blender-ue4-pipeline-note",{"title":500,"description":505},"_legacy\u002F2020\u002F2020-04-11-blender-ue4-pipeline-note",[171,496],"rStqfgrMz4Rim14QEzNGZDIhyjXIBdOStHTnvMtMDcM",{"id":727,"title":728,"body":729,"date":711,"description":733,"extension":151,"meta":818,"navigation":155,"path":827,"seo":828,"stem":829,"tags":830,"__hash__":832},"blogs\u002F_legacy\u002F2020\u002F2020-04-11-blender-ue4-texture-python-material-fix.md","Blender和UE4的贴图修复脚本",{"type":8,"value":730,"toc":813},[731,734,737,740,743,746,749,753,756,761,764,770,773,777,780,790,796,799,805,807,810],[11,732,733],{},"最近遇到了Blender导出的材质没有贴图的问题，刚好顺便研究下UE4和Blender的脚本结构。",[11,735,736],{},"当前UE4版本为UE4.25.0；当前Blender版本为Blender2.82a。",[11,738,739],{},"首先，目前Blender和UE4的脚本版本是不同的。Blender使用的是python3，而UE4停留在python2.7。",[11,741,742],{},"这边使用Blender其实主要是作为模型导出的中继，比如MMD、VRM、XPS以及一些其他的非主流模型，通过Blender这边导出为FBX，然后输入到UE4去。",[11,744,745],{},"由于Blender和UE4的材质结构是不同的，到了UE4那边之后肯定要重新调整节点。所以对于缺少贴图的情况，最致命的问题就是，我们很难把材质名称和它使用的贴图对应起来。",[11,747,748],{},"因此这边就是实现这个过程的自动化，首先，从Blender这边导出材质使用到的贴图，然后在UE4那边还原到材质中去。",[21,750,752],{"id":751},"blender贴图导出","Blender贴图导出",[11,754,755],{},"blender这边的script编辑器里面有个需要注意的地方，就是他的输出不是在左边的那个窗口输出的。虽然左边的console窗口可以用于测试指令，但是如果你在右侧的脚本上点击运行的话。输出其实是在外部的那个Console里面的。如果默认没有的话，需要在菜单中打开",[11,757,758],{},[222,759],{"alt":224,"src":760},"\u002Fwp-content\u002Fuploads\u002F2020\u002F04\u002Fimage_thumb-2.png",[11,762,763],{},"Blender的脚本文档还是比较丰富的，不过由于功能比较多，这边只是做一个简单的材质中的贴图导出，所以并不一定是一个好的实现",[34,765,768],{"className":766,"code":767,"language":39},[37],"# blender script\n# python3\n\nimport bpy\nimport json\n\nEXPORT_PATH = 'F:\u002Fmaterials.json'\n\nclass MatCollector:\n    MatDic = {}\n\n    # Collet material texture for single material\n    def _CollectMaterial(self, InMaterial):\n        MatTextures = []\n        for ts_node in InMaterial.node_tree.nodes:\n            if ts_node.bl_static_type == 'TEX_IMAGE' and ts_node.image:\n                MatTextures.append(ts_node.image.filepath)\n\n        tstr_MatName = InMaterial.name\n        if len(MatTextures) == 0:\n            print(\"[ExportMaterialTextures] \u003C\"+tstr_MatName+\"> has no texture\")\n            return\n\n        self.MatDic[tstr_MatName] = MatTextures\n        print(\"[ExportMaterialTextures] Material \" + tstr_MatName + \" with \" + str(len(MatTextures)) + \" texture\")\n        pass\n\n# List all materials and try pickout texture info\ndef ExportMaterialTextures(InPath):\n    print(\"[ExportMaterialTextures] Begin: \"+ InPath)\n    ts_MatCollector = MatCollector()\n    for Mat in bpy.data.materials:\n        ts_MatCollector._CollectMaterial(Mat)\n        \n    print(\"[ExportMaterialTextures] Material with texture count \" + str(len(ts_MatCollector.MatDic)))\n    with open(InPath, 'w') as f:\n        json.dump(ts_MatCollector.MatDic, f)\n\n    print(\"[ExportMaterialTextures] End\")\n\n\ndef main():\n    ExportMaterialTextures(EXPORT_PATH)\n\nif __name__ == \"__main__\":\n    main()\n",[41,769,767],{"__ignoreMap":43},[11,771,772],{},"脚本的作用很简单，就是遍历场景内所有的材质，然后将每个材质中带有的所有贴图都输出出来。",[21,774,776],{"id":775},"ue4导入","UE4导入",[11,778,779],{},"当前UE4的编辑器Python插件是Beta状态，需要打开才能使用。",[11,781,782,783,786,787,789],{},"友情提醒：",[784,785],"br",{},"\n使用本文中的脚本前请注意备份项目文件!",[784,788],{},"\n脚本操作可能会导致不可预期的结果，请在了解这种危险的情况下使用脚本操作！\nUE4这边在完成FBX的导入之后，会看到所有的材质都是白的。所以我们这边用脚本，将对应的贴图添加到材质中去。不过，要在代码中操作材质是很麻烦的，连接的工作还是保留在了材质制作中。",[34,791,794],{"className":792,"code":793,"language":39},[37],"# unreal script\n# python2.7\n\nimport json\nimport unreal\n\nfrom sets import Set\n\nIMPORT_PATH = 'F:\u002Fmaterials.json'\nIMPORT_TARGET = '\u002FGame\u002FChara\u002F'\nTEXTURE_PATH = '\u002FGame\u002FChara\u002FTextures\u002F'\nTEXTURE_BASE = 'F:\u002Fz'\n\n\nclass MatTextureImporter:\n    IsStateGood = True\n\n    def ReadFromConfig(self):\n        unreal.log(\"[MaterialTextureImport] Begin\")\n\n        # Load from Json config\n        with open(IMPORT_PATH, 'r') as myfile:\n            data=myfile.read()\n        MatConf = json.loads(data)\n\n        # Try import texture\n        self._PreLoadTexture(MatConf)\n\n        if self.IsStateGood == False:\n            unreal.log_error(\"[MaterialTextureImport] Asset state not good, abort\")\n            return\n\n        # Try apply texture to material\n        unreal.log(\"[MaterialTextureImport] Total target material: \" + str(len(MatConf)))\n        with unreal.ScopedSlowTask(len(MatConf), \"Modifing materials\") as slow_task:\n            for ts_key in MatConf:\n                if slow_task.should_cancel():\n                    break\n                \n                slow_task.enter_progress_frame(1)\n                self._AddMaterialTexture(ts_key, MatConf[ts_key])\n\n        unreal.log(\"[MaterialTextureImport] End\")\n        return  \n\n    def _PreLoadTexture(self, InDic):\n        print(\"[MaterialTextureImport] Pre load textures - Begin\")\n\n        # Build texture list\n        tset_TexturePath = Set()\n        for ts_MatKey in InDic:\n            for ts_Texture in InDic[ts_MatKey]:\n                ts_TextureName = unreal.Paths.get_base_filename(ts_Texture)\n                ts_TextureUePath = TEXTURE_PATH + ts_TextureName\n\n                if unreal.EditorAssetLibrary.does_asset_exist(ts_TextureUePath):\n                    ts_TextureData = unreal.EditorAssetLibrary.find_asset_data(ts_TextureUePath)\n                    if ts_TextureData.is_valid():\n                        if ts_TextureData.asset_class == 'Texture2D':\n                            # We will never find a good name while operation takes more than once, so just ignore\n                            unreal.log_warning(\"[MaterialTextureImport] Texture \" + ts_TextureUePath + \" alread exist\")\n                            continue\n                        else:\n                            # While asset name conflicts, we will stop operation, as replacing or rename may break our project\n                            unreal.log_error(\"[MaterialTextureImport] Asset \" + ts_TextureUePath + \" alread exist and it is not a texture!\")\n                            self.IsStateGood = False\n                            return\n\n                tset_TexturePath.add(ts_Texture)\n\n        print(\"[MaterialTextureImport] Total texture need load: \" + str(len(tset_TexturePath)))\n\n        # Build import tasks\n        import_tasks = []\n        for ts_Texture in tset_TexturePath:\n            print(\"[MaterialTextureImport] - \" + ts_Texture)\n            AssetImportTask = unreal.AssetImportTask()\n            AssetImportTask.set_editor_property('filename', TEXTURE_BASE + ts_Texture)\n            AssetImportTask.set_editor_property('destination_path', TEXTURE_PATH)\n            AssetImportTask.set_editor_property('save', True)\n            import_tasks.append(AssetImportTask)\n\n        # Import textures\n        AssetTools = unreal.AssetToolsHelpers.get_asset_tools() \n        AssetTools.import_asset_tasks(import_tasks)\n\n        print(\"[MaterialTextureImport] Pre load textures - End\")\n        \n        return\n\n    def _AddMaterialTexture(self, InName, InList):\n        InName = InName.replace(' ', '_')\n        InName = InName.replace('.', '_')\n\n        print(\"[MaterialTextureImport] Material: \" + InName)\n\n        # Try load material\n        matpath = \"Material'\" + IMPORT_TARGET + InName + '.' + InName + \"'\"\n        ts_LoadedMat = unreal.load_asset(matpath)\n        if ts_LoadedMat is None:\n            unreal.log_warning(\"[MaterialTextureImport] Invalid material path: \" + matpath)\n            return\n\n        # Add texture to material\n        #unreal.MaterialEditingLibrary.delete_all_material_expressions(ts_LoadedMat)\n        td_TextureAdded = 0\n        for ts_TexturePath in InList:\n            ts_TextureName = unreal.Paths.get_base_filename(ts_TexturePath)\n            ts_TextureUePath = TEXTURE_PATH + ts_TextureName\n            ts_LoadedTexture = unreal.EditorAssetLibrary.load_asset(ts_TextureUePath)\n\n            if ts_LoadedTexture is None:\n                unreal.log_warning(\"[MaterialTextureImport] Could not load texture \"+ ts_TextureUePath)\n                continue\n\n            ts_TextureNodeBc = unreal.MaterialEditingLibrary.create_material_expression(ts_LoadedMat, unreal.MaterialExpressionTextureSample, -400, 250 * td_TextureAdded)\n            if ts_TextureNodeBc is None:\n                unreal.log_warning(\"[MaterialTextureImport] Could not create node\")\n                continue\n\n            ts_TextureNodeBc.set_editor_property(\"texture\", ts_LoadedTexture)\n            ts_TextureNodeBc.set_editor_property(\"desc\", ts_TextureName)\n            td_TextureAdded += 1\n\n        print(\"[MaterialTextureImport] Material get  \" + str(td_TextureAdded) + \" texture\")\n\n        return\n\ndef main():\n    ts_MatChanger = MatTextureImporter()\n    ts_MatChanger.ReadFromConfig()\n\nif __name__ == \"__main__\":\n    main()\n\n",[41,795,793],{"__ignoreMap":43},[11,797,798],{},"在制作中遇到的另一个问题是，由于模型那边Blender的材质使用到了复杂的Group Node，这边也对应的实现了Material Function，但是需要将材质改为使用Attribute输出。于是又写了个批量修改的脚本",[34,800,803],{"className":801,"code":802,"language":39},[37],"import unreal\n\ndef list_assets(InPath, InClass):\n    MatchedAssets = []\n\n    for ts_AssetPath in unreal.EditorAssetLibrary.list_assets(InPath):\n        ts_AssetData = unreal.EditorAssetLibrary.find_asset_data(ts_AssetPath)\n        if ts_AssetData.asset_class == InClass:\n            MatchedAssets.append(ts_AssetPath)\n\n    return MatchedAssets\n\n\n\ndef ModifyMaterials():\n    ts_Mats = list_assets(\"\u002FGame\u002FChara\u002F\", 'Material')\n    print(ts_Mats)\n    ts_MatModified = []\n    for ts_matPath in ts_Mats:\n        ts_mat = unreal.EditorAssetLibrary.load_asset(ts_matPath)\n        if ts_mat is None:\n            unreal.log_warning(\"[MatModify] No material in \" + ts_mat)\n            continue\n        #ts_mat.set_editor_property(\"use_material_attributes\", True)  \n\n        #unreal.MaterialEditingLibrary.recompile_material(ts_mat)\n        ts_MatModified.append(ts_mat)\n\n    unreal.EditorAssetLibrary.save_loaded_assets(ts_MatModified)\n\ndef main():\n    ModifyMaterials()\n\nif __name__ == \"__main__\":\n    main()\n\n",[41,804,802],{"__ignoreMap":43},[21,806,132],{"id":132},[11,808,809],{},"Blender和UE4两边的Python脚本都还是挺好用的。",[11,811,812],{},"不过UE4这边在批量修改完材质后会有一个卡顿的感觉，使用SlowTask包裹依然没有改善，不过由于是自用的脚本，就不纠结了。",{"title":43,"searchDepth":140,"depth":141,"links":814},[815,816,817],{"id":751,"depth":140,"text":752},{"id":775,"depth":140,"text":776},{"id":132,"depth":140,"text":132},{"layout":153,"status":154,"published":155,"author":819,"author_login":158,"author_email":159,"wordpress_id":820,"wordpress_url":821,"date_gmt":822,"excerpt":823},{"display_name":157,"login":158,"email":159,"url":43},2815,"\u002F?p=2815","2020-04-11 13:04:30 +0000",{"type":8,"value":824},[825],[11,826,733],{},"\u002F2020-04-11-blender-ue4-texture-python-material-fix",{"title":728,"description":733},"_legacy\u002F2020\u002F2020-04-11-blender-ue4-texture-python-material-fix",[171,496,831],"Python","YOQkIQJdqcC6WBD4OxarTU9B2stqZH2vyA88rL6ZNz8",{"id":834,"title":835,"body":836,"date":711,"description":840,"extension":151,"meta":908,"navigation":155,"path":917,"seo":918,"stem":919,"tags":920,"__hash__":922},"blogs\u002F_legacy\u002F2020\u002F2020-04-11-try-ue4-vrm.md","UE4的VRM插件试用",{"type":8,"value":837,"toc":904},[838,841,850,853,861,864,867,870,873,878,881,884,887,890,893,896,901],[11,839,840],{},"最近试用了下UE4的VRM插件。",[11,842,843,844,849],{},"感觉上VRoid这边的社区不是很活跃的样子，最近blender用的VRM插件也因为某些原因[",[99,845,848],{"href":846,"rel":847},"https:\u002F\u002Fgithub.com\u002FiCyP\u002FVRM_IMPORTER_for_Blender2_8\u002Fissues\u002F7#issuecomment-606706478",[103],"停止更新","]了。",[11,851,852],{},"不过VRoid其实是基于Gltf的，在使用上还是大致不会有很大的问题。",[11,854,855,856,860],{},"插件地址：",[99,857,858],{"href":858,"rel":859},"https:\u002F\u002Fgithub.com\u002Fruyo\u002FVRM4U",[103],"。由于官方有提供很多效果截图，这边就不贴图了~",[11,862,863],{},"VRM4U的功能主要包括三个方面：MToon支持、动画辅助和移动开发用的骨骼缩减。",[11,865,866],{},"主要感兴趣的还是动画风格渲染的MToon的实现，VRM4U这边的目标是使用非侵入式的方式来达到类似于VRoid官方MToon的效果。",[21,868,869],{"id":869},"光照",[11,871,872],{},"作为基础的MToon材质是基于UnLit的，为了防止ToneMap和Exposure的影响，在材质中进行了相应的抵消操作。",[11,874,875],{},[222,876],{"alt":224,"src":877},"\u002Fwp-content\u002Fuploads\u002F2020\u002F04\u002Fimage_thumb.png",[11,879,880],{},"对于UnLit的材质，为了模拟阴影效果，使用一个SceneCapture来对周围的阴影进行捕捉，并通过RenderTarget重新应用在了模型上。",[11,882,883],{},"光照效果方面，则直接使用自定义Shader节点来获取光照的信息并重新赋予到材质上。SkyLight有GetSkySHDiffuseSimple, GetSkySHDiffuse；DirectionalLight则使用DirectionalLightColor并通过光源于发现的关系来决定阴影的构造。",[11,885,886],{},"天空光源和AO也同样采用了类似的方式进行处理，基础的材质节点非常的复杂，如果之后有需要相关效果的，可以详细的看看具体的实现方式。",[21,888,889],{"id":889},"描边",[11,891,892],{},"描边特效方面，由于要兼容前向渲染，所以VRM没有采用PostProcess的方式。而是使用了模型复制的方式。",[11,894,895],{},"通过复制一个模型，并将其反转，同时在材质上使用Normal和WorldOffset来进行了某种程度的扩展。",[11,897,898],{},[222,899],{"alt":224,"src":900},"\u002Fwp-content\u002Fuploads\u002F2020\u002F04\u002Fimage_thumb-1.png",[11,902,903],{},"反转的模型与原始模型的同步使用的是SetMasterPoseComponent，保证其能与动画同步。",{"title":43,"searchDepth":140,"depth":141,"links":905},[906,907],{"id":869,"depth":140,"text":869},{"id":889,"depth":140,"text":889},{"layout":153,"status":154,"published":155,"author":909,"author_login":158,"author_email":159,"wordpress_id":910,"wordpress_url":911,"date_gmt":912,"excerpt":913},{"display_name":157,"login":158,"email":159,"url":43},2809,"\u002F?p=2809","2020-04-11 09:45:49 +0000",{"type":8,"value":914},[915],[11,916,840],{},"\u002F2020-04-11-try-ue4-vrm",{"title":835,"description":840},"_legacy\u002F2020\u002F2020-04-11-try-ue4-vrm",[171,921],"VRM","IeCASCWTQq2bPnX_mEG99EGT1_a_iJ0kRx39UGj_NTI",85,1788763178167]