💡 学点儿丨URP Shader编辑指北[2] - Unlit Shader详细拆解
靠北，这篇写了超级长结果被草稿覆盖了.....我是真的很emo😇😇😇😇😇 就极精简地复述一遍....好难过😭😭 文件结构 URP管线的Shader都位于Packages/Universal RP/Shaders目录，是Shader的主要框架和相关的Pass。通用的渲染方法函数一般位于Packages/Universal RP/Shaders Library目录，这是一个为SRP提供的函数包，可以为我们省去许多重复性的工作。另外涉及到Material面板的文件位于Packages/Universal RP/Editor/Shader GUI. 本文代码块的第一行表示代码所在的目录位置，就不多重复了~ 有关拓展阅读：[URP]内置shader解析笔记_1 Properties [Universal RP/Shaders/Unlit.shader] Properties { [MainTexture] _BaseMap("Texture", 2D) = "white" {} [MainColor] _BaseColor("Color", Color) = (1, 1, 1, 1) _Cutoff("AlphaCutout", Range(0.0, 1.0)) = 0.5 // BlendMode _Surface("__surface", Float) = 0.0 _Blend("__mode", Float) = 0.0 _Cull("__cull", Float) = 2.0 [ToggleUI] _AlphaClip("__clip", Float) = 0.0 [HideInInspector] _BlendOp("__blendop", Float) = 0.0 [HideInInspector] _SrcBlend("__src", Float) = 1.0 [HideInInspector] _DstBlend("__dst", Float) = 0.0 [HideInInspector] _ZWrite("__zw", Float) = 1.0 // Editmode props _QueueOffset("Queue offset", Float) = 0.0 // ObsoleteProperties [HideInInspector] _MainTex("BaseMap", 2D) = "white" {} [HideInInspector] _Color("Base Color", Color) = (0.5, 0.5, 0.5, 1) [HideInInspector] _SampleGI("SampleGI", float) = 0.0 // needed from bakedlit } URP的Shader都使用了Custom Editor，可以看出Properties的声明和面板样式并不相同。其具体样式可见最后一行的索引： [Universal RP/Shaders/Unlit.shader] CustomEditor "UnityEditor.Rendering.Universal.ShaderGUI.UnlitShader" 这个文件位于Universal RP/Editor/ShaderGUI/Shaders，UnityEditor.Rendering.Universal.ShaderGUI是它所在的命名空间。这些脚本文件都继承于BaseShaderGUI类。在BaseShaderGUI中，定义了有关属性的面板和更新关键字的方法。 ShaderGUI BaseShaderGUI继承于ShaderGUI，需要使用UnityEditor命名空间。 它包含了六个代码块： EnumsAndClasses 在这个代码块中，定义了各个选项有关的枚举量，例如SurfaceType,BlendMode等。并定义了一个类Styles。 在Styles中，包含了以static readonly stings[]的形式记录了上述的枚举关键字，和以static readonly GUIContent的方式记录了GUI文本和鼠标悬停时的提示信息。例如： [Universal RP/Editor/ShaderGUI/BaseShaderGUI.cs] public static readonly GUIContent SurfaceOptions = EditorGUIUtility.TrTextContent("Surface Options", "Controls how URP Renders the material on screen."); Variables 这个代码块主要是对变量的声明。 首先是一个MaterialEditor类的面板，是承载后续所有内容的主体。 然后是15个MaterialProperty的变量，既包含渲染器的处理方式（如Blendmode、alphaclip等），也包含基础的表面输入（如basemap、basecolor等）。 布尔量m_FirstTimeApply用于在第一次访问时运行初始化。 接下来MaterialHeaderScopeList用于绘制面板，这里在构造时就声明了其默认展开方法，其中Expandable变量在声明时就是以二进制为单位的，这里就可以通过位运算表示是否展开。 [Universal RP/Editor/ShaderGUI/BaseShaderGUI.cs] // By default, everything is expanded, except advanced readonly MaterialHeaderScopeList m_MaterialScopeList = new MaterialHeaderScopeList(uint.MaxValue & ~(uint)Expandable.Advanced); General Functions 既然是对材质面板的定义，那就先从OnGUI()开始看。 [Universal RP/Editor/ShaderGUI/BaseShaderGUI.cs] public override void OnGUI(MaterialEditor materialEditorIn, MaterialProperty[] properties) { if (materialEditorIn == null) throw new ArgumentNullException("materialEditorIn"); materialEditor = materialEditorIn; Material material = materialEditor.target as Material; FindProperties(properties); // MaterialProperties can be animated so we do not cache them but fetch them every event to ensure animated values are updated correctly // Make sure that needed setup (ie keywords/renderqueue) are set up if we're switching some existing // material to a universal shader. if (m_FirstTimeApply) { OnOpenGUI(material, materialEditorIn); m_FirstTimeApply = false; } ShaderPropertiesGUI(material); } ShaderGUI的OnGUI方法需要两个输入：面板MaterialEditor，和属性们MaterialProperty[]，如果找不到输入会抛出报错，如果输入正常，则会定义目标材质。 为了呈现才材质的动态效果，每一帧都会重新获取材质属性： [Universal RP/Editor/ShaderGUI/BaseShaderGUI.cs] public virtual void FindProperties(MaterialProperty[] properties) { var material = materialEditor?.target as Material; if (material == null) return; surfaceTypeProp = FindProperty(Property.SurfaceType, properties, false); blendModeProp = FindProperty(Property.BlendMode, properties, false); cullingProp = FindProperty(Property.CullMode, properties, false); zwriteProp = FindProperty(Property.ZWriteControl, properties, false); ztestProp = FindProperty(Property.ZTest, properties, false); alphaClipProp = FindProperty(Property.AlphaClip, properties, false); // ShaderGraph Lit and Unlit Subtargets only castShadowsProp = FindProperty(Property.CastShadows, properties, false); queueControlProp = FindProperty(Property.QueueControl, properties, false); // ShaderGraph Lit, and Lit.shader receiveShadowsProp = FindProperty(Property.ReceiveShadows, properties, false); // The following are not mandatory for shadergraphs (it's up to the user to add them to their graph) alphaCutoffProp = FindProperty("_Cutoff", properties, false); baseMapProp = FindProperty("_BaseMap", properties, false); baseColorProp = FindProperty("_BaseColor", properties, false); emissionMapProp = FindProperty(Property.EmissionMap, properties, false); emissionColorProp = FindProperty(Property.EmissionColor, properties, false); queueOffsetProp = FindProperty(Property.QueueOffset, properties, false); } 属性的赋值使用的是ShaderGUI的FindProperty()方法，会根据名称从MaterialProperty[]中查找属性值，若未找到则返回null；第三个变量是是否强制要求变量存在，如果设置为true，没找到变量时会抛出异常。 注意，URP中统一使用的变量名是_BaseMap和_BaseColor如果希望管线自动识别，最好也使用这个变量名。 前文提到，使用布尔值m_FirstTimeApply来记录是否初始化: public virtual void OnOpenGUI(Material material, MaterialEditor materialEditor) { var filter = (Expandable)materialFilter; // Generate the foldouts if (filter.HasFlag(Expandable.SurfaceOptions)) m_MaterialScopeList.RegisterHeaderScope(Styles.SurfaceOptions, (uint)Expandable.SurfaceOptions, DrawSurfaceOptions); if (filter.HasFlag(Expandable.SurfaceInputs)) m_MaterialScopeList.RegisterHeaderScope(Styles.SurfaceInputs, (uint)Expandable.SurfaceInputs, DrawSurfaceInputs); if (filter.HasFlag(Expandable.Details)) FillAdditionalFoldouts(m_MaterialScopeList); if (filter.HasFlag(Expandable.Advanced)) m_MaterialScopeList.RegisterHeaderScope(Styles.AdvancedLabel, (uint)Expandable.Advanced, DrawAdvancedOptions); } 初始化的工作主要是向MaterialHeaderScopeList写入信息，当展开标题栏时，会调用Action的方法。 [UnityEditor.Rendering] public void RegisterHeaderScope<TEnum>(GUIContent title, TEnum expandable, Action<Material> action) where TEnum : struct, IConvertible 由于每个Header自己根据展开与否绘制自己的属性，统一的绘图指令就只要绘制这些Header就可以了，所以OnGUI中最后一个绘制函数非常简单： [Universal RP/Editor/ShaderGUI/BaseShaderGUI.cs] public void ShaderPropertiesGUI(Material material) { m_MaterialScopeList.DrawHeaders(materialEditor, material); } 在这个代码块中还包含一个检测属性是否更改的方法，调用ShaderGUI的ValidateMaterial(）方法： [Universal RP/Editor/ShaderGUI/BaseShaderGUI.cs] [Obsolete("MaterialChanged has been renamed ValidateMaterial", false)] public virtual void MaterialChanged(Material material) { ValidateMaterial(material); } Material Data Functions 这个代码块中声明了一些方法函数，主要用于写入材质的关键字信息。对于Unlit Shader而言，其实只用到了其中的一小部分。 前文提到ValidateMaterial()方法，用于保证Shader和Pass的关键字在面板更改时一并更改。这个方法在BaseShaderGUI没定义，UnlitShader.cs子类中对它进行了override： [Universal RP/Editor/ShaderGUI/shaders/UnlitShader.cs] // material changed check public override void ValidateMaterial(Material material) { SetMaterialKeywords(material); } 这里SetMaterialKeywords()就来自Material Data Functions代码块： [Universal RP/Editor/ShaderGUI/BaseShaderGUI.cs] // this is the function used by Lit.shader, Unlit.shader GUIs public static void SetMaterialKeywords(Material material, Action<Material> shadingModelFunc = null, Action<Material> shaderFunc = null) { UpdateMaterialSurfaceOptions(material, automaticRenderQueue: true); // Setup double sided GI based on Cull state if (material.HasProperty(Property.CullMode)) material.doubleSidedGI = (RenderFace)material.GetFloat(Property.CullMode) != RenderFace.Front; // Temporary fix for lightmapping. TODO: to be replaced with attribute tag. if (material.HasProperty("_MainTex")) { material.SetTexture("_MainTex", material.GetTexture("_BaseMap")); material.SetTextureScale("_MainTex", material.GetTextureScale("_BaseMap")); material.SetTextureOffset("_MainTex", material.GetTextureOffset("_BaseMap")); } if (material.HasProperty("_Color")) material.SetColor("_Color", material.GetColor("_BaseColor")); // Emission if (material.HasProperty(Property.EmissionColor)) MaterialEditor.FixupEmissiveFlag(material); bool shouldEmissionBeEnabled = (material.globalIlluminationFlags & MaterialGlobalIlluminationFlags.EmissiveIsBlack) == 0; // Not sure what this is used for, I don't see this property declared by any Unity shader in our repo... // I'm guessing it is some kind of legacy material upgrade support thing? Or maybe just dead code now... if (material.HasProperty("_EmissionEnabled") && !shouldEmissionBeEnabled) shouldEmissionBeEnabled = material.GetFloat("_EmissionEnabled") >= 0.5f; CoreUtils.SetKeyword(material, ShaderKeywordStrings._EMISSION, shouldEmissionBeEnabled); // Normal Map if (material.HasProperty("_BumpMap")) CoreUtils.SetKeyword(material, ShaderKeywordStrings._NORMALMAP, material.GetTexture("_BumpMap")); // Shader specific keyword functions shadingModelFunc?.Invoke(material); shaderFunc?.Invoke(material); } 前文提到URP使用的变量名是_BaseMap和_BaseColor，为了兼容老版本的_MainTex和_Color，这里其实是将面板上的信息进行了转移（x）。 (还有程序员自己都不知道为什么存在的变量，笑死 Drawing Functions 主要就是各种属性的详细绘制方法，于上述MaterialHeaderScopeList或子类来调用，就不详细展开了。 Helper Functions 一些压缩面板用的工具函数，也不详细展开了。 UnlitShaderGUI UnlitShaderGUI基本上就是继承自BaseMapGUI。AssignNewShaderToMaterial()使从别的Shader切换至UnlitShader时可以自动识别一些基础变量。 using System; using UnityEngine; namespace UnityEditor.Rendering.Universal.ShaderGUI { internal class UnlitShader : BaseShaderGUI { // material changed check public override void ValidateMaterial(Material material) { SetMaterialKeywords(material); } // material main surface options public override void DrawSurfaceOptions(Material material) { if (material == null) throw new ArgumentNullException("material"); // Use default labelWidth EditorGUIUtility.labelWidth = 0f; base.DrawSurfaceOptions(material); } // material main surface inputs public override void DrawSurfaceInputs(Material material) { base.DrawSurfaceInputs(material); DrawTileOffset(materialEditor, baseMapProp); } public override void AssignNewShaderToMaterial(Material material, Shader oldShader, Shader newShader) { if (material == null) throw new ArgumentNullException("material"); // _Emission property is lost after assigning Standard shader to the material // thus transfer it before assigning the new shader if (material.HasProperty("_Emission")) { material.SetColor("_EmissionColor", material.GetColor("_Emission")); } base.AssignNewShaderToMaterial(material, oldShader, newShader); if (oldShader == null || !oldShader.name.Contains("Legacy Shaders/")) { SetupMaterialBlendMode(material); return; } SurfaceType surfaceType = SurfaceType.Opaque; BlendMode blendMode = BlendMode.Alpha; if (oldShader.name.Contains("/Transparent/Cutout/")) { surfaceType = SurfaceType.Opaque; material.SetFloat("_AlphaClip", 1); } else if (oldShader.name.Contains("/Transparent/")) { // NOTE: legacy shaders did not provide physically based transparency // therefore Fade mode surfaceType = SurfaceType.Transparent; blendMode = BlendMode.Alpha; } material.SetFloat("_Blend", (float)blendMode); material.SetFloat("_Surface", (float)surfaceType); if (surfaceType == SurfaceType.Opaque) { material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT"); } else { material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); } } } } SubShader 面板说了一大堆，让我们回到Shader本身！Unlit有两个SubShader，面向不同的TargetShaderModel。2.0支持所有平台，4.5只支持DirectX11+，Unlit的两个SubShader差别不大，本篇主要关注4.5版本。 Shader Model 4.5 SubShader { Tags {... "ShaderModel"="4.5"} ... Pass { Name "Unlit" HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma target 4.5 ... ENDHLSL } ... } Shader Model 2.0 SubShader { Tags {... "ShaderModel"="2.0"} ... Pass { Name "Unlit" HLSLPROGRAM #pragma only_renderers gles gles3 glcore d3d11 #pragma target 2.0 ... ENDHLSL } ... } Pass URP只支持一个渲染Pass，对于UnlitShader就是"Unlit"。但还另外存在三个Pass，在渲染过程中的特定阶段会调用，他们是"DepthOnly"，"DepthNormalsOnly"（这两个顾名思义），和 "Meta"（烘焙lightmap时调用）。 今天我们只关注"Unlit"这个Pass。 宏编译与参数设置 [Universal RP/Shaders/Unlit.shader] Pass { Name "Unlit" HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma target 4.5 #pragma shader_feature_local_fragment _SURFACE_TYPE_TRANSPARENT #pragma shader_feature_local_fragment _ALPHATEST_ON #pragma shader_feature_local_fragment _ALPHAPREMULTIPLY_ON // ------------------------------------- // Unity defined keywords #pragma multi_compile_fog #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION #pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3 #pragma multi_compile _ DEBUG_DISPLAY #pragma vertex UnlitPassVertex #pragma fragment UnlitPassFragment #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitForwardPass.hlsl" ENDHLSL } 中间的一串#pragma可以是Shader根据不同关键字编译成不同的版本。 Unity shader_feature在最终版本中不包含未使用的着色器变体。所以shader_feature适用于在我们在编辑器中，选中材质，设置它使用的shader的宏，如果在程序中动态的去设置可能无效。而对于multi_compile，会把所有的变体都编译进程序里，所以适合需要在程序运行中动态改变状态的宏，适合全局设置 。 拓展阅读：unity shader 变种（多重编译 multi_compile） 在定义了顶点着色器和片元着色器之后........... 就是说是不是成熟的软件开发都很会拆文件啊.....一个Shader调用八百个文件...... [Universal RP/Shaders/UnlitInput.hlsl] #ifndef UNIVERSAL_UNLIT_INPUT_INCLUDED #define UNIVERSAL_UNLIT_INPUT_INCLUDED #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl" CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _Cutoff; half _Surface; CBUFFER_END #ifdef UNITY_DOTS_INSTANCING_ENABLED UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata) UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor) UNITY_DOTS_INSTANCED_PROP(float , _Cutoff) UNITY_DOTS_INSTANCED_PROP(float , _Surface) UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata) #define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float4 , Metadata_BaseColor) #define _Cutoff UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_Cutoff) #define _Surface UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float , Metadata_Surface) #endif #endif Input这里首先是为了SRP Batch将变量扔进CBUFFER，然后是一些有关DOTS的看不懂设置。本文有关DOTS和Instance的内容先都一律跳过，毕竟我是个看完DOTS定义就下班了的小废物。 [Universal RP/Shaders/UnlitForwardPass.hlsl] #ifndef URP_UNLIT_FORWARD_PASS_INCLUDED #define URP_UNLIT_FORWARD_PASS_INCLUDED #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Unlit.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" 虽然Unlit完全不包含光照计算，include了Lighting库其实是为了间接includeDBuffer.hlsl和AmbientOcclusion.hlsl，至于Unlit.hlsl后面再说（又拆一个至于吗至于吗至于吗）。 终于到了Shader的主体部分。在URP中，Attributes和Varyings对应Built-In中的a2v和v2f，算是一致的命名规范。 [Universal RP/Shaders/UnlitForwardPass.hlsl] struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; #if defined(DEBUG_DISPLAY) float3 normalOS : NORMAL; float4 tangentOS : TANGENT; #endif UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float2 uv : TEXCOORD0; float fogCoord : TEXCOORD1; float4 positionCS : SV_POSITION; #if defined(DEBUG_DISPLAY) float3 positionWS : TEXCOORD2; float3 normalWS : TEXCOORD3; float3 viewDirWS : TEXCOORD4; #endif UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; UnlitShader要做的实际上只是采样一个基础贴图，所以结构体中关键的参数其实只有坐标位置和uv。 顶点着色器 [Universal RP/Shaders/UnlitForwardPass.hlsl] Varyings UnlitPassVertex(Attributes input) { Varyings output = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); output.positionCS = vertexInput.positionCS; output.uv = TRANSFORM_TEX(input.uv, _BaseMap); #if defined(_FOG_FRAGMENT) output.fogCoord = vertexInput.positionVS.z; #else output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z); #endif #if defined(DEBUG_DISPLAY) // normalWS and tangentWS already normalize. // this is required to avoid skewing the direction during interpolation // also required for per-vertex lighting and SH evaluation VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); half3 viewDirWS = GetWorldSpaceViewDir(vertexInput.positionWS); // already normalized from normal transform to WS. output.positionWS = vertexInput.positionWS; output.normalWS = normalInput.normalWS; output.viewDirWS = viewDirWS; #endif return output; } 忽略instance的部分，首先是坐标空间的转换。 [Universal RP/Shaders/UnlitForwardPass.hlsl] VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); output.positionCS = vertexInput.positionCS; 这个方法其实一次性计算了OS到CSVSWSNDC的所有坐标，后面计算雾效、debug时也用到了CS以外的数据。方法位于ShaderLibrary的ShaderVariablesFunctions.hlsl，后面的很多工具函数都在这个文件之中。 [Universal RP/ShaderLibrary/ShaderVariablesFunctions.hlsl] VertexPositionInputs GetVertexPositionInputs(float3 positionOS) { VertexPositionInputs input; input.positionWS = TransformObjectToWorld(positionOS); input.positionVS = TransformWorldToView(input.positionWS); input.positionCS = TransformWorldToHClip(input.positionWS); float4 ndc = input.positionCS * 0.5f; input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w; input.positionNDC.zw = input.positionCS.zw; return input; } uv的变换跟Built-In是一样的： [Universal RP/Shaders/UnlitForwardPass.hlsl] output.uv = TRANSFORM_TEX(input.uv, _BaseMap); 雾坐标的计算可以分别在顶点/片元着色器中进行，需要传入深度值z坐标。如果逐顶点，用的是CS的z坐标，如果逐片元则传入的是VS的z坐标。 [Universal RP/ShaderLibrary/ShaderVariablesFunctions.hlsl] real ComputeFogFactor(float zPositionCS) { float clipZ_0Far = UNITY_Z_0_FAR_FROM_CLIPSPACE(zPositionCS); return ComputeFogFactorZ0ToFar(clipZ_0Far); } real ComputeFogFactorZ0ToFar(float z) { #if defined(FOG_LINEAR) // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start)) float fogFactor = saturate(z * unity_FogParams.z + unity_FogParams.w); return real(fogFactor); #elif defined(FOG_EXP) || defined(FOG_EXP2) // factor = exp(-(density*z)^2) // -density * z computed at vertex return real(unity_FogParams.x * z); #else return real(0.0); #endif } real是一个定义于SRP Core的Common.hlsl的宏，对于支持half的平台，real就是half，否则就是float。 UNITY_Z_0_FAR_FROM_CLIPSPACE这个宏定义于Universal RP/ShaderLibrary/core.hlsl。 有关深度值的拓展阅读：深入URP之Shader篇10: 深度值专题(1) 最后，虽然debugview不是我们的重点，但因为涉及到法线视角的转换，也稍微看一下： [Universal RP/Shaders/UnlitForwardPass.hlsl] #if defined(DEBUG_DISPLAY) // normalWS and tangentWS already normalize. // this is required to avoid skewing the direction during interpolation // also required for per-vertex lighting and SH evaluation VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); half3 viewDirWS = GetWorldSpaceViewDir(vertexInput.positionWS); // already normalized from normal transform to WS. output.positionWS = vertexInput.positionWS; output.normalWS = normalInput.normalWS; output.viewDirWS = viewDirWS; #endif [Universal RP/ShaderLibrary/ShaderVariablesFunctions.hlsl] VertexNormalInputs GetVertexNormalInputs(float3 normalOS, float4 tangentOS) { VertexNormalInputs tbn; // mikkts space compliant. only normalize when extracting normal at frag. real sign = real(tangentOS.w) * GetOddNegativeScale(); tbn.normalWS = TransformObjectToWorldNormal(normalOS); tbn.tangentWS = real3(TransformObjectToWorldDir(tangentOS.xyz)); tbn.bitangentWS = real3(cross(tbn.normalWS, float3(tbn.tangentWS))) * sign; return tbn; } // Computes the world space view direction (pointing towards the viewer). float3 GetWorldSpaceViewDir(float3 positionWS) { if (IsPerspectiveProjection()) { // Perspective return GetCurrentViewPosition() - positionWS; } else { // Orthographic return -GetViewForwardDir(); } } 片元着色器 [Universal RP/Shaders/UnlitForwardPass.hlsl] half4 UnlitPassFragment(Varyings input) : SV_Target { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); half2 uv = input.uv; half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv); half3 color = texColor.rgb * _BaseColor.rgb; half alpha = texColor.a * _BaseColor.a; AlphaDiscard(alpha, _Cutoff); #if defined(_ALPHAPREMULTIPLY_ON) color *= alpha; #endif InputData inputData; InitializeInputData(input, inputData); SETUP_DEBUG_TEXTURE_DATA(inputData, input.uv, _BaseMap); #ifdef _DBUFFER ApplyDecalToBaseColor(input.positionCS, color); #endif #if defined(_FOG_FRAGMENT) #if (defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)) float viewZ = -input.fogCoord; float nearToFarZ = max(viewZ - _ProjectionParams.y, 0); half fogFactor = ComputeFogFactorZ0ToFar(nearToFarZ); #else half fogFactor = 0; #endif #else half fogFactor = input.fogCoord; #endif half4 finalColor = UniversalFragmentUnlit(inputData, color, alpha); #if defined(_SCREEN_SPACE_OCCLUSION) && !defined(_SURFACE_TYPE_TRANSPARENT) float2 normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS); AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(normalizedScreenSpaceUV); finalColor.rgb *= aoFactor.directAmbientOcclusion; #endif finalColor.rgb = MixFog(finalColor.rgb, fogFactor); return finalColor; } 有关纹理采样，SAMPLE_TEXTURE2D是一个定义在SRP Core shader library中的一个跨平台的宏，例如： dx11是 #define SAMPLE_TEXTURE2D(textureName, samplerName, coord2) textureName.Sample(samplerName, coord2) GLES2是 #define SAMPLE_TEXTURE2D(textureName, samplerName, coord2) tex2D(textureName, coord2) 接下来大段是关于alpha的处理： 如果定义了_ALPHATEST_ON，则会使用_Cutoff对alpha进行clip： [Universal RP/ShaderLibrary/ShaderVariablesFunctions.hlsl] void AlphaDiscard(real alpha, real cutoff, real offset = real(0.0)) { #ifdef _ALPHATEST_ON if (IsAlphaDiscardEnabled()) clip(alpha - cutoff + offset); #endif } 如果定义了_ALPHAPREMULTIPLY_ON，则会对color预乘alpha。 DBUFFER存储Decal贴花的信息。如果物体开启了接受Decal，这里就会应用DBUFFER里的数据。 [Universal RP/Shaders/UnlitForwardPass.hlsl] #ifdef _DBUFFER ApplyDecalToBaseColor(input.positionCS, color); #endif [Universal RP/ShaderLibrary/DBuffer.hlsl] void ApplyDecalToBaseColor(float4 positionCS, inout half3 baseColor) { FETCH_DBUFFER(DBuffer, _DBufferTexture, int2(positionCS.xy)); DecalSurfaceData decalSurfaceData; DECODE_FROM_DBUFFER(DBuffer, decalSurfaceData); // using alpha compositing https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch23.html, mean weight of 1 is neutral // Note: We only test weight (i.e decalSurfaceData.xxx.w is < 1.0) if it can save something baseColor.xyz = baseColor.xyz * decalSurfaceData.baseColor.w + decalSurfaceData.baseColor.xyz; 基础色完成后，进行雾坐标的计算。如果在顶点着色器中计算过了，这里就不用再算一遍了。注意在片元着色器中用的是View Space，所以要注意z的正负。 [Universal RP/Shaders/UnlitForwardPass.hlsl] #if defined(_FOG_FRAGMENT) #if (defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)) float viewZ = -input.fogCoord; float nearToFarZ = max(viewZ - _ProjectionParams.y, 0); half fogFactor = ComputeFogFactorZ0ToFar(nearToFarZ); #else half fogFactor = 0; #endif #else half fogFactor = input.fogCoord; #endif 前文提到UnlitForwardPass还include了一个Unlit.hlsl，唯一功能就是 [Universal RP/Shaders/UnlitForwardPass.hlsl] half4 finalColor = UniversalFragmentUnlit(inputData, color, alpha); UniversalFragmentUnlit()方法有一个重载，将所有信息装进SurfaceData，再进行最终的计算。我觉得这里这么做是为了跟其他Shader计算PBR光照时保持一致的结构，Unlit其实没有做啥.... [Universal RP/ShaderLibrary/Unlit.hlsl] #ifndef UNLIT_INCLUDED #define UNLIT_INCLUDED #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Debug/Debugging3D.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Input.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceData.hlsl" half4 UniversalFragmentUnlit(InputData inputData, SurfaceData surfaceData) { half3 albedo = surfaceData.albedo; #if defined(DEBUG_DISPLAY) half4 debugColor; if (CanDebugOverrideOutputColor(inputData, surfaceData, debugColor)) { return debugColor; } #endif half4 finalColor = half4(albedo + surfaceData.emission, surfaceData.alpha); return finalColor; } // Deprecated: Use the version which takes "SurfaceData" instead of passing all of these arguments. half4 UniversalFragmentUnlit(InputData inputData, half3 color, half alpha) { SurfaceData surfaceData; surfaceData.albedo = color; surfaceData.alpha = alpha; surfaceData.emission = 0; surfaceData.metallic = 0; surfaceData.occlusion = 1; surfaceData.smoothness = 1; surfaceData.specular = 0; surfaceData.clearCoatMask = 0; surfaceData.clearCoatSmoothness = 1; surfaceData.normalTS = half3(0, 0, 1); return UniversalFragmentUnlit(inputData, surfaceData); } #endif 回到UnlitForwardPass，对于Opaque的表面，计算SSAO： [Universal RP/Shaders/UnlitForwardPass.hlsl] #if defined(_SCREEN_SPACE_OCCLUSION) && !defined(_SURFACE_TYPE_TRANSPARENT) float2 normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS); AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(normalizedScreenSpaceUV); finalColor.rgb *= aoFactor.directAmbientOcclusion; #endif 首先获取屏幕空间坐标： [Universal RP/ShaderLibrary/ShaderVariablesFunctions.hlsl] float2 GetNormalizedScreenSpaceUV(float2 positionCS) { float2 normalizedScreenSpaceUV = positionCS.xy * rcp(GetScaledScreenParams().xy); TransformNormalizedScreenUV(normalizedScreenSpaceUV); return normalizedScreenSpaceUV; } 然后采样SSAO，Unlit用的是directAmbientOcclusion，会对采样结果用_AmbientOcclusionParam.w做插值。 [Universal RP/ShaderLibrary/AmbientOcclusion.hlsl] AmbientOcclusionFactor GetScreenSpaceAmbientOcclusion(float2 normalizedScreenSpaceUV) { AmbientOcclusionFactor aoFactor; #if defined(_SCREEN_SPACE_OCCLUSION) && !defined(_SURFACE_TYPE_TRANSPARENT) float ssao = SampleAmbientOcclusion(normalizedScreenSpaceUV); aoFactor.indirectAmbientOcclusion = ssao; aoFactor.directAmbientOcclusion = lerp(half(1.0), ssao, _AmbientOcclusionParam.w); #else aoFactor.directAmbientOcclusion = 1; aoFactor.indirectAmbientOcclusion = 1; #endif #if defined(DEBUG_DISPLAY) switch(_DebugLightingMode) { case DEBUGLIGHTINGMODE_LIGHTING_WITHOUT_NORMAL_MAPS: aoFactor.directAmbientOcclusion = 0.5; aoFactor.indirectAmbientOcclusion = 0.5; break; case DEBUGLIGHTINGMODE_LIGHTING_WITH_NORMAL_MAPS: aoFactor.directAmbientOcclusion *= 0.5; aoFactor.indirectAmbientOcclusion *= 0.5; break; } #endif return aoFactor; } half SampleAmbientOcclusion(float2 normalizedScreenSpaceUV) { float2 uv = UnityStereoTransformScreenSpaceTex(normalizedScreenSpaceUV); return half(SAMPLE_TEXTURE2D_X(_ScreenSpaceOcclusionTexture, sampler_ScreenSpaceOcclusionTexture, uv).x); } 最最最最最后，对物体应用雾效。 [Universal RP/Shaders/UnlitForwardPass.hlsl] finalColor.rgb = MixFog(finalColor.rgb, fogFactor); 到这里其实就是个简单插值了，没啥可说的。 [Universal RP/ShaderLibrary/ShaderVariablesFunctions.hlsl] half3 MixFog(half3 fragColor, half fogFactor) { return MixFogColor(fragColor, unity_FogColor.rgb, fogFactor); } half3 MixFogColor(half3 fragColor, half3 fogColor, half fogFactor) { #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) half fogIntensity = ComputeFogIntensity(fogFactor); fragColor = lerp(fogColor, fragColor, fogIntensity); #endif return fragColor; } UnlitForwardPass部分到此结束！没想到简简单单一个Unlit让我翻了那么多个文件..... 写这篇参考了很多happyfire大佬最近的日更文章，但是URP版本不太一样，新的URP真的加了好多内容。 这篇真的好长，实在是重写到吐血...就当温故而知新了😇😇😇😇😇 晚安！下一篇读DepthOnlyPass！
💡 学点儿丨URP Shader编辑指北[1] - URP Shader Structure
说来惭愧，最近也做了不少把built-in shader转URP/HDRP的工作...但一直在很没出息地用Shader Graph.....趁这几天闲，好好掰扯掰扯URP和HDRP的Shader区别。 本篇引擎版本为Unity 2021.3.3f1c1 Universal RP 12.1.6 从Built-In到URP PASS可用差别 之前Built-in管线写前向渲染时，使用一个pass计算主光，再用多个其他pass叠加光照。但URP中不支持多个渲染Pass。 URP提供的几个默认Shader中包含多个Pass，但是只能有一个渲染Pass： 渲染Pass "LightMode" = "UniversalForward" 输出到帧缓存，只能有一个 投影Pass "LightMode" = "ShadowCaster" 深度Pass "LightMode" = "DepthOnly" 其他Pass 专门用于烘焙的Pass，专门用于2D渲染的Pass等 通过Render Feature可以实现之前多Pass的描边等效果。 为了使URP认识并能渲染所需的pass，要记得在SubShader中加上tag： "RenderPipeline"="UniversalPipeline" CG to HLSL URP和HDRP都使用了HLSL，包含Shader语言的代码块要改为： HLSLPROGRAME ... ENDHLSL 头文件变化 包含的头文件也需要从"UnityCG.cginc"改为 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" Lighting相关的头文件为 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" 对应的一些函数名称也会有变化。 CBUFFER 为使用SRP Batcher，所有贴图外的暴露参数需要用CBUFFER_START(UnityPerMaterial)与CBUFFER_END包裹，并置于Pass之前。 CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; float _Color; CBUFFER_END Basic Shader Structure 以下是一个可以有URP编译的基础Shader： // This shader fills the mesh shape with a color predefined in the code. Shader "Example/URPUnlitShaderBasic" { // The properties block of the Unity shader. In this example this block is empty // because the output color is predefined in the fragment shader code. Properties { } // The SubShader block containing the Shader code. SubShader { // SubShader Tags define when and under which conditions a SubShader block or // a pass is executed. Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" } Pass { // The HLSL code block. Unity SRP uses the HLSL language. HLSLPROGRAM // This line defines the name of the vertex shader. #pragma vertex vert // This line defines the name of the fragment shader. #pragma fragment frag // The Core.hlsl file contains definitions of frequently used HLSL // macros and functions, and also contains #include references to other // HLSL files (for example, Common.hlsl, SpaceTransforms.hlsl, etc.). #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" // The structure definition defines which variables it contains. // This example uses the Attributes structure as an input structure in // the vertex shader. struct Attributes { // The positionOS variable contains the vertex positions in object // space. float4 positionOS : POSITION; }; struct Varyings { // The positions in this struct must have the SV_POSITION semantic. float4 positionHCS : SV_POSITION; }; // The vertex shader definition with properties defined in the Varyings // structure. The type of the vert function must match the type (struct) // that it returns. Varyings vert(Attributes IN) { // Declaring the output object (OUT) with the Varyings struct. Varyings OUT; // The TransformObjectToHClip function transforms vertex positions // from object space to homogenous clip space. OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz); // Returning the output. return OUT; } // The fragment shader definition. half4 frag() : SV_Target { // Defining the color variable and returning it. half4 customColor = half4(0.5, 0, 0, 1); return customColor; } ENDHLSL } } }
💡学点儿丨关于纹理的亿点点补课1 - 纹理压缩
之前写插件的时候，写入纹理时发现与原图的压缩格式不同，做设计课前再来摸会儿🐟补补课。 纹理导入 文件格式 文件格式是图像为了存储信息而使用的对信息的特殊编码方式，它存储在磁盘中，或者内存中，但是并不能被GPU所识别，因为以向量计算见长的GPU对于这些复杂的计算无能为力。 Unity 支持读取以下文件格式 BMP，EXR，GIF，HDR，IFF，JPG，PICT，PNG，PSD，TGA，TIFF Unity 可导入多层PSD文件，导入时自动展平，但图层在资源本身中维护。 这些文件格式当被游戏读入后，还是需要经过CPU解压成R5G6B5，A4R4G4B4，A1R5G5B5，R8G8B8, A8R8G8B8等像素格式，再传送到GPU端进行使用。 纹理尺寸大小 理想情况下，纹理尺寸大小应该是每边为2的整数次幂。Unity 允许使用NOPT（非2的幂）纹理大小；但是，NPOT纹理大小通常需要多一点内存，并且GPU的采样速度可能更慢。 如果平台或GPU不支持NPOT纹理带下，Unity会对纹理进行缩放和填充已达到下一个2的幂的大小。纹理导入后可将Advanced选项选择Not Power of 2，在导入时放大NPOT纹理资源。 纹理压缩 在解释一切之前，先划重点： PC 不带透明通道压缩成DXT1，带透明通道压缩为ETC5，不被4整除的回退到RGBA32 Android 不带透明通道压缩成ETC1，带透明通道压缩成ETC2，不被4整除的回退到RGBA32 IOS 不带透明通道压缩成RGB PVRTC，带透明通道压缩成RGBA PVRTC ，不是2的整数次幂回退到RGBA32 Texture图片拖入的时候，如下图所示; unity会默认设置ToNerest，这样会自动保证Android平台下图片被4整除，IOS平台下图片是2的整除次幂，所以默认情况下图片都可以得到最好的压缩。 BPP 每像素位数 (bpp) 表示单个纹理像素所需的存储量。较低的 bpp 值也意味着 GPU 使用较少的内存带宽来读取纹理像素。GPU 内存带宽通常是帧率的瓶颈，因此纹理压缩有助于避免这一问题。 未压缩纹理 Format BPP 1024*1024(Mb) A8R8G8B8(ARGB32) 32 4 A4R4B4G4(ARGB16) 16 2 R8G8B8(RGB24) 24 3 disk, 4 GPU R5G6B5(RGB16) 16 2 压缩纹理 Format BPP 1024*1024(Mb) RGB Compressed DXT1 4 0.5 RGB Compressed ETC2 4 0.5 RGBA Compressed ETC2 8 1 RGB Compressed PVRTC 4 bits 4 0.5 RGB Compressed PVRTC 2 bits 2 0.25 纹理资源的视觉质量越高，每像素的位数就越高；因此，这会增大构建大小、加载时间和运行时内存使用。在某种程度上，所有纹理压缩格式都是有损的。通常，为了获得最佳运行时性能和磁盘占用空间，对于大多数纹理资源，应该选择目标设备支持的纹理压缩格式，并且实现所需的视觉质量的最小每像素位数。 不同于png、jgp这种硬盘压缩方式而言，DXT,ETC等纹理压缩方式可以在游戏运行中无需CPU解压就被GPU直接采样，可以极大的减少内存和带宽的占用，提升运行效率。 DXT压缩 DXT是一种有损纹理压缩算法，微软的DirectX中支持，DXT的格式包括DXT1~DXT5，其中DXT1和DXT5较为多见，可以认为所有的PC端显卡都支持DXT压缩,是PC下的标准压缩格式。 DXT压缩要求像素长宽必须是4的倍数。另外RGB的24位转为了16位颜色，16位中R&B各占5位，但是G占了6位，这是因为人眼对绿色最为敏感。 DXT1 DXT1格式主要适用于不具透明度的贴图或仅具一位Alpha的贴图（非完全透明则即完全不透明），对于完全RGB565格式的贴图，DXT1具有4：1的压缩比，即平均每个像素颜色占4位. DXT1将每4×4个像素块视为一个压缩单位，压缩后的4×4个像素块占用64位，其中有2个16位的RGB颜色和16个2位索引. DXT5 DXT5要比DXT1大一倍，对alpha通道的储存作了改进，它依旧用64bit储存16个alpha信息。前面2个字节（16bit）保存了当前块的最大alpha值和最小alpha值。接下来的48 bit，每个像素占用3bit空间，刚好描述 4x4个像素。 当alpha_0大于alpha_1 时，我们后面的3bit可以表示8级的插值；反之则保留110和111分别表示alpha为0和255的情况,中间可以有 6 级过度的插值。 DXT并没有在移动端得到很大的支持，在iOS设备中支持的是PVR压缩，在Android中支持的是ETC压缩。 PVRTC PVRTC (PowerVR Texture Compression)由Imagination公司专为PowerVR显卡核心设计，由于专利原因一般它只被用于苹果的设备，仅Iphone、Ipad和部分PowerVR的安卓机支持。 PVRTC不同于DXT和ETC这类基于块的算法，而将整张纹理分为了高频信号和低频信号，低频信号由两张低分辨率的图像A和B表示，这两张图在两个维度上都缩小了4倍，高频信号则是全分辨率但低精度的调制图像M，M记录了每个像素混合的权重。要解码时，A和B图像经过双线性插值（bilinearly）宽高放大4倍，然后与M图上的权重进行混合。 有别于其他压缩方案，PVRTC 利用了跨块边界的颜色的相似性。因此这种方案本身支持平滑梯度。 ETC ETC（Ericsson Texture Compression）最初为移动设备开发，如今它是安卓的标准压缩方案。将图像中的chromatic和luminance分开存储的方式，而在解码时使用luminance对chromatic进行调制进而重现原始图像信息。 其原理简单来说，是将4x4 的像素块编码为2x4或4x2像素的两个块的方法，每个块指定一个基色，每个像素的颜色通过一个编码为相对于这些基色偏移的灰度值确定。 Unity几种ETC纹理压缩格式： RGB ETC1 4 bit ：4 bits/pixel，对RGB压缩比6:1，不支持Alpha，绝大部分安卓设备都支持。 RGB ETC2 4 bit ：4 bits/pixel，对RGB压缩比6:1。不支持Alpha，ETC2兼容ETC1，压缩质量可能更高，但对于色度变化大的块误差也更大，需要在OpenGL ES 3.0和OpenGL 4.3以上版本。 RGBA ETC2 8bit ：8 bits/pixel，对RGBA压缩比4:1。支持完全的透明通道，版本要求同上。 RGB +1bit Alpha ETC2 4bit ：4 bits/pixel。支持1bit的Alpha通道，也就是只支持镂空图，图片只有透明和不透明部分，没有中间的透明度。 EAC：核心原理与ETC相同，但它只用于单通道或双通道数据，OpenGL ES 3.0和OpenGL 4.3后的设备大部分支持，但由于安卓平台五花八门的兼容性，一般不建议用单双通道贴图。 Crunch 压缩 Crunch 是一种压缩格式，基于 DXT 或 ETC 压缩，通过提供额外的可变比特率压缩。当 Unity 加载一个经过 Crunch 压缩的纹理时，它会在 CPU 上将该纹理解压缩为 DXT 或 ETC，然后将 DXT 或 ETC 压缩纹理数据上传至 GPU。 Crunch 压缩有助于纹理在磁盘上使用尽可能少的空间，但对于运行时内存使用量没有影响。Crunch 纹理可能需要很长时间进行压缩，但在运行时的解压缩速度非常快。您可以调整 Crunch 压缩的有损程度，以在文件大小和质量之间取得平衡。 ATI1/2 ATI1/2是ATI公司开发的纹理压缩格式，也被称为BC4,其每个数据块存储单个颜色的数据通道。以与DXT5中的Alpha数据相同的方式进行编码。常用于存储高度图，光滑度贴图。效果与原始图像基本没有差异。 ATI2也被称为BC5，每一个块中存储两个颜色通道的数据，同上以与DXT5中Alpha数据相同的方式进行编码，相当于存储了两个BC4块。 如果是在将法线存储在XY双通道中采用BC5格式压缩，由于每个通道都有自己的索引，因此法线贴图XY信息可以比在BC1中保留更多的保真度，缺点是需要使用两倍内存，也需要更多的带宽才能将纹理传递到着色器中。 格式选择 下表显示了每个平台上可用的纹理压缩格式选项以及生成的压缩文件大小（基于 256 像素平方图像）。选择纹理压缩格式需要在文件大小和质量之间取得平衡；质量越高，文件越大。 讲完了！ 回看一下这四张图片的压缩选项，由于我的项目默认平台是PC，因此默认的压缩是DXT形式。 _albedo&_height：不含A通道信息，所以压缩格式是DXT1。不过_height其实我们只需要一个通道的信息，使用BC4会更加合理。 _mask：含A通道信息，所以压缩格式是DXT5，不含Gamma。 _normal：DXTnm是基于DXT5，Unity的法线压缩方法：压缩后R(归为1)G(不变)B(归为1)A(原R通道值)。 A通道精度最高（自己独占64bit），用来存储视觉上影响较大的左右分量x；RGB中G精度最高，用来存储分量y。 参考文章： Texture formats Unity 解决包体过大问题记录和纹理相关知识点 纹理压缩简介 DXT PVR ETC [译介] 纹理压缩技术 DXT纹理压缩 ETC1纹理压缩原理
前两天写了一个手动拖入通道来生成maskmap的工具： 在这个的基础上又研究了一下，实现了对于文件夹目录的一键maskmap生成！还做了一键修正贴图导入设置的功能。 留个小尾巴，等有空了可以再写一个自动生成对应的hdrp材质并贴图。不过不清楚第三方资源库的材质和模型的关系..可能还需要从prefab中extract....算了！再说！ 更新下载地址👉TextureTool👈 资产相关类 有关Unity内的资源文件，有这样几个比较关键的类： AssetDatabase UnityEditor : An Interface for accessing assets and performing operations on assets. AssetDatabase是对资产操作的主要接口，可以复制、删除、查询创建文件或文件夹。 e.g. 获取资产路径： string path = AssetDatabase.GetAssetPath(metallic) ; 查找资产： var texture2Ds = AssetDatabase.FindAssets ("t:Texture2D", paths); 第一个字符串是filter，可以使用name/label("l:")/type("t:")检索。其返回值是资产string形式的GUID，需要使用AssetDatabase.GUIDToAssetPath获得其路径，然后使用AssetDatabase.LoadAssetAtPath加载资产。 刷新资源列表 AssetDatabase.Refresh(); 上一篇中，创建材质的方式是直接在对应路径写入文件，所以需要刷新并导入材质，让引擎识别新加入的文件。 AssetImporter UnityEditor ：Base class from which asset importers for specific asset types derive. AssetImporter感觉是记录引擎识别资产的方式，可能与.meta文件相关？ e.g. 利用userData防止重复编辑 var im = AssetImporter.GetAtPath (path); if (im.userData.Contains ("R2;")) continue; im.userData += "R2;"; 转成对应类型的Importer(如TextureImporter),可对其进行详细设置的修改。 对于材质而言，常用的可以设置Texturetype(转normal)，Tetureshapr(转cube)，sRGBTexture等。 （靠 这不写个材质一键检查非srgb？) （写了写了） DirectoryInfo System.IO : Exposes instance methods for creating, moving, and enumerating through directories and subdirectories. This class cannot be inherited. 有关路径的操作，不只限于Editor。 e.g. 查询路径是否存在 // Check if the target directory exists, if not, create it. if (Directory.Exists(target.FullName) == false) { Directory.CreateDirectory(target.FullName); } 获得文件夹中文件的info [FileSystemInfo][12][] fileSystemInfos = directoryInfo.GetFileSystemInfos(); 获取路径和资产 如果已经有某个资产，需要获得它的路径，可以使用AssetDatabase.GetAssetPath()方法，比如上一篇用到的： string path = AssetDatabase.GetAssetPath(metallic); 或者可以让用户选择要操作的文件夹路径： private void SelectFolderPath() { GUILayout.BeginHorizontal(); { FolderPath = EditorGUILayout.TextField("Textures Folder", FolderPath); if (GUILayout.Button("...", GUILayout.Width(25))) { FolderPath = EditorUtility.OpenFolderPanel("Select a folder", "Assets", ""); } EditorGUIUtility.labelWidth = 0; } GUILayout.EndHorizontal(); } EditorUtility.OpenFolderPanel()会打开一个选择文件夹的窗口，参数分别为[窗口名]，[默认文件夹],[默认名]。上一篇中自定义保存用到了类似的EditorUtility.SaveFilePanel()。 这里学到了一个处理路径的小trick，可以只记录项目文件夹后的路径信息。定义此参数时就可以设定它的get/set方式： private string _folderPath; protected string FolderPath { get { if (string.IsNullOrEmpty(_folderPath)) _folderPath = "Assets"; return _folderPath; } set { string pstr = Directory.GetParent(Application.dataPath).FullName; pstr = pstr.Replace("\\", "/"); if (value != null && value.Contains(pstr)) _folderPath = value.Replace(pstr + "/", ""); else _folderPath = value; } } 获得文件夹路径后，就可以通过AssetDatabase.FindAsset找到资源： var texture2Ds = AssetDatabase.FindAssets ("t:Texture2D", new[]{FolderPath}); foreach (var textureguid in texture2Ds) { var texturepath = AssetDatabase.GUIDToAssetPath(textureguid); var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturepath); //other operations HERE } 批量更改图片导入设置 unity不能自动识别从第三方网站下载的贴图类型，根据贴图存储的信息不同，需要进行不同的导入设置： 前面提到这些设置信息是存储在TextureImporter中的，只要从路径获取对应的Importer就可以了。注意修改后要SaveandReimport()，相当于setting面板点击apply。 目前还没有用到AssetBundle，之后如果需要就也会写啦！ 实现代码如下： private void FixSettings() { //获取文件夹下全部材质 var texture2Ds = AssetDatabase.FindAssets ("t:Texture2D", new[]{FolderPath}); foreach (var textureguid in texture2Ds) { var texturepath = AssetDatabase.GUIDToAssetPath(textureguid); //从路径获取importer var importer = AssetImporter.GetAtPath(texturepath) as TextureImporter; //只处理2D材质 if (importer.textureShape == TextureImporterShape.Texture2D) { //法线贴图，type设置为normal if (texturepath.Contains("_normal")||texturepath.Contains("_Normal")) { if (importer.textureType != TextureImporterType.NormalMap) { importer.textureType = TextureImporterType.NormalMap; importer.SaveAndReimport(); } continue; } //固有色贴图，开启sRGB if (texturepath.Contains("_albedo") || texturepath.Contains("_Albedo") || texturepath.Contains("_basecolor") || texturepath.Contains("_BaseColor") || texturepath.Contains("_baseColor")|| texturepath.Contains("_color") ) { if (!importer.sRGBTexture) { importer.sRGBTexture = true; importer.SaveAndReimport(); } continue; } //其他贴图，不开启sRGB else { if (importer.sRGBTexture) { importer.sRGBTexture = false; importer.SaveAndReimport(); } } } 批量生成mask map 利用了之前mapCreator的函数，还写了一个删除不需要的贴图的功能。其实这里还应该有一个弹出窗口警告不可撤销⚠但是我累了，，1.1再说，，， 有感而发：对于批处理工具而言，命名真的好重要哇。 private void BatchMask() { var texture2Ds = AssetDatabase.FindAssets ("t:Texture2D", new[]{FolderPath}); string currentTexture = ""; maptype = 0; bool hasM=false, hasAO=false,hasDM = false, hasR=false; foreach (var textureguid in texture2Ds) { var texturepath = AssetDatabase.GUIDToAssetPath(textureguid); //获取前缀前的材质名，用于识别贴图组 string exactname = Reverse(texturepath); exactname = exactname.Split(new char[]{'_'}, 2)[1]; exactname = Reverse(exactname); //当读入新的贴图名时，生成上一贴图组的maskmap if (exactname != currentTexture) { //当至少含有一张生成maskmap所需贴图时 if (hasM || hasR || hasDM || hasAO) { //将没有的贴图改为默认值，以免用到上一贴图组的贴图 if(!hasM) metallic = null; if(!hasAO) occlusion = null; if(!hasDM) detailmask = null; if(!hasR) smoothness = null; GenPictureAuto(); //如果需要删除hdrp不需要的贴图 if (deleteunused) { string deletepath; if (hasM) { deletepath = AssetDatabase.GetAssetPath(metallic); AssetDatabase.DeleteAsset(deletepath); } if (hasAO) { deletepath = AssetDatabase.GetAssetPath(occlusion); AssetDatabase.DeleteAsset(deletepath); } if (hasDM) { deletepath = AssetDatabase.GetAssetPath(detailmask); AssetDatabase.DeleteAsset(deletepath); } if (hasR) { deletepath = AssetDatabase.GetAssetPath(smoothness); AssetDatabase.DeleteAsset(deletepath); } AssetDatabase.Refresh(); } } //重置信息 hasM = false; hasR = false; hasDM = false; hasAO = false; currentTexture = exactname; } //若为金属度贴图 if (texturepath.Contains("_metallic") || texturepath.Contains("_Metallic")) { hasM = true; metallic = AssetDatabase.LoadAssetAtPath<Texture2D>(texturepath); } //若为ao else if(texturepath.Contains("_ao") || texturepath.Contains("_AO") || texturepath.Contains("_occlusion")|| texturepath.Contains("_Occlusion")) { hasAO = true; occlusion = AssetDatabase.LoadAssetAtPath<Texture2D>(texturepath); } //若为Smoothness else if(texturepath.Contains("_smoothness") || texturepath.Contains("_Smoothness")) { hasR = true; useRoughness = false; smoothness = AssetDatabase.LoadAssetAtPath<Texture2D>(texturepath); } //若为Roughness else if(texturepath.Contains("_Roughness") || texturepath.Contains("_roughness")) { hasR = true; useRoughness = true; smoothness = AssetDatabase.LoadAssetAtPath<Texture2D>(texturepath); } //若为DetailMask else if(texturepath.Contains("_DetailMask") || texturepath.Contains("_detailmask")) { hasDM = true; detailmask = AssetDatabase.LoadAssetAtPath<Texture2D>(texturepath); } } } 改变图片大小 加载图片，将其画在一张RenderTexture中，再新建一张Texture2D，从rt中读出来处理过的数据，再存进原路径中。 目前的这个功能还很不满意！用RT计算应该不是很合理的图像采样方法，放大缩小都会丢失很多细节... 不过还是先放一下~权当复习RT和图像写入了~ private void ResizeTextures() { var tempTexture = new Texture2D((int) resizeWH.x, (int) resizeWH.y, TextureFormat.ARGB32, false); //获取文件夹下全部材质 var texture2Ds = AssetDatabase.FindAssets ("t:Texture2D", new[]{FolderPath}); foreach (var textureguid in texture2Ds) { var texturepath = AssetDatabase.GUIDToAssetPath(textureguid); var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturepath); //若不需要重新调整大小，则跳过 Vector2 currentsize = new Vector2(texture.width, texture.height); if(currentsize == resizeWH) continue; var rt = RenderTexture.GetTemporary((int) resizeWH.x, (int) resizeWH.y, 0, RenderTextureFormat.ARGB32); rt.DiscardContents(); Graphics.Blit(texture,rt); RenderTexture.active = rt; tempTexture.ReadPixels(new Rect(0,0,rt.width,rt.height),0,0); tempTexture.Apply(); if (texturepath.EndsWith(".png")) { //如果该材质含有tif标签，则跳过 var im = AssetImporter.GetAtPath (texturepath); if (im.userData.Contains ("FromTIF;")) continue; File.WriteAllBytes(texturepath, tempTexture.EncodeToPNG()); } else if (texturepath.EndsWith(".jpg")) { File.WriteAllBytes(texturepath, tempTexture.EncodeToJPG()); } else if (texturepath.EndsWith(".tif")) { Debug.LogWarningFormat(texturepath + "has been saved as PNG."); string newname = texturepath.Substring(0, texturepath.Length - 3) + "png"; File.WriteAllBytes(newname, tempTexture.EncodeToPNG()); //为tif转换的png图片增加标签 var im = AssetImporter.GetAtPath(newname); im.userData += "FromTIF;"; } RenderTexture.ReleaseTemporary(rt); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } 参考文章 Unity编辑器 - 资源批处理工具基类 [编辑器]Unity批量处理图片
写在前面 实习以来帮hdrp改了不少资产，每次进ps做maskmap真的好麻烦，试试写个脚本工具来完成这个工作。 它可以—— 点击下载👉MapCreator👈 （下一个目标：批处理！） Unity Editor Window 先放一个完整的代码（Rider的高亮 好爱）： 首先创建一个工具窗口。新建脚本，编辑器窗口继承自EditorWindow类，需要使用UnityEditor命名空间。 using UnityEngine; using UnityEditor; public class ExampleWindow : EditorWindow{ MenuItem可以在菜单栏创建选项。 GetWindow方法可以打开窗口/聚焦到已打开的该窗口，的类型应为所定义的窗口类。 [MenuItem("Window/Tools/Example")] public static void ShowWindow() { GetWindow<ExampleWindow>("Example"); } GetWindow可选参数包括Utility和Title，对于窗口的类型和标题。标题也可以直接赋值窗口类的titleContent.text属性。 UI元素一般属于GUILayout和EditorGUILayout两个类。有一些功能是重复的，不过都可以一样用~ void OnGUI() { //WindowCode GUILayout.Label("A Lable.");//or EditorGUILayout.LabelField("Also A LableField"); GUILayout.Button("A Button"); GUILayout.Toggle(aBool,"A Toggle"); //or EditorGUILayout.Toggle("Also A Toggle",anotherBool); EditorGUILayout.TextField("A TextField"); EditorGUILayout.ColorField("A ColorField", aColor); EditorGUILayout.CurveField("A CurveField", aCurve); EditorGUILayout.Slider(value, 0.0f, 1.0f); } 更详细的内容可以参考： Get to work 工具的框架设计如图： 接下来就一步一步解释一下这个工具都做了啥—— 首先声明我们需要的变量： using System; using System.IO; using UnityEditor; using UnityEngine; public class MapCreator : EditorWindow { private Material mat; private int maptype = 0,_maptype; private string[] maptypes = new string[] {"Mask Map", "Detail Map", "Custom"}; private Texture2D metallic, occlusion, detailmask, smoothness, deAbedo, deNormal, deSmoothness; private bool useRoughness; //用以记录输入是否更新 private bool changed; private Texture2D _metallic, _occlusion, _detailmask, _smoothness, _deAbedo, _deNormal, _deSmoothness; private bool _useRoughness; 我们希望每更换一张贴图时，preview都会刷新，所以设置了一组同名参数以记录参数是否变化。 //创建菜单栏 [MenuItem("Window/Tools/MapCreator")] //创建窗口 static void OpenMapCreator() { MapCreator mapCreator = GetWindow<MapCreator>("Map Creator"); mapCreator.Init(); } //初始化材质 private void Init() { if (mat == null) { mat = new Material(Shader.Find("Hidden/MapCreator"));//create material mat.hideFlags = HideFlags.HideAndDontSave; } } //窗口界面 public void OnGUI() { //选择生成贴图类型 GUILayout.Space(10); maptype = EditorGUILayout.Popup("Map Type", maptype, maptypes); GUILayout.Space(10); //绘制贴图盒 if (maptype == 0 )//mask map { useRoughness = EditorGUILayout.Toggle("use Roughness?", useRoughness); MaskMapInput(); } else if (maptype == 1) //detail map { useRoughness = EditorGUILayout.Toggle("use Roughness?", useRoughness); DetailMapInput(); } else//custom map { EditorGUILayout.Space(20); CustomInput(); } if (maptype != _maptype || useRoughness!=_useRoughness) { changed = true; _maptype = maptype; _useRoughness = useRoughness; } //绘制预览贴图 Preview(); //绘制创建按钮 CreateMask(); } 贴图盒部分由于要重复使用多次，包装成了一个独立的函数： private Texture2D TextureField(string name, Texture2D texture) { EditorGUILayout.BeginVertical(); var style = new GUIStyle(GUI.skin.label); style.alignment = TextAnchor.MiddleCenter; style.fixedWidth = 80; GUILayout.Label(name,style); Texture2D result = EditorGUILayout.ObjectField(texture, typeof(Texture2D), false, GUILayout.Width(80),GUILayout.Height(80)) as Texture2D; EditorGUILayout.EndVertical(); return result; } 使用这个函数，可以在不同maptype下绘制不同贴图选择盒： private void MaskMapInput() { EditorGUILayout.BeginHorizontal("Box"); _metallic = TextureField("Metallic", metallic); if (_metallic != metallic) { metallic = _metallic; changed = true; } _occlusion = TextureField("Occlusion", occlusion); if (_occlusion != occlusion) { occlusion = _occlusion; changed = true; } _detailmask = TextureField("Detailmask", detailmask); if (_detailmask != detailmask) { detailmask = _detailmask; changed = true; } if (!useRoughness) { _smoothness = TextureField("Smoothness", smoothness); } else { _smoothness = TextureField("Roughness", smoothness); } if (_smoothness != smoothness) { smoothness = _smoothness; changed = true; } EditorGUILayout.EndHorizontal(); } //DetailMapInput(),CustomMapInput()见源码 每当贴图类型或者输入贴图变化时，会通过这个函数更新输出预览： private Texture2D preview = null; private void Preview() { EditorGUILayout.BeginHorizontal(); GUILayout.Label("Preview Output"); EditorGUILayout.EndHorizontal(); //居中绘制贴图框 EditorGUILayout.BeginHorizontal(); Rect previewRect = new Rect(this.position.width / 2 - 75, 200, 150, 150); //初始绘制默认纹理 if (preview == null) { preview = Texture2D.blackTexture; } //每当输入参数改变时，刷新材质 if (changed) { //path只用于自动更名保存，这里其实不需要 string path = ""; preview = GetTexture(ref path); changed = false; } //在贴图框中绘制贴图 EditorGUI.DrawPreviewTexture(previewRect,preview); EditorGUILayout.Space(170); EditorGUILayout.EndHorizontal(); } 这里是最重要的部分，GetTexture创建RT并使用Blit()将输出结果渲染至RT上。传入贴图时，还读取了贴图的尺寸和路径信息，以便维持输出贴图与原贴图的一致性。 private Texture2D GetTexture(ref string path) { //保持新创建的贴图与原贴图尺寸相同 Vector2 rtSize = new Vector2(2048, 2048); //用于路径更名 char[] splitor = {'_'}; //传入shader所需参数 Shader.SetGlobalInt("maptype", maptype); if (!useRoughness) Shader.SetGlobalInt("useRoughness", 0); else Shader.SetGlobalInt("useRoughness", 1); //传入shader所需贴图 if (maptype == 1) { if (deAbedo != null) { mat.SetTexture("_deAbedo", deAbedo); rtSize.x = deAbedo.width; rtSize.y = deAbedo.height; path = AssetDatabase.GetAssetPath(deAbedo); path = Reverse(path); path = path.Split(splitor, 2)[1]; path = Reverse(path); } //....其他贴图略，详见源码 } //创建RT作为Blit()的渲染目标target RenderTexture tempRT = new RenderTexture((int)rtSize.x, (int)rtSize.y, 16, RenderTextureFormat.ARGB32); tempRT.Create(); //创建texture作为Blit()的源材质source Texture2D temp2 = new Texture2D(tempRT.width, tempRT.height, TextureFormat.ARGB32, false); //计算贴图 Graphics.Blit(temp2,tempRT,mat); //暂存当前激活的RT RenderTexture preview = RenderTexture.active; //激活渲染结果RT并从中读取数据 RenderTexture.active = tempRT; Texture2D output = new Texture2D(tempRT.width, tempRT.height, TextureFormat.ARGB32, false); output.ReadPixels(new Rect(0,0,tempRT.width,tempRT.height),0,0); output.Apply(); //还原激活的RT RenderTexture.active = preview; return output; } 最后是贴图的写入方法： //根据其他贴图的路径自动保存贴图 private void GenPictureAuto() { string savepath = ""; Texture2D output = GetTexture(ref savepath); if(maptype == 0) savepath += "_mask"; else if (maptype == 1) savepath += "_detailmap"; byte[] bytes = output.EncodeToPNG(); FileStream file = File.Open(savepath + ".png", FileMode.Create); BinaryWriter writer = new BinaryWriter(file); writer.Write(bytes); file.Close(); Texture2D.DestroyImmediate(output); output = null; AssetDatabase.Refresh(); } //手动保存贴图 private void GenPictureCustom() { string savepath = ""; Texture2D output = GetTexture(ref savepath); string savePath = EditorUtility.SaveFilePanel("Save", Application.dataPath, "Texture.png", "png"); File.WriteAllBytes(savePath,output.EncodeToPNG()); AssetDatabase.Refresh(); } Shader 众所周知() Blit()使用的Shader很简单，只需要改一下通道即可，这里就只放Frag()部分： fixed4 frag (v2f i) : SV_Target { fixed4 col; if(maptype == 0||maptype == 2) { col.r = tex2D(_Metallic,i.uv).r; col.g = tex2D(_Occlusion,i.uv).r; col.b = tex2D(_DetailMask,i.uv).r; if(useRoughness == 1 && maptype == 0) { col.a = 1-tex2D(_Smoothness,i.uv).r; } else { col.a = tex2D(_Smoothness,i.uv).r; } } else { fixed4 deabedo = tex2D(_deAbedo,i.uv); col.r = deabedo.r*0.3+deabedo.g*0.59+deabedo.b*0.11; col.g = tex2D(_deNormal,i.uv).g; if(useRoughness==0) { col.b = tex2D(_deSmoothness,i.uv).r; } else { col.b = 1-tex2D(_deSmoothness,i.uv).r; } col.a = tex2D(_deNormal,i.uv).r; } return col; } 参考 https://blog.csdn.net/enternalstar/article/details/122135726 https://blog.csdn.net/qq_23936433/article/details/107433042
🌿 | 风吹草低低低低 - 植被动画Ⅰ:顶点色实现
写在前面 好久没写博客了...最近做完Substance的素材后就在赶中期进度，再加上休了一整个清明周，真是咕了好久。这几天帮忙整理SpeedTree到HDRP的流程，就顺便补一补植被动画的课。 Basic Vertex Animation Shader 首先是比较经典的顶点动画实现方法，这里写了一个Subgraph用来计算顶点位置。 首先，我们希望植被是围绕生长的位置旋转，所以顶点位置的运算在object space进行，以(0,0,0)为旋转中点。 使旋转角度与距离相关，就可以让靠近枝头的顶点旋转得更远，使枝条看起来更柔软。加入基础的正弦动画，则可以得到这样的整体摆动效果。 另外我们希望风强较大时，植被整体会产生倒伏，在周期性摇摆前又增加了一次整体的旋转。 Details from VertexColor 在speedtree或其他dcc软件中可以利用顶点色存储分簇/随机等信息，为动画添加细节。 speedtree在view-render中开启顶点色显示。 在每个节点属性的vertex color一栏可以设置顶点颜色。如果没有这一栏，需要在tree运算器的Compute Flag中开启Vertex Color。 这里记录两种颜色通道的分配方式，首先是kk的教程版： Chris’s version Red Channel R通道用来区分树冠的层次，第一级树枝线性增长，后续层级皆继承父级，总体上红色从内至外分布。 在shader中，用红通道增加了风向的扰动，也用来控制从树干到枝头渐变的各种效果。 Blue Channel B通道用来区分不同的分簇，第一级树枝随机分布，后续层级皆继承父季，使每一簇颜色不同。 在shader中，用蓝通道增加了y方向的变化。 Green Channel G通道用来增加随机性，树枝线性增长，树叶继承后增加随机性。 在shader中用来增加叶片小枝条的动画。 着色后的预览效果如下： ps.这里踩了一个小坑，speedtree有多个版本，之前使用的Unity Subscription Edition就没做顶点色导出的功能，只能换成Game Indie Edition导出为fbx才能使用顶点色信息😤😤。 利用顶点色可以增添更多shader的动画细节： R通道是动画总控，是最主要的mask。后续BG的噪声都可以乘以R以过滤掉主要的树干。 BG通道可以分别为分簇和小叶片增加动画细节，将时间与通道值相加再求sine可以使动画获得不同的相位差。这里首先为分簇增加了y方向的的动画，实现方法为构造一个y值周期变化的Vector3，并与先前的坐标相加。分簇（B通道）的频率低一些，幅度大一些，叶片（G通道）的频率高一些，幅度小一些。为了让叶片的变化差别更大，这里周期长度、相位和幅度都用到了G通道的值： 到这里其实基本的Shader已经完成了。为了更好的视觉效果，我们还可以为叶片再添加一些旋转的细节。 用教程中的素材进行说明，在这里额外引入了一张贴图，r通道标记叶子的左侧，g通道标记叶子的右侧，b通道标记了整体的渐变。 这样单一四边形的叶片也可以拥有简单的弯折动画： Lumberyard version Blue Channel - Overall Stiffness 低频噪声控制整体摆动，255-最小幅度，0-最大幅度。 Red Channel - Leaves Stiffness 外围的高频噪声，0-最小幅度，255-最大幅度。在lumberyard engine中，红色通道需要读取面的法线，叶片的法线必须拥有水平分量。 Green Channel - Leaves Phase 噪声的相位差，也是以一个高频噪声的方式呈现。 speedtree的wind空间可以自动生成lumberyard的顶点色，所以一般不用手动编辑。 Other versions Firewatch中的顶点色设置： Crysis中的顶点色设置：
参考教程：《Windup》实时渲染动画全流程技术美术高级课程 安装ProGrid&ProBuilder ProBuilder可以直接在PackageManager-UnityRegistry中找到。 ProGrid就相对麻烦一些，2021之前的Unity版本，需要在ProjectSetting中开启PreviewPackage，然后即可在PackageManager中安装ProGrid。 2021之后版本，需要在PackageManager中点击➕，选择Add Package from git URL，输入com.unity.progrids，即可安装Progrid。 安装好后，可以在Tools下打开二者的工具栏。 ProBuilder Probuilder的第一个工具是创建几何体，可以开启ProGrid的第三个吸附开关，方便吸附格点；右下角的子菜单可以调节分段数。 可以使用Progrid中包含的的grid贴图创建材质，这张帖图是边长1m的方格，可以帮助控制比例。注意搜索时要点一下小眼睛图标，显示Package中的内容。 在Edit-Preference中将刚刚的grid材质拖放给Probuilder的material即可作为默认创建材质。 注意ProBuilder的工具栏是ContentSensitive的，当切换选择模式时会有不同的工具菜单，比如边模式下可以增加循环边。 ProBuilder的操作和常规建模工具很像，可以分别对顶点/边/面进行编辑，也可以选中面shift挤出。 使用Cinemachine 法一 使用VirtualCamera 在运镜的关键位置创建cinemachine-virtual camera。timeline中创建cinemachine track，将main camera拖入对象。将关键镜头视角的camera拖入track，即可运算镜头动画。 利用cinemachine的功能，可以设置look at、damping、soft zone、FOV等，也会在timeline中自动运算动画。 **法二 使用Dolly Camera with Track 创建Dolly Camera with Track，会生成一个cam和一个dolly track，然后可以通过dolly track的关键点编辑轨道路径。 除了将cam放入cinemachine track以外，还需要单独为这个cam创建一个animation track，记录track的position属性。
用到的模型还是之前OverHit的素材。 Mat 皮肤材质制作 可以先用ps粗略定位色块和位置，然后进sp细化。注意皮肤的色调可以参考Pantone的这张皮肤色卡，避免光线导致的色偏。 windup增加皮肤质感的方法非常简单粗暴，因为是小孩子皮肤，只要增加一点色偏就可以了：ps三原色纹理笔刷糊糊，透明度5%。 Wrinkle Map wrinkle map保留了皮肤挤压时的法线信息，常用于写实渲染中。最新的Enemies直接用shadergraph实现的皮肤皱纹，继续等源文件下载ing 学术文字码码： SSS 引擎中的SSS实现分两部分， Separable Subsurface Scattering Rig QuickRig in Maya 首先切换至Rigging菜单页，在skeleton标签下找到Quick Rig Tool。 没有一步步截图，这里需要1.创建新的绑定；2.加入需要绑骨的mesh；3.创建骨骼结点；4.调整关节位置，可以将镜像调整后的骨骼。 创建骨骼，因为我们的模型不是Tpose，最好勾选T-Stand Correction。注意这里其实不应该创建control rig，会影响后续加骨骼。只要skeleton only就可以了。 创建手部骨骼，使用Rigging标签下的Great Joints工具。注意最好在工具设置中勾选Projected Centering，这样新建立的骨骼节点会建立在mesh管的中心。参考windup的教程，手指的比例可以参考1/2，1/4，1/4. 更改命名，使用skeleton-mirror joint镜像对侧骨骼。 将手指骨骼拖入hand层级下（需要拖拽小logo），会自动生成中间连接的骨骼： 创建蒙皮，可以使用Skin菜单下的Paint Skin Weight绘制权重。 导入Unity 导入引擎后，在Rig下将AnimationType改为Humaniod，然后Apply应用。 进入Configure界面可以确认骨骼是否识别正确。 可以创建Timeline预览骨骼绑定结果。这里用的是BasicMotions的免费Package。 使用Mixamo动作素材 使用Mixamo的素材之前，需要先下载一个T-pose模型以创建Mixamo骨骼到Humaniod骨骼的重映射。 首先对T-pose动作进行Humaniod骨骼映射，然后其他下载的动作在Avatar Definition中选择CopyFromOtherAvatar，即可用于我们的模型。
周五要面试，狠狠进行一个临阵磨枪。 PBR 划重点： 物理原理和微表面理论 能量守恒：入射=反射+折射，金属无折射，非金属折射部分散射后出射→漫反射 BxDF 渲染方程： 求入射方向半球积分。 BRDF漫反射项：传统Lambert（无roughness/fresnel，Unity移动端用），基于物理Oren-Nayar/Disney（Unity）等 BRDF镜面反射项：D(NDF法线分布函数)/F（菲涅尔）/G（几何函数） Microfacet Cook-Torrance BRDF 其中分母 4(n·l)(n·v）：校正因子（correctionfactor），作为微观几何的局部空间和整个宏观表面的局部空间之间变换的微平面量的校正。 业内常用：D - GGX，F - Schlink（lerp((1-(v·h)^5,1,F0)），G - Schlink-GGX（UE）/Smith-GGX（Unity） 这里留一个毛老师的F0速查表，注意金属的F0是float3，非金属是float。 白炉测试：验证入射总能量为1的光线的反射光线分布是否归一化的测试 基于物理光照：漫反射（辉度环境映射）+环境光照（IBL） 环境光照Image Based Lighting：分解为预过滤环境贴图+环境BRDF 毛星云的白皮书： 【基于物理的渲染（PBR）白皮书】（一） 开篇：PBR核心知识体系总结与概览 【基于物理的渲染（PBR）白皮书】（二） PBR核心理论与渲染光学原理总结 SRP My SRP 咱时间有限，官方文档的内容就不重复了，划重点： A Render Pipeline Instance. This is an instance of a class defines the functionality of your render pipeline. Its script inherits from RenderPipeline, and overrides its Render() method. A Render Pipeline Asset. This is an asset in your Unity Project that stores data about which Render Pipeline Instance to use, and how to configure it. Its script inherits from RenderPipelineAsset and overrides its CreatePipeline() method. 其中Instance的Render方法中有一个很重要的变量ScriptableRenderContext，用于执行绘图命令。需要将命令输入CommandBuffer类，然后调用Context的ExecuteCommandBuffer方法依次执行命令。 CommandBuffer和ScriptableRenderContext都用于记录我们的指令，我们可以创建多个CommandBuffer用于记录不同的指令，但是ScriptableRenderContext只有一个，并且是Unity为我们提供好的。每一个CommandBuffer可以有自己的名称（使用name属性设置），可以记录各种命令，可以使用Clear方法被清空。所有的CommandBuffer最终都需要合并到ScriptableRenderContext中，并且在调用Submit方法的时候被一次性执行。 c#补课时间： Var是C#3.5新增的一个关键字，用来代替某些具体的类型，由编译器自动判断变量类型，编译器可以根据变量的初始化值“推断”变量的类型。 必须在定义时初始化，必须是局部变量。 C#中，方法的参数传递有四种类型：传值（by value），传址（by reference），输出参数（by output），数组参数（by array）。传址参数需要修饰符ref，输出参数需要修饰符out，数组参数需要修饰符params。 对比了一下URP的代码，首先是一些C#补课： sealed作为形容词是封闭的，未知的。sealed修饰类时说明该类将不能被继承或重写，修饰方法（函数或属性）时可防止扩充类重写此方法（函数或属性）。通常用于实现第三方类库时不想被客户端继承，或用于没有必要再继承的类以防止滥用继承造成层次结构体系混乱。恰当的使用sealed修饰符也可以在一定程度上提高运行效率，因为不用考虑继承类会重写该成员。 Partial是局部类型的标志。 局部类型可以实现将一个类、结构或接口分成几个部分，分别放在在几个不同的.cs文件中（当然也可以放在同一个.cs文件中）。 在程序进行编译之后，将会合并成一个完整的类。 延迟渲染设置 这里直接使用了知乎的设置： 一共有 4 块颜色缓冲和一块深度缓冲。其中 GT0 使用 ARGB32 格式作为 Albedo。GT1 则直接照抄 Unity 使用 ARGB2101010 格式存储世界空间下的法线。GT2 使用 ARGB64 格式，RG 存 Motion Vector，BA 存 roughness 和 metallic，16 bit per channel 足够保证精度。GT3 使用 ARGBFloat 格式，其中 RGB 存储 emission color 而 A 存储 occlusion，因为之后想支持 HDR 和 Bloom 所以需要更高的精度。 Multi Render Target（MRT）技术允许像素着色器输出多个颜色，他们被暂存到一组纹理缓冲区中，以便后续光照阶段使用。 这个步骤需要现在pipeline的构造函数中创建gdepth和gbuffers4纹理，然后render函数中将其设置为rendertarget，并开放为全局变量。对应的shader在片元着色器通过SV_target0/1/2/3可以输出到不同渲染目标。
ShaderLanguage Shader Language目前主要有3种语言：基于OpenGL的OpenGL Shading Language，简称GLSL;基于DirectX的High Level Shading Language,简称HLSL;还有NVIDIA公司的C for Graphic，简称Cg语言。 Unity使用的Shader程序嵌入的小片段是用Cg/HLSL编写的，从“CGPROGRAM”开始，到“CGEND”结束。 ShaderToy使用的WebGL程序由JavaScript编写的句柄和OpenGL Shading Language（GLSL）编写的着色器代码组成，该语言类似于C或C++，并在电脑的图形处理器（GPU）上执行。 所以想在Unity中查看ShaderToy的shader首先要将GLSL语言转化为HLSL/CG语言—— 关键词替换 在开始之前，把shadertoy中的代码粘贴到pass下的CGPROGRAM语句段内： 可直接CtrlF替换 Syntax vec → float mat → floatx fract( → frac( mix( → lerp( texture( → tex2D(/tex3D(/texCUBE KeyWords iTime → _Time.y 需要前文定义 #define atan(x,y) atan2(y,x) #define mod(x,y) x-y*floor(x/y) 手动修改 语义修正 Matrix Multiplication * → mul() 注意顺序：A*=B → A=mul(B,A) A*B → mul(B,A) Type Casting vec3(0) → float3 a = 1或 float3(0,0,0) 需要前文定义/外挂脚本 iMouse 需要在properties 定义fixed4变量，然后在CG程序块里再次声明同名变量。接下来要外挂C#脚本，可以参考乐乐老师的版本：【ShaderToy】开篇 Channel 需要在properties 定义对应种类的变量(2D/3D/CUBE)，然后在CG程序块里再次声明同名变量。 代码结构 （诚邀美女们下载这个VSCode的粉嫩主题-Dracula Official🍓 以jb猫为例 虽然raymarching modeling is not 4 UNITY，但是它可爱。所以就它了! 除去之前提到的注意事项，还有一点点关于AA的修正：因为没有iResolution，所以smoothstep给了定值；另外在主函数中上下左右取样AA的offset用了ddx(uv.x)来做单位，其实不太确定对不对... 但总之It works！ 其实这个写法还是不太规范，uv的运算借用了maintex的运算矩阵来控制offset和scale，但其实没有用到maintex。 注意预览动态材质时要开启alway refreshing： 完整的代码在下面： Shader "Unlit/Shadertoy" { Properties { _iChannel0 ("Channel0",CUBE) = "" {} _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; samplerCUBE _iChannel0; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex);; return o; } #define TMIN 0.01 #define TMAX 200. #define RAYMARCH_TIME 128 #define PRECISION .001 #define AA 3 #define PI 3.1415926 #define S(v,r) smoothstep( r, r+0.005, v ) float logo(float2 uv); //========SDFunctions======== float sdSphere(float3 p, float3 o, float r){ return length(p-o)-r; } float sdCapsule(float3 p,float3 a, float3 b,float r){ float3 ab = b - a; float3 ap = p - a; float t = dot(ab, ap)/dot(ab,ab); t = clamp(t,0.,1.); float3 c = a +t*ab; return length(p-c)-r; } //https:www.iquilezles.org/www/articles/distfunctions/distfunctions.htm float sdRoundCone( float3 p, float r1, float r2, float h ) { // sampling independent computations (only depend on shape) float b = (r1-r2)/h; float a = sqrt(1.0-b*b); // sampling dependant computations float2 q = float2( length(p.xz), p.y ); float k = dot(q,float2(-b,a)); if( k<0.0 ) return length(q) - r1; if( k>a*h ) return length(q-float2(0.0,h)) - r2; return dot(q, float2(a,b) ) - r1; } //===============TRANSFORM================= float2x2 rotate(float a){ return float2x2(cos(a),sin(a),-sin(a),cos(a)); } float smUni( float d1, float d2, float k ) { float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 ); return lerp( d2, d1, h ) - k*h*(1.0-h); } //===============RENDER=================== //Scene float f(float3 p){ p.x=abs(p.x); float d; {//face float3 p0 = float3(p.x*.8,p.y+.4,p.z); d = sdSphere(p0,float3(0,0,0),1.5); p0 = float3(p.x*.6,p.y+.6,p.z+.7); float d0 = sdSphere(p0,float3(0,0,0),1.); d = smUni(d,d0,.3); } float angle = sin(_Time.y*5.)*.1; {//ear float3 p0 = float3(p.x-.85,p.y-.3,p.z*1.2+.3); p0.xy = mul(rotate(-.3),p0.xy); p0.yz = mul(rotate(-.3),p0.yz); float d0 = sdRoundCone(p0,.8,.2,1.2)/1.2; d = smUni(d,d0,.3); } {//nose float d0 = sdCapsule(p,float3(0,0,-1.5),float3(0,-.6,-1.7),.3); d = smUni(d,d0,.05); } {//nose float d0 = sdSphere(p,float3(.3,-.6,-1.65),.45); d = smUni(d,d0,.05); } {//eye float3 p0 = float3(p.x-.6,p.y+.1,p.z+1.45); p0.xy = mul(rotate(-1.2),p0.xy); p0.yz = mul(rotate(-0.3),p0.yz); float d0 = sdRoundCone(p0,.06,.08,.2); d = smUni(d,d0,.05); } {// float3 p0 = float3(p.x-.9,p.y+.5,p.z+1.3); p0.xy = mul(rotate(.2+angle*.5),p.xy); float d0 = sdCapsule(p0,float3(0,0,0),float3(.8,0,0),.1); p0.xy = mul(rotate(-.4+angle*.5),p.xy); d0 = smUni(d0,sdCapsule(p0,float3(0,0,0),float3(.8,0,0),.1),.05); d = smUni(d,d0,.05); } return d; } float rayMarch(in float3 ro, in float3 rd) { float t = TMIN; for(int i = 0; i < RAYMARCH_TIME ; i++) { float3 p = ro + t * rd; float d = f(p); t += d; if(d < PRECISION || t > TMAX) break; } return t; } // https://www.iquilezles.org/www/articles/normalsSDF/normalsSDF.htm float3 calcNormal(in float3 p) { const float h = 0.0001; const float2 k = float2(1, -1); return normalize(k.xyy * f(p + k.xyy * h) + k.yyx * f(p + k.yyx * h) + k.yxy * f(p + k.yxy * h) + k.xxx * f(p + k.xxx * h)); } float3x3 setCamera(in float3 camtar, in float3 campos, in float camro){ float3 z = normalize(camtar-campos); float3 cp = float3(sin(camro),cos(camro),0.); float3 x = normalize(cross(cp,z)); float3 y = cross(z,x); return float3x3(x,y,z); } float3 render(float2 uv){ float3 lightPos = float3(-5., 5.,-5);//light //SET Camera float3 cam_tar = float3(0,-.5,0);//cam target float angle = pow(sin(_Time.y+.3),3.)*.5; float3 cam_pos = float3(sin(angle),0,-cos(angle))*10.;//cam position float3 rd = float3(uv,4.); //decide view width rd = normalize(mul(rd,setCamera(cam_tar,cam_pos,0.)));//viewing frustum float t = rayMarch(cam_pos,rd);//raymarching float3 color = lerp(float3(0,0,1),float3(0,0,.6),uv.y*.5+.5);//background if(t > TMAX) return color; float3 p = cam_pos + t*rd; float3 n = calcNormal(p); float3 l = normalize(lightPos-p);//lightDir float3 h = normalize(l-rd);// float3 diffusecol = 1;//diffuse color float3 specol = 1;//specular color float3 cmrfl = .5*float3(1,1,1)*texCUBE(_iChannel0,reflect(rd,n)).rgb;//cubemap reflection float dif = clamp(dot(l,n),0.,1.);//diffuse float spe = pow(clamp(dot(h,n),0.,1.),50.);//specular float fresnel = pow(clamp(1. - dot(n,-rd),0.,1.),5.); color = cmrfl + .1*dif*diffusecol + smoothstep(.6,1.,spe)*specol;//metal color = smoothstep(0.,1.,color);//add contrast color += fresnel*.4; return color; } fixed4 frag (v2f i) : SV_Target { float2 uv = i.uv - 0.5;//i.uv是0~1， float3 color = 0; #if AA>1 for(int m = 0; m < AA; m++) { for(int n = 0; n < AA; n++) { float2 offset = 2. * (float2(float(m), float(n)) / float(AA) - .5) * ddx(i.uv.x); uv = i.uv+offset; #else uv = i.uv; #endif color += render(uv); #if AA>1 } } color /= float(AA*AA); #endif color = lerp(color,float3(1,1,1),logo(uv)); return float4(color,1.0); } float logo(float2 uv){ float n = 0.; n += S(.05,abs(length(uv-float2(-1.23,0))-.12)); n *= 1.-S(-1.25,uv.x); n += S(.17,abs(uv.y))*S(.05,abs(uv.x+1.29)); n += S(.05,abs(length(uv-float2(-1.5,0))-.12)); n += S(.05,abs(length(uv-float2(-0.9,0))-.12)); n += (1.+S(-.6,uv.x)-S(.05,uv.y))*S(.05,abs(length(uv-float2(-0.6,.03))-.09)); n += (1.-S(-.6,uv.x)+S(-.05,uv.y))*S(.05,abs(length(uv-float2(-0.6,-.04))-.09)); return n; } ENDCG } } }