> 技术文档 > StartWithUnityChan(3)——后续探索

StartWithUnityChan(3)——后续探索

个人经验总结,尝试有更深的理解,未必都正确。


基本上公开课上的内容除了后处理就都弄完了,包括多级阴影贴图, 半透明效果,外描线等其实都是些固定的写法,现在对我来说,主要是记录,慢慢积累经验。

接下来,按照我的想法把整个作品的完成度提高:


随时间变化的贴图

首先把课上来不及展示的随时间变化的贴图给实现起来:

这个效果的实现其实可以看成三个部分:1.贴图,2.纹理变形(可以不做或做别的)3.随时间改变纹理贴图的采样结果

第一步贴图没什么好说的熟练的一套:

float4 tex = tex2D(_MainTex, i.uv);

记得声明好需要的变量就行

第二个问题 变形

主要是算法部分也很简单:

float distToCenter = length(i.uv - 0.5);float colorGradientSample = distToCenter * _ColorGradientTiling + _Time.y * _ColorGradientSpeed;half3 colorGradient = tex2D(_ColorGradient, float2(colorGradientSample, 0.5));col *= colorGradient;float alphaGradientSample = distToCenter * _AlphaGradientTiling + _Time.y * _AlphaGradientSpeed;half alphagradient = tex2D(_AlphaGradient, float2(alphaGradientSample, 0.5));alpha *= alphagradient;

根据时间变化(_Time)来在变化的在颜色和透明度的彩条上采样,这样一个uv坐标对应的片段(一片像素)就得到了随时间循环变化的颜色和透明度。(不过眼见的朋友可能发现了这里并没有类似取模的操作,怎么能循环呢。事实上这是tex2D函数内置的wrap mode,自动平铺重复,   即uv = frac(uv))效果:

接下来实现聚光灯斑点效果

half2 grid = fmod(i.uv, _GridSize) / _GridSize;half distToGrid = length(grid - 0.5);alpha *= step(distToGrid, _SpotSize);

使用fmod(a,b)(对a 取b的模)将uv 坐标限制在_grideSize大小中,并归一化(0-1)。使用step(a,b)(a>=b?0:1)。让uv在_SoptSize规定半径外alpha为0,达到小斑点的效果:


 

SpringBone

应用骨骼动画就不说了,搜一下,都是固定步骤。不过在我开始播放时3,发现他的两个麻花辫像面包一个杵着,好难受,接受不了。想了一下用刚体和弹簧模拟一下,结果更恶心了。好在这个项目里还带有SpringBone插件,可以用。但从来没用过的我放上一看:

日本語。。。本来就不知道怎么用的说。不过没关系we don\'t need talk,just show me the code!

先看看网上别人用类似的东西怎么用知道,我们要给需要活动的骨骼加上Springbone的脚本,再给主物体添加Manager的脚本。那么一切应该就是从manager开始的了


SpringManager1

先看Awake() 

private void Awake() { FindSpringBones(true); var boneCount = springBones.Length; for (int boneIndex = 0; boneIndex < boneCount; boneIndex++) { springBones[boneIndex].Initialize(this); } if (boneIsAnimatedStates == null || boneIsAnimatedStates.Length != boneCount) { boneIsAnimatedStates = new bool[boneCount]; } }...public void FindSpringBones(bool includeInactive = false) { var unsortedSpringBones = GetComponentsInChildren(includeInactive); var boneDepthList = unsortedSpringBones .Select(bone => new { bone, depth = GetObjectDepth(bone.transform) }) .ToList(); boneDepthList.Sort((a, b) => a.depth.CompareTo(b.depth)); springBones = boneDepthList.Select(item => item.bone).ToArray(); }

当挂载这个游戏物体被激活时,就会开寻找自己的springbone们,通过对自己子物体查找组件的方式把所有子物体上的SpirngBone对象都添加到自己的spirngBones变量中,然后对每个spirngBone执行他自己的初始化工作。

从这里就可以知道,SpringManager应放在模型级别最高的物体上,这样他才能遍历所有可能的子物体添加所有需要管理的springbone.

