开场白
做一款咖啡主题的休闲游戏时,我被一个问题难住了:怎么让3D物体既色彩鲜艳又不失立体感?Unity的URP标准Lit材质虽然牛,但总觉得有点"用力过猛",暗部黑得像锅底,颜色也容易被光照"洗"得没那么鲜艳。折腾了一圈后,我搞了个自定义的彩色箱子材质,简单粗暴又好用。这篇博客就聊聊我是怎么从头到尾搞定这个Shader的心路历程,顺便吐槽下踩过的坑。
问题在哪儿?
标准材质的"坑"用URP的Lit材质时,我发现几个让人头大的问题:
- 颜色不够鲜:光照一打,颜色就没那么饱和,感觉像被"漂白"了。
- 暗部太黑:阴影直接变成黑漆漆一块,色彩细节全没了。
- 参数太烦:Metallic、Smoothness这些参数,美术看了直摇头,调起来像解谜。
游戏到底想要啥?我们这款休闲游戏的美术需求很简单:
- 颜色得鲜艳,像咖啡店的杯子那样抓眼球。
- 暗部不能是纯黑,得保留点色彩,比如咖啡杯暗处带点暖棕色。
- 参数得直观,美术能一秒上手,不用翻文档。
- 立体感不能丢,光影得有点层次。
摸索的过程
先搞懂光照咋算的要搞Shader,先得弄明白URP的光照逻辑。核心就是这个公式:
ini
float NdotL = dot(normalWS, mainLight.direction);
float lightIntensity = saturate(NdotL);
翻译成人话:物体表面法线和光源方向的夹角决定光亮不亮。夹角小,亮;夹角大,暗。简单粗暴,但也给了我灵感:暗的地方能不能不黑,而是用自定义颜色?找到救命的参数瞎折腾了几天,我发现两个神器:
- _ShadowColor:暗部的颜色,可以随便定,比如紫色、蓝色啥的。
- _ShadowStrength:控制暗部颜色有多"重",调这个就能决定阴影是"淡淡的"还是"浓浓的"。
有了这俩,我感觉离目标不远了!色彩混合的"魔法公式"核心突破是这个混合逻辑:
ini
float shadowIntensity = 1 - lightIntensity;
half3 finalColor = lerp(baseColor, _ShadowColor.rgb, shadowIntensity * _ShadowStrength);
啥意思呢?shadowIntensity是暗部强度,0是亮处,1是最暗。lerp就是在基础颜色和暗部颜色间"滑来滑去",_ShadowStrength决定滑得多狠。这公式让我第一次看到暗部不是黑的,激动得差点拍桌子!
一步步搞定
- 定义材质参数先把材质属性写好,尽量让美术一看就懂:
scss
Properties
{
_BaseColor("基础颜色", Color) = (1,1,1,1)
_BaseMap("基础贴图", 2D) = "white" {}
_ShadowColor("暗部颜色", Color) = (0.3,0.3,0.5,1)
_ShadowStrength("暗部强度", Range(0,1)) = 0.5
_Darkness("整体暗度", Range(0,1)) = 0
_Smoothness("高光强度", Range(0,1)) = 0.5
}
我把_Metallic改名叫_Darkness,意思是"整体暗度",美术一听就知道干啥用的,省得解释半天。
- 顶点着色器顶点着色器没啥花头,标准流程,传点数据给片段着色器:
ini
Varyings vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
output.normalWS = TransformObjectToWorldNormal(input.normalOS);
output.positionWS = TransformObjectToWorld(input.positionOS.xyz);
return output;
}
- 光照计算片段着色器开始干正事,先算光照:
ini
Light mainLight = GetMainLight();
float NdotL = dot(input.normalWS, mainLight.direction);
float lightIntensity = saturate(NdotL);
float shadowIntensity = 1 - lightIntensity;
shadowIntensity就是暗部强度的"灵魂",接下来全靠它。
- 色彩混合这部分是核心,稍微搞复杂点:
ini
half3 finalColor = lerp(baseColor, _ShadowColor.rgb, shadowIntensity * _ShadowStrength);
finalColor = lerp(finalColor * lightIntensity, _ShadowColor.rgb, shadowIntensity * _ShadowStrength);
第一行:把基础颜色和暗部颜色混起来。第二行:再根据光照强度调一下,保证暗部有颜色而不是黑乎乎的。两次lerp看着简单,调的时候可没少翻车。
- 整体明暗控制用_Darkness来控制整体亮度,简单粗暴:
ini
finalColor *= (1 - _Darkness);
美术说想暗点?调高_Darkness就行,傻瓜式操作。
- 加点高光为了让物体更有立体感,我加了高光:
ini
float3 viewDirWS = GetWorldSpaceNormalizeViewDir(input.positionWS);
float3 halfDir = normalize(mainLight.direction + viewDirWS);
float NdotH = dot(input.normalWS, halfDir);
float specular = pow(saturate(NdotH), _Smoothness * 100 + 1);
specular *= _Smoothness;
finalColor += mainLight.color * specular * (1 - _Darkness);
高光让物体看起来更有"质感",而且跟_Darkness联动,保证整体风格统一。
调参的血泪史
挑暗部颜色试了一堆颜色后,我总结了点经验:
- 蓝色系((0.3,0.3,0.5,1)):冷色调,适合清爽的物体。
- 紫色系((0.4,0.2,0.6,1)):有点神秘,适合装饰品。
- 橙色系((0.6,0.4,0.2,1)):暖色调,咖啡杯用这个超有感觉。
暗部强度咋调?_ShadowStrength得看风格:
- 0.3-0.5:轻快风格,暗部像蒙了层纱。
- 0.5-0.7:中规中矩,色彩和光影平衡。
- 0.7-0.9:高对比,适合想突出光影的场景。
调的时候美术老跟我battle,说"太暗了""太淡了",调到最后我都觉得自己是调色大师了。实际效果咋样?在游戏里,这材质的表现真没让我失望:
- 咖啡杯:橙色暗部,暖洋洋的,像刚泡好的咖啡。
- 装饰物:蓝色暗部,清新感拉满,场景瞬间活了。
- 背景:调高_ShadowStrength,层次感强了不少,场景不再"平"。
总结和碎碎念
搞完这个Shader,我有几点感悟:
- 简单点好:PBR很牛,但有时候简单点更对味。
- 参数得友好:技术再牛,美术用着不爽等于白干。
- 色彩有魔法:暗部颜色选对了,能让玩家心情都不一样。
这过程其实挺折腾,中间改代码改到半夜,调参数调到怀疑人生。但看到最后效果,觉得值了!这彩色箱子材质虽然不是啥高科技,但完美解决了我们游戏的需求,算是个小骄傲吧。
本文章权益归属火腾(www.firedance.cn),转载请注明来源于火腾(www.firedance.cn)。