【Unity技术美术】实现一个可以与玩家碰撞的(塞尔达同款)草地_unity 草
前言
这几年,卡通风格的游戏如雨后春笋般涌出,而其中相当一部分游戏,在草地渲染中,下了不少功夫。
草地,是游戏场景的重要组成部分,是游戏营造空气感的手段之一。
塞尔达中的草地
本文章将基于另外一篇教程,带您实现一个可以与玩家碰撞的草地
使用
如果您并不想了解原理,单纯想要运用,我这里已经准备好了成品(免费的哦)
链接: 百度网盘 提取码: 6666
实测unity2021-unity6都可以使用,支持build in pipeline和urp,选择需要的版本导入工程即可。
导入工程后,创建两个变换(缩放,坐标,旋转)完全一样的的平面,一个作为土地,另一个作为草坪
将Grass/Material/Grass.material材质应用到Grass平面上
就可以生成一片草地。(如果要更改土地的外观,更改Land平面的材质即可)
在材质面板,您可以调节关于草的参数(高度,宽度,密度,摇摆程度等)
材质面板
添加角色碰撞,只需要将包内的GetPlayerPos脚本挂入场景内任何一个物体上
在玩家上挂挂载GrassCollider脚本,即可让玩家与草地发生交互
可以调节碰撞体半径和碰撞体中心(紫色的圈代表碰撞体)
效果
最多支持100个玩家同时对草地进行碰撞
原理和实现
本文章基于下面这篇教程,该教程,详细的讲述了在unity中如何实现一个随风飘荡的草地。我将会在下面这篇教程的基础上,为草地加入与玩家碰撞的功能,实现一个可以随风飘荡,且可以与玩家碰撞的草地。
https://roystan.net/articles/grass-shader.html
由于原教程基于build in渲染管线,使用本文章的代码也基于build in渲染管线。如果要使用基于urp的草材质,可以直接去上述链接下载(免费的哦)
纠错
个人觉得,上述教程中有个点,讲的是不正确的(个人观点哈,没别的意思,我有时候也会讲错一些东西,欢迎大家来纠正)。
在原教程中,作者为了让风的摆动看起来更自然,使用了坐标当作uv,对风的噪声图进行采样。但是作者使用的,并不是世界坐标系,而是本地坐标系。这样会导致,如果草地是好几个相同的面拼接的,其摆动仍然会不自然(看起来是一块一块的)。
这个场景由4个草平面组成,可以很明显的看到各个平面上草的摆动是完全一样的
但是如果使用世界坐标系作为uv进行采样,则不会有这个问题
float3 worldPos = mul(unity_ObjectToWorld, float4(pos, 1)).xyz;float2 uv = worldPos.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y;
可以看到各个平面草的摆动不一样了,而且衔接的十分自然
我们可以写个脚本将角色们的坐标传入着色器
using System.Collections.Generic;using UnityEngine;namespace Grass.Script{ [ExecuteInEditMode] public class GetPlayerPos : MonoBehaviour { //public GameObject player; private GrassCollider[] _cols; private List _poss = new List(); public Material material; // Start is called before the first frame update void Start() { _cols = GameObject.FindObjectsOfType(); } // Update is called once per frame void Update() { _poss.Clear(); foreach (var col in _cols) { _poss.Add(new Vector4(col.Position.x,col.Position.y, col.Position.z, col.radius)); } if (_poss != null) { material.SetVectorArray(\"_Players\",_poss.ToArray()); } else { print(\"no player\"); } } }}//LZX completed this script in 2024/05/06//LZX-TC-VS-2024-05-05-001
十分简单的脚本,每帧将角色的坐标传入指定的材质中。
poss数组是个四维向量的数组,其中xyz代表坐标,w代表半径
然后我们编写GrassCollider类
using UnityEngine;namespace Grass.Script{ [ExecuteInEditMode] public class GrassCollider : MonoBehaviour { public float radius = 1; public Vector3 centryOffcet = Vector3.zero; public Vector3 Position { get { return this.transform.position + centryOffcet; } } // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { } // Update is called once per frame void Update() { } private void OnDrawGizmos() { Gizmos.color = new Color((float)0xed / 0xff, (float)0x65 / 0xff, 1,1); Gizmos.DrawWireSphere(Position, radius); } }}
作为寻找玩家的根据和记录玩家碰撞体的半径和中心
当然,我们也需要在着色器中,定义_Players数组
uniform float4 _Players[100];
我们通过uniform来修饰这个变量,代表其是所有片段公用的
这是个四维向量的数组,其中xyz代表坐标,w代表碰撞半径
我们的草,会与影响范围内的玩家作用,我们在shader中编写一个函数,让每个顶点计算与自己作用的玩家是哪个
float4 nearestPlayer(float3 vetexPos){float4 res = float4(0,0,0,0);float minDis = 100000;for(int i = 0;i < 100;i++){float3 pos = float3(_Players[i].x,_Players[i].y,_Players[i].z);float3 disDir = pos-vetexPos;if(length(disDir)<_Players[i].w){return float4(pos,_Players[i].w);}if(length(disDir)<minDis){minDis = length(disDir);res = float4(pos,_Players[i].w);}}return res;}
碰撞算法
在这个着色器中,草的顶点是在几何着色器中计算的。
所以,我们计算碰撞,也在几何着色器中计算。
我们可以通过传入的角色坐标,配合着色器中计算的worldPos,计算出玩家与该顶点的距离,和向量
float3 worldPos = mul(unity_ObjectToWorld, float4(pos, 1)).xyz;float3 playerDir = normalize(_PlayerPos - worldPos);//计算玩家与该顶点向量float playerDistance = distance(_PlayerPos, worldPos);//计算玩家与该顶点距离
知道了玩家的距离和向量,我们该怎么确定,该顶点绕哪个轴旋转呢?
其实很简单,草的旋转轴,就是与playerDir垂直的向量。
我们可以自己编写个函数,来计算与某个向量垂直的向量
float3 GetPerpendicularVector(float3 v){ float3 perp = float3(-v.y, v.x, 0); // 简单交换 x 和 y if (length(perp) < 0.001) { // 如果 perp 太小,调整 z 分量 perp = float3(0, -v.z, v.y); } return normalize(perp);}
然后在几何着色器中,调用该函数,计算与playerIDr垂直的向量
float3 playerAixs = GetPerpendicularVector(playerDir);
playerAixs就是要求的旋转轴
旋转轴确定了,那么旋转角度怎么确定呢?
我们知道,旋转的角度肯定和角色的距离成反比(是反比,不是反比例),所以我们可以使用以下算法得出旋转的角度
弧度=角色碰撞体半径-距离
假设玩家碰撞体半径为1
其中x代表该顶点与玩家的距离,y代表弧度,y*π代表旋转的角度
然后我们规定只有y大于0时,草才会旋转(在半径外的草不受影响)
写成代码
float playerSample = max(_PlayerRadius - playerDistance,0);playerSample = playerSample * UNITY_PI;
playerSample就是我们需要的旋转角度,当然了这种算法是有可能在玩家碰撞体半径过大时出现草翻转360又转回来的情况,不过也无伤大雅,因为半径如此大的角色会将重新翻转回来的草遮挡住,我们只需要让玩家周围的草有着看起来自然的扭曲即可。
通过计算出的角度计算出旋转矩阵,然后加入到顶点最终的旋转矩阵(其变换顺序在facingRotationMatrix之后,在windRotation之前),得到最终的顶点旋转矩阵
写作数学公式:
transformationMatrix = tangentToLocal * windRotation * playerRotationMatrix * facingRotationMatrix * bendRotationMatrix
注意:公式中越靠前的矩阵变换越靠后
代码实现:
float3x3 playerRotationMatrix = AngleAxis3x3(playerSample, playerAixs);。。。。。。float3x3 transformationMatrix = mul(mul(mul(mul(tangentToLocal, windRotation),playerRotationMatrix), facingRotationMatrix), bendRotationMatrix);
完整的几何着色器
[maxvertexcount(3)]void grassGeo(triangle vertexOutput IN[3], inout TriangleStream triStream){ float3 pos = IN[0].vertex;float3 vNormal = IN[0].normal;float4 vTangent = IN[0].tangent;float3 vBinormal = cross(vNormal, vTangent) * vTangent.w;float3x3 tangentToLocal = float3x3(vTangent.x, vBinormal.x, vNormal.x,vTangent.y, vBinormal.y, vNormal.y,vTangent.z, vBinormal.z, vNormal.z);float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0, 0, 1));float3x3 bendRotationMatrix = AngleAxis3x3(rand(pos.zzx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1, 0, 0));float3 worldPos = mul(unity_ObjectToWorld, float4(pos, 1)).xyz;float2 uv = worldPos.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y;float4 Player = nearestPlayer(worldPos);float3 PlayerPos = Player.xyz;float PlayerRadius = Player.w;float3 playerDir = normalize(PlayerPos - worldPos);float playerDistance = distance(PlayerPos, worldPos);float3 playerAixs = GetPerpendicularVector(float3(playerDir.x,playerDir.z,playerDir.y));//float3 playerAixs = GetPerpendicularVector(playerDir);float playerSample = max(PlayerRadius - playerDistance,0);float3x3 playerRotationMatrix = AngleAxis3x3(UNITY_PI * playerSample, playerAixs);float2 windSample = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindStrength;float3 wind = normalize(float3(windSample.x, windSample.y, 0));float3x3 windRotation = AngleAxis3x3(UNITY_PI * windSample, wind); float3x3 transformationMatrix = mul(mul(mul(mul(tangentToLocal, windRotation),playerRotationMatrix), facingRotationMatrix), bendRotationMatrix);//在transformationMatrix下边构建一个用于底部顶点的矩阵float3x3 transformationMatrixFacing = mul(tangentToLocal, facingRotationMatrix);float height = (rand(pos.zyx) * 2 - 1) * _BladeHeightRandom + _BladeHeight;float width = (rand(pos.xzy) * 2 - 1) * _BladeWidthRandom + _BladeWidth;float3 tangentNormal = float3(0, -1, 0);float3 localNormal = mul(transformationMatrixFacing, tangentNormal);// 应用在底部的两个顶点triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(width, 0, 0)), float2(0, 0),localNormal));triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(-width, 0, 0)), float2(1, 0),localNormal));localNormal = mul(transformationMatrix, tangentNormal);triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0, 0, height)), float2(0.5, 1),localNormal));}
这样,我们就实现了,一个可以与玩家碰撞的草地
希望我的文章对您有帮助,也欢迎在评论区和我讨论