如何用Unity制作逼真的自然场景?

游戏中的风景越真实、自然,玩家的代入感也就越强。本文将分析Unity官方制作的演示短片——《Book of the Dead》中,风与植物的交互原理,帮助大家制作出更自然的风景。

《Book of the Dead》是由Unity官方制作的一个演示短片,其中有大量的植物,不仅渲染上有着照片级的真实感,而且风与植物的交互也非常的自然。其所有的自然资源都是来自照片扫描技术,而且使用了HDRP高清渲染管线,场景在Unity Asset Store上可以下载得到。本文主要分析短片中风与植物交互的原理。

一、支持的植物结构

场景中,对于风与植物交互的模拟,支持三种结构:

Hierachy Pivot:层次嵌套Pivot,用于模拟树或者其他有多重层次结构的植物

Single Pivot Color:单Pivot,用于模拟草

Procedural Animation:程序动画,用于模拟浮萍等无pivot的植物

对于树的模拟最为复杂,它属于Hierachy Pivot结构,最多支持3个层次嵌套:

主干,连接地面

Level 0分支,连接着主干

Level 1分支,连接着Leval 0分支

二、代码入口

本文重点分析Hierachy Pivot结构的实现原理。风与植物的交互一般用程序顶点动画实现,随意找到一棵树的shader,顺藤摸瓜可以在VS中找到如下代码:

可以看到,每个顶点的uv3通道中存的是pivot信息,即该顶点受哪些pivot影响。

#if USE_VEGETATION_ANIM
float3 positionWS = GetAbsolutePositionWS(positionRWS);
APPLY_VEGETATION_ANIM_TIMENUDGE(positionWS, normalWS, input.uv3/*pivotData*/, input.color.rgb/*pivotColor*/, GetObjectAbsolutePositionWS(), time.x);
positionRWS = GetCameraRelativePositionWS(positionWS);
#endif

注意的是,这里的uv3是float3类型。

struct AttributesMesh
{
//...
//forest-begin: Added vertex animation
#if defined(_ANIM_SINGLE_PIVOT_COLOR) || defined(_ANIM_HIERARCHY_PIVOT)
float3 uv3 : TEXCOORD3;
//...
};
Hierachy Pivot结构的植物,最终调用的是AnimateVegetationHierarchyPivot。
#if defined(USE_VEGETATION_ANIM) && defined(_ANIM_SINGLE_PIVOT_COLOR)
#define APPLY_VEGETATION_ANIM_TIMENUDGE(worldPos, normalWorld, pivotData, pivotColor, objectRoot, timeNudge) { AnimateVegetationSinglePivot(worldPos, normalWorld, pivotData, pivotColor, timeNudge); }
#elif defined(USE_VEGETATION_ANIM) && defined(_ANIM_HIERARCHY_PIVOT)
#define APPLY_VEGETATION_ANIM_TIMENUDGE(worldPos, normalWorld, pivotData, pivotColor, objectRoot, timeNudge) { AnimateVegetationHierarchyPivot(worldPos, normalWorld, pivotData, pivotColor, objectRoot, timeNudge); }
#elif defined(USE_VEGETATION_ANIM) && defined(_ANIM_PROCEDURAL_BRANCH)
#define APPLY_VEGETATION_ANIM_TIMENUDGE(worldPos, normalWorld, pivotData, pivotColor, objectRoot, timeNudge) { AnimateVegetationProceduralBranch(worldPos, normalWorld, objectRoot, timeNudge); }

三、PivotData解码

pivotData是float3类型,先用asuint转成uint3,一共是32x3=96个bit,分成两段前后48bit,分别存Pivot0和Pivot1的信息,分别用UnpackPivot0和UnpackPivot1解出来。

uint3 packedData =asuint(pivotData);

float3 pivotPos0, pivotPos1, pivotFwd0, pivotFwd1;

bool pivotEnabled0 =UnpackPivot0(packedData, pivotPos0, pivotFwd0);

bool pivotEnabled1 =UnpackPivot1(packedData, pivotPos1, pivotFwd1);

Pivot0和Pivot1刚好是对称排列。

接下来分析Pivot0是如何解码出来的,48bit里面,高32bit存Pivot Pos,低16位存Pivot Fwd,细节如下图所示。最终解出来的Pos是模型空间的坐标,树的建模应该是树干的根在模型空间的原点,Pos.x和Pos.z是有正负的,而树只能向上长,于是Pos.y必然是大于0。对于一般的树而言垂直方向范围一般大于水平方向的范围,于是用12bit保存Pos.y的值,稍微比x和z多2个bit的精度。

满足packedData.y & 0xFFFF0000时,即高16位有值时,代表有Pivot0的信息,才需要解析。

