[{"data":1,"prerenderedAt":637},["ShallowReactive",2],{"\u002F2021-11-16-note-blue-protocol-anime-render":3},{"id":4,"title":5,"body":6,"date":617,"description":12,"extension":618,"meta":619,"navigation":622,"path":630,"seo":631,"stem":632,"tags":633,"__hash__":636},"blogs\u002F_legacy\u002F2021\u002F2021-11-16-note-blue-protocol-anime-render.md","CEDEC2021-《Blueprotocol》的渲染实现笔记",{"type":7,"value":8,"toc":591},"minimark",[9,13,24,27,31,34,37,40,47,50,55,58,61,192,195,198,201,206,209,219,222,227,230,242,247,250,255,260,263,266,271,274,277,280,338,341,345,348,351,359,362,367,370,373,376,384,389,392,395,398,403,406,409,412,417,419,423,426,429,432,437,440,445,448,451,454,468,471,474,479,482,485,488,491,494,497,500,503,506,509,512,515,519,522,525,530,534,537,540,543,548,551,556,560,563,568,578,581,584,587],[10,11,12],"p",{},"CEDEC2021上《BLUE PROTOCOL》的分享的渲染部分。",[10,14,15,16,23],{},"PPT：[",[17,18,22],"a",{"href":19,"rel":20},"https:\u002F\u002Fcedil.cesa.or.jp\u002Fcedil_sessions\u002Fview\u002F2389",[21],"nofollow","BLUE PROTOCOLにおけるアニメ表現技法について　～実装編～"," ]。",[10,25,26],{},"《BLUE PROTOCOL》虽然是用UE4进行实现的，但是由于他们的方法有对引擎进行改造，所以对于不希望进行引擎修改的情况可能不是很有参考作用。",[28,29,30],"h2",{"id":30},"简介",[10,32,33],{},"渲染实现上，是基于UE4的PBR渲染的。为了实现动画风格的渲染，在UE4的材质系统上，添加了一个新的ShaderModel，对BasePass和PostProcess进行了改造。",[10,35,36],{},"这样做的主要目的，一个是可以结合PBR的渲染效果，另一个是降低渲染的成本。",[10,38,39],{},"原始的PTT中有一定的篇幅在介绍UE4原本的渲染过程，由于没有现场的视频，不是很好推断想要表达的内容。总体而言，应当是对UE4原有的渲染过程进行一个总结，为之后介绍修改的部分进行铺垫。",[10,41,42],{},[43,44],"img",{"alt":45,"src":46},"","\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fue4-default-pass-300x156.png",[10,48,49],{},"实际的修改主要集中在对现有的Pass进行改造以及添加一些新的Pass，改造后的Pass流程：",[10,51,52],{},[43,53],{"alt":45,"src":54},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fimage-300x131.png",[10,56,57],{},"详细的修改部分在后面会有，总体而言感觉改动不大。主要修改如介绍说的，主要集中在BasePass上，所以其实单看BasePass的话，修改还是挺大的。",[10,59,60],{},"GBuffer的使用情况：",[62,63,64,86],"table",{},[65,66,67],"thead",{},[68,69,70,74,77,80,83],"tr",{},[71,72,73],"th",{},"#",[71,75,76],{},"Red",[71,78,79],{},"Green",[71,81,82],{},"Blue",[71,84,85],{},"Alpha",[87,88,89,107,124,141,158,175],"tbody",{},[68,90,91,95,98,101,104],{},[92,93,94],"td",{},"GBufferA",[92,96,97],{},"ComputeNomal.x",[92,99,100],{},"ComputeNomal.y",[92,102,103],{},"ComputeNomal.z",[92,105,106],{},"PerObjectGBufferData",[68,108,109,112,115,118,121],{},[92,110,111],{},"GBufferB",[92,113,114],{},"ToonHairMaskOffsetShadowMask",[92,116,117],{},"SpecularMask",[92,119,120],{},"SpecularValue",[92,122,123],{},"ShadingModelIDSelectiveOutputMask",[68,125,126,129,132,135,138],{},[92,127,128],{},"GBufferC",[92,130,131],{},"BaseColor.r",[92,133,134],{},"BaseColor.g",[92,136,137],{},"BaseColor.b",[92,139,140],{},"AmbientOcclusion",[68,142,143,146,149,152,155],{},[92,144,145],{},"GBufferD",[92,147,148],{},"ShadowColor.r",[92,150,151],{},"ShadowColor.g",[92,153,154],{},"ShadowColor.b",[92,156,157],{},"NdotL",[68,159,160,163,166,169,172],{},[92,161,162],{},"GBufferE",[92,164,165],{},"-",[92,167,168],{},"RimLightMask",[92,170,171],{},"DiffuseOffset",[92,173,174],{},"RimLightWidth",[68,176,177,180,183,186,189],{},[92,178,179],{},"GBufferF",[92,181,182],{},"OutlineWidth",[92,184,185],{},"OutlineID",[92,187,188],{},"OutlinePaint",[92,190,191],{},"OutlineZShift",[28,193,194],{"id":194},"光照",[10,196,197],{},"光照使用的Mesh资源总共有两组法线以及顶点颜色数据，两组法线分别是在模型制作工具中预先处理的用于动画风格阴影的法线，以及在导入时自动计算的法线。",[10,199,200],{},"动画风格阴影的法线(Import Normal)没有介绍具体采用的方案，从效果图上看应该采用的是比较通用的处理方式。",[10,202,203],{},[43,204],{"alt":45,"src":205},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fimport-normal-300x240.png",[10,207,208],{},"导入计算的法线(Compute Normal)，有描述计算方式：",[210,211,212,216],"ol",{},[213,214,215],"li",{},"计算三角面的法线",[213,217,218],{},"每个顶点的法线是相邻三个面的平均",[10,220,221],{},"应该用的就是UE4默认的那种方式，没有考究过。导入计算方面，对于脸部这种分成了好几个部件的情况，会统一导入，防止在边缘出现计算问题。",[10,223,224],{},[43,225],{"alt":45,"src":226},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fcompute-normal-merge-300x245.png",[10,228,229],{},"顶点颜色方面，使用情况如下：",[231,232,233,236,239],"blockquote",{},[10,234,235],{},"Red: Diffuse Offset",[10,237,238],{},"Blue: Ambient Occlusion",[10,240,241],{},"Green: DepthShift(轮廓线用)",[243,244,246],"h3",{"id":245},"basepass","BasePass",[10,248,249],{},"主要计算用于后面光照计算的必要数据：",[10,251,252],{},[43,253],{"alt":45,"src":254},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fbase-pass-300x124.png",[256,257,259],"h4",{"id":258},"basecolor与shadowcolor","BaseColor与ShadowColor",[10,261,262],{},"BaseColor为明亮处的颜色，ShadowColor为暗处的颜色。",[10,264,265],{},"ShadowColor基于BaseColor进行计算：",[10,267,268],{},[43,269],{"alt":45,"src":270},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fshadow-color-300x156.png",[10,272,273],{},"针对头发的颜色，BaseColor和ShadowColor都是直接在材质中指定颜色。如果头发是两种颜色的情况，会采用梯度图来进行过渡。头发的BaseColor和ShadowColor可以不依赖上面的规则进行设定。",[256,275,157],{"id":276},"ndotl",[10,278,279],{},"基于光源方向和ImportNormal进行计算：",[281,282,286],"pre",{"className":283,"code":284,"language":285,"meta":45,"style":45},"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",[287,288,289,324],"code",{"__ignoreMap":45},[290,291,294,298,302,306,309,312,316,319,321],"span",{"class":292,"line":293},"line",1,[290,295,297],{"class":296},"s0idv","HalfNoL ",[290,299,301],{"class":300},"sLXdl","=",[290,303,305],{"class":304},"scAT4"," dot",[290,307,308],{"class":296},"(Normal, Light) ",[290,310,311],{"class":300},"*",[290,313,315],{"class":314},"stP0Q"," 0.5",[290,317,318],{"class":300}," +",[290,320,315],{"class":314},[290,322,323],{"class":296},";\n",[290,325,327,330,332,335],{"class":292,"line":326},2,[290,328,329],{"class":296},"ToonNoL ",[290,331,301],{"class":300},[290,333,334],{"class":304}," step",[290,336,337],{"class":296},"(Threshold, HalfNoL);\n",[10,339,340],{},"PPT中解释了为什么要在BasePass中进行NdotL，除了精度上的考虑之外，主要是对要将什么搭载到GBuffer上的设计考量。ImportNormal只会在这个步骤中用，在这里计算完之后就不需要在GBuffer中传递ImportNormal了。",[256,342,344],{"id":343},"specular","Specular",[10,346,347],{},"主要是材质中直接传过来的SpecularValue，以及计算SpecularMask。",[10,349,350],{},"SpecularMask用于表达主光源的高光，基于Blinn Phong反射模型进行：",[281,352,357],{"className":353,"code":355,"language":356},[354],"language-text","H = normalize(Light + View);\nNoH = dot(Normal,\nToonNoH = step(Threshold, NoH);\n","text",[287,358,355],{"__ignoreMap":45},[10,360,361],{},"头发的高光有另外的处理逻辑：",[10,363,364],{},[43,365],{"alt":45,"src":366},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fhair-specular-mask-300x134.png",[10,368,369],{},"似乎会根据头发本身与重心的距离以及其ID，与光源进行配合动态的缩放。",[243,371,372],{"id":372},"方向光",[10,374,375],{},"方向光的计算主要分为两个过程：",[210,377,378,381],{},[213,379,380],{},"使用NdotL(GBufferD.Alpha)来处理BaseColor和ShadowColor(ShadowColor作为Subsurface处理)",[213,382,383],{},"通过BaseColor的亮度结合SpecularValue以及SpecularMask计算高光",[10,385,386],{},[43,387],{"alt":45,"src":388},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fdirectional-light-300x164.png",[10,390,391],{},"使用的就是上面BasePass过程中生成的5张结果。",[243,393,394],{"id":394},"点光源",[10,396,397],{},"无视法线，直接按照与光源的距离来进行计算。",[10,399,400],{},[43,401],{"alt":45,"src":402},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fpoint-light-300x183.png",[10,404,405],{},"同时为Toon添加专用的参数来进行效果调节。",[243,407,408],{"id":408},"天光和间接光",[10,410,411],{},"将法线视为向上(0,0,1)来处理。",[10,413,414],{},[43,415],{"alt":45,"src":416},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fskylight-300x182.png",[10,418,405],{},[243,420,422],{"id":421},"rimlight","RimLight",[10,424,425],{},"在光照后添加了一个新的PostRimLight的Pass来处理RimLight计算。",[10,427,428],{},"计算使用RimLightWidth(GBufferE.Alpha)和RimLightMask(GBufferE.Green)来进行。",[10,430,431],{},"通过深度信息作Sobel filter计算",[10,433,434],{},[43,435],{"alt":45,"src":436},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Frim-light-depth-300x134.png",[10,438,439],{},"再通过RimLightMask屏蔽掉不希望的部分之后，叠加到Specular上。",[10,441,442],{},[43,443],{"alt":45,"src":444},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Frim-light-mask-300x127.png",[28,446,447],{"id":447},"轮廓线",[10,449,450],{},"轮廓线的绘制在PostOutlinePass中，这个Pass放在了光照和半透明计算之间，确保轮廓线能在水中等情况下受到折射的影响，同时保证半透明物体上不绘制轮廓线。",[10,452,453],{},"轮廓线主要有以下的计算来源：",[210,455,456,459,462,465],{},[213,457,458],{},"基于深度： sobel filter，比较深度并只在深度更大的地方绘制，通过depthshift限制一些地方(脸上)的绘制",[213,460,461],{},"基于ID：sobel filter，在值较大的上绘制",[213,463,464],{},"基于法线： 内积，主要针对深度差不多，ID也分不开的地方",[213,466,467],{},"预先绘制的：从G-Buffer中取出",[10,469,470],{},"使用到的资源包括之前的顶点颜色G通道中的DepthShift，以及额外的通过TexCoord1来获取的第二组顶点颜色。另外，有一张预先绘制的轮廓线贴图。",[10,472,473],{},"绘制过程主要使用GBufferF：",[10,475,476],{},[43,477],{"alt":45,"src":478},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fline-300x129.png",[10,480,481],{},"Red: 从TexCoord1.x中获取轮廓线粗细，并根据摄像距离和分辨率进行调整",[10,483,484],{},"Green: 从TexCoord1.y中获取轮廓线ID",[10,486,487],{},"Blue: 从预先绘制的贴图来绘制，同时使用轮廓模型防止绘制溢出",[10,489,490],{},"Alpha: 从VertexColor.g获取DepthShift，在基于深度计算时使用，绘制时基于参数进行反相",[243,492,493],{"id":493},"眉轮廓线",[10,495,496],{},"动画风格中常见的将眉毛绘制到前发之上的手法。",[10,498,499],{},"绘制上，将除头发以外的部分在BasePass中绘制，而头发单独在上面图中的ToonHairPass绘制。",[10,501,502],{},"眉毛的罗廓线由基于ID的方法生成，首先，在BasePass中生成一个眉毛本身的Mask，之后头发绘制时在更新轮廓用的ID的时候，在这个Mask上的部分不再更新。由此，在进行轮廓线生成的时候，就能够通过ID来产生眉毛的轮廓了。",[243,504,505],{"id":505},"抗锯齿",[10,507,508],{},"用上面的方法绘制的轮廓线会由于TAA导致模糊。",[10,510,511],{},"这里通过利用ResponsiveAA来解决，将轮廓线绘制到stencil来减轻轮廓线被TAA模糊的情况。",[28,513,514],{"id":514},"阴影",[243,516,518],{"id":517},"shadowmap","ShadowMap",[10,520,521],{},"对于本身的ShadowMap的逻辑改造，主要目的是移除Toon材质上的自阴影。",[10,523,524],{},"处理方式上，根据是否是Toon材质将ShadowMap分开处理，这样就可以在移除自阴影的同时接受背景物体的阴影。",[10,526,527],{},[43,528],{"alt":45,"src":529},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002FShadowMaps-284x300.png",[243,531,533],{"id":532},"offsetshadow","OffsetShadow",[10,535,536],{},"为了补偿由于没有自阴影导致的图像信息不足，添加了一个在屏幕空间进行偏移的阴影。",[10,538,539],{},"处理这个的是最上面图中的OffsetShadowDepth和OffsetShadowMerge。",[10,541,542],{},"过程的原理描述起来其实很简单，首先，在OffsetShadowDepth中，根据深度信息产生一个向下的偏移。",[10,544,545],{},[43,546],{"alt":45,"src":547},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002FOffset-Shadow-Depth--300x169.png",[10,549,550],{},"然后在OffsetShadowMerge中，根据深度对比将信息描绘到G-Buffer中的NdotL中去(GBufferD.Alpha)。",[10,552,553],{},[43,554],{"alt":45,"src":555},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002FOffset-Shadow-Merge-300x149.png",[28,557,559],{"id":558},"postprocess","PostProcess",[10,561,562],{},"后处理的方面比较简单，使用了柔光滤镜。在动画风格中经常使用到，虽然Bloom也能达到类似的效果，但还是另外的进行了实现。",[10,564,565],{},[43,566],{"alt":45,"src":567},"\u002Fwp-content\u002Fuploads\u002F2021\u002F11\u002Fpost-process-300x154.png",[231,569,570],{},[10,571,572,573,577],{},"柔光滤镜，应该和[",[17,574,576],{"href":575},"\u002F2021-01-24-ue4-fest-npr-note\u002F#i-10","这个","]是一样的。",[28,579,580],{"id":580},"总结",[10,582,583],{},"虽然改造引擎的方法可能不是很通用，但是其中的一些思路还是比较有参考作用的。对于动画风格渲染，需要注意的部分有很多共通的地方，不过由于UE4的渲染流程是写死的，所以不会像U3D之类的那样灵活。",[10,585,586],{},"PPT后面还有一些关于Actor的动态Instance相关的优化，由于不是很关注这块，所以没有详细的看过，这里就不记录了。",[588,589,590],"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":45,"searchDepth":326,"depth":592,"links":593},3,[594,595,607,611,615,616],{"id":30,"depth":326,"text":30},{"id":194,"depth":326,"text":194,"children":596},[597,603,604,605,606],{"id":245,"depth":592,"text":246,"children":598},[599,601,602],{"id":258,"depth":600,"text":259},4,{"id":276,"depth":600,"text":157},{"id":343,"depth":600,"text":344},{"id":372,"depth":592,"text":372},{"id":394,"depth":592,"text":394},{"id":408,"depth":592,"text":408},{"id":421,"depth":592,"text":422},{"id":447,"depth":326,"text":447,"children":608},[609,610],{"id":493,"depth":592,"text":493},{"id":505,"depth":592,"text":505},{"id":514,"depth":326,"text":514,"children":612},[613,614],{"id":517,"depth":592,"text":518},{"id":532,"depth":592,"text":533},{"id":558,"depth":326,"text":559},{"id":580,"depth":326,"text":580},"2021-11-16","md",{"layout":620,"status":621,"published":622,"wordpress_id":623,"wordpress_url":624,"date_gmt":625,"excerpt":626},"post","publish",true,3557,"\u002F?p=3557","2021-11-15 17:18:59 +0000",{"type":7,"value":627},[628],[10,629,12],{},"\u002F2021-11-16-note-blue-protocol-anime-render",{"title":5,"description":12},"_legacy\u002F2021\u002F2021-11-16-note-blue-protocol-anime-render",[634,635],"UE4","CEDEC","k3Wf7wAjkskIvTzdrhF42jFwPhW8WAtbJHzIkxt3V-4",1788763177142]