赞
踩
2022网易MG比赛,策划案里关卡背景是一个信中被损坏的场景,主角要去修复这个场景的故事。然后原本的场景呈现黑白色的效果,但主角旁边画面是彩色的,关卡结局还要有镜头拉远,然后整个画面以主角为中心扩散恢复颜色的视觉效果。
项目是URP管线,场景为2D SpriteRenderer,使用了shaderGraph,摄像机纹理实现了效果
原场景: 只有一块区域是彩色效果其他黑白:
游戏实机展示:
一开始刚拿到这个设计的时候,首先研究如何将画面变灰,很简单啊直接将unity自带的后处理加上color Adjustments,然后saturation拉到最低
很好效果完美实现,此贴完结(撒花)
-----------------------------------------------------------------
咳咳,开个玩笑。后处理只能对整个画面进行处理,而我们的需求是对场景内的一块区域动态处理它的显示效果,所以只能自己去写shader了。
先想想这个shader是写在屏幕空间呢还是世界空间呢?写在屏幕空间,做一层UI Mask,根据Image贴图和这个像素点的采样决定是否对其做灰色也是一个可行的方法,性能方面也比较优解。但这次我们需要以玩家为中心显示彩色区域,中间也有很多摄像机拉远的游戏逻辑,这时候去做世界空间到屏幕空间的转换也不太方便。因此这里就采用了给场景内的sprite上shader了。
先实现的变灰色图逻辑吧,本人去网上一顿查找,成功找到了一个shader(这里省去了一些代码,关键看fragment shader的实现)
- Tags
- {
- "Queue"="Transparent"
- "IgnoreProjector"="True"
- "RenderType"="Transparent"
- "PreviewType"="Plane"
- "CanUseSpriteAtlas"="True"
- }
-
- Cull Off
- Lighting Off
- ZWrite Off
- Blend One OneMinusSrcAlpha
-
- Pass
- {
- //这里省去了一些代码,关键看fragment shader的实现
-
- sampler2D _MainTex;
- float _GrayScale;
-
- fixed4 frag(v2f IN) : SV_Target
- {
- if(_Open > 0)
- {
- fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
- //乘以一个灰度系数
- float cc = (c.r * 0.299 + c.g * 0.518 + c.b * 0.184);
- cc *= _GrayScale;
- c.r = c.g = c.b = cc;
-
- c.rgb *= c.a;
-
- return c;
- }
- else
- {
- return tex2D(_MainTex, IN.texcoord) * IN.color;
- }
- }
- ENDCG
- }
取灰度系数为一,其实里面最核心的一行代码就是:
- //c为frag中该点color
- c.r = c.g = c.b = (c.r * 0.299 + c.g * 0.518 + c.b * 0.184) * 灰度系数;
可以看到,变灰度图实际上是在片元着色器中,通过采样后对该点的RGB,对一个系数相乘再相加,做一些转换,最后在统一赋值给rbg实现的效果。
我们新建一个urp2DLit的ShaderGraph,在里面实现里面同样的逻辑效果,记得主采样贴图的参数的reference要正确设置成_MainTex才可以获取到sprite中的贴图
接着我们设置如何让画面变彩色。思路很简单先传入一个Vector3参数(世界空间坐标点),判断这个点的距离(float)是否小于一个值,如果是则输出sample Texture原本的color值,如果不是输出灰色转换后的color就好了。
新建两个参数 ,分别为Vector3和float
使用Position节点可以在Graph中获取该点位置,在使用Distance节点计算距离后Comparison距离值就好了!
最后用Branch节点分支 选择该店像素值(当然shader中最好不要用Branch,在GPU中做If的运算开销很大,后面会讲优化)
写一个脚本再同步一下参数,这里没有用unity的材质属性块MaterialPropertyBlock,因此修改时是直接修改所有材质的参数,如果需要也可以加上。
- using UnityEngine;
-
- public class Gray2DController : MonoBehaviour
- {
- public Material material;
- //pos就是场景中需要的彩色点
- public Transform pos;
- //
- public float distance;
-
- // Update is called once per frame
- void Update()
- {
- if(material != null)
- {
- material.SetVector("_Diastance", distance);
- if(pos != null)
- material.SetVector("_Pos", pos.position);
- }
- }
- }
去场景里拿一张图测试一下效果
很好,看起来shader成功运行了起来。很高兴,又写完了一个需求,当天做完后忙其他事情了。
后几天,美术终于花完了场景最终稿,给我发了场景图层拆分,当我兴奋的搭完场景然后安装上我的材质后,然而:
我: ???????
好吧即然出现了问题,就只能继续研究了,随便看看能不能优化下原来的想法。
即然一张图是没有任何问题的,那就只能是多图情况下的混合模式的问题了。观察最开始CG代码的shader,里面的混合模式是:
Blend One OneMinusSrcAlpha
ShaderGraph中也可以设置混合模式,然而一顿研究和查找资料后发现:
(这张图的介绍来源Intro to Shader Graph | Cyanilux)
shaderGraph默认的四种混合居然不支持Blend One OneMinusSrcAlpha。那只能走走别的方向了,仔细想想,原来的shader也确实不太完美,因为我们要的是整个画面的黑白,而上面的shader则需要给每个场景物品都安装这个shader跑计算的话,无疑是降低了性能和扩展性的。(因为对于每个物品都要计算一遍距离Distance是很低效的)
这时候突然想到,如果这个shader混合模式达不到我们想要的效果,而且最好只做一张图片的处理的话,那我们完全可以在场景空间中在贴一张贴图,然后在场景里再放一个摄像机专门渲染这张帖图就行啦,然后玩家的主摄像机只需要渲染这张贴图的图层就好了!(其实这种方法应该早点想到的我是笨比)这样子也解决了性能的问题。
开始行动!首先我们新建一个Layer
在项目资源中右键,选择创建一个渲染器纹理
在场景中新建一个相机,这个相机是用来渲染场景物品的并输出到贴图上的,因此在剔除中取消勾选刚刚新建的RendererTexture层,然后将刚刚创建好的Texture加载输出上。
调整摄像机在场景的位置,选择创建的RenderTexture资源,调整大小,就可以在预览窗口中看到场景样子了(这里我用了场景大小,实际上直接用屏幕分辨率1920*1080,然后将刚刚的摄像机放到主摄像机子物品下做位置同步也是可以的)
然后我们修改shader参数,实际上就是把采样的SampleTexture从MainTex改成刚刚的摄像机贴图就好了
然后我们在场景中新建一个sprite,设置它的layer和大小,然后拖入这个shader材质,我们这个摄像机贴图就做好了。
我们回到场景中的主摄像机,可以让它只渲染这个贴图,当然将这个贴图放在场景最前面挡住后面的正常物品也是可以,但是这样运行时渲染管线还要额外做深度剔除的计算所以就可以直接在设置中直接剔除其他的物品。
这样我们这个效果就成功实现了。最后在来一个优化,还记得我们前面用了if节点,通过距离大小判断选择,这样一是if性能开销大,二是两个色域过渡时也比较尖锐(因为if是简单的二值判断然后选择)
我们可以放弃if节点,改用lerp在两个色域中做插值,就可以优化渐变效果,插值需要一个0-1的参数,我们选择使用“Saturate”节点,这个节点可以将一个浮点值映射到[0,1]的范围内,即输入大于1时=1,输入小于0时等于0,然后将结果连接到Lerp节点就可以了。
这样交界就有了一个柔和的过渡了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。