在独立游戏里的渲染开发踩坑笔记

2023-11-21 09:24:52 浏览数 (1)

背景

《玩具帝国》是一款Windows 安卓平台的双端游戏,使用Unity URP进行开发。画面属于写实风格。

纸片渲染

纸片渲染的核心要点是“透光性”,体现在游戏里,有以下几点:

  • 透光性好,受光面与背光面亮度接近。
  • 双面显示阴影。如果有一个面受到投影,那这个面的反面也会显示出阴影。
  • 投出的阴影上仍然有透光,不是完全的阴影

第一个问题很好解决,首先开启双面渲染,然后直接指定面的法线,让它满足与光线点乘为正。由于游戏场景的光源始终是固定的,纸片也的走向也是固定的,所以只要给每个场景的所有纸片指定一个特定的法线值即可。

代码语言:javascript复制
Light mainLight = GetMainLight(inputData,inputData.shadowMask,aoFactor);
half nDotL=dot(mainLight.direction,_LightingNormal);

进一步,还需要考虑纸片对光线透射与吸收,所以正反两面的颜色还是略有区别的。所以我在这里直接以albedo的rgb加权求和得到一个光线吸收率,对ndotl进行缩放。

第二个问题不需要专门的处理,只要走的是双面渲染,正常采Shadow Map,就可以让两个面获得相同的阴影。不过这里做了模拟纸片发生散射阴影处被提亮的trick:

代码语言:javascript复制
half shadow = mainLight.shadowAttenuation;
half3 sssShadow=saturate(1-mainLight.distanceAttenuation*shadow)*_SubsurfaceStrength*nDotL*surfaceData.albedo;

至于第三个问题,直接用半透明阴影的做法,在Shadow Caster中加了个Dither,用高度做控制:

代码语言:javascript复制
half p=GenerateHashedRandomFloat(input.positionCS);
half height=input.positionOS.y;
half k=pow(smoothstep(_Min,_Max,height),_ShadowContrast);
half a=SampleAlbedoAlpha(input.uv,TEXTURE2D_ARGS(_BaseMap,sampler_BaseMap)).a;

a=(a .2*(1-k))*k;
p=lerp(p,1-p,step(0.5,a));
clip(a-p);

区别还是很明显的:

之前 之后

实物投影和纸片投影的区别

由于游戏中的士兵很多,还必须进行合批:

士兵在受到攻击时会闪红,所以需要为它准备Per Instance信息:

代码语言:javascript复制
UNITY_INSTANCING_BUFFER_START(Props)
  UNITY_DEFINE_INSTANCED_PROP(float,_HitTime)
UNITY_INSTANCING_BUFFER_END(Props)

原本想传递闪红瞬间时间值,由材质计算具体的闪红。但发现由于精度原因,在游戏进行时间变长后闪红的计算误差会越来越大,最后改成通过程序传值,不知道有没有规避的方法=。=。

还没有做图集,所以目前只对同种士兵合批

茸毛灌木丛

游戏里的灌木丛按设定上是茸毛球

核心思路是让球面Mesh上的每一个四边面都变成Billboard,但法线信息依然照旧,用来计算光影、散射,做出假的体积感

只需要将模型拓扑成四边面,让四个点分别在UV的四个角即可。

代码语言:javascript复制
float3 FurVertex(float2 uv,float3 positionOS)
{
  half3 localOffset=half3(remap(uv,0,1,-1,1),0);
  half3x3 rotateMat=AngleAxis3x3(_WindStrength*(Wind(positionOS,_TimeScale)),normalize(GetCameraPositionWS()-TransformObjectToWorld(half3(0,0,0))));
  localOffset=.1*normalize(mul(UNITY_MATRIX_I_M,mul(rotateMat,mul(UNITY_MATRIX_I_V,localOffset))));
  localOffset=lerp(0,localOffset,_Shape);
  return localOffset;
}

//…Vertex Shader
half3 localOffset=FurVertex(output.uv,input.positionOS);
VertexPositionInputs vertexInput=GetVertexPositionInputs(input.positionOS.xyz localOffset);

最后加上风动

代码语言:javascript复制
 float Wind(float3 posOS,half timeScale)
{
  half3 posWS=TransformObjectToWorld(posOS);
  half random=sin(dot(posWS,half3(125,3651.52456,38512.352)));
  return 2.36*sin(2.35*random*timeScale*_Time 10.52) 3.255*cos(6.325*timeScale*random*_Time 491.463);
}

类似的操作在我的新书《Unity Shader入门与实战》中有详细的阐述,这是我面向对Shader一无所知的菜鸟人群撰写的Unity Shader入门读物(〃∀〃),都是以最简单最浅显的语言对Unity Shader开发中基本技术进行讲解,欢迎各位老爷支持。

当当 《Unity Shader入门与实战》《Unity Shader入门与实战》【摘要 书评 试读】- 京东图书

高亮与描边

鼠标移动到建筑上会产生高亮效果,同时建筑上带有外描边效果。这也是相当经典的案例了,在Unity URP刚发布的时候,官方案例就有类似的例子,在此也是用Render Feature处理的。逻辑里将需要描边或高亮的物体设置到对应的Rendering Layer中即可。

描边用了三个Pass,分别处理:

  1. Stencil Mask,用来指示建筑内部范围,用于剔除内描边
  2. Outline Pass 1,但关闭深度测试,显示被遮挡时的轮廓
  3. Outline Pass 2,但开启深度测试,显示未被遮挡时的轮廓
代码语言:javascript复制
Pass
{
	Ztest Off
    
    // ...
    // 高亮效果
}
// 被遮挡时的描边
Pass 
{
    Cull Front
    Ztest LEqual
    ZWrite On
    Stencil
	{
		Ref 1
		Comp NotEqual
		Pass keep
	}					
    // ...
}
// 未被遮挡时的描边
Pass 
{
    Cull Front
    Ztest Greater
    ZWrite On
    Stencil
	{
		Ref 1
		Comp NotEqual
		Pass keep
	}
    // ...
}
/// 剔除内描边
Pass 
{
    Blend Zero One
    Ztest Off
    Zwrite Off
    
    Stencil
	{
		Ref 1
		Comp Always
		Pass Replace
	}
}

描边用的软法线存在顶点色里,用houdini可以简单实现,Blender的几何节点也能很容易拉出来

Houdini

Blender

这个经典例子的操作在我的新书《Unity Shader入门与实战》中也有详细的阐述(〃∀〃),再次拉出来。

当当 《Unity Shader入门与实战》《Unity Shader入门与实战》【摘要 书评 试读】- 京东图书

特效

VAT

城楼上抖动的旗帜是用Houdini中物理解算布料后,导出VAT实现的。根据需要可以在材质里调节风动的强度,这样在与风场结合时可以非常方便。

至于顶点数量比较多的模型,可以烘焙成骨骼动画导入

破碎特效

( ˇωˇ)众所周知Unity里没有Chaos,所以建筑破碎也是在Houdini里切的,效果还凑合。

爆炸/烟雾特效

Embergeni出Flipbook真的好用,向各位老爷们推荐,六点光照、Motion Vector都可以直出,不过游戏里的光照都是固定死的,所以我只需要一张最基础的图就够用了。

在Unity里用的是VFX Graph,( ˇωˇ)用起来没有Niagara那么爽,不过比原来的particle system还是强上不少的。游戏里的火焰效果与逻辑有一部分重合,这部分可以直接移动到VFX Graph里去算。

0 人点赞