评论

收藏

Unity 大面积花草风吹动效果以及编辑

游戏开发 游戏开发 发布于:2021-07-18 09:26 | 阅读数:563 | 评论:0

项目场景需求一个遍布鲜花的小岛,由于运行在手机上,所以对效率有一定的要求。

环境unity2017.3.f1,使用simpleLOD这个插件,方便做mesh合并,以及LOD处理

先放1张最终的效果图。
DSC0000.jpg

1.shader编写
先找来一个花的模型,贴图模型大致如下:
DSC0001.jpg

shaderVS阶段做一个顶点运动。大致思路是花越靠近地面的,摇晃幅度越小,反之幅度越大。这个高度可以用顶点坐标来做,不过要兼容静态烘焙,或者地面不平等情况,无法获取准确高度。我这里采用UV的思路(美术保证草的根在贴图底部)。
代码如下
Shader "custom/2-sided_grass"
{
  Properties 
  {
    _Color ("Main Color", Color) = (1,1,1,1)
    _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 0)
    _Shininess ("Shininess", Range (0.01, 10)) = 0.078125
    _MainTex ("Base (RGB) TransGloss (A)", 2D) = "white" {}
    _BumpMap ("Normalmap", 2D) = "bump" {}
    _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
    _Direction("Direction",Vector) =(0,0,0,0) //运动的方向
    _TimeScale("TimeScale",float) = 1    //时间
    _TimeDelay("TimeDelay",float) = 1   //延迟
  }
  SubShader 
  {
    Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
    LOD 400
    Cull Off
  
    CGPROGRAM
    #pragma surface surf BlinnPhong alphatest:_Cutoff vertex:vert
    #pragma target 3.0
  
    sampler2D _MainTex;
    sampler2D _BumpMap;
    fixed4 _Color;
    half _Shininess;
    fixed4 _Direction;
    half _TimeScale;
    half _TimeDelay;
    struct Input 
    {
      float2 uv_MainTex;
      float2 uv_BumpMap;
    };
  
    void vert(inout appdata_full v) 
    {
      fixed4 worldPos =  mul(unity_ObjectToWorld,v.vertex);
      half dis =  v.texcoord.y; //这里采用UV的高度来做。也可以用v.vertext.y
      half time = (_Time.y + _TimeDelay) * _TimeScale;
      v.vertex.xyz += dis * (sin(time + worldPos.x) * cos(time * 2 / 3) + 0.3)* _Direction.xyz;  //核心,动态顶点变换
    } 
    void surf (Input IN, inout SurfaceOutput o) 
    {
      fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
      o.Albedo = tex.rgb * _Color.rgb;
      o.Gloss = tex.rgb * _Color.rgb;
      o.Alpha = tex.a * _Color.a;
      o.Specular = _Shininess;
      o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
    }
    ENDCG 
  }  
  FallBack "Transparent/Cutout/VertexLit"
}
2.简单的笔刷
能够在场景里面编辑。能够调整大小,草密度,随机大小,绕Y轴旋转等,我们写一个简单的管理脚本,也方便做Mesh合并以及LOD。GrassGroup代码如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GrassGroup : MonoBehaviour
{
  [Tooltip("是否打开编辑")]
  public bool editorMode = false;
  [Tooltip("预制体")]
  public GameObject grassPrefab = null;
  [Tooltip("地形")]
  public Terrain terrain = null;
  [Tooltip("随机朝向")]
  public bool roodomRotationY = true;
  [Tooltip("随时缩放最小值")]
  public float minScale = 1;
  [Tooltip("随时缩放最小值")]
  public float maxScale = 1;
  [Tooltip("半径")]
  [HideInInspector]
  public float radius = 1;
  [Tooltip("数量")]
  [HideInInspector]
  public int count = 1;
  // Use this for initialization
  void Start ()
  {
    editorMode = false;
  }
  /// <summary>
  /// 生成子草
  /// </summary>
  /// <param name="postion"></param>
  public void AddGrassNode(Vector3 postion)
  {
    if (grassPrefab == null)
    {
      Debug.LogError("草预制件不能为空!!!!!");
      return;
    }
    if (terrain == null)
    {
      Debug.LogError("地形不能为空!!!!!");
      return;
    }
    for (int i = 0;i<count; i++)
    {
      GameObject go = GameObject.Instantiate(grassPrefab);
      go.transform.SetParent(transform);
      Vector2 p = Random.insideUnitCircle * radius;//将位置设置为一个半径为radius中心点在原点的圆圈内的某个点X.  
      Vector2 pos2 = p.normalized * (p.magnitude);
      Vector3 pos3 = new Vector3(pos2.x, 0, pos2.y) + postion;
      float y = terrain.SampleHeight(pos3);
      Vector3 pos = new Vector3(pos3.x ,y, pos3.z);
      go.transform.position = pos;
      if (roodomRotationY)
        go.transform.Rotate(new Vector3(0, 0, 1),Random.Range(0,360) );
      float scale = Random.Range(minScale, maxScale);
      go.transform.localScale = new Vector3(scale, scale, scale);
      go.name = "grass_" + transform.childCount.ToString();
    }
  }
}
Editor代码,简单写了下(注:HeGizmosCircle 是一个画圆的代码,稍微修改了下,网上找的https://www.cnblogs.com/TravelingLight/archive/2013/08/27/3286242.html)
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(GrassGroup))]
public class GrassGroup_Inspector : Editor
{
  private GrassGroup grassGroup = null;
  private float m_Theta = 0.1f; // 值越低圆环越平滑  
  private Color m_Color = Color.blue; // 线框颜色  
  private HeGizmosCircle heGizmosCircle = null;
  void OnEnable()
  {
    grassGroup = target as GrassGroup;
    if (heGizmosCircle == null)
      heGizmosCircle = GameObject.FindWithTag("HeGizmosCircle").GetComponent<HeGizmosCircle>();
  }
  void OnDisable()
  {
    if (heGizmosCircle != null)
    {
      heGizmosCircle.SetEnable(grassGroup.editorMode);
    }     
  }
  public override void OnInspectorGUI()
  {
    base.OnInspectorGUI();
    GUILayout.BeginHorizontal();
    GUILayout.Label("radius(半径):" + grassGroup.radius.ToString());
    grassGroup.radius = GUILayout.HorizontalSlider(grassGroup.radius, 0, 10, null);
    if (heGizmosCircle != null)
      heGizmosCircle.m_Radius = grassGroup.radius;
    GUILayout.EndHorizontal();
    GUILayout.BeginHorizontal();
    GUILayout.Label("count(数量):" + grassGroup.count.ToString());
    grassGroup.count = System.Convert.ToInt32(GUILayout.HorizontalSlider(grassGroup.count, 1, 100, null));
    GUILayout.EndHorizontal();
  }
  [MenuItem("地图编辑/创建GrassGroup")]
  static void CreateGrassGroup()
  {
    GameObject go = new GameObject("GrassGroup");
    GrassGroup group = go.AddComponent<GrassGroup>();
    go.transform.position = Vector3.zero;
  }
  public void OnSceneGUI()
  {
    if (grassGroup ==null || !grassGroup.editorMode)
      return;
    if (grassGroup.editorMode)
    {
      Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
      RaycastHit hitInfo;
      if (Physics.Raycast(ray, out hitInfo, 1 << 8))
      {
        heGizmosCircle.transform.position = hitInfo.point + new Vector3(0,0.2f,0);
        if (Event.current.type == EventType.MouseDown)
        {
          grassGroup.AddGrassNode(hitInfo.point);
        }       
      }
    }
  }
}
3.地图编辑以及合并mesh.
代码基本写完了,大致如下
DSC0002.png

