UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技 - 渲染
基于物理的渲染(PBR, Physically Based Rendering)采用了物理真实的光照模型，符合自然世界的直观认知规律，近年来比较流行。 PBR可理解为是一套渲染标准，标准化有利于简化美术流程，但是只使用标准PBR达不到具体项目的渲染需求，因为游戏风格差异化大多来自对渲染的自定义，比如卡通化PBR渲染： 异度之刃2的游戏截图 上图为NS主机游戏异度之刃2的游戏截图，角色脸部和头发都是比较卡通的，其它部分的金属质感盔甲和场景也都是写实风格的。 异度之刃2在非真实建模的前提下，使用真实渲染和非真实渲染结合，配合后期调色，形成了游戏特有的卡渲风格。游戏中角色脸部和头发渲染这种具有PBR属性，但又风格化的渲染就属于扩展PBR。 一、标准PBS PBR的核心是PBS（Physically Based Shading）着色模型，具体实现由各大引擎自己负责，Unity的PBS实现封装为Standard，Unreal 4（下称UE4）中实现封装为Default Lit。本文的扩展是在Unity的标准PBS的基础上去修改和扩展，这里先剖析Unity的标准PBS的代码实现。 1.1 BRDF BRDF（双向反射分布函数）光照模型是PBS的重要组成部分，用于描述光在物体表面的反射情况。该模型基于微表面理论，认为光在物体表面反射的光量是物体表面的所有微小表面漫反射和镜面反射光量的总和，符合能量守恒： 1） 反射的光总量不大于入射的光总量，且漫反射和镜面反射是互斥关系； 2） 粗糙的表面反射的光线分散且暗，光滑的表面反射集中且亮。 Unity的BRDF的内部实现文件为UnityStandardBRDF.cginc，主要实现函数为BRDF?_Unity_PBS。Unity的BRDF实现按平台分为3个档次，这里讨论的是针对Console/PC平台，光照模型更加精确的第1档实现BRDF1_Unity_PBS。 BRDF的漫反射部分为Disney漫反射模型，该计算模型基于表面粗糙度，主要实现代码为： half nlPow5 = Pow5 (1-nl); half nvPow5 = Pow5 (1-nv); half Fd90 = 0.5 + 2 * lh * lh * roughness; half disneyDiffuse = (1 + (Fd90-1) * nlPow5) * (1 + (Fd90-1) * nvPow5); half diffuseTerm = disneyDiffuse * nl; 代码中diffuseTerm 为计算得到的漫反射部分。 Disney漫反射模型与不考虑表面粗糙度的Lambert漫反射模型实际效果区别不大，所以在Unity的第2,3档中diffuse计算用的是更简单的Lambert模型。 BRDF的镜面反射部分基于Torrance-Sparrow微表面模型，公式类似为： 主要实现代码为： #if UNITY_BRDF_GGX half V = SmithJointGGXVisibilityTerm (nl, nv, roughness); half D = GGXTerm (nh, roughness); #else half V = SmithBeckmannVisibilityTerm (nl, nv, roughness); half D = NDFBlinnPhongNormalizedTerm (nh, RoughnessToSpecPower (roughness)); #endif F = FresnelTerm (specColor, lh); half specularTerm = (V * D * F) * (UNITY_PI/4); 代码中SpecularTerm 为计算得到的镜面反射部分，实现上基本遵守了Torrance-Sparrow的公式。 其中，法线分布D和几何衰减G按是否采用GGX计算模型会有些不同。这里附一张不同粗糙度的法线分布函数（NDF）曲线示意图： 上图中，X轴为Half半角向量和表面Normal的夹角弧度，Y轴为NDF返回值，可看出Smoothness越高的函数曲线越陡峭，可解释“粗糙的表面反射的光线分散且暗，光滑的表面反射集中且亮”能量守恒。 公式中的Frensnel部分的代码实现为： inline half3 FresnelTerm (half3 F0, half cosA) { half t = Pow5 (1 - cosA); // ala Schlick interpoliation return F0 + (1-F0) * t; } F = FresnelTerm (specColor, lh) FresnelTerm 的函数曲线符合之前《理论基础》文章所示的Fresnel曲线： 其中，FresnelTerm 函数的第1个参数specColor对应着示意图中的Base Reflectivities。 接下来分析“Diffuse和Specular互斥”能量守恒。UnityStandardUtils.cginc文件包含了主要内部实现代码： inline half OneMinusReflectivityFromMetallic(half metallic) { // We'll need oneMinusReflectivity, so // 1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic) // store (1-dielectricSpec) in unity_ColorSpaceDielectricSpec.a, then // 1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) = // = alpha - metallic * alpha half oneMinusDielectricSpec = unity_ColorSpaceDielectricSpec.a; return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec; } inline half3 DiffuseAndSpecularFromMetallic (half3 albedo, half metallic, out half3 specColor, out half oneMinusReflectivity) { specColor = lerp (unity_ColorSpaceDielectricSpec.rgb, albedo, metallic); oneMinusReflectivity = OneMinusReflectivityFromMetallic(metallic); return albedo * oneMinusReflectivity; } 代码根据金属度计算漫反射和镜面反射比例，当Metallic为1时，反射率接近1，函数返回的diffColor接近0，表示几乎不反射漫反射。 Unity的内置变量Unity_ColorSpaceDielectricSpec定义了绝缘体的高光颜色和反射率，不完全为0，是一个经验值。 Unity还提供了Specular Setup工作流程来控制漫反射和镜面反射比例。内部实现代码为： // Diffuse/Spec Energy conservation inline half3 EnergyConservationBetweenDiffuseAndSpecular (half3 albedo, half3 specColor, out half oneMinusReflectivity) { oneMinusReflectivity = 1 - SpecularStrength(specColor); #if !UNITY_CONSERVE_ENERGY return albedo; #elif UNITY_CONSERVE_ENERGY_MONOCHROME return albedo * oneMinusReflectivity; #else return albedo * (half3(1,1,1) - specColor); #endif } 代码中用1减去镜面反射比例，得到漫反射比例。当传入的specColor为白色时，SpecularStrength返回1，结果漫反射比例为0，发生完美镜面反射。 计算得到的diffColor和specColor作为比例系数用于最终漫反射和镜面反射计算： // BRDF = kD / pi + kS * (D * V * F) / 4 代码中的 kD和kS对应着diffColor和specColor。 1.2 IBL 在材质上反应出周围的环境也是PBS的重要组成部分。在光照模型中一般把周围的环境当作一个大的光源来对待，不过环境光不同于实时光，而是作为间接光(Indirect Light)通过IBL(Image Based Lighting)来实现。间接光计算也包含漫反射部分和镜面反射部分。 UnityGlobalIllumination.cginc文件包含了主要内部实现代码： inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld) { UnityGI o_gi; ResetUnityGI(o_gi); ..... #if UNITY_SHOULD_SAMPLE_SH o_gi.indirect.diffuse = ShadeSHPerPixel (normalWorld, data.ambient, data.worldPos); #endif #if defined(LIGHTMAP_ON) // Baked lightmaps fixed4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data.lightmapUV.xy); half3 bakedColor = DecodeLightmap(bakedColorTex); #ifdef DIRLIGHTMAP_COMBINED ...... #elif DIRLIGHTMAP_SEPARATE ..... #else // not directional lightmap o_gi.indirect.diffuse = bakedColor; ...... #endif #endif #ifdef DYNAMICLIGHTMAP_ON ...... #endif o_gi.indirect.diffuse *= occlusion; return o_gi; } Unity内置了Unity_Lightmap、Unity_SHAr等全局变量，来从预先烘焙好的Lightmap贴图或Light Probe中读取颜色，其中UNITY_SHOULD_SAMPLE_SH代码段处理的是从light probe中读取颜色值。一般渲染时静态物体读取Lightmap，非静态物体读取Light Probe。 UnityGI_Base函数返回的颜色值为间接光的漫反射部分。 inline half3 UnityGI_IndirectSpecular(UnityGIInput data, half occlusion, half3 normalWorld, Unity_GlossyEnvironmentData glossIn) { half3 specular; #if UNITY_SPECCUBE_BOX_PROJECTION // we will tweak reflUVW in glossIn directly (as we pass it to Unity_GlossyEnvironment twice), so keep original to pass into BoxProjectedCubemapDirection half3 originalReflUVW = glossIn.reflUVW; #endif #if UNITY_SPECCUBE_BOX_PROJECTION glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[0], data.boxMin[0], data.boxMax[0]); #endif #ifdef _GLOSSYREFLECTIONS_OFF specular = unity_IndirectSpecColor.rgb; #else half3 env0 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE(unity_SpecCube0), data.probeHDR[0], glossIn); #if UNITY_SPECCUBE_BLENDING ...... #else specular = env0; #endif #endif return specular * occlusion; } Unity用Reflection Probe来保存预先烘焙好的环境光反射贴图，通过内置变量Unity_SpecCube0，Unity_SpecCube1访问。 UnityGI_IndirectSpecular返回的颜色值为间接光的镜面反射部分。 另外，“粗糙的表面反射的光线分散且暗，光滑的表面反射集中且亮”能量守恒在这里同样被遵守，函数输入参数包含粗糙度信息，用于环境光贴图的LOD取值： half mip = roughness * UNITY_SPECCUBE_LOD_STEPS; half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex, glossIn.reflUVW, mip); 表面越粗糙，用于采样Mipmap贴图的LOD值越高，UNITY_SAMPLE_TEXCUBE_LOD采样的结果越模糊，反之亦然。 1.3 BRDF+IBL 正如光照计算公式中多个光源的强度是叠加关系，PBS模型光照计算的结果是实时光BRDF与间接光IBL之和。BRDF1_Unity_PBS函数最后的颜色返回值代码： half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity)); half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm) + specularTerm * light.color * FresnelTerm (specColor, lh) + surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv); 附自定义Standard Shader得到的结果分解图： 二、扩展 扩展PBR的具体内容是对标准PBS代码进行修改和扩展。除了核心的光照计算之外还需要对标准PBS的其他代码进行修改和扩展，这里附一张Unity的Standard实现示意图： Unity的Standard Forward绘制调用示意图 在此基础上去扩展，对内主要包括修改和扩展上图中的数据结构、光照模型和绘制过程。对外给用户提供可选择的Shading Model： UE4的Shading Model和Unity扩展代码文件 上图左为UE4引擎的Shading Model，右为本文在Unity中按UE4方式扩展实现的代码文件列表，其中TT_Unity???.cginc文件为Unity内部的Unity???.cginc文件的修改扩展版本。 其中，TT_UnityStandardBRDF.cginc扩展了各个Shadering Model的实现： half4 SKIN_BRDF_PBS(half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi, half4 sssTex) { ... } half4 HAIR_BRDF_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi, float3 tangentWorld = float3(0, 0, 1), half2 anisoCtrl = half2(1, 1)) { ... } half4 CLEARCOAT_BRDF_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi, float3 normal_clearcoat, UnityIndirect gi_clearcoat) { ... } half4 FABRIC_BRDF_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi) { ... } TT_UnityStandardCore.cginc根据Shadering Model的类型选择实现： half4 fragForwardBaseInternal (VertexOutputForwardBase i) { ... #if _SKIN half4 c = SKIN_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect, sssTex); #elif _HAIR half4 c = HAIR_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect, s.tangentWorld, anisoMap.rg); #elif _CLEARCOAT half4 c = CLEARCOAT_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect, s.normalWorld_clearcoat, gi_clearcoat.indirect); #elif _FABRIC half4 c = FABRIC_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect); #else half4 c = UNITY_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect); #endif ... } ... half4 fragForwardAddInternal (VertexOutputForwardAdd i) { ... #if _SKIN half4 c = SKIN_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, light, noIndirect, sssTex); #elif _HAIR half4 c = HAIR_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, light, noIndirect); #elif _CLEARCOAT half4 c = CLEARCOAT_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, light, noIndirect, s.normalWorld_clearcoat, noIndirect); #elif _FABRIC half4 c = FABRIC_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, light, noIndirect); #else half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, light, noIndirect); #endif ... } 具体的Shader文件去定义Shadering Model的类型，比如TT_Character_Skin.shader定义了_SKIN类型： SubShader { Tags{ "RenderType" = "Opaque" "PerformanceChecks" = "False" } LOD 300 CGINCLUDE ... #define _SKIN 1 ... } Unity的原生内置着色器cginc文件在类似目录：C:\Program Files\Unity 2018.1.0b13\Editor\Data\CGIncludes\，也可到官网下载Unity安装版本对应的内置着色器代码。 下面对各个Shading Model的理论模型和具体实现依次大致介绍。 2.1 Subsurface The Subsurface Shading Model simulates the effect of Subsurface Scattering. This is a real-world phenomenon in which light penetrates a surface and then diffuses throughout it. It can be most readily seen on such objects as ice, wax candles, and skin. the final colour of our pixels depend is the sum of two components. The first one is the “traditional” lighting. The second one is the light contribution from a virtual light source illuminating the back of our model. This gives the impression that light from the original source actually passed through the material. Subsurface理论模型 Subsurface模型实现核心代码： half4 SKIN_BRDF_PBS(half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi, half4 sssTex) { ... // Translucency float3 H = normalize(light.dir + normal * _Distortion); float transDot = pow(saturate(dot(viewDir, -H)), _Power) * thickness * _ThicknessScale; half3 lightScattering = transDot * _SubColor; ... } Subsurface模型实现效果图 2.2 Skin The Preintegrated Skin Shading Model is very similar in nature to the Subsurface model, but geared toward low performance cost skin rendering on human characters. the higher curvature on the nose creates stronger incident light gradient, Which will result in a lot more visible scattering. 皮肤的Ramp图 Skin模型实现核心代码： inline fixed4 SKIN_BRDF_PBS(half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi, half4 sssTex) { ... // Skin Lighting float2 brdfUV; // Half-Lambert lighting value based on blurred normals. brdfUV.x = dotNL * 0.5 + 0.5; brdfUV.y = curvature; // Curvature amount. Multiplied by light's luminosity so brighter light = more scattering. half3 diffuseTerm = diffColor * light.color * tex2D( _BRDFTex, brdfUV ).rgb; ... Skin模型实现效果图 2.3 ClearCoat The Clear Coat Shading Model can be used to better simulate multilayer materials that have a thin translucent layer of film over the surface of a standard material. In addition to this, the Clear Coat Shading Model can also be used with either a metal or nonmetal surfaces. In fact, it was specifically designed to model this second class of smooth colored films over a non-colored metal. Some examples include acrylic or lacquer clear coats, and colored films over metals such as soda cans and car paint. Paint Layers示意图 ClearCoat模型实现核心代码： half4 fragForwardBaseInternal (VertexOutputForwardBase i) { ... #if _CLEARCOAT FragmentCommonData s_clearcoat = (FragmentCommonData) 0; s_clearcoat.specColor = _ReflectionSpecular.rgb; s_clearcoat.smoothness = _ReflectionGlossiness; s_clearcoat.normalWorld = s.normalWorld_clearcoat; s_clearcoat.eyeVec = s.eyeVec; s_clearcoat.posWorld = s.posWorld; UnityGI gi_clearcoat = FragmentGI(s_clearcoat, occlusion, i.ambientOrLightmapUV, atten, mainLight); half4 c = CLEARCOAT_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect, s.normalWorld_clearcoat, gi_clearcoat.indirect); #endif ... } half4 CLEARCOAT_BRDF_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi, float3 normal_clearcoat, UnityIndirect gi_clearcoat) { half4 c = BRDF1_Unity_PBS(diffColor, specColor, oneMinusReflectivity, smoothness, normal, viewDir, light, gi); // SPECULAR & SMOOTHNES diffColor = 0; specColor = _ReflectionSpecular.rgb; smoothness = _ReflectionGlossiness; oneMinusReflectivity = 1 - SpecularStrength(specColor); c += BRDF1_Unity_PBS(diffColor, specColor, oneMinusReflectivity, smoothness, normal_clearcoat, viewDir, light, gi_clearcoat); return c; } ClearCoat模型实现效果图 2.4 Cloth For fabrics, like black velvet, the most distinguishing features are due to rim lighting (both forward and backward scattering). If the light is in the same direction as the viewer then specular contributes most towards the edge of the object due to backscattering and how the fabric is constructed. Tiny fibers are attached to the surface so that they try to stand up straight. When the light and view direction are aligned the light will backscatter when the surface normal is 90 degrees from the light or view direction. Additionally, if the light is behind the objects the fibers will forward scatter light through giving a nice rim light effect. 理想反射与实际织物的反射对比 Cloth模型实现核心代码： inline float FabricD (float NdotH, float roughness) { return 0.96 * pow(1 - NdotH, 2) + 0.057; } inline half FabricScatterFresnelLerp(half nv, half scale) { half t0 = Pow4 (1 - nv); half t1 = 0.4 * (1 - nv); return (t1 - t0) * scale + t0; } half4 FABRIC_BRDF_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi) { ... float D = FabricD (nh, roughness); ... half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm) + specularTerm * light.color * FresnelTerm (specColor, lh) + _FabricScatterColor * (nl*0.5 + 0.5) * FabricScatterFresnelLerp(nv, _FabricScatterScale); ... Cloth模型实现效果图 2.5 Hair 我们日常生活中有很多物体呈现各向异性反射效果。比如：拉丝金属，毛发，光碟等。一般这种反射效果是由物体表面的微表面特性导致的：物体表面主要由大量的方向一致的细长划痕或纤维微表面组成。 比如，拉丝金属物件表面由大量平行的丝状划痕组成；光碟的表面由一圈一圈的环形细小轨道（用于存放数据）组成；头发的表面由大量的头发丝组成等。沿着这些划痕或纤维的法线分布不同于通常的垂直于表面的法线分布， 使得物体整体的光照反射表现呈现各向异性。 各项异性模型 Hair模型实现核心代码： inline half AnisoDCore(half smoothness, half3 normalWorld, half3 tangentWorld, half3 halfDir, half nh, half D, half gloss, half spec, half mask) { half3 Y = cross(normalWorld, tangentWorld); half RoughnessX = SmoothnessToRoughness(saturate(smoothness * gloss)); RoughnessX += !RoughnessX * 1e-4f; half mx = RoughnessX * RoughnessX; half XdotH = dot(tangentWorld, halfDir); half YdotH = dot(Y, halfDir); half d = XdotH * XdotH / (mx * mx) + YdotH * YdotH + nh * nh; d += !d * 1e-4f; half Da = 1 / (UNITY_PI * mx * d * d); D = lerp(Da, D, mask); D *= lerp(spec, 1, mask); return D; } inline half3 JitterTangent(half3 T, half3 N, float shift) { half3 shiftedT = T + shift * N; return normalize(shiftedT); } inline half AnisoD(half smoothness, half3 normalWorld, half3 tangentWorld, half3 halfDir, half nh, half D, half2 anisoCtrl) { half jitter = anisoCtrl.r; half mask = anisoCtrl.g; half3 tangentWorld1 = JitterTangent(tangentWorld, normalWorld, 0 + _TangentShift1); half AnisoDLow = AnisoDCore(smoothness, normalWorld, tangentWorld1, halfDir, nh, D, _AnisoGloss1, _AnisoSpec1, mask); half3 tangentWorld2 = JitterTangent(tangentWorld, normalWorld, jitter + _TangentShift2); half AnisoDHigh = AnisoDCore(smoothness, normalWorld, tangentWorld2, halfDir, nh, D, _AnisoGloss2, _AnisoSpec2, mask); return AnisoDLow + AnisoDHigh; } half4 HAIR_BRDF_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi, float3 tangentWorld = float3(0, 0, 1), half2 anisoCtrl = half2(1, 1)) { ... float D = GGXTerm(nh, roughness); D = AnisoD(smoothness, normal, tangentWorld, halfDir, nh, D, anisoCtrl); ... } Hair模型实现效果图 完整代码见：https://lab.uwa4d.com/lab/5b69a13ed7f10a201f02360ad 三、Further 目前的代码框架扩展实现了上面列举的几种常见的Shading Model，未来的工作就是在这些Shading Model的基础上去风格化，或者扩展新的Shading Model。 另外，说明一下从源码级别自定义PBR的意义：目前的材质节点编辑器，比如Amplify Shader Editor，生成的是Surface中间代码，内部走的是Standard流程；Shader Forge（目前已停更），生成的代码调用的也是内置的Standard接口；Unity官方的SRP配套使用的Shader Graph只支持Standard材质。 参考文献： https://marmoset.co/posts/physically-based-rendering-and-you-can-too https://www.alanzucconi.com/2017/08/30/fast-subsurface-scattering-1 https://docs.unrealengine.com/en-us/Engine/Rendering/Materials/MaterialProperties/LightingModels http://web.engr.oregonstate.edu/~mjb/cs519/Projects/Papers/HairRendering.pdf 这是侑虎科技第497篇文章，感谢作者Young供稿。欢迎转发分享，未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者主页：https://www.zhihu.com/people/chen-yong-59-86，作者也是U Sparkle活动参与者，UWA欢迎更多开发朋友加入U Sparkle开发者计划，这个舞台有你更精彩！
Unity引擎模块分析—注重效率的你不妨这么做！
UWA GOT是UWA推出的本地性能优化工具，其Online功能限时免费中，并受到了大量开发者的试用和积极反馈。其中，它的总体性能分析模式主要便于研发团队直观地定位、验证和解决逻辑代码中的效率瓶颈。 然而我们知道，引擎模块的性能同样至关重要，仅仅逻辑代码的检测还无法实现系统、完整地定位瓶颈的目的。所以今天，我们在该工具中推出Unity引擎模块的检测。您只需要拥有一个UWA GOT，在本地真机上测试后即可把数据同步到官网，这些详细的数据就能立马呈现。 详细的引擎模块参数 目前，UWA GOT Online报告支持渲染、UI、粒子、动画、加载和物理这六大主流引擎模块的重要参数检测。如下图，在渲染模块页面中，我们可以查看不透明、半透明、视域裁剪CPU和相机后处理等CPU耗时的走势以及某一帧上的耗时。 我们也可以缩放某段测试区域，结合运行截图更直观地定位某些峰值处的表现。 对于引擎模块的重要参数，我们提供了UWA推荐值和相关的优化建议，便于研发团队进行有的放矢的排查和优化。 精准的逻辑代码堆栈 堆栈信息是我们定位性能瓶颈的鹰眼，这次版本的更新不仅丰富了引擎模块的重要参数检测，在逻辑代码中也可以查看更细致的引擎函数堆栈信息，并且通过总体、指定场景和指定帧来定位具体的调用详情。 特别的，在堆栈信息列表中，我们也提供了历史数据对比功能，针对于研发团队定制的性能或功能回归测试，我们可以直接在报告中查看指定重要参数的变化趋势，便于开发团队可以在第一时间验证测试结果。 这次的新功能小编就安利到这里啦！目前，该功能也在限免使用中，研发团队只需要拥有一个UWA GOT，就可以在总体性能分析模式中体验到该功能！还不快打开你的黑科技试一试呀^_^！ 关于UWA GOT Online UWA GOT 是UWA推出的本地性能测评工具，它方便研发团队在本地真机测试，并能在编辑器中搭建本地服务器查看性能数据。现在，在UWA GOT现有的本地测评模式上，我们增加了线上同步和分析功能，研发团队可以将本地测评的数据快速同步到UWA官网，生成在线测评报告，提供更高效的性能分析数据及有针对性的优化建议。同时，贴心的版本对比功能，方便研发团队轻松监测每次测试中的重要性能参数变化趋势，从质量管控上为项目的研发保驾护航。
本期聚集话题：PBR在手游开发中的适用性、如何设计渲染等级（进阶版）、如何判断硬件支持GPU Instancing、25个你需要规避的DrawCall增长的问题... 这是第109篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题，建议阅读时间15分钟，认真读完必有收获。 UWA 问答社区：answer.uwa4d.com UWA QQ群：793972859（仅限技术交流） 渲染 Q1：我们是MMO游戏，现在想给主角（包括其它玩家）使用PBR材质（Untiy自带的Standard），其它角色和场景不使用。不知这种情况下性能是否能抗住，同时我也想了解下目前手游中对于PBR材质的使用情况。 UWA：目前手游中使用PBR材质的项目明显增多，特别是MMO、RPG等项目中，主要的角色、NPC和大Boss等都开始往PBR方面走了。 如果PBR仅是在以上情况下使用时，Unity引擎自带的PBR是没有太大性能问题的，因为一般角色占据的屏幕面积都会比较小，而过剧情时，虽然屏幕面积占据较大，但时间一般都很短，所以给总体性能造成的压力不大。 但是UWA暂时还不建议大家在地形等常常在屏幕中占据较大面积区域的物体上使用PBR（尤其是中低端设备），因为它确实会造成较高的GPU压力，我们在UWA Day 2018上也对此进行了定量分析（类似下图），这点需要研发团队在使用时注意。但是，一切应该以自身项目测试为主，具体查看项目在自己设定的低配机器上到底性能如何。 最后，在使用Standard Shader时，除了关注性能外，还要关注下它对于内存的影响，可以参考这篇文章：【求知探新】Unity中ShaderLab内存优化 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ae2f40132493548fbd32bbc 渲染 Q2：UWA DAY 2018大会中说有25种造成DrawCall增长的原因，我想知道是哪25种呢？ 导致批处理失败的原因如下： 1、Additional Vertex Streams — 对象使用MeshRenderer.additionalVertexStreams设定了额外的顶点信息流。 2、Deferred Objects on Different Lighting Layers — 该物件位于另一不同的光照层中。 3、Deferred Objects Split by Shadow Distance — 两个物体中有一个在阴影距离范围内而另一个不是。 4、Different Combined Meshes — 该对象属于另一个已合并的静态网格。 5、Different Custom Properties — 该对象设定了不同的MaterialProperyBlock。 6、Different Lights — 该物件受不同的前向光照（Forward Light）影响。 7、Different Materials — 该对象使用不同的材质。 8、Different Reflection Probes — 该对象受不同的反射探头（Reflection Probe）影响。 9、Different Shadow Caster Hash — 该对象使用其他的阴影投射着色器，或是设定了不同的着色器参数/关键词，而这些参数/关键词会影响阴影投射Pass的输出。 10、Different Shadow Receiving Settings — 该对象设定了不同的“Receive Shadows”参数，或是一些对象在阴影距离内，而另一些在距离之外。 11、Different Static Batching Flags — 该对象使用不同的静态批处理设定。 12、Dynamic Batching Disabled to Avoid Z-Fighting — Player Settings中关闭了动态批处理，或在当前环境中为避免深度冲突而被临时关闭。 13、Instancing Different Geometries — 使用GPU Instancing渲染不同的网格或子网格。 14、Lightmapped Objects — 对象使用了不同的光照贴图，或在相同的光照贴图中有不同的光照贴图UV转换关系。 15、Lightprobe Affected Objects — 对象受其他光照探头（Light Probe）影响。 16、Mixed Sided Mode Shadow Casters — 对象的“Cast Shadows”设定不同。 17、Multipass — 对象使用了带多个Pass的着色器。 18、Multiple Forward Lights — 该物件受多个前向光渲染影响。 19、Non-instanceable Property Set — 为instanced着色器设定来non-instanced属性。 20、Odd Negative Scaling — 该对象的缩放为很奇怪的负值，例如(1,-1,1)。 21、Shader Disables Batching — 着色器使用“DisableBatching”标签显式关闭了批处理。 22、Too Many Indices in Dynamic Batch — 动态批处理索引过多（超过32k）。 23、Too Many Indices in Static Batch — 静态批处理中的组合网格索引过多。对于OpenGL ES来说是48k，OSX是32k，其他平台是64k。 24、Too Many Vertex Attributes for Dynamic Batching — 欲进行动态批处理的子网格拥有超过900个顶点属性。 25、Too Many Vertices for Dynamic Batching — 欲进行动态批处理的子网格顶点数量超过300个。 感谢果果爸@UWA问答社区提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ae1eef932493548fbd32ba1 GPU Q3：我在国产安卓机上发现不少据说支持ES 3.0、但是实际上却不支持GPU Instancing功能的情况。那么现在大家都是如何判断硬件支持GPU Instancing的？ UWA：建议使用SystemInfo.supportsInstancing，链接： https://docs.unity3d.com/ScriptReference/SystemInfo-supportsInstancing.html 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ae714b00956834900752685 渲染 Q4：接我们上期的问答《如何设计项目中的渲染等级》推出后，又有两位行业大咖分享了他们的灼见： 王亮：我们的项目还在开发中，也经历了同样的过程，最后也形成了类似的文档。这个过程中我的体会是，虽然感觉最后形成的是一个表，会根据这个表提供机型的预设配置。但是实际上针对自己的项目，高中低主力机型，FPS的目标，渲染效果的要求不同，结果可能会大不一样。 抛砖引玉分享一下我的思路： 1、首先是确定测试用例 我们分别选取主城镇、不同战斗类型的最高面数场景作为场景用例 2、角色方面选择不同战斗类型的最高面数角色 3、接下来配置测试关卡，编写采样脚本 本次测试数据采用脚本记录帧数功能 测试方法集成到了Cheat上，用于测试指定时间的帧数范围和平均帧数粗略地描述性能 满怪渲染：主角绕着小怪 把测试的所有小怪聚拢 保持同屏全部渲染 满怪释放技能：尽量保证打中尽可能多的小怪 验证粒子压力 4、请测试同学跑测试组合用例，并且采样数据 我选取第2次测试报告作展示如下： 其实中间经过了几次迭代。表格里的最后一列是针对帧数目标，经过反复调整配置形成的数据。 5、最后根据测试数据，调整分级表的配置 最终形成的这次配置如下，红色是本次测试，调整和新增的内容： 以及本次测试用发现的问题： 感谢王亮@UWA问答社区提供了回答 文雅：正好我也整理过项目的LOD规则，可能不限于题主描述的渲染等级。一套好的LOD系统需要美术在资源上做大量的资源分级工作，需要程序搭建完善的LOD框架和辅助工具，每增加一个功能模块都需要把LOD考虑进来。 一、如何确定项目的LOD分档和性能标准 LOD（Level of Detail），这里的D代表Detail而不是Distance，也就是说，所有游戏画面和游戏功能的细节都可以进行分级，不必受限于距离。 1、如何制定性能标准？ 在分级之前，需要确定目标（省电、正常的游戏体验、噱头），我们要在哪一种机型上面运行哪些表现效果和功能，达到怎样的性能目标（帧数FPS、内存占用、Drawcall、同屏三角面数等）。 ◆ 经验总结： 1）画质的表现力与性能开销是相互矛盾的 2）需要持续维护，相关功能系统的设计都需要考虑到不同画质 3）极简画质牺牲了可玩性，同时会增加LOD框架的复杂性，使用最简单暴力的方式去处理 ◆ 兼容性问题： 1）低配画质需要考虑的适配问题 • 可能不支持Shader定义8张以上的贴图纹理 • 可能不支持ETC2格式的贴图纹理 • 可能不支持OpenGL ES3.0 2）高配画质需要考虑的问题 • 支持PBR、线性空间 • 奇怪的Bugs 3）需要解决各个画质下的兼容性问题，建立黑名单和白名单。 2、如何找出可以进行分级的细节？ ◆ 优先找出开销大的点 • 后处理效果Bloom、HDR、ToneMapping、MotionBlur、DOF等 • 实时光照和阴影、水面实时反射 • PBR物理光照 • Ragdoll、DydamicBones等物理系统 • 日夜循环、特效天气系统 ◆ 细节模块的LOD功能考虑 • 场景 / 角色 / 特效 / 摄像机相关 / 其他系统模块 二、LOD模块细分 1、场景相关 ◆ Shader LOD • shader.globalMaximumLOD来指定不同画质的LOD值 • Shader内部定义多个SubShader，逐个降低计算和纹理采样 使用Shader LOD存在一个问题，在Properties中定义的贴图，在低级别的SubShader中虽然不被采样和计算，但是仍会有内存占用。建议和角色一样，设计LOD框架时考虑两份prefab，支持场景转一份低配场景文件给低配使用。 ◆ 制作时合理使用高低模 ◆场景物件分级Level_1、Level_2、Level_3 • 含场景特效、场景动画物件等，在不同的级别显示/隐藏 最早期我们采用距离来控制场景特效的显示。 缺点： 1、实时监控距离的开销 2、每个场景需要单独配置合理的距离值 使用层级来控制更为直接，在制作LOD时最优先考虑最简单暴力的方式，这样美术规范也不会太复杂，后期也不需要过多的维护。 ◆ 场景光照切换 • Light/Light_High 涉及实时灯光对角色、场景阴影绘制的控制 • 场景光照图切换 ◆ 物件的Layer层决定阴影 • 场景文件中配置的Cast Shadows和Receive Shadows设置给烘焙使用，不方便存储下来 • 设计不同的Layer来决定物件是否产生阴影和接受阴影 ◆ 角色产生阴影 • 高配时玩家和Npc角色的Layer修改为Shadow，低配时修改为Player/Npc • 中配时玩家自身和Boss角色的Layer修改为Shadow，其他玩家为Player • Shadow层绘制实时阴影，Player/Npc会使用圆片来绘制脚底阴影 ◆ 角色接收阴影 • 仅在高配画质开启接收阴影 ◆ 裁剪距离、雾效距离 • 设计裁剪距离与雾效距离按照百分比减小的算法，防止数值异常 ◆ 后期处理效果 • 全局后期处理 通过全局后期处理管理后期的开启、关闭，控制自定义的选项。 • 场景后期处理 场景配置高、低两种后期处理方案，低配时只使用ColorGrading • UI和剧情动画中的后期处理只在高配时开启 比如：UI中使用的Bloom、剧情中使用的RadialBlur运动模糊等 ◆ 单个场景的特效等级 通过配置表定义场景的类型和同屏可显示的特效数量和等级 2、角色相关 ◆ 角色材质、shader、贴图 • 通过配置表，在不同画质调用不同的prefab • A.prefab和A_Low.prefab使用A.mat和A_Low.mat • A_Low.mat使用的Shader降低计算和贴图纹理采样 • A_Low.mat使用的_MainTex替换低精度贴图纹理 ◆ LODGroup • 角色的模型面数标准提升后，制作高模、低模两档，通过LODGroup功能按距离降档 ◆ SubShader • 高配和中配的光照模型切换，PBR切换Blinn-Phong ◆ 角色部件 • 部件在配置表中可配置为空，在低配时可以不显示背部挂件 ◆ 角色骨骼Skin Count • Skin支持的最大骨骼数降级2-Bones 1-Bones • DynamicBone基于物理系统的动态骨骼效果的开关 • Npc死亡动作基于物理系统的表现的开关 ◆ 同屏角色数量 • 不同画质设置不同的同屏显示角色上限 • 无逻辑任务的客户端NPC开关 3、特效相关 ◆ 天气系统 • 制作不同数量级的粒子系统 ◆ 脚步特效 • 开启和关闭脚步特效，可对玩家自身和其他玩家分开控制 ◆ 掉落特效 • 复杂特效可以制作简化版本 以上三种特效都可以通过简单的配置表功能完成。 ◆ 技能特效 • 通过配置表，在不同画质调用不同的Prefab ◆ 技能特效的标准 • 制作阶段控制特效的性能开销 • 工具辅助生成*_Low.Prefab • 严格控制*_Low.Prefab的性能开销 4、其他模块 ◆ 渲染分辨率 • 使用不同的渲染分辨率，且限制最高分辨率为1080p ◆ 开放视角 • 视角上下左右旋转和 • 摄像机最远最近距离 ◆ 脚本控制 特殊情况下需要制作可以在四种画质下进行切换的配置，通过脚本控制： • Prefab切换 • Material切换 5、不适合LOD的系统模块 ◆ UI • UI上的图素和特效不适合做显隐和优化 • UI场景不适合做烘焙和实时光的切换 • UI角色可以根据界面的重要程度选择使用高级或低级 • 管理好加载和卸载后，可以考虑替换高低配UI资源 ◆ 剧情 感谢文雅@UWA问答社区提供了回答 欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5acc208b425802635474fc7d 今天的分享就到这里。当然，生有涯而知无涯。在漫漫的开发周期中，您看到的这些问题也许都只是冰山一角，我们早已在UWA问答网站（answer.uwa4d.com）上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入，也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。 官网：www.uwa4d.com 官方技术博客：blog.uwa4d.com 官方问答社区：answer.uwa4d.com 官方技术QQ群：793972859（仅限技术交流） 本文封面图来源： https://www.artstation.com/artwork/nWlL6
本期聚集话题：PBR在手游开发中的适用性、如何设计渲染等级（进阶版）、如何判断硬件支持GPU Instancing、25个你需要规避的DrawCall增长的问题... 这是第108篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题，建议阅读时间15分钟，认真读完必有收获。 UWA 问答社区：answer.uwa4d.com UWA QQ群：793972859（仅限技术交流） 渲染 Q1：现思考如何设计项目中的渲染等级，求有经验的大佬指个方向，例如ShaderLOD，还有其他什么思路吗？谢谢！ 整理了一下我们游戏中效果分级的内容，供参考： 感谢贾伟昊@UWA问答社区提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5acc208b425802635474fc7d 渲染 Q2：我用2017.2.0渲染一张地图，为什么在小米4上Camera.Render会有很高的耗时？一个空场景，为什么Camera.Render耗时这么高？ UWA：我们做了一个简单测试来研究一下这个问题，即在场景中只渲染一个Cube。 机型： （1） 华为6Plus （2） 小米4 Profiler截图如下，蓝色框：华为6Plus 红色框：小米4 由上图可知，小米4的Camera.Render耗时不稳定，有时候会出现10+ms的峰值。 通过查看Unity Profiler以及和其他设备进行对比，发现了以下两点是造成小米4上耗时不稳的原因： 1、Culling不稳 下图为小米4和华为6Plus设备上，Culling操作的耗时比较。蓝框为华为6Plus设备，红框为小米4设备。通过Timeline可以看到是场景SceneNode的计算导致。 2、Drawing不稳 渲染耗时也同样不稳定，如下图所示：通过Timeline发现，小米4设备上，主线程的渲染进程会经常等待子线程完成后再进行后续操作，而同样的App，在华为6Plus上则不会如此表现，如下图所示。 小米4： 华为6Plus： 因此，对于上述测试做个总结： （1） Culling中的SceneNode计算操作在小米4设备上经常伴随更为耗时的CPU开销； （2） 小米4上更容易表现出主线程渲染等待子线程的情况。 以上两点是题主问题的主要原因，但至于更深层次的解释，这个恐怕要联系手机研发厂商甚至底层芯片硬件厂商了。但需要说明的是，这仅是一个简单测试例子的表现情况，复杂场景并不一定同样适合。但测试和分析方法都是不变的，题主后续如有需求，可自行测试寻找答案即可。 欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ada00363dba2a108c7e3236 渲染 Q3：我们用的Unity 5.5场景带有2级LOD，我们使用Lightmap渲染场景，结果次级的LOD模型渲染出来的阴影面是纯黑的，这个该怎么办呢？烦请大神指点！ Untiy官方在5.5版本里其实是不支持LOD的Lightmap的，即便是到了2017.4版本里，官方文档也是让你去使用Light Probes：https://docs.unity3d.com/Manual/LODForBakedGI.html 当然它也说到： “When you use the Progressive Lightmapper, there is no need to place Light Probes around the LOD Group to generate baked indirect lighting. However, to make Realtime GI affect the Renderers in the LOD Group, you must include the Light Probes.” 我们也是在用Unity 5.5版本，没有试过Progressive Lightmapper，不知道效果如何。所以第一个解决方案是升级到Unity 5.6或者Unity 2017尝试下Progressive Lightmapper。 如果想在Unity 5.5的下面做，我们是使用手动拷贝烘焙后的scale和offset的方式来实现，就是在运行时将原始模型的光照题图信息完整地拷贝给LOD低层级的模型。但是这有一个非常强的限制：要保证两个模型的UV2是完全可以对应上的，否则UV2可能会出现一些错乱。使用Simpylgon这样的中间件可以做到尽量保持UV2不变。 这样做有两个问题： 如果是减少了很多面的模型，比如删除掉了一个组件，结果组件露出来的面因为之前是被遮挡的，会是黑色的。不过大部分情况下减面很多的LOD模型也会距离很远，这种通常可以接受； 运行时设置光照信息的材质不能进行静态合批，否则在设备上会有问题。 另外还有一个就是因为是运行时修改的，所以在编辑器模式下如果不让设置组件可以运行的话，会有预览效果不对的问题，运行状态下可以保证正确。 原始LOD0烘焙效果： LOD1为了避免黑所以不参与烘焙的效果： 运行时修改了光照贴图参数的效果： 设置光照贴图参数的Component代码： public class RendererLightMapSetting : MonoBehaviour { public int lightmapIndex; public Vector4 lightmapScaleOffset; public void SaveSettings(SceneLightMapSetting setting) { if (!IsLightMapGo(gameObject)) { return; } Renderer renderer = GetComponent<Renderer>(); if (setting) { lightmapIndex = setting.GetGlobalIndex(renderer.lightmapIndex); } else { lightmapIndex = renderer.lightmapIndex; } lightmapScaleOffset = renderer.lightmapScaleOffset; } public void SetSettings(Renderer renderer) { if (renderer != null) { lightmapIndex = renderer.lightmapIndex; lightmapScaleOffset = renderer.lightmapScaleOffset; } } public void LoadSettings() { if (!IsLightMapGo(gameObject)) { return; } Renderer renderer = GetComponent<Renderer>(); renderer.lightmapIndex = lightmapIndex; renderer.lightmapScaleOffset = lightmapScaleOffset; } public static bool IsLightMapGo(GameObject go) { if (go == null) { return false; } Renderer renderer = go.GetComponent<Renderer>(); if (renderer == null) { return false; } return true; } void Awake() { if (Application.isPlaying) { LoadSettings(); } } } 然后，给美术提供一些可以做一键设置和清理的功能，逻辑可以自己写。 感谢贾伟昊@UWA问答社区提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ad836acbdcd031091afdb80 渲染 Q4：什么情况下Transparent的材质在Render.OpaqueGeometry过程中渲染？ UWA：Render.OpaqueGeometry和Render. TransparentGeometry的执行，是依据RenderQueue<2450来区分的，RenderQueue的大小可以完全控制物体渲染的顺序，比如把半透物件渲染顺序调整到不透明物件之前，让Grass在Terrain前面画，在一些使用多层纹理的项目中，是一个比较不错的选择。通过RenderQueue来控制物体的渲染顺序，改变RQ来达到想要做的特殊事情。 欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ad6be3e9f0be03717942a9b 2D Q5：之前是看UWA相关的视频，把这个脚本挂在预设上的，然后通过这次的测试结果看到这个函数在解压的时候占了很高的堆内存，这个脚本是在解压资源的时候也会被调用么？ 另外，我们昨天用UWA GOT在跟踪解压时堆内存的问题时，发现并没有出现网站上报告的PolygonImage这个问题啊？是因为什么呢？ UWA：PolygonImage的实现是作为一个MeshEffect来修改对应的Image的几何（把四边形变为多边形），过程中会出现一定量的堆内存分配（多边形越复杂，分配的堆内存越多）。 而MeshEffect的触发，只发生在对应的Image被重建的时候：被激活、修改颜色（透明度）、修改长宽等等… 所以该效果不建议使用在频繁变化的Image上，会造成大量的堆内存分配。 而题主在UWA GOT中没有观察到这一项，很可能是因为没有打开（激活）某些使用了PolygonImage的界面。 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ad555284063550c15b52e3a 今天的分享就到这里。当然，生有涯而知无涯。在漫漫的开发周期中，您看到的这些问题也许都只是冰山一角，我们早已在UWA问答网站（answer.uwa4d.com）上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入，也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。 官网：www.uwa4d.com 官方技术博客：blog.uwa4d.com 官方问答社区：answer.uwa4d.com 官方技术QQ群：793972859（仅限技术交流） 本文封面图来源：经典动作游戏《战神》 http://jonvilma.com
本期聚集话题：RTS游戏的寻路系统、AssetBundle打包后md5不同、UGUI中3D血条优化问题、如何快速改变场景里一部分物件的外观效果... 这是第107篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题，建议阅读时间15分钟，认真读完必有收获。文末，我们的互动话题是：能耗发热怎么办？期待你的灼见！ UWA 问答社区：answer.uwa4d.com UWA QQ群：793972859（仅限技术交流） 本文封面图来源：RTS手游《战地指挥官》 http://zdzhg.53mayi.com 寻路 Q1：我们在做一个RTS游戏，开始用的是Unity自带的NavMesh的寻路，但发现这个并不适合RTS多人寻路，因为总会出现阻挡和闪跳的问题。看Asset Store上的A* path插件评论说在碰撞上有问题，不知道大家是否能提供比较成熟的解决方案？ 我们上一个项目是RTS项目，刚开始寻路问题解决方案过程中遇到过和你相似的疑惑，修改探索和调教了很长时间，分享几点供题主参考下。 先上结论：我们采用了两层结构 A Start Pathfinding Project Pro + Unity NavMesh。 1）主体寻路方案使用 A Start Pathfinding Project Pro 这个插件中传统的网格形式，并进行了深度修改扩展，主要是满足寻路过程的通路性需要考虑到单位半径大小，这样寻路精度高； 2）使用Unity NavMesh系统作为寻路过程中的碰撞系统，因为你说的很对，这个寻路插件的碰撞有问题，稍后解释。 以上两部分组成“寻路算法+精确碰撞”。接下来说下以上方案的原因和细节。 1、NavMesh寻路速度快但不够精确。RTS还是适合使用传统的四方形NavGrid格子寻路，而且现有的任何寻路算法默认都是不支持考虑寻路单位半径大小对寻路过程的影响，但我们策划绝不允许1米宽的缝隙被一个两米宽的胖子挤过去，如果使用Navmesh并且动态生成阻挡会出现寻路路径是有了，可是由于碰撞问题大的单位会卡在缝隙这里来回“发抖”的情况；这个只能自己动手修改，所以我们买了该插件然后自己从里到外修改，具体修改算法原理和修改后的示意图，你可以参考这里：http://www.cnblogs.com/yaukey/p/rts_unit_traverse_size_based_path_finding.html。 修改完后，每次寻路会将单位的直径大小当做寻路参数之一传入进行寻路。 使用这个插件开启两个线程左右来寻路是我后来调教出的、一个比较理想的结果。然后是地图中各种单位会出生和死亡，当单位站定不动，需要生成阻挡；移动了需要移除阻挡恢复此处的通路性，通过再修改下插件中的DynamicObscule这个组件（原生功能太简单）来实现；我们具体是每个单位都有一个对应的大小的CapsuleCollider空对象挂上这个脚本，单位站定将该物体放置到单位的位置并激活它，单位移动隐藏该空物体即可，这个脚本会引起地图通路性的更新，这个更新是多线程的，这一点是要注意的，通过回调事件来做响应的事情，切记不要在同一帧或者下一帧去做，因为即时性不能保证。这样就能让整个地图一直根据单位来更新通路性。 2、寻路的通路性问题解决了，现在来解决碰撞问题。这个插件自带了碰撞算法，使用的rvo算法，但是据我当初各种调教实验，它的碰撞都达不到我们的要求，单位多了拥挤的时候总是会有些单位站立后变得重叠了，最不能接受的是两个单位完全重叠。在这里我花了相当多的时间都不能修改出的满意的效果。后来想起来recastnavigation作者亲口说过虽然Unity的NavMesh基于此，但是Detour系统经过深度的改写，Detour包含了巡航和碰撞，而实际使用也发现NavMeshAgent之间的碰撞是很精确的，于是我想了个取巧的办法，给地表再铺一层跟AStar插件的寻路面积一样大的NavMesh，但是没有任何的阻挡（理解为一片纯蓝色），同时给寻路单位添加一个NavMeshAgent，然而这个东西只用它的碰撞，而不使用寻路（没有阻挡，任何寻路都是两点直线，可以考虑几乎无开销），这样最后实际的碰撞效果才算达到我们满意的效果。 以上就是我所说的两层含义，我再说下坑和问题。 1）当初开发Unity版本是Unity 5.3.7-5.3.8，插件版本5.x，寻路插件中底层会有Physics.CheckCapsule调用，但是多线程大量调用，会导致底层PhysX物理引擎崩溃，游戏闪退，不必现但是大量的话很容易一下就挂，插件官方论坛也有这个Bug讨论，后来我将此换成了两个 Physics.CheckSphere 调用，目前2017版本没有测试过； 2）我在1中使用的方法，在UWA性能评测中，每次都是Dynamic Collider超标，这个需要自己取舍了，但是总体我们游戏的瓶颈不在寻路一块，消耗在这些测试中看来比较理想； 3）插件中由于是多线程更新，有很多的完成事件回调，基本都是利用这些链接游戏系统中，不够了自己加，否则依然是容易在未更新完成时得到错误的结果； 4）有可能的话再优化下内部的一些堆内存分配，并且和游戏系统结合后注意释放和断开引用，否则容易内存泄漏，造成寻路插件被引用以至于后面引用很长一串对象都不得释放； 5）寻路精度和碰撞精度在游戏中的表现，需要花比较多的时间去调教参数，有时候表现不好未必是方案不好，然后如果你们对实时同步性要求较高，这个方案还必须再进一步完善和扩展下。 我们使用这个寻路方案的项目是：《战地指挥官》，你可以下载看看寻路是否符合你的要求，苹果安卓各平台都有。以上是我暂时想起来的一些心得，希望对题主有所帮助：） 感谢Yaukey@UWA问答社区提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ad0553b910222635366dd34 渲染 Q2：我想要快速改变场景里一部分物件的外观效果，考虑到对DrawCall的影响、修改材质的耗时等等，怎样做是最高效的呢？ 如果只是修改材质属性，那么可以用 MaterialPropertyBlock，如修改Color : MaterialPropertyBlock mpb = new MaterialPropertyBlock(); mpb.SetColor("_FillColor", Color.red); GetComponent<MeshRenderer>().SetPropertyBlock(mpb); 因为 MaterialPropertyBlock 的修改效率高，另外还可以配合 GPU Instacing合并 DrawCall。可以参考这篇文章：https://blog.uwa4d.com/archives/1983.html 感谢ice@UWA问答社区提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5acc75e8425802635474fc88 资源管理 Q3：我们在Unity 5.6.5版本下写了一个AssetBundle打包工具。用了最新的Pipeline那个API，就是带manifest的。举个例子，我们打包一个Perfab，这个Prefab有Texture、Shader和脚本。问题来了，我们在不同的电脑，都在Android平台下，打出的AssetBundle不一样，用UnityStudio分析了一下，是Shader的md5不一样导致的，请问大家有什么建议吗？ Unity一直有这个问题的，就算是同一台电脑，删掉Library再打也会不一样。我分享一下我们打补丁的工具的对比流程： 1）首先基于SVN找出两个版本差异的AssetBundle，缩小范围； 2）然后对比对应AB的manifest文件，看AssetHash是否真的变化了，过滤掉没有变化的。 同时检查了一下TypeTreeHash，如果发生了变化给出警告。 顺带吐槽一下这个API： https://docs.unity3d.com/ScriptReference/BuildPipeline.GetHashForAssetBundle.html 它的实现就是去直接读同名的manifest里的AssetHash值，很容易让人误以为这API是去计算hash。 感谢littlesome@UWA问答社区提供了回答 UWA：MD5的问题一直存在，建议直接通过AssetBundle的Hashid来进行判断。 相关链接： https://docs.unity3d.com/ScriptReference/BuildAssetBundleOptions.AppendHashToAssetBundleName.html 感谢欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ace1d68425802635474fc9e UI Q4：我们使用UGUI WorldSpace Canvas 制作了小兵（小兵会经常移动、数量在几十个左右） 3D UI血条，血条UI会自动根据距离相机的远近形成合理的渲染顺序。但是这样的方式导致每个血条都需要一个Canvas， 一个Canvas 就是一个DrawCall。所以我们把所有的血条整合进同一个WorldSpace Canvas下面，这样所有的血条UI只有一个DrawCall，但是这样就无法形成合理的遮挡关系。 于是我们通过每隔几帧计算所有血条UI距离相机的距离进行排序，更改其transform.SetSiblingIndex 来模拟实现遮挡顺序问题，但是这样一来会增加CPU的计算。所以想问问大家对于数量较多的3D UI血条的优化是怎么处理的？ 我们上述的优化方案是否可靠？感谢指点。 UWA：首先UGUI里要改变遮挡确实只能改SiblingIndex，如果需要排序和修改的血条量比较大的话确实容易带来明显开销。但“几十个”算不算大确实不好说，建议先测试下这种做法在低端机上的表现，主要关注的就是算距离、排序、改SiblingIndex的开销总和，如果也就1~3ms那还是可以接受的，毕竟是隔几帧做一次的。 但如果开销非常明显，甚至引起降帧了，那么UWA建议改用Unity2D的Sprite试试，这个是天然用相机距离排序的，不需要自己来做了。只是Unity2D在某些版本上还不支持Sliced等模式（2017.2是支持的），可能视觉效果上会有影响。 PS：接触下来，其实不少游戏是不对血条排序的，可能是因为不太容易被关注到，所以偷懒的办法就是低端机就不排了… 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ace15b7425802635474fc9c 今天的分享就到这里。当然，生有涯而知无涯。在漫漫的开发周期中，您看到的这些问答也许都只是冰山一角，像这样实用的知识点在UWA问答平台（answer.uwa4d.com）上还有一打！UWA欢迎热爱进步的你加入，也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。
本期聚集话题：卡通渲染的制作流程、没用物理模块，为什么还会有物理引擎的耗时？SpriteAtlas的late bind机制、ASTC在安卓平台使用的可行性、UI 性能分析... 这是第106篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题，建议阅读时间15分钟，认真读完必有收获。文末，我们的互动话题是：资源的同步和异步加载，哪个更受你的青睐，我们期待你的灼见！ UWA 问答社区：answer.uwa4d.com UWA QQ群：793972859（仅限技术交流） 本文封面图来源：ACT手游《崩坏3》 https://www.bh3.com/picture.html 制作 Q1：我最近在Unity中制作角色的卡通渲染效果，目前的工作流程是这样的：1）程序在Unity中写Shader调效果；2）美术在3dmax修改模型顶点色和法线等资源，修改完导出给程序；3）程序将资源导入Unity中验证效果。目前需要频繁地修改资源、导出导入资源，效率比较低下。 所以我有以下两个问题： 1、如何在3ds max中写着色器，美术修改模型后能及时看到效果？ 2、成熟的制作流程是什么样的？ A1：这个不难的，让渲染程序员简单看一下Maya或者Max的文档，几个小时基本都是可以搞定的，Max或者Maya都有例子和文档，比如Max的话就是用DirectX Shader Material，不用把这个想得很难，好好看文档。 我觉得本质上主要是想要所见即所得，所有的工作也都是为了这个目的。卡通渲染对美术资源的修改大多需要定制化，例如靠刷顶点色控制描边，靠改顶点法线去改善光照，所以如果把Shader做到Max里可以大大提高美术的制作流程，而不是总是导入导出。通常的流水线是：维护一个和引擎预览效果一致的Max（或Maya）Shader材质，再加一个自定义的法线编辑工具（如果美术觉得内置的不够用，比如Max的法线编辑比较困难），这样在修改美术资源保证效果一致性就好了。 感谢乐乐@UWA问答社区提供了回答 题主补充：对于第一个问题，读书哥提供了一个方法：Max里可以用HLSL写，有DX9/11的Sample，但是问题挺多的，Driver建议用Nitrous Direct 3D 9，记得关掉Gamma/LUT和SSAO。 附上参考文档链接：http://docs.autodesk.com/3DSMAX/16/ENU/3ds-Max-SDK-Programmer-Guide/index.html?url=files/GUID-0C7A600E-7954-42B0-86EE-586A379A2CAD.htm,topicNumber=d30e34269 欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ac0dbc1910222635366dc85 UI Q2：我们打开UI界面Profiler峰值显示 Canvas.SendWillRenderCanvases消耗了50%CPU开销，Canvas.SendWillRenderCanvases 消耗80ms，Graphic.Rebuild 消耗了72ms，请问有什么办法可以优化呢？ UWA：这是某个/某些大量的UI界面元素的重建开销，建议如下： （1）先通过“动静分离”原则检测是否有不必要的静态UI网格参与了重建； （2）UI界面是否过于复杂，UI层级或者网格量是否过大； （3）在UWA性能简报中查看对应的解决方案和相关视频，磨刀不误砍材工。下图中可以看到这些参数在每次测评中的走势，耗时下降的很快：） 该问题由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ac49b99910222635366dca9 物理 Q3：如下图，我们物理模块中没有用到任何碰撞对和Rigidbody，为什么还会有物理模块的耗时呢？ UWA：题主的Unity版本是多少的？如果是Unity 5.4版本以后，那确实不太正常，因为Unity引擎在5.4后对物理系统做了调整。 如果是Unity 5.4之前版本，那么就是比较正常了，因为无论你是否使用，物理模块每帧都在运转，你可以理解为是空转。对此，题主需要查看报告中“重要参数解析->引擎性能参数”中的“Physics.Processing”或“Physics.Simulate”一页，里面有这两个函数的调用次数。很多时候，物理模块即便没怎么用，但由于是每20ms运行一次（默认情况下），也会在同一帧中运行很多次，如下图所示。所以，物理模块有个特点，就是当你项目卡的时候，它会让你的项目更卡：）对于这种情况，题主只需要去优化其他模块，当其他模块的耗时下来后，你会发现物理模块的耗时自然也就下来了。 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ac4e681910222635366dcbd 资源管理 Q4：我最新在使用Unity 2018.1.0 Beta 13的版本碰到一个问题，不过我相信低版本也可能有同样的问题。我使用Unity 2017新出的SpriteAtlas进行Sprite打包，并且生成了AssetBundle，发现这种方式和tag不一样，自己还需要处理延迟绑定，因此需要自己去实现SpriteAtlasManager.atlasRequested的回调。 于是我将所有SpriteAtlas的Include Build都去掉，打包了AssetBundle，但是在实现SpriteAtlasManager.atlasRequested碰到一个问题：如果我使用同步接口，也就是AssetBundle.LoadFromFile和AssetBundle.LoadAsset的接口加载SpriteAtlas，并且在SpriteAtlasManager.atlasRequested内部就直接回调action，则不会有问题；如果我使用异步接口，也就是AssetBundle.LoadFromFileAsync以及AssetBundle.LoadAssetAsync，等加载完成之后再回调action，也就是在SpriteAtlasManager.atlasRequested以外，则会导致图片无法加载，无论是编辑器还是打包出来(我测试的Windows的包)，都会导致UGUI界面是花的。 我在官方咨询但是没有结果：https://forum.unity.com/threads/spriteatlasmanager-atlasrequested-is-not-invoked-in-editor.524801，想请教一下这个算是Unity的Bug还是我的某些理解操作有问题导致的？ UWA：首先“发现这种方式和tag不一样，自己还需要处理延迟绑定”，这里并不是必须的，如果勾选了Include in build，是不需要自己处理binding的，直接加载实例化就可以了。只是需要注意这个Bug，目前会导致Bundle中图集冗余：https://answer.uwa4d.com/question/5a822325847802258a06509e 如果题主就是希望做late binding的，那么从题主的描述中看，应该是说SpriteAtlasManager.atlasRequested中必须直接回调action才有效，用了异步（相当于隔了几帧）之后才回调action就无效。 所以做了个例子尝试复现，但没有出现类似的问题，下面是测试代码，UI Prefab（里面用到了某Sprite）和对应的SpriteAtlas做依赖打包，分别记为prefabBundle和saBundle。加载的代码如下，版本是2017.2.2。 IEnumerator Start () { var loadOp = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + prefabBundle); yield return loadOp; var prepre = loadOp.assetBundle.LoadAsset<GameObject>("prop_parachute"); GameObject.Instantiate(prepre); } void OnEnable() { SpriteAtlasManager.atlasRequested += RequestLateBindingAtlas; } void OnDisable() { SpriteAtlasManager.atlasRequested -= RequestLateBindingAtlas; } void RequestLateBindingAtlas(string tag, System.Action<SpriteAtlas> action) { StartCoroutine(LoadFromAssetBundle(tag, action)); } IEnumerator LoadFromAssetBundle(string tag, System.Action<SpriteAtlas> action) { var loadOp = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + saBundle); yield return loadOp; var loadOp2 = loadOp.assetBundle.LoadAssetAsync<SpriteAtlas>(tag); yield return loadOp2; var sa = loadOp2.asset as SpriteAtlas; action(sa); } 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ac430f0910222635366dc99 资源管理 Q5：当下ASTC在安卓平台使用的可行性是如何的呢？官方文档所说， 几个主流GPU支持ASTC的要求都不高，请问在安卓设备上使用ASTC还有其他什么需要注意的吗？ 就 ASTC压缩方式本身来说是没有问题的。对纹理尺寸无要求，支持多个品质等级。但问题在于 OpenGL ES 3.2 以后才支持，也就是一大批中低端机就没戏了。比如红米2，小米4。iOS 平台的 iPhone 6 之后才支持。不支持的结果就是纹理会自动转成 RGB24/RGBA32。所以我们项目尝试了一下 ASTC 又用回 ETC2 了。 感谢史亚征@UWA问答社区提供了回答 欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ac2eb4c425802635474fbef 互动话题 资源的同步加载和异步加载，你更倾向于哪种方式？你是否有什么高见呢？ 期待你的分享哦！
本期聚集话题：材质（贴图）之间的性能影响、粒子系统不断修改发射点的性能最优解、如何渲染一个低分辨率的DepthTexture、Unity中物体实时光照的性能开销... 这是第105篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题，建议阅读时间15分钟，认真读完必有收获。文末，我们的互动话题是：.net 4.6是否值得尝鲜？ UWA 问答社区：answer.uwa4d.com UWA QQ群：793972859（仅限技术交流） 制作 Q1：请教一个问题，我们项目中有一种粒子系统需要不断地修改发射点，用worldspace模式确实可以达到一定的效果，但是目前需要在一帧里从若干个位置同时发射粒子，用多个相同的Particle System确实可以达到目的，但总觉得有些浪费，容易造成频繁的创建和销毁，如果都缓存的话总数可能就会很大了，不清楚有没有可能用一个Particle System来实现这个效果？ 可以参考这个文档: https://docs.unity3d.com/ScriptReference/ParticleSystem.Emit.html 通过调用ParticleSystem的Emit方法来发射粒子，可以在一帧里用不同的EmitParams进行多次调用Emit。这样的话，粒子系统本身就不用移动了，只是每次Emit的位置参数不同。 感谢zZ@UWA社区提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5abde6cd425802635474fbb1 资源管理 Q2：情景1：一张512x256的纹理。情景2：两张256x256的纹理。请问以上哪个情景消耗会更大呢？ 进一步再说，一个同样的角色，情景1：一个材质球，一张512x256的纹理，情景2：两个材质球，各256x256的纹理，这种情况下又是哪个情景消耗更大呢？判断依据和原理是什么呢？ UWA：就内存方面，设置相同的情况下，二者应该是一致的（某些需要长宽相等的压缩格式除外）。 在CPU开销方面，那么二者基本上也可能是差异很小的。如果是一个材质，一张和两张的区别在于采样次数。一张纹理会减少一次纹理的绑定开销，理论上减小的耗时开销是在ns级别的，所以在绝大多数实际情况下CPU端的差异都可以忽略不计；如果是两个材质，影响的因素就会多了，可能会造成更多的Draw Call、更多的RenderState切换等等，这就是为什么要采用Atlas Packing的优化原因。 至于GPU上的开销，多一张纹理的Shader采样肯定要多一些计算，所以Shader Instruction和Cycles肯定也会更多一些。但是否真的是会带来更大的实际开销呢？不一定，因为GPU压力不光是要看一个Shader的Instruction，同时还要看Runtime到底有多少像素是通过这个Shader来渲染的，如果本身占比很小，那么多一次采样和少一次采样，在实际情况下没啥区别。 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5abdca59910222635366dc77 渲染 Q3：我想渲染一个低分辨率的DepthTexture，有什么比较方便的做法吗？ 一种方式是自己创建一个低分辨率的RenderTexture，然后通过Shader Replacement来渲染一个DepthTexture（Shader可以参考Unity的ShadowCaster的实现）。 还有一种是通过_LastCameraDepthTexture来访问其他的低分辨率相机（如果有的话）渲染出来的DepthTexture。 感谢zZ@UWA社区提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5abde736910222635366dc78 渲染 Q4：我想了解下Unity中物体接受灯光、产生阴影、接受阴影的性能开销情况。之所以这么问，是想看看Unity中的以上三种的性能开销。另外，在移动端是否有开启以上三种，或者部分的可能性，谢谢。 UWA：题主的“接受灯光”是指接受Real time光照吗？如果是的话，那么这种性能开销是没法预估的，因为它跟你的渲染场景复杂程度、接受光照物体的多少等都有很大关系，并不能简单理解成“开启就耗时、绝对不可以开启”。建议看看这篇文章：【求知探新】独立游戏《Abi》场景耗时分析 “产生阴影”和“接受阴影”是实时阴影功能的两个选项而已，建议在UWA性能报告中关注Shadows.RenderShadowMap和Shadows.PrepareShadowMap两项，从而来看到你项目的具体性能耗时情况。 目前，实时光照和实时阴影在很多移动项目中都已经开始大量使用。 最后，UWA Blog中有很多精彩的实时阴影文章，建议查看，希望对题主有所帮助。 https://blog.uwa4d.com/search/实时阴影 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ab8b537d35eb22c10a0a426 管理 Q5：我们项目正在立项初期，在Mono和IL2CPP之间犹豫徘徊。到底是Unity ILCPP编译好还是Mono的编译更好？IL2CPP的性能是否真有显著的变化？麻烦用过的朋友来分享下，谢谢！ A1：IL2CPP还是Mono，现阶段主要是安卓上的选择。 首先，这个问题在立项的时候无须纠结，就现在IL2CPP的成熟程度，随时打开就能用了，无须多虑。前期为了出包快，用Mono出。正式版本或者做性能和兼容性测试的版本，上IL2CPP，妥妥的。（当然指的是没有用.net做dll代码热更新的情况） 从个人在项目中尝试和得知的一些信息，稍微总结一下： Mono的好处： 1）久经考验； 2）出包速度快； 3）包体略小； 4）支持安卓上DLL动态载入程序集热更新（本人没有用过，不肯定IL2CPP完全不支持）。 IL2CPP的好处： 1）没有堆内存只涨不降的问题； 2）C#侧的计算密集型算法性能暴增N倍（我们项目的同地图寻路、自动建筑位置规划等算法的速度都有3倍以上提升）； 3）Lua热更无任何问题，反射调用还是导出式调用都很完美支持（应该不能算优点，但确实在我的预料之外）。 再说一下IL2CPP的问题： 构建时间长好多，安卓NDK的windows版工具链怎么就能性能这么低呢！AMD 16核32线程撕裂者走起，NDK编译时完美支持并行化的，核多了编译慢也不是事儿了； IL2CPP后，如果仅编译ARMv7的版本，部分模拟器无法启动游戏。用FAT（ARMv7 + x86）就可以了，而且模拟器上运行也非常流畅了，当然包又大了20多MB（我们的c#代码就是这么多…）。 Mono模式已经不可能支持64位了，面向未来，IL2CPP是唯一的选项，Unity现在已经做得很好了。 感谢郑昊提供了以上回答 A2：补充一点，安卓2019年想上架Google Play必须要提供64位的程序库，只能走IL2CPP这一条路了。 另外IL2CPP在安卓上还个坑 “Failed to extract resources needed by Il2CPP”，应该是目前是唯一阻碍我们使用的问题了（亲测在2017.3也会重现） https://forum.unity.com/threads/failed-to-extract-resources-needed-by-il2cpp-unity-5-4-0f3.419593/ 简单描述一下这个问题，在安卓上Unity要把IL2CPP需要用到的资源解压出来（assets/bin/Data/Managed目录，里面有Mono的config，及各种meta数据），每次启动都会这么干，关键问题是这个操作还会失败。其实Unity直接从apk里读就好了，但愿官方能早日完善这个问题。 该回答由littlesome@UWA问答提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5abdea21425802635474fbb4 互动话题 Unity 2017中可以选择.net 4.6了，那要不要使用4.6的特性呢？你有什么高见吗？ 欢迎来拍砖！
如何更深入地掌握研发项目的物理性能？
本期我们聚集了这几个话题：如何提高Unity物理引擎Physx的性能、如何禁止Unity自动编译脚本、使用Texture2DArray时开启Mipmap效果... 这是第104篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题，建议阅读时间15分钟，认真读完必有收获。文末，我们的互动话题是，Mono和IL2CPP，哪个更受你的青睐，我们期待你的灼见！ UWA 问答社区：answer.uwa4d.com UWA QQ群：793972859（仅限技术交流） 封面图片来自Google 物理 Q1：如何提高Unity物理引擎Physx的性能？我在场景中加入100个具有物理刚体的小人，并充分碰撞，发现低端机的帧率只能跑到10帧以下。 UWA：如果需要充分进行物理计算，这些拥挤在一起的小人很可能会造成大量的物理碰撞，进而造成很高的物理耗时。 在优化之前，题主需要先了解一些基础物理数值推荐，并不是所有设备都能流畅跑起100个自由制作的蒙皮模型的。所以，建议先看这篇文章：https://blog.uwa4d.com/archives/Simple_PA_Physics.html 另外，题主还需要看一下真机上的具体耗时瓶颈是否真正耗时在物理上，100个角色，如果Mesh量很大，渲染耗时也是不可忽视的，所以建议通过Unity Profiler来先确定一下真机上性能瓶颈。 最后，是否每个小人都需要精确的物理计算，是否可以通过一些射线求交、范围判断等性价比高的判断方法来代替纯物理碰撞？这需要题主从设计的角度上来进行权衡。 你并没有什么办法直接调用PhysX的API，所以所谓优化物理引擎的性能，倒不如说优化相关组件的性能。首先优先使用CharacterController而非RigidBody。然后尽量避免使用MeshCollider，用BoxCollider、SphereCollider和CapsuleCollider代替，如果不可避免，那么钩上Convex。当然，最好的优化就是不要用物理碰撞。射线能解决很多问题，但是射线也不是免费的，它的消耗跟场景里的面数正相关，当然设置射线的maxDistance和LayerMask能节省掉不必要的消耗。最好就是用简单的物理模型来描述问题，自己写代码实现。 感谢凯奥斯提供了回答 欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ab47292d35eb22c10a0a403 编辑器 Q2：有什么方法可以禁止Unity自动编译脚本呢？ UWA：可以把Preferences > General > Auto Refresh的勾去掉，不过如果这样，如果资源在外部被修改，也不会自动重新导入了，会有些影响。 另外，也可以试试这两个函数EditorApplication.LockReloadAssemblies/ UnlockReloadAssemblies，参考文档：https://docs.unity3d.com/ScriptReference/EditorApplication.LockReloadAssemblies.html 该问题由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ab8a76dd35eb22c10a0a41f 渲染 Q3：我使用Texture2DArray的时候发现没有Mipmap效果，想问下怎么让Mipmap生效？原始的纹理都是开启Mipmap的。 UWA：在创建Texture2DArray时需要注意倒数第二个参数，设为true就表示开启Mipmap，参考文档： https://docs.unity3d.com/ScriptReference/Texture2DArray-ctor.html 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ab7afd60b827e2c0bfdd18f 制作 Q4：现在有一张外部生成的Texture可以作为mainTex渲染，但希望获取它的内容保存下来，但是GetPixels是Texture2D的函数，所以想问问这样的Texture的内容该如何获取？ UWA：可以先通过Graphics.Blit把内容画到Render Texture里面，然后通过ReadPixels从RenderTexture生成一张Texture2D，假设sourceTex是原始的Texture，tex2D是最终生成的Texture2D，则代码如下： RenderTexture renderTexture = new RenderTexture(sourceTex.width, sourceTex.height, 32); Graphics.Blit(sourceTex, renderTexture); Texture2D tex2D = new Texture2D(sourceTex.width, sourceTex.height, TextureFormat.RGBA32, false); RenderTexture oldRT = RenderTexture.active; RenderTexture.active = renderTexture; tex2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); tex2D.Apply(); RenderTexture.active = oldRT; 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ab8a73c0b827e2c0bfdd196 Shader Q5：我正在尝试自定义Terrain的Shader，发现内置Shader的末尾有这样两行，想问问其中的“Dependency”是做什么用的，后面指定的Shader起什么作用？ Dependency “AddPassShader” = “Hidden/TerrainEngine/Splatmap/Diffuse-AddPass” Dependency “BaseMapShader” = “Diffuse” UWA：Unity的文档中应该是没有描述过这个的作用， 不过在这篇博客里分析的比较清楚，https://alastaira.wordpress.com/2013/12/07/custom-unity-terrain-material-shaders 这是当Terrain使用的纹理超过4张时，Unity会用其中的AddPassShader来进行第二次渲染，并且blend上去。而BaseMapShader是用在低端显卡上的，只用一张纹理来显示的Shader。 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ab8b537d35eb22c10a0a426 互动话题 近年来，随着Unity官方对IL2CPP的大力支持和改进，越来越多的开发者在立项初期在Mono和IL2CPP之间犹豫徘徊。到底是Unity ILCPP编译好还是Mono的编译更好？IL2CPP的性能是否真有显著的变化？用过的朋友不妨来交流下你的心得！
在写这篇文章之前，我先给大家展示下，下图是Journey里面的沙丘渲染是很久之前一直想做的，但总是没时间。最近因为项目中在做一个沙丘的场景，所以趁这个机会来做一下。 在这里事先声明，在场景再现中所用到的技术基本上是在尊重Journey原作的精神下进行的个人创作。由于素材，参数以及个人能力等原因，要完全重现Journey里面的场景十分困难，也没有意义。另外在制作时不会考虑开销，一切以效果为重。本作仅希望通过场景复原过程分享与收获一些风格化渲染的经验。 场景最终效果： 官方参考图，是游戏里的实景： 图片来源：Sand Rendering in Journey 这是GDC的一个讲座，现在已经可以免费观看了。讲座用的PPT下载链接：advances.realtimerendering.com 忍不住多方几张官方的插图，因为实在是太漂亮了： 沙子上有blingbling的闪光 为了防止分辨率下降，建议大家下载ppt观看。 好的，鉴赏完毕，接下来是正片。 一、建模 Modeling 讲座里说了是用一个高度贴图height map进行建模。 用于建模的height map 基本方法是把图片下载下来之后，在Maya新建一个平面，把面片数调节为50*50： 然后打开Surfaces->Sculpt Geometry Tool。打开小窗后，选择Attribute Maps->Import 点击import，然后在路径导航窗口中选中刚才我们使用的height map。导入后每个点的高度变化可能有些小，所以我做了沿Y轴的缩放，完成后效果如下： 讲座中提到了他们使用B-spline让模型更加平滑。这部分内容主要和建模有关系，这里就不作展开了。 我希望进行渲染的内容为该场景内的一小部分，所以单独为这个镜头建了一个模，如下图： 基本上就是用Maya的Sculpting工具修修改改一点点捏出来的。Journey风格的山丘，特点就是山头很尖，一定要尖，做山头的时候可以改用soft select 来调节vertex。 改完后把模型导入Unity，打上背光： 阳光的颜色为（253，208，179）。背景的日光有些丑，所以替换成Jouney的抱抱山和游戏场景中灰黄色的天空。山是游戏截图（估计真实的游戏也就是一个图片( :3 )），天空是一个外部剔除的球体，大概长这样： 修改过后镜头里的画面： 这里还导入了一个Unity自带的FirstPersonController。这样就可以愉快地在沙丘上跑了。写Shader前的准备工作基本完成。接下来本文就讲座所提及的内容，以倒叙的方式实现（谁叫讲座也是倒过来讲的呢）。 二、基本的明暗 Diffuse 新建一个新的Unlit Shader，建好后加上材质给我们的模型贴上。 首先是一些基本的向量转换的工作，这里贴一下顶点函数： struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; float4 tangent :TANGENT; }; struct v2f { float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; // position of this vertex in world float3 view : TEXCOORD2; // view direction, from this vertex to viewer float3 tangentDir : TEXCOORD3; // tangent direction in world float3 bitangentDir : TEXCOORD4; // bitangent direction in world float3 normal : NORMAL; UNITY_FOG_COORDS(5) float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; // here we don't want the main texture to affect the uv o.worldPos = mul(unity_ObjectToWorld ,v.vertex).xyz; o.view = normalize(WorldSpaceViewDir(v.vertex)); o.normal = normalize( mul( unity_ObjectToWorld , v.normal).xyz ) ; o.tangentDir = normalize( mul( unity_ObjectToWorld , float4( v.tangent.xyz, 0) ).xyz ); o.bitangentDir = normalize( cross( o.normal , o.tangentDir) * v.tangent.w ); UNITY_TRANSFER_FOG(o,o.vertex); return o; } 视线向量View Direction和法线向量Normal基本上是做渲染必须的。UV的话没有做转换，因为除了MainTexture之外还有贴图需要使用，所以就不作转换了。World Position是一个还算比较常用的向量，所以这里就顺手写了。之后需要使用Normal Map，所以这里需要引入Tangent和Bitangent向量。 基本的明暗关系，作者使用的是OrenNayar模型，具体的代码在PPT里罗列如下： 并且用如下方法增加对比度： 最后公式修改如下： fixed OrenNayarDiffuse( fixed3 light, fixed3 view, fixed3 norm, fixed roughness ) { half VdotN = dot( view , norm ); // the function is modifed here // the original one is LdotN = saturate( dot ( light , norm )) half LdotN = saturate( 4 * dot( light, norm * float3( 1 , 0.3 , 1 ) )); half cos_theta_i = LdotN; half theta_r = acos( VdotN ); half theta_i = acos( cos_theta_i ); half cos_phi_diff = dot( normalize( view - norm * VdotN ), normalize( light - norm * LdotN ) ); half alpha = max( theta_i, theta_r ) ; half beta = min( theta_i, theta_r ) ; half sigma2 = roughness * roughness; half A = 1.0 - 0.5 * sigma2 / (sigma2 + 0.33); half B = 0.45 * sigma2 / (sigma2 + 0.09); return saturate( cos_theta_i ) * (A + (B * saturate( cos_phi_diff ) * sin(alpha) * tan(beta))); } 把这个公式引入，面片着色器修改如下： fixed4 frag( v2f i ): SV_Target { float4 mainColor = tex2D( _MainTex , _MainTex_ST.xy * i.uv.xy + _MainTex_ST.zw ); float3 lightDirection = normalize( UnityWorldSpaceLightDir( i.worldPos ) ); float4 lightColor = _LightColor0; float3 viewDirection = normalize( i.view ); float3 halfDirection = normalize( viewDirection + lightDirection); float4 ambientCol = unity_AmbientSky; float4 diffuseColor = lightColor * mainColor * OrenNayarDiffuse( lightDirection , viewDirection , normal , _Roughness) ; return diffuseColor } 实现效果如下： 嗯，只有单光源看起来效果不是很好呀，所以多光源支持写起来。 首先在Pass头部，CGPROGRAM之前写上Lighting On。然后在面片着色器里加上循环代码： for ( int k = 1 ; k < 4 ; ++k ) { // handle up to 4 lights float4 lightColork = unity_LightColor[k]; float3 lightDirectionk = unity_LightPosition[k].xyz - i.worldPos * unity_LightPosition[k].w; if ( lightColork.x + lightColork.y + lightColork.z >0 ) { float4 diffuseColk = lightColork * mainColor * ( OrenNayarDiffuse( lightDirectionk , viewDirection , normal , _Roughness) ); diffuseCol += diffuseColk; } } 接下来是打光环节，本人也不是专业的灯光，并且在制作过程中灯光也会不断的进行调整。所以这部分仅供参考。主光源（Key Light）不变，是一个从远处往回打的灯光（就是背光的角度），然后Fill Light 打在侧面，做到让场景变得柔和。Rim Light 把山的轮廓强调一下。三个灯光的按添加顺序展示效果如下。 然后是不同Roughness的对比图，左边是Roughness 为0，右边是Roughness为1。好像没什么区别喉。其实更主要的区别会在之后添加specualar的时候看到，在这里我们选择Roughness为0.5。 三、沙丘表面纹理 Height Map 原作者的方法是把下述的四个高度贴图（Height Map）整合起来做成沙丘表面的纹理。 X Z 方向的贴图用于不同法线朝向的表面，Steep 和 Shallow贴图分别用于不同的坡度的表面。（原话为：For each vertex of the detail heightmap, we chose the X- or the Z-column based on which derivative was greater, and we lerped between the shallow and steep rows based on the total steepness of the terrain.） 嗯，这么来说的确有些难理解，实际上我们做一下实验就知道了。 if ( _IsNormalXZ > 0 ) { if ( abs( temNormal.z / temNormal.x ) > 1 ) return float3( abs( temNormal.z ) , 0 , 0 ); else return float3( 0 , 0 , abs( temNormal.x ) ); } 这里根据模型法向的xz分量大小，对模型进行红色和蓝色的着色，可以看到，这样的分类把模型按方向分成了四份。 然后上实景的参考图： 可以看到沙丘的纹路总是沿着沙丘的斜面出现。再看看高度贴图，都是有方向性的，是不是可以理解为，X Z 方向的高度贴图实际上是对应不同方向的纹路？实验的结果如下： 中间是带有纹理方向选择的，左右是只含单个方向的纹理的，对比来看的确中间的纹理比较有立体感。不过这个纹理的衔接处还是比较突兀，所以我用了一个atan函数平滑了一下，代码如下： float3 GetSurfaceNormal( float2 uv , float3 temNormal ) { // get the power of xz direction float xzRate = atan( abs( temNormal.z / temNormal.x) ) ; float3 steepX = UnpackNormal( tex2D( _NormalMapSteepX , _NormalMapSteepX_ST.xy * uv.xy + _NormalMapSteepX_ST.zw ) ) ; float3 steepZ = UnpackNormal( tex2D( _NormalMapSteepZ , _NormalMapSteepZ_ST.xy * uv.xy + _NormalMapSteepZ_ST.zw ) ) ; return lerp( steepX , steepZ , xzRate ) ; } 和视频中介绍的高度贴图不同，我使用的是法向贴图，嗯，好像法向贴图的代码写起来比较简单。我用的贴图长这样（都是网上可以搜到的图） 再导入贴图后，要确认贴图的类型（Texture Type）勾选为Normal map。并且使用的时候需要进行一个坐标转换，转换方法如下： fixed4 frag(v2f i ):SV_Target { ... // Get the surface normal detected by the normal map float3 normalSurface = normalize(GetSurfaceNormal( i.uv , i.normal ) ); // 'TBN' transform the world space into a tangent space // with the inverse matrix, we can transport the normal from tangent space to world float3x3 TBN = float3x3( normalize( i.tangentDir ) , normalize( i.bitangentDir ) , normalize( i.normal )); TBN = transpose( TBN); // equals to i.tangent * ns.x + i.bitangent * ns.y + i.normal * ns.z float3 normal = mul( TBN , normalSurface ); // Merge the surface normal with the model normal normal = normalize( normal * _SurfaceNormalScale + i.normal); ... } 这个转换简单来说就是把在表面坐标系（以Tangent, Bitangent 和 Normal为轴）里的法线向量转换为在世界坐标系（以XYZ为轴）里的法线向量。这个法线方向转换很重要，不然光照效果就会乱了套（不要问我是怎么发现的）。同时这里加入了_SurfaceNormalScale参数，来控制山体表面纹路的深浅。完成后山的法线分布应该是这样的： 之后要做斜度的方向上的分解，和XZ方向类似，同样使用了atan函数进行平滑，具体的代码如下： float3 GetSurfaceNormal( float2 uv , float3 temNormal ) { // get the power of xz direction // it repersent the how much we should show the x or z texture float xzRate = atan( abs( temNormal.z / temNormal.x) ) ; xzRate = saturate( pow( xzRate , 9 ) ); // get the steepness // the shallow and steep texture will be lerped based on this value float steepness = atan( 1/ temNormal.y ); steepness = saturate( pow( steepness , 2 ) ); float3 shallowX = UnpackNormal( tex2D( _NormalMapShallowX , _NormalMapShallowX_ST.xy * uv.xy + _NormalMapShallowX_ST.zw ) ) ; float3 shallowZ = UnpackNormal( tex2D( _NormalMapShallowZ , _NormalMapShallowZ_ST.xy * uv.xy + _NormalMapShallowZ_ST.zw ) ) ; float3 shallow = shallowX * shallowZ * _ShallowBumpScale; float3 steepX = UnpackNormal( tex2D( _NormalMapSteepX , _NormalMapSteepX_ST.xy * uv.xy + _NormalMapSteepX_ST.zw ) ) ; float3 steepZ = UnpackNormal( tex2D( _NormalMapSteepZ , _NormalMapSteepZ_ST.xy * uv.xy + _NormalMapSteepZ_ST.zw ) ) ; float3 steep = lerp( steepX , steepZ , xzRate ) ; return normalize( lerp( shallow , steep , steepness ) ); } 好了，这部分完成以后，场景的渲染效果如下（_SurfaceNormalScale分别为0.1,0.5和2） ： 四、风格化高光 Ocean Specualar 首先真的很感慨前辈们在那个光照理论还不很成熟的年代能够做出这么好的艺术效果。在讲座中作者表示这个像海洋高光一样的效果其实是不真实的，但是他们试验之后觉得这个效果好，所以在游戏里就加上了（渲染这种东西有时候真的是凭感觉的呀）。 所谓海面高光是什么意思呢，大概就是这样子的吧： 大概就是有中间的一条光束，所以那要怎么实现呢？讲座里没给出具体的算法。所以这部分本人基本靠蒙的。 先是实验了一些Smith GGX，Beckman之类的模型，效果都不是很好。后来突然想起，之前在做水特效的时候，有出现过类似的效果，所以就去查看了一下。发现好像就是最基本的Blinn模型。。。对，就是Blinn，效果反而意外的不错： float MySpecularDistribution( float roughness, float3 lightDir , float3 view , float3 normal , float3 normalDetail ) { // using the blinn model // base shine come use the normal of the object // detail shine use the normal from the detail normal image float3 halfDirection = normalize( view + lightDir); float baseShine = pow( max( 0 , dot( halfDirection , normal ) ) , 10 / baseRoughness ); float shine = pow( max( 0 , dot( halfDirection , normalDetail ) ) , 10 / roughness ) ; return baseShine * shine; } 这里的高光分为BaseShine和Shine。BaseShine是用来确定高光的边界，使用的法线是之前所说的加上Normal Map之后的Normal。shine是用来做纹理，就是做出那种波光粼粼的效果，这里的Normal实际上是使用了一个新的细节纹理法向贴图，并且把贴图缩小来做到细小的纹理效果。不过由于素材的原因，和目标效果始终有一些微小的差距。最终效果如下： 使用的细节纹理： 和之前的diffuse叠加（线性叠加）在一起后，效果如下： 后期处理的时候加一些Bloom效果会进一步提高整体的视觉效果，先卖个关子~ 然后我也试着结合BRDF的知识手动添加了一些高光，但是效果也一般，所以这里就不展开说明了，只是上个效果图： 五、贴图过滤 Anisotropic Filtering（略） 这部分的作用是让在远距离的贴图能够更加清晰，一个比较典型的例子是这样的（注意看远处部分的瓷砖）： 这个算法在Unity里已经被整合好了。在Editor->Quality Settings里面有一个Anisotropic Textures的选项用于开关这个效果，默认打开。这部分也就不详细说明了（感觉当年Journey团队真辛苦，连这个也要自己做）。 六、亮片效果 Glitter 亮片效果是什么呢，就是在沙子上blingbling的那种效果： 单拎出来是这样滴： 按照讲座中作者的话来说，他们理解的亮片就是沙堆中有一部分的沙子正好朝向观察者，那么它们就会朝你发射光线，从而产生blingbling的效果。嗯，理论上是这样没错，但是这叫我怎么写呀。关于Glitter的效果可以参考2017的siggraph里的这篇文章： http://blog.selfshadow.com/publications/s2017-shading-course/dreamworks/s2017_pbs_dreamworks_notes.pdf 个人总结下来，做Glitter效果可以分为两个步骤，一个是噪点的制作，另一个是高光的制作。 在讲座中，作者讲了他们的高光制作的过程，一个灰常任性的制作过程。因为传统的高光函数为的参数为pow(N·H，n)，这个公式在上面的海洋高光中也用到了。作者觉得Glitter需要的更多关于人眼方位的信息，所以就把这个公式里的H换成了V，即观察者方向向量。 两者的对比大概是这样的： 不过光有高光函数是不够的，因为闪光点并不是在沙子上均匀的出现的，相反，它们的分布十分随机，这就需要我们设计一个随机分布的函数来获取这些亮点分布的位置。在讲座中没有说明噪点函数的生成具体方法，所以接下来又要进入瞎蒙时间。 首先准备一个噪点图，这次我用的图大概长这样： 然后根据这个噪点图，进行噪点的处理。这个函数是凭经验试验出来的，放在这里仅供参考： float3 GetGlitterNoise( float2 uv ) { return tex2D( _GlitterTex , _GlitterTex_ST.xy * uv.xy + _GlitterTex_ST.zw ) ; } float GliterDistribution( float Glitterness , float3 lightDir , float3 normal, float3 view , float2 uv , float3 pos ) { ... float p1 = GetGlitterNoise( uv + float2 ( 0 , _Time.y * 0.0005 + view.x * 0.0050 )).r; float p2 = GetGlitterNoise( uv + float2 ( _Time.y *0.001 , _Time.y * 0.001 + view.y * 0.003 )).g; float sum = p1 * p2; // making discrete noise float glitter = max( 0 , pow( sum , _Glitterness ) * _GlitterMutiplyer - 0.5 ) * 2; ... } 制作噪点的效果： 颗粒感可以做到这样，而且在场景走起来的时候blingbling的感觉还是有的，不过闪光点的密度太大了。之前不是有一个用N·V求出来的分布函数嘛，那我们就把它拿来当做蒙版吧（这一趴系完全的瞎蒙，如果有读者知道具体的算法可以留言处告诉我）。完整的代码是这样的： float GliterDistribution( float3 lightDir , float3 normal, float3 view , float2 uv , float3 pos ) { float specBase = saturate( 1 - dot( normal , view ) * 2 ); float specPow = pow( specBase , 10 / _GlitterRange ); // A very random function to modify the glitter noise float p1 = GetGlitterNoise( uv + float2 ( 0 , _Time.y * 0.001 + view.x * 0.006 )).r; float p2 = GetGlitterNoise( uv + float2 ( _Time.y * 0.0006 , _Time.y * 0.0005 + view.y * 0.004 )).g; float sum = 4 * p1 * p2; float glitter = pow( sum , _Glitterness ); glitter = max( 0 , glitter * _GlitterMutiplyer - 0.5 ) * 2; float sparkle = glitter * specPow; return sparkle; } 添加了Glitter效果的沙丘： 七、其他 Mipmap 这是一个在Texture上的类似于LOD的系统，在讲座中有提到，这个技术能够让沙子颗粒感更强。这里就略过了。 距离雾 Fog 通过调用宏实现，在新建Shader的时候自带的代码，这里沿用下来。 后期调整 Post Effect 目前来说渲染的场景长这样： 和Journey游戏里的场景好像还有点距离，不过其实只要加上一些些Camera特效就会好很多了。 首先是Bloom，把高光部分的光照质感做出来。这里使用的是Unity官方出的Post Processing Stack插件(Post Processing Stack - Asset Store)。 然后调一下颜色： 这里是把Post Processing里的ToneMap 调成Natural，并添加了一个LUT： 最后使用的是一个叫Beautify的插件(Beautify - Asset Store)，添加了一下锐化的效果，并且增加了一点对比度： 对比一下官方提供的图片： 本工程已经同步到Github上了，链接： https://github.com/AtwoodDeng/JourneySand 这是侑虎科技第369原创文章，感谢作者在木（邓佳迪）供稿。欢迎转发分享，未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者知乎：https://zhuanlan.zhihu.com/zaimu，同时，作者也是U Sparkle活动参与者哦，UWA欢迎更多开发朋友加入U Sparkle开发者计划，这个舞台有你更精彩！
关于Unity UI的动静图集分离，你需要了解这些！
本期我们聚集了这些话题：UI 图集静态分离、Unity烘焙问题、Unity渲染异常、Animation.RebuildInternalState......(UWA一下，你就知道~） 这是第101篇UWA技术知识分享的推送！（此处应有掌声）随着大家在UWA平台上的发言愈加活跃，我们也源源不断地收到了许多技术大咖的热心回复和真知灼见。 UWA 问答社区：answer.uwa4d.com UWA QQ群：793972859（仅限技术交流） UI 管理 Q1：对于Unity的静态图集和动态图集分离，我有些疑问。我们UI制作方法如下： Prefab (UIHero) Prefab的静态图集 (UIHeroAtlas) Prefab上可能的动态图集 (UIHeroIcon) 如头像 生成的Bundle： Prefab一个bundle,它包含了静态的图片 动态图集一个Bundle 后来，为了能动态合批减少几个DrawCall，可能静态图集上有2-3张图片，在动态图片的上面当一个小边框背景。然后美术想把它放到动态图集，这样图片重叠时，它和动态图集的图也会是一个DrawCall。 我们的问题是：在使用AB的方式加载这个UI-prefab时，会不会加载整个动态的图集（UIHeroIcon）呢？还是说不会加载整个UIHeroIcon，只是把这两个静态图加载，动态UIHeroIcon照样不会加载？这种方式是否会造成热更新UIHeroIcon动态图集后，导致UIHero-prefab无法加载这两个静态图片的情况？那么我该如何取舍？ 1.会不会加载取决于你，Unity只是给出依赖列表，是否加载自行控制。 2.不会加载整个UIHeroIcon。 3.这里差不了几个DrawCall。而前景可能有很多种，美术的方案不一定通用，以后可能会拆回来。我的理解是，是静态图集中有2个Sprite A和B，然后在动态图集中也复制一份A和B。重叠时，如果UI深度不同，不一定可以合并。资源结构为重，不然之后维护会很麻烦。 感谢UWA问答社区 @史云柯 提供了回答 UWA补充：补充下史云柯回答中的第三点，建议评估下DrawCall能节省多少。举个例子，如果是个10人MOBA，那么在团战过程中10个Icon可能是叠在一起的，由于图集穿插就有可能增加到20个DrawCall，对于中低端设备节省20个DrawCall的话，性价比还是可以的。如果最差情况也就10个不到，那么确实性价比不高了。 此问答来自于UWA 问答社区，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5a94dd200b827e2c0bfdd022 渲染 Q2：请问各位Unity5.3烘焙得到的LightingData.asset、Lightmap-0_comp_dir.exr、Lightmap-0_comp_light.exr各有什么作用呢？打包和加载的时候实际上是用了哪一个文件，还是全都需要？ UWA：LightingData.asset的介绍可以看这两个文档： https://docs.unity3d.com/Manual/LightmapSnapshot.html https://answer.uwa4d.com/question/5a4f4367e041ef3c50c9ce64 简单说，这里面存的就是场景中各个受Lightmap影响的物件的相关信息，Editor下这个文件可能会很大；但真机上，在不使用precomputed gi的情况下这部分的数据打包到手机上应该是很小的（毕竟对于一个物体来说，主要的Lightmap信息就是lightmapIndex和lightmapScaleOffset）。 另外，这部分信息是和场景文件绑定的，不能单独管理。所以默认情况下，Lightmap只能随场景一起加载。如果需要动态加载lightmap，可以参考这篇文章：http://www.xuanyusong.com/archives/3807；而后面两个.exr文件就是储存光照信息的纹理，_dir是光的方向信息，_light是光强信息（最简单的模式下只需要这一张）。这些文件是运行时需要的，不过exr是支持HDR的格式，在手机上会默认转成其他格式的，大小与exr是不同的。 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5a9a69720b827e2c0bfdd0a7 动画系统 Q3：Camera.Render里面Animation.RebuildInternalState耗时较多，调用比较频繁，这个主要是哪些情况会调用呢？ UWA：之所以出现这一项，主要可以说明一下两点： （1）题主项目用的是Animation老版本动画系统； （2）对含有Animation组件的GameObject进行了大量频繁的Active或者Instantiate操作，一般这种情况比较常见于特效、UI HUD、角色/怪物等。 对此，研发团队可以通过上述两点查看是否如此。 该问答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5a953c61d35eb22c10a0a29d UI 管理 Q4：在UI优化中，是每张图集的内存占用对效能影响显著，还是图集数量（draw call）显著？比如8 张 1024 图集 和 2 张 2048 图集哪个更优？ 其实没有统一的标准说8 张 1024 图集 和 2 张 2048 图集那个会哪个更优，比如如果4张1024的贴图会一直在同一个UI中出现，同时加载同时渲染，那通常合并成一张2048的图集会更合理，而如果这4张1024的贴图在只在很少情况下才会同时出现1-2张，那合并成2048的贴图就是内存浪费。 这种极端情况的处理方式相信题主可以想到，所以这里是一个根据具体需求的选择，而没有哪个更好，具体的选择标准还要看项目是内存更加吃紧还是UI的DrawCall更加吃紧。 个人建议是，对于可能会常驻内存的，通用的那些资源，可以给予更高的图集大小的限定，比如允许最大2048甚至4096一张；而对于单个游戏功能内部的独有图集，我们是建议UI控制在1024以内，如果使用量到了3张1024的级别，才允许使用2048的贴图尺寸。毕竟如果分配合理，多一张贴图理论上可以做到只多1个DrawCall，而2张1024如果要用2048的贴图，空白的地方会比较多，或者不合理的合并，会导致内存占用比较浪费。 感谢UWA问答社区 @贾伟昊 提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5a947cf00b827e2c0bfdd014 渲染 Q5：Unity 5.5.2f1版本的项目中有少量模型在使用Cull Back的Shader时，渲染异常。我新建Unlit Shader，添加Cull Back的tag；用该Shader渲染角色的时候，发现如下图问题： 将Cull Back改为Cull Off 、Cull Front时，效果正常。 所以，我怀疑该模型的正反面反了；但是据我们美术说，3dmax中网格的正反面是由法线方向决定的,而法线方向是正确的，所以不是模型资源的问题。 所以修改Shader，将法线作为片段着色器的输出后，得到下图结果： 看上面这图，法线好像也没啥问题，那问题原因在哪呢？相关的例子我已经提交至UWA问答官网。 UWA：三角形面片的front和back不是由法线决定的，是由三角形的三个顶点投影到屏幕上的顺序决定。默认情况下gles是以逆时针方向的面为front，顺时针为back。虽然上面法线方向是对的，但是它跟face culling没有关系。从上面结果看应该是美术输出时候三角形面片顶点顺序弄反了，让他们改过来应该就行了。 关于face culling可以参考这两个函数： https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glCullFace.xml https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glFrontFace.xml 其实，face culling和法线反了是两种相像。 face culling的，你在那个Shader里加一句Cull Off，如果显示正常如上图，就说明面反了。 法线反了，选中模型时在右下角的预览(或直接拖到场景里打上光)，你会看到黑乎乎的，为什么呢。dot(lightdir,normal)，因为法线反了，这个值基本为0，diffuse 乘上这个值也会为0。 感谢UWA问答社区 @Kent 提供了回答 欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5a97763fd35eb22c10a0a2c3 今天的分享就到这里。当然，生有涯而知无涯。在漫漫的开发周期中，您看到的这些问答也许都只是冰山一角，像这样实用的知识点在UWA问答平台（answer.uwa4d.com）上还有一打！UWA欢迎热爱进步的你加入，也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。