// Needs to match shader packing in baking tool
bool UnpackPivot0(uint3 packedData, inout float3 pivotPos0, inout float3 pivotFwd0) {
if(packedData.y & 0xFFFF0000) {
pivotPos0.x = UnpackFixedToSFloat(packedData.x, 8.f, 10, 22);
pivotPos0.y = UnpackFixedToUFloat(packedData.x, 32.f, 12, 10);
pivotPos0.z = UnpackFixedToSFloat(packedData.x, 8.f, 10, 0);
pivotFwd0.x = UnpackFixedToSFloat(packedData.y, 1.f, 8, 24);
pivotFwd0.z = UnpackFixedToSFloat(packedData.y, 1.f, 7, 17);
pivotFwd0.y = sqrt(1.f - saturate(dot(pivotFwd0.xz, pivotFwd0.xz))) * (((packedData.y >> 16) & 1) ? 1.f : -1.f);
pivotFwd0 = normalize(pivotFwd0);
return true;
}
return false;
}

其中Pos.x用UnpackFixedToSFloat解出来,10bit实际存的是百分比[0, 1],由于x可能是负数,编码时把[-1, 1]映射到[0, 1],于是这里把[0, 1]反映射回[-1, 1],再乘以传入的range,可以看出Pos.x的范围是[-8f, 8f]。从其他硬编码的参数可以看出,树的建模尺寸是长宽16x16,高是32。

float UnpackFixedToSFloat(uint val, float range, uint bits, uint shift) {
const uint BitMask = (1 << bits) - 1;
val = (val >> shift) & BitMask;
float fval = val / (float)BitMask;
return (fval * 2.f - 1.f) * range;
}

Fwd是分支(树干或者树枝)的方向,由于是单位向量,所以只存了x和z分量,y分量可以通过公式反算出来,开方后丢失了符号信息,于是用1位存符号。

pivotFwd0.x = UnpackFixedToSFloat(packedData.y, 1.f, 8, 24);
pivotFwd0.z = UnpackFixedToSFloat(packedData.y, 1.f, 7, 17);
pivotFwd0.y = sqrt(1.f - saturate(dot(pivotFwd0.xz, pivotFwd0.xz))) * (((packedData.y >> 16) & 1) ? 1.f : -1.f);
pivotFwd0 = normalize(pivotFwd0);

四、整体流程

伪代码如下所示,有点跟骨骼动画类似,顶点受骨骼的变换影响,而每个骨骼会受其父骨骼的变换影响,最终顶点受骨骼的级联变换影响。树的顶点至少受主干的影响,因为任何顶点肯定要么是属于主干或者属于其他分支,而其他分支必然直接或间接连着主干,最复杂的情况是顶点在level1分支上,level1分支连着level0分支,level分支连着主干,需要计算累计变换。

//任何顶点肯定是在主干上或连接着主干

计算主干受风力影响导致的旋转;

旋转作用于顶点pos和normal;

if (有pivot0信息)//主干连接着level0分支
{
计算level0分支受风力影响导致的旋转;
旋转作用于顶点pos和normal;
if (有pivot1信息)//level0分支连接着level1分支
{
计算level1分支受风力影响导致的旋转;
旋转作用于顶点pos和normal;
}
}

4.1 主干受风影响

对于每个枝干受风吹后弯曲程度,由如下变量控制,整体的弯曲程度可以由Wind Elasticity Lvl x系列变量控制,其中Lvl B是主干。

树被风吹有个特点,离地面越远部分,被吹弯曲的越厉害,所以会有个缩放系数来控制旋转量,地面处为0(树根),离地面越远的部分这个缩放系数越大。有种预烘培做法,是用顶点模型空间的y除以整个树的高度,计算出缩放系数并把它烘到点色或者其他通道上,这里的做法是运行时计算,用变量_WindRangeLvlB调节受风的范围,它是模型空间的量,其实跟预烘培的效果差不多。lvBElasticity是最终的弹性缩放系数。

//主干风力影响代码

float lvBRelativeObjectScale = mul(GetActualObject2World(), float4(0, _WindRangeLvlB, 0, 0)).y;
float3 windFwd = GetWindDirection(objectRoot);
float3 lvBBaseGustWind = GetTreeBaseGustWind(objectRoot, timeNudge);
float3 lvBPos = objectRoot;
//主干fwd直接取模型空间y轴方向
float3 lvBFwd = float3(0, 1, 0); //TODO: grab from rotation matrix
float lvBElasticity = _WindElasticityLvlB;
float lvBDistScale = saturate((worldPos.y - objectRoot.y) / lvBRelativeObjectScale);
lvBElasticity *= lvBDistScale;

对于风吹草的模拟一般在顶点加上风力方向的偏移就可以得到比较好的效果,因为一般草都比较矮小,但是对于树这种比较高的复杂结构,用草的方式模拟会有种树被拉扯变长的感觉,所以一般的方案是用旋转代替顶点偏移。

lvBWindAxis为旋转轴,windFwd与lvBFwd如果同向或者反向时候,主干应该是不会旋转,后面的枝干level 0做了这种情况的修正,主干这里可能从设计上就不会有垂直于地面的风向吧。