再看start()

 private void Start() { // Must get the ForceProviders in Start and not Awake or Unity will complain that // \"the scene is not loaded\" forceProviders = GameObjectUtil.FindComponentsOfType().ToArray(); }

看起来是获取了和力相关的组件,也许会通过这个变量计算各个骨骼的动态变化(尝试看了一下foreProviders, 没看懂,就当成黑盒吧)

下面再看看update()

不过到这里可能有的朋友不理解我这样看的原因,这与脚本组件的生命周期有关

脚本(MonoBehaviour)生命周期

(虽然这样写结构不太好,不过我认为这样思路最顺)

我们写一个脚本就是想要某个游戏物体或者游戏中的某个概念在游戏进行中执行我们的需求。

一个脚本的从初始化,执行,到被销毁的的过程会调用这些函数:

  1. Awake():脚本挂载的游戏物体加载时、激活时执行,可以又来加载一些组件执行的前置变量
  2. Enable():脚本本身启用时执行,注册事件监听器。。。?不懂,是在注册一些什么
  3. start():所有的前两者执行力完毕后执行,可以放置各个组件之间的依赖(Awake中已经收集好)
  4. update,LateUpdate, FixedUpdate
    1. update:每帧调用一次
    2. LateUpdate:每帧执行后调用
    3. FixedUpdate:一个固定时间执行,不受帧率影响,相当与真实时间一致
  5. 其他交互事件:碰撞,鼠标等
    1. OnTriggerEnter/Stay/Exit(),
    2. OnCollisionEnter/Stay/Exit(),
    3. OnMouseDown/Enter/Exit()
  6. 销毁相关:OnDisable(),OnDestroy()

unity会以上面的顺序执行函数


所以现在就要看update相关的函数,知道在每帧要干什么:

SpringManager2

(加一个数字,我真聪明嘿嘿)

private void LateUpdate() { if (automaticUpdates) { UpdateDynamics(); } }...public void UpdateDynamics() { var boneCount = springBones.Length; if (isPaused) { for (var boneIndex = 0; boneIndex  0) ? (1f / simulationFrameRate) : Time.deltaTime; for (var boneIndex = 0; boneIndex < boneCount; boneIndex++) { var springBone = springBones[boneIndex]; if (springBone.enabled) {  var sumOfForces = GetSumOfForcesOnBone(springBone);  springBone.UpdateSpring(timeStep, sumOfForces);  springBone.SatisfyConstraintsAndComputeRotation( timeStep, boneIsAnimatedStates[boneIndex] ? dynamicRatio : 1f); } } }

为什么是LateUpdate不是其他?虽然这个组件是希望模拟真实的物理效果,但其并不是单独的物理过程,依赖动画本身,例如帧率太高,而FiexedUpdate执行频率不变,就会人一直在动,但头发卡卡的。

函数内部执行是就是对每一个spirngbone做力的获取和影响。


SpringBone

springBone的初始化,由springManager对每个springbone执行

public void Initialize(SpringManager owner) { manager = owner; var childPosition = ComputeChildPosition(); var localChildPosition = transform.InverseTransformPoint(childPosition); boneAxis = localChildPosition.normalized; initialLocalRotation = transform.localRotation; actualLocalRotation = initialLocalRotation; sphereColliders = sphereColliders.Where(item => item != null).ToArray(); capsuleColliders = capsuleColliders.Where(item => item != null).ToArray(); panelColliders = panelColliders.Where(item => item != null).ToArray(); lengthLimitTargets = (lengthLimitTargets != null) ? lengthLimitTargets.Where(target => target != null).ToArray() : new Transform[0]; InitializeSpringLengthAndTipPosition(); }...public Vector3 ComputeChildPosition() { var children = GetValidChildren(transform); var childCount = children.Count; if (childCount == 0) { // This should never happen Debug.LogWarning(\"SpringBone「\" + name + \"」没有有效子节点\");//原版是日文,改过来了 return transform.position + transform.right * -0.1f; } if (childCount == 1) { return children[0].position; } var initialTailPosition = new Vector3(0f, 0f, 0f); var averageDistance = 0f; var selfPosition = transform.position; for (int childIndex = 0; childIndex < childCount; childIndex++) { var childPosition = children[childIndex].position; initialTailPosition += childPosition; averageDistance += (childPosition - selfPosition).magnitude; } averageDistance /= childCount; initialTailPosition /= childCount; var selfToInitial = initialTailPosition - selfPosition; selfToInitial.Normalize(); initialTailPosition = selfPosition + averageDistance * selfToInitial; return initialTailPosition; }

按照这个逻辑,每个springBone 会记录所有子物体的物理信息,但没有子节点的不需要,所以这个springBone挂载对象只要有子节点的物体。


角度限制

这个基点怎么选呢?

在springManger中每帧调用springbone的SatisfyConstraintsAndComputeRotation()

public void SatisfyConstraintsAndComputeRotation(float deltaTime, float dynamicRatio) { if (manager.enableLengthLimits) { currTipPos = ApplyLengthLimits(deltaTime); } var hadCollision = false; if (manager.collideWithGround) { hadCollision = CheckForGroundCollision(); } if (manager.enableCollision & !hadCollision) { hadCollision = CheckForCollision(); } if (manager.enableAngleLimits) { ApplyAngleLimits(deltaTime); } ......private void ApplyAngleLimits(float deltaTime) { if ((!yAngleLimits.active && !zAngleLimits.active) || pivotNode == null) { return; } var origin = transform.position; var vector = currTipPos - origin; var pivot = GetPivotTransform(); var forward = -pivot.right; if (yAngleLimits.active) { yAngleLimits.ConstrainVector(  -pivot.up, -pivot.forward, forward, angularStiffness, deltaTime, ref vector); } if (zAngleLimits.active) { zAngleLimits.ConstrainVector(  -pivot.forward, -pivot.up, forward, angularStiffness, deltaTime, ref vector); } currTipPos = origin + vector; }

看逻辑,这个基点(pivot)选择与角度限制,也就是用这个基点的位置与挂载脚本的骨骼来计算角度。所以你是希望他一节一节的就每次选择上一个父节点的物体。当然如果不需要角度限制,就不用管这个。


碰撞检测

SatisfyConstraintsAndComputeRotation()中除了角度限制,还有碰撞检测CheckForCollision(),与角色身体有交互。

//Initialize()...capsuleColliders = capsuleColliders.Where(item => item != null).ToArray();//CheckForCollision()... for (int i = 0; i < capsuleColliders.Length; ++i) { var collider = capsuleColliders[i]; if (collider.enabled) {  var currentCollisionStatus = collider.CheckForCollisionAndReact( headPosition, ref currTipPos, scaledRadius, ref hitNormal);  hadCollision |= currentCollisionStatus != CollisionStatus.NoCollision; } }

将需要做碰撞检测的碰撞体加入列表即可

以上就可以使用springBone来模拟头发的真实效果了,调一下参数达到喜欢的效果就好了


半透明发光

由于使用的是UnLit shader 不能直接是使用_Emission (好吧一开始我都不知道有这个方法),我选择对半透明物体颜色部分直接加倍,提升亮度:

#if defined (IS_EMISSION) col += tex2D(_MainTex,i.uv)*_EmissionStrength*albedo.a; return half4(col,albedo.a); #endif

在将outLine的颜色改为高亮的HDR:

#if defined (IS_EMISSION) col = _EmissionColor.rgb*0.05; #endif

结合达到我比较理想的半透明发光效果


光源渲染

我还想能多个聚光灯对角色进行打光,就像舞台上一样打光

这需要多光源渲染,在built_in 管线里需要两个pass来完成,

第一个pass的tags有\"LightMode\" = \"ForwardBase\" 这将只渲染主光源,而不会额外渲染其他光源。需要再添加一个一样的pass tag为\"LightMode\" = \"ForwardAdd\",这样其他额外的光源就会逐个执行这个pass达到多个光源渲染的目的。

因为我额外使用的是聚光灯,所以除了计算光源位置还需要角度,距离来模拟真实的效果,代码如下:

#if defined(SPOT_LIGHTING) float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); float dis = length(_WorldSpaceLightPos0.xyz - i.worldPos.xyz); float lightAngle = dot(lightDir, normalize(_WorldSpaceLightPos0.xyz)); float spotAngle = _LightPositionRange.w; float3 toPixl = normalize(i.worldPos.xyz - _WorldSpaceLightPos0.xyz); float spotAtten = smoothstep(spotAngle,spotAngle+0.1,lightAngle);#endif

除了最后记得加上角度相关的衰减,其他都一样

这短短几行可是让我摸索了好久啊,原本靠脚本来获取位置和其他信息,后面发现UnityWorldSpaceLightDir()在额外光源也可以,同时光源位置就在_WorldSpaceLightPos0,这在主光源也是,然后就简单了。

平行光

三个聚光灯

变好看了?。。。反正我感觉挺好(都忙了半天了)


镜头动画

最后在加一点东西,因为本身是跳舞,就给他弄一段镜头,使用Cinemachine

我看的教程:Unity高级虚拟摄像系统 - Cinemachine的使用

讲的挺好,没什么可说的,用的多了就熟练了。

最终成果

结果:```unityChan初体验```_哔哩哔哩_bilibili