勾上EditorMode即可在场景中编辑
DSC0003.png

使用SimpleLOD合并mesh,分组做LOD(我这里没做LOD,面有点多)。
看看最后shader参数,以及DrawCall等效率
DSC0004.png

这张去掉地形,只有天空盒子,花
DSC0005.jpg

面有点高,可以分组做LOD
DSC0006.png

补充一下,由于光照效果理想,修改了光照函数,改成贴图颜色了。
Shader "custom/2-sided_grass"
{
Properties 
{
_Color ("Main Color", Color) = (1,1,1,1)
_SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 0)
_Shininess ("Shininess", Range (0.01, 10)) = 0.078125
_MainTex ("Base (RGB) TransGloss (A)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
_Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
_Direction("Direction",Vector) =(0,0,0,0)
_TimeScale("TimeScale",float) = 1
_TimeDelay("TimeDelay",float) = 1
}
SubShader 
{
Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
LOD 400
Lighting Off
Cull Off
CGPROGRAM
#pragma surface surf myLightModel alphatest:_Cutoff vertex:vert
#pragma target 3.0
sampler2D _MainTex;
sampler2D _BumpMap;
fixed4 _Color;
half _Shininess;
fixed4 _Direction;
half _TimeScale;
half _TimeDelay;
struct Input 
{
float2 uv_MainTex;
float2 uv_BumpMap;
};
  //修改为主要贴图的颜色 
    //lightDir :点到光源的单位向量   viewDir:点到摄像机的单位向量   atten:衰减系数   
    float4 LightingmyLightModel(SurfaceOutput s, float3 lightDir,half3 viewDir, half atten)   
    {   
      float4 c ;   
      c.rgb =  s.Albedo;  
      c.a = s.Alpha;   
      return c;   
    }  
void vert(inout appdata_full v) 
{
fixed4 worldPos =  mul(unity_ObjectToWorld,v.vertex);
half dis =  v.texcoord.y;
half time = (_Time.y + _TimeDelay) * _TimeScale;
v.vertex.xyz += dis * (sin(time + worldPos.x) * cos(time * 2 / 3) + 0.3)* _Direction.xyz;//核心,动态顶点变换
} 
void surf (Input IN, inout SurfaceOutput o) 
{
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = tex.rgb * _Color.rgb;
o.Gloss = tex.rgb * _Color.rgb;
o.Alpha = tex.a * _Color.a;
o.Specular = _Shininess;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
}
ENDCG
}
FallBack "Transparent/Cutout/VertexLit"
}
游戏中效果
DSC0007.jpg

修改了一个顶点着色器shader
Shader "custom/TwoSideGrass"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1, 1, 1, 1)
_Cutoff ("Alpha cutoff", Range(0, 1)) = 0.5
_Direction("Direction", Vector) = (0, 0, 0, 0)
_TimeScale("TimeScale", Float) = 1
_TimeDelay("TimeDelay", Float) = 1
}
SubShader
{
Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" "IgnoreProjector"="True" }
LOD 400
Cull Off
Pass
{
//Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
// make fog work
#pragma multi_compile_fog
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _TimeScale, _TimeDelay;
float4 _Direction;
v2f vert (appdata v)
{
v2f o;
float4 worldPos =  mul(unity_ObjectToWorld,v.vertex);
float dis =  v.uv.y;
float time = (_Time.y + _TimeDelay) * _TimeScale;
v.vertex.xyz += dis * (sin(time + worldPos.x) * cos(time * 0.667) + 0.3) * _Direction.xyz;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed _Cutoff;
fixed4 _Color;
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv) * _Color;
clip(col.a - _Cutoff);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col*2;
}
ENDCG
}
}
}

关注下面的标签,发现更多相似文章