然后就是把旋转作用到顶点的pos和normal上,旋转的锚点是世界空间下的objectRoot,应该是模型空间的原点。

float lvBWindRotAngle = lvBBaseGustWind.x * lvBElasticity;
//对旋转角度进行log2衰减
lvBWindRotAngle = log2(1.f + abs(lvBWindRotAngle)) * sign(lvBWindRotAngle);
float3 lvBWindAxis = cross(lvBFwd, windFwd);
float4 lvBWindQuat = QuaternionFromAxisAngle(lvBWindAxis, lvBWindRotAngle);
worldPos = QuaternionRotatePointAbout(worldPos, lvBPos, lvBWindQuat);
worldNrm = QuaternionRotateVector(worldNrm, lvBWindQuat);

4.2 支干Level0受风影响

逻辑基本与主干差不多,不同的地方是lv0Fwd的方向用lv0BaseGustWind和lvBDistScale做了调整,猜测是为了让弯曲旋转更自然。

旋转角度lv0WindRotAngle根据windFwd和lv0Fwd的是否平行,进行了相应的衰减。

//当lv0BaseGustWind.y为0时,平行风,旋转轴为y轴模拟更自然
lv0Fwd.y *= lv0BaseGustWind.y * lvBDistScale;
lv0Fwd = normalize(lv0Fwd);
float3 lv0WindAxis = cross(windFwd, lv0Fwd);
float3 lv0WindRight = cross(windFwd, lv0WindAxis);
float lv0PerpendicularFactor = dot(lv0Fwd, lv0WindRight);
float lv0AngleFactor = lv0PerpendicularFactor * lv0PerpendicularFactor;
lv0AngleFactor *= sign(lv0PerpendicularFactor);
lv0WindRotAngle *= lv0AngleFactor;

4.3 支干Level1受风影响

Level1是树最外层的部分了,整体流程与LevelB和Level0差不多,另外多了一些扑动的处理,树的外端末枝(树叶或者小树枝)被风吹的时候往往是有较剧烈的摇晃,而且呈一定的随机周期性运动,这部分计算出来的是顶点偏移,并在最后的旋转前先加到顶点坐标上。

float vertexFlutterPhase = dot(worldPos, _WindFlutterPhase);
float windFlutterCos = cos(WIND_PI2 * (_WindTime + timeNudge + vertexFlutterPhase) / (_WindTreeFlutterGustVariancePeriod * _WindFlutterPeriodScale));
float windFlutterStrength = lv1Elasticity * _WindFlutterElasticity * _WindFlutterScale * (_WindTreeFlutterStrength + saturate((max(0.f, lv0BaseGustWind.z) - _WindTreeFlutterGustStrengthOffset) / _WindTreeFlutterGustStrengthScale) * _WindTreeFlutterGustStrength);
float3 lv1WindAxis = cross(windFwd, lv1Fwd);
worldPos += lv1WindAxis * windFlutterCos * windFlutterStrength;

原文地址:https://www.cnblogs.com/dyf214/p/14381205.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


这篇文章主要介绍了Unity游戏开发中外观模式是什么意思,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家...
这篇文章主要介绍Unity中地面检测方案的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!1.普通射线在角色坐标(一般是脚底)...
这篇文章主要介绍了Unity游戏开发中如何消除不想要的黄色警告,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带...
这篇文章主要介绍了Unity中有多少种渲染队列,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解
这篇文章主要介绍Unity中如何实现Texture,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!了解Texture2D 如上图,Texture2D是一张
小编给大家分享一下Unity中DOTS要实现的特点有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让...
这篇文章给大家分享的是有关unity中如何实现UGUI遮罩流光特效的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。下面是核心shader:Sh...
这篇文章主要为大家展示了“Unity中如何实现3D坐标转换UGUI坐标”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下...
这篇文章主要介绍了Unity游戏开发中设计模式的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家...
这篇文章主要介绍了Unity中如何实现仿真丝袜渲染,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了...
这篇文章给大家分享的是有关Unity插件OVRLipSync有什么用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。项目需要接入对话口型动...
这篇文章主要介绍了Unity性能优化之DrawCall的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家...
这篇文章给大家分享的是有关Unity给力插件之Final IK怎么用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。这插件有什么用:一般游...
这篇文章给大家分享的是有关Unity中如何内嵌网页插件UniWebView的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。一、常见Unity中内...
小编给大家分享一下Unity如何做流体物理的几个轮子,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让...
小编给大家分享一下Unity中Lod和Occlusion Culling的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收...
这篇文章将为大家详细讲解有关Unity中LineRenderer与TrailRenderer有什么用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获...
这篇文章主要介绍了Unity中coroutine问题的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起...
这篇文章将为大家详细讲解有关unity中spine怎么用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。骨骼动画首先我们来看到...
这篇文章主要为大家展示了“Unity Shader后处理中如何实现简单均值模糊”